From 2d49ce56de5ed314aa099f3ce4aa569b3e22b561 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Thu, 18 May 2023 20:11:52 +0530 Subject: [PATCH] feat(connector): [Authorizedotnet] implement Capture flow and webhooks for Authorizedotnet (#1171) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Co-authored-by: Jagan --- .../router/src/connector/authorizedotnet.rs | 368 ++++++--- .../connector/authorizedotnet/transformers.rs | 416 +++++++--- .../tests/connectors/authorizedotnet.rs | 774 ++++++++++++------ 3 files changed, 1085 insertions(+), 473 deletions(-) diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 3e44328677..83e9613883 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -3,6 +3,7 @@ mod transformers; use std::fmt::Debug; +use common_utils::{crypto, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; use transformers as authorizedotnet; @@ -10,11 +11,12 @@ use crate::{ configs::settings, consts, core::errors::{self, CustomResult}, + db::StorageInterface, headers, - services::{self, logger}, + services::{self, ConnectorIntegration}, types::{ self, - api::{self, ConnectorCommon}, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, utils::{self, BytesExt}, }; @@ -22,6 +24,22 @@ use crate::{ #[derive(Debug, Clone)] pub struct Authorizedotnet; +impl ConnectorCommonExt for Authorizedotnet +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + _req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )]) + } +} + impl ConnectorCommon for Authorizedotnet { fn id(&self) -> &'static str { "authorizedotnet" @@ -46,7 +64,7 @@ impl api::ConnectorAccessToken for Authorizedotnet {} impl api::PaymentToken for Authorizedotnet {} impl - services::ConnectorIntegration< + ConnectorIntegration< api::PaymentMethodToken, types::PaymentMethodTokenizationData, types::PaymentsResponseData, @@ -55,62 +73,121 @@ impl // Not Implemented (R) } -impl - services::ConnectorIntegration< - api::Session, - types::PaymentsSessionData, - types::PaymentsResponseData, - > for Authorizedotnet +impl ConnectorIntegration + for Authorizedotnet { // Not Implemented (R) } -impl - services::ConnectorIntegration< - api::AccessTokenAuth, - types::AccessTokenRequestData, - types::AccessToken, - > for Authorizedotnet +impl ConnectorIntegration + for Authorizedotnet { // Not Implemented (R) } impl api::PreVerify for Authorizedotnet {} -impl - services::ConnectorIntegration< - api::Verify, - types::VerifyRequestData, - types::PaymentsResponseData, - > for Authorizedotnet +impl ConnectorIntegration + for Authorizedotnet { // Issue: #173 } -impl - services::ConnectorIntegration< - api::Capture, - types::PaymentsCaptureData, - types::PaymentsResponseData, - > for Authorizedotnet -{ - // Not Implemented (R) -} - -impl - services::ConnectorIntegration +impl ConnectorIntegration for Authorizedotnet { fn get_headers( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(self.base_url(connectors).to_string()) + } + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; + let authorizedotnet_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(authorizedotnet_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: types::Response, + ) -> CustomResult { + use bytes::Buf; + + // Handle the case where response bytes contains U+FEFF (BOM) character sent by connector + let encoding = encoding_rs::UTF_8; + let intermediate_response = encoding.decode_with_bom_removal(res.response.chunk()); + let intermediate_response = + bytes::Bytes::copy_from_slice(intermediate_response.0.as_bytes()); + + let response: authorizedotnet::AuthorizedotnetPaymentsResponse = intermediate_response + .parse_struct("AuthorizedotnetPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + get_error_response(res) + } +} + +impl ConnectorIntegration + for Authorizedotnet +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // This connector does not require an auth header, the authentication details are sent in the request body - Ok(vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsSyncType::get_content_type(self).to_string(), - )]) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -175,7 +252,6 @@ impl data: data.clone(), http_code: res.status_code, }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) } fn get_error_response( @@ -186,23 +262,16 @@ impl } } -impl - services::ConnectorIntegration< - api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > for Authorizedotnet +impl ConnectorIntegration + for Authorizedotnet { fn get_headers( &self, - _req: &types::PaymentsAuthorizeRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // This connector does not require an auth header, the authentication details are sent in the request body - Ok(vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - )]) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -221,7 +290,6 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - logger::debug!(request=?req); let connector_req = authorizedotnet::CreateTransactionRequest::try_from(req)?; let authorizedotnet_req = utils::Encode::::encode_to_string_of_json( @@ -261,7 +329,6 @@ impl res: types::Response, ) -> CustomResult { use bytes::Buf; - logger::debug!(authorizedotnetpayments_create_response=?res); // Handle the case where response bytes contains U+FEFF (BOM) character sent by connector let encoding = encoding_rs::UTF_8; @@ -272,40 +339,30 @@ impl let response: authorizedotnet::AuthorizedotnetPaymentsResponse = intermediate_response .parse_struct("AuthorizedotnetPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, }) - .change_context(errors::ConnectorError::ResponseDeserializationFailed) } fn get_error_response( &self, res: types::Response, ) -> CustomResult { - logger::debug!(authorizedotnetpayments_create_error_response=?res); get_error_response(res) } } -impl - services::ConnectorIntegration< - api::Void, - types::PaymentsCancelData, - types::PaymentsResponseData, - > for Authorizedotnet +impl ConnectorIntegration + for Authorizedotnet { fn get_headers( &self, - _req: &types::PaymentsCancelRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - )]) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -324,9 +381,9 @@ impl &self, req: &types::PaymentsCancelRouterData, ) -> CustomResult, errors::ConnectorError> { - let connector_req = authorizedotnet::CancelTransactionRequest::try_from(req)?; + let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; let authorizedotnet_req = - utils::Encode::::encode_to_string_of_json( + utils::Encode::::encode_to_string_of_json( &connector_req, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -361,17 +418,15 @@ impl let intermediate_response = bytes::Bytes::copy_from_slice(intermediate_response.0.as_bytes()); - let response: authorizedotnet::AuthorizedotnetPaymentsResponse = intermediate_response + let response: authorizedotnet::AuthorizedotnetVoidResponse = intermediate_response .parse_struct("AuthorizedotnetPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(authorizedotnetpayments_create_response=?response); types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, }) - .change_context(errors::ConnectorError::ResponseDeserializationFailed) } fn get_error_response( @@ -386,19 +441,16 @@ impl api::Refund for Authorizedotnet {} impl api::RefundExecute for Authorizedotnet {} impl api::RefundSync for Authorizedotnet {} -impl services::ConnectorIntegration +impl ConnectorIntegration for Authorizedotnet { fn get_headers( &self, - _req: &types::RefundsRouterData, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // This connector does not require an auth header, the authentication details are sent in the request body - Ok(vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self).to_string(), - )]) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -417,7 +469,6 @@ impl services::ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - logger::debug!(refund_request=?req); let connector_req = authorizedotnet::CreateRefundRequest::try_from(req)?; let authorizedotnet_req = utils::Encode::::encode_to_string_of_json( @@ -450,7 +501,6 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { use bytes::Buf; - logger::debug!(response=?res); // Handle the case where response bytes contains U+FEFF (BOM) character sent by connector let encoding = encoding_rs::UTF_8; @@ -461,14 +511,12 @@ impl services::ConnectorIntegration +impl ConnectorIntegration for Authorizedotnet { fn get_headers( &self, - _req: &types::RefundsRouterData, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // This connector does not require an auth header, the authentication details are sent in the request body - Ok(vec![( - headers::CONTENT_TYPE.to_string(), - types::RefundSyncType::get_content_type(self).to_string(), - )]) + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -556,7 +601,6 @@ impl services::ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha512)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let security_header = request + .headers + .get("X-ANET-Signature") + .map(|header_value| { + header_value + .to_str() + .map(String::from) + .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound) + .into_report() + }) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()?? + .to_lowercase(); + let (_, sig_value) = security_header + .split_once('=') + .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed) + .into_report()?; + hex::decode(sig_value) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + Ok(request.body.to_vec()) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: authorizedotnet::AuthorizedotnetWebhookObjectId = request + .body + .parse_struct("AuthorizedotnetWebhookObjectId") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + match details.event_type { + authorizedotnet::AuthorizedotnetWebhookEvent::RefundCreated => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + authorizedotnet::get_trans_id(details)?, + ), + )) + } + _ => Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + authorizedotnet::get_trans_id(details)?, + ), + )), + } + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("whsec_verification_{}_{}", self.id(), merchant_id); + let secret = match db.find_config_by_key(&key).await { + Ok(config) => Some(config), + Err(e) => { + crate::logger::warn!("Unable to fetch merchant webhook secret from DB: {:#?}", e); + None + } + }; + Ok(secret + .map(|conf| conf.config.into_bytes()) + .unwrap_or_default()) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: authorizedotnet::AuthorizedotnetWebhookEventType = request + .body + .parse_struct("AuthorizedotnetWebhookEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + Ok(api::IncomingWebhookEvent::from(details.event_type)) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let payload = serde_json::to_value(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(payload) } } @@ -603,23 +731,33 @@ fn get_error_response( .parse_struct("AuthorizedotnetPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::info!(response=?response); - - Ok(response - .transaction_response - .errors - .and_then(|errors| { - errors.into_iter().next().map(|error| types::ErrorResponse { - code: error.error_code, - message: error.error_text, + match response.transaction_response { + Some(transaction_response) => Ok({ + transaction_response + .errors + .and_then(|errors| { + errors.into_iter().next().map(|error| types::ErrorResponse { + code: error.error_code, + message: error.error_text, + reason: None, + status_code, + }) + }) + .unwrap_or_else(|| types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: None, + status_code, + }) + }), + None => { + let message = &response.messages.message[0].text; + Ok(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: message.to_string(), reason: None, status_code, }) - }) - .unwrap_or_else(|| types::ErrorResponse { - code: consts::NO_ERROR_CODE.to_string(), - message: consts::NO_ERROR_MESSAGE.to_string(), - reason: None, - status_code, - })) + } + } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 75f5dfc192..0ff92ca196 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -3,7 +3,7 @@ use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{CardData, RefundsRequestData}, + connector::utils::{CardData, PaymentsSyncRequestData, RefundsRequestData}, core::errors, types::{self, api, storage::enums}, utils::OptionExt, @@ -13,6 +13,10 @@ use crate::{ pub enum TransactionType { #[serde(rename = "authCaptureTransaction")] Payment, + #[serde(rename = "authOnlyTransaction")] + Authorization, + #[serde(rename = "priorAuthCaptureTransaction")] + Capture, #[serde(rename = "refundTransaction")] Refund, #[serde(rename = "voidTransaction")] @@ -191,8 +195,9 @@ struct AuthorizationIndicator { #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] -struct TransactionVoidRequest { +struct TransactionVoidOrCaptureRequest { transaction_type: TransactionType, + amount: Option, ref_trans_id: String, } @@ -205,9 +210,9 @@ pub struct AuthorizedotnetPaymentsRequest { #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct AuthorizedotnetPaymentCancelRequest { +pub struct AuthorizedotnetPaymentCancelOrCaptureRequest { merchant_authentication: MerchantAuthentication, - transaction_request: TransactionVoidRequest, + transaction_request: TransactionVoidOrCaptureRequest, } #[derive(Debug, Serialize, PartialEq)] @@ -219,8 +224,8 @@ pub struct CreateTransactionRequest { #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct CancelTransactionRequest { - create_transaction_request: AuthorizedotnetPaymentCancelRequest, +pub struct CancelOrCaptureTransactionRequest { + create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest, } #[derive(Debug, Serialize, PartialEq, Eq)] @@ -249,7 +254,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest { authorization_indicator: c.into(), }); let transaction_request = TransactionRequest { - transaction_type: TransactionType::Payment, + transaction_type: TransactionType::from(item.request.capture_method), amount: item.request.amount, payment: payment_details, currency_code: item.request.currency.to_string(), @@ -269,10 +274,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest { } } -impl TryFrom<&types::PaymentsCancelRouterData> for CancelTransactionRequest { +impl TryFrom<&types::PaymentsCancelRouterData> for CancelOrCaptureTransactionRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCancelRouterData) -> Result { - let transaction_request = TransactionVoidRequest { + let transaction_request = TransactionVoidOrCaptureRequest { + amount: item.request.amount, transaction_type: TransactionType::Void, ref_trans_id: item.request.connector_transaction_id.to_string(), }; @@ -280,7 +286,27 @@ impl TryFrom<&types::PaymentsCancelRouterData> for CancelTransactionRequest { let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?; Ok(Self { - create_transaction_request: AuthorizedotnetPaymentCancelRequest { + create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest { + merchant_authentication, + transaction_request, + }, + }) + } +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for CancelOrCaptureTransactionRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let transaction_request = TransactionVoidOrCaptureRequest { + amount: Some(item.request.amount_to_capture), + transaction_type: TransactionType::Capture, + ref_trans_id: item.request.connector_transaction_id.to_string(), + }; + + let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?; + + Ok(Self { + create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest { merchant_authentication, transaction_request, }, @@ -306,7 +332,7 @@ pub type AuthorizedotnetRefundStatus = AuthorizedotnetPaymentStatus; impl From for enums::AttemptStatus { fn from(item: AuthorizedotnetPaymentStatus) -> Self { match item { - AuthorizedotnetPaymentStatus::Approved => Self::Charged, + AuthorizedotnetPaymentStatus::Approved => Self::Pending, AuthorizedotnetPaymentStatus::Declined | AuthorizedotnetPaymentStatus::Error => { Self::Failure } @@ -316,9 +342,9 @@ impl From for enums::AttemptStatus { } #[derive(Debug, Clone, Deserialize, PartialEq)] -struct ResponseMessage { +pub struct ResponseMessage { code: String, - text: String, + pub text: String, } #[derive(Debug, Clone, Deserialize, PartialEq)] @@ -331,14 +357,14 @@ enum ResultCode { #[serde(rename_all = "camelCase")] pub struct ResponseMessages { result_code: ResultCode, - message: Vec, + pub message: Vec, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub(super) struct ErrorMessage { - pub(super) error_code: String, - pub(super) error_text: String, +pub struct ErrorMessage { + pub error_code: String, + pub error_text: String, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] @@ -356,10 +382,51 @@ pub struct TransactionResponse { #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AuthorizedotnetPaymentsResponse { - pub transaction_response: TransactionResponse, + pub transaction_response: Option, pub messages: ResponseMessages, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizedotnetVoidResponse { + pub transaction_response: Option, + pub messages: ResponseMessages, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VoidResponse { + response_code: AuthorizedotnetVoidStatus, + auth_code: String, + #[serde(rename = "transId")] + transaction_id: String, + network_trans_id: Option, + pub account_number: Option, + pub errors: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub enum AuthorizedotnetVoidStatus { + #[serde(rename = "1")] + Approved, + #[serde(rename = "2")] + Declined, + #[serde(rename = "3")] + Error, + #[serde(rename = "4")] + HeldForReview, +} + +impl From for enums::AttemptStatus { + fn from(item: AuthorizedotnetVoidStatus) -> Self { + match item { + AuthorizedotnetVoidStatus::Approved => Self::VoidInitiated, + AuthorizedotnetVoidStatus::Declined | AuthorizedotnetVoidStatus::Error => Self::Failure, + AuthorizedotnetVoidStatus::HeldForReview => Self::Pending, + } + } +} + impl TryFrom< types::ResponseRouterData< @@ -379,50 +446,115 @@ impl types::PaymentsResponseData, >, ) -> Result { - let status = enums::AttemptStatus::from(item.response.transaction_response.response_code); - let error = item - .response - .transaction_response - .errors - .and_then(|errors| { - errors.into_iter().next().map(|error| types::ErrorResponse { - code: error.error_code, - message: error.error_text, - reason: None, - status_code: item.http_code, + match &item.response.transaction_response { + Some(transaction_response) => { + let status = enums::AttemptStatus::from(transaction_response.response_code.clone()); + let error = transaction_response.errors.as_ref().and_then(|errors| { + errors.iter().next().map(|error| types::ErrorResponse { + code: error.error_code.clone(), + message: error.error_text.clone(), + reason: None, + status_code: item.http_code, + }) + }); + let metadata = transaction_response + .account_number + .as_ref() + .map(|acc_no| { + Encode::<'_, PaymentDetails>::encode_to_value( + &construct_refund_payment_details(acc_no.clone()), + ) + }) + .transpose() + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "connector_metadata", + })?; + Ok(Self { + status, + response: match error { + Some(err) => Err(err), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + transaction_response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: metadata, + network_txn_id: transaction_response.network_trans_id.clone(), + }), + }, + ..item.data }) - }); + } + None => Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(get_err_response(item.http_code, item.response.messages)), + ..item.data + }), + } + } +} - let metadata = item - .response - .transaction_response - .account_number - .map(|acc_no| { - Encode::<'_, PaymentDetails>::encode_to_value(&construct_refund_payment_details( - acc_no, - )) - }) - .transpose() - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "connector_metadata", - })?; - - Ok(Self { - status, - response: match error { - Some(err) => Err(err), - None => Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction_response.transaction_id, - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: metadata, - network_txn_id: item.response.transaction_response.network_trans_id, - }), - }, - ..item.data - }) +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + AuthorizedotnetVoidResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + match &item.response.transaction_response { + Some(transaction_response) => { + let status = enums::AttemptStatus::from(transaction_response.response_code.clone()); + let error = transaction_response.errors.as_ref().and_then(|errors| { + errors.iter().next().map(|error| types::ErrorResponse { + code: error.error_code.clone(), + message: error.error_text.clone(), + reason: None, + status_code: item.http_code, + }) + }); + let metadata = transaction_response + .account_number + .as_ref() + .map(|acc_no| { + Encode::<'_, PaymentDetails>::encode_to_value( + &construct_refund_payment_details(acc_no.clone()), + ) + }) + .transpose() + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "connector_metadata", + })?; + Ok(Self { + status, + response: match error { + Some(err) => Err(err), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + transaction_response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: metadata, + network_txn_id: transaction_response.network_trans_id.clone(), + }), + }, + ..item.data + }) + } + None => Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(get_err_response(item.http_code, item.response.messages)), + ..item.data + }), + } } } @@ -571,22 +703,11 @@ impl TryFrom<&types::PaymentsSyncRouterData> for AuthorizedotnetCreateSyncReques type Error = error_stack::Report; fn try_from(item: &types::PaymentsSyncRouterData) -> Result { - let transaction_id = item - .response - .as_ref() - .ok() - .map(|payment_response_data| match payment_response_data { - types::PaymentsResponseData::TransactionResponse { resource_id, .. } => { - resource_id.get_connector_transaction_id() - } - _ => Err(error_stack::report!( - errors::ValidationError::MissingRequiredField { - field_name: "transaction_id".to_string() - } - )), - }) - .transpose() - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + let transaction_id = Some( + item.request + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + ); let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?; @@ -612,6 +733,8 @@ pub enum SyncStatus { Voided, CouldNotVoid, GeneralError, + #[serde(rename = "FDSPendingReview")] + FDSPendingReview, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -623,7 +746,8 @@ pub struct SyncTransactionResponse { #[derive(Debug, Deserialize)] pub struct AuthorizedotnetSyncResponse { - transaction: SyncTransactionResponse, + transaction: Option, + messages: ResponseMessages, } impl From for enums::RefundStatus { @@ -639,9 +763,9 @@ impl From for enums::RefundStatus { impl From for enums::AttemptStatus { fn from(transaction_status: SyncStatus) -> Self { match transaction_status { - SyncStatus::SettledSuccessfully | SyncStatus::CapturedPendingSettlement => { - Self::Charged - } + SyncStatus::SettledSuccessfully => Self::Charged, + SyncStatus::CapturedPendingSettlement => Self::CaptureInitiated, + SyncStatus::AuthorizedPendingCapture => Self::Authorized, SyncStatus::Declined => Self::AuthenticationFailed, SyncStatus::Voided => Self::Voided, SyncStatus::CouldNotVoid => Self::VoidFailed, @@ -659,14 +783,22 @@ impl TryFrom, ) -> Result { - let refund_status = enums::RefundStatus::from(item.response.transaction.transaction_status); - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.transaction.transaction_id.clone(), - refund_status, + match item.response.transaction { + Some(transaction) => { + let refund_status = enums::RefundStatus::from(transaction.transaction_status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: transaction.transaction_id, + refund_status, + }), + ..item.data + }) + } + None => Ok(Self { + response: Err(get_err_response(item.http_code, item.response.messages)), + ..item.data }), - ..item.data - }) + } } } @@ -685,21 +817,28 @@ impl types::PaymentsResponseData, >, ) -> Result { - let payment_status = - enums::AttemptStatus::from(item.response.transaction.transaction_status); - Ok(Self { - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction.transaction_id, - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, + match item.response.transaction { + Some(transaction) => { + let payment_status = enums::AttemptStatus::from(transaction.transaction_status); + Ok(Self { + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + transaction.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + }), + status: payment_status, + ..item.data + }) + } + None => Ok(Self { + response: Err(get_err_response(item.http_code, item.response.messages)), + ..item.data }), - status: payment_status, - ..item.data - }) + } } } @@ -724,3 +863,78 @@ fn construct_refund_payment_details(masked_number: String) -> PaymentDetails { card_code: None, }) } + +impl From> for TransactionType { + fn from(capture_method: Option) -> Self { + match capture_method { + Some(enums::CaptureMethod::Manual) => Self::Authorization, + _ => Self::Payment, + } + } +} + +fn get_err_response(status_code: u16, message: ResponseMessages) -> types::ErrorResponse { + types::ErrorResponse { + code: message.message[0].code.clone(), + message: message.message[0].text.clone(), + reason: None, + status_code, + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizedotnetWebhookObjectId { + pub webhook_id: String, + pub event_type: AuthorizedotnetWebhookEvent, + pub payload: AuthorizedotnetWebhookPayload, +} + +#[derive(Debug, Deserialize)] +pub struct AuthorizedotnetWebhookPayload { + pub id: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizedotnetWebhookEventType { + pub event_type: AuthorizedotnetWebhookEvent, +} + +#[derive(Debug, Deserialize)] +pub enum AuthorizedotnetWebhookEvent { + #[serde(rename = "net.authorize.payment.authorization.created")] + AuthorizationCreated, + #[serde(rename = "net.authorize.payment.priorAuthCapture.created")] + PriorAuthCapture, + #[serde(rename = "net.authorize.payment.authcapture.created")] + AuthCapCreated, + #[serde(rename = "net.authorize.payment.capture.created")] + CaptureCreated, + #[serde(rename = "net.authorize.payment.void.created")] + VoidCreated, + #[serde(rename = "net.authorize.payment.refund.created")] + RefundCreated, +} + +impl From for api::IncomingWebhookEvent { + fn from(event_type: AuthorizedotnetWebhookEvent) -> Self { + match event_type { + AuthorizedotnetWebhookEvent::AuthorizationCreated + | AuthorizedotnetWebhookEvent::PriorAuthCapture + | AuthorizedotnetWebhookEvent::AuthCapCreated + | AuthorizedotnetWebhookEvent::CaptureCreated + | AuthorizedotnetWebhookEvent::VoidCreated => Self::PaymentIntentSuccess, + AuthorizedotnetWebhookEvent::RefundCreated => Self::RefundSuccess, + } + } +} + +pub fn get_trans_id( + details: AuthorizedotnetWebhookObjectId, +) -> Result { + details + .payload + .id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound) +} diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index f209d1bdec..6892fc16f4 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -1,283 +1,543 @@ -use std::{marker::PhantomData, str::FromStr}; +use std::str::FromStr; use masking::Secret; -use router::{ - configs::settings::Settings, - connector::Authorizedotnet, - core::payments, - db::StorageImpl, - routes, services, - types::{self, storage::enums, PaymentAddress}, +use router::types::{self, api, storage::enums}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, }; -use tokio::sync::oneshot; -use crate::connector_auth::ConnectorAuthentication; - -fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { - let auth = ConnectorAuthentication::new() - .authorizedotnet - .expect("Missing Authorize.net connector authentication configuration"); - - types::RouterData { - flow: PhantomData, - merchant_id: String::from("authorizedotnet"), - customer_id: Some(String::from("authorizedotnet")), - connector: "authorizedotnet".to_string(), - payment_id: uuid::Uuid::new_v4().to_string(), - attempt_id: uuid::Uuid::new_v4().to_string(), - status: enums::AttemptStatus::default(), - payment_method: enums::PaymentMethod::Card, - connector_auth_type: auth.into(), - auth_type: enums::AuthenticationType::NoThreeDs, - description: Some("This is a test".to_string()), - return_url: None, - request: types::PaymentsAuthorizeData { - amount: 100, - currency: enums::Currency::USD, - payment_method_data: types::api::PaymentMethodData::Card(types::api::Card { - card_number: cards::CardNumber::from_str("5424000000000015").unwrap(), - card_exp_month: Secret::new("10".to_string()), - card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("999".to_string()), - card_issuer: None, - card_network: None, - }), - confirm: true, - statement_descriptor_suffix: None, - statement_descriptor: None, - setup_future_usage: None, - mandate_id: None, - off_session: None, - setup_mandate_details: None, - capture_method: None, - browser_info: None, - order_details: None, - email: None, - session_token: None, - enrolled_for_3ds: false, - related_transaction_id: None, - payment_experience: None, - payment_method_type: None, - router_return_url: None, - webhook_url: None, - complete_authorize_url: None, - }, - payment_method_id: None, - response: Err(types::ErrorResponse::default()), - address: PaymentAddress::default(), - connector_meta_data: None, - amount_captured: None, - access_token: None, - session_token: None, - reference_id: None, - payment_method_token: None, - connector_customer: None, - } -} - -fn construct_refund_router_data() -> types::RefundsRouterData { - let auth = ConnectorAuthentication::new() - .authorizedotnet - .expect("Missing Authorize.net connector authentication configuration"); - - types::RouterData { - flow: PhantomData, - connector_meta_data: None, - merchant_id: String::from("authorizedotnet"), - customer_id: Some(String::from("authorizedotnet")), - connector: "authorizedotnet".to_string(), - payment_id: uuid::Uuid::new_v4().to_string(), - attempt_id: uuid::Uuid::new_v4().to_string(), - status: enums::AttemptStatus::default(), - auth_type: enums::AuthenticationType::NoThreeDs, - payment_method: enums::PaymentMethod::Card, - connector_auth_type: auth.into(), - description: Some("This is a test".to_string()), - return_url: None, - request: router::types::RefundsData { - amount: 100, - currency: enums::Currency::USD, - refund_id: uuid::Uuid::new_v4().to_string(), - connector_transaction_id: String::new(), - refund_amount: 1, - webhook_url: None, - connector_metadata: None, - reason: None, - connector_refund_id: None, - }, - response: Err(types::ErrorResponse::default()), - payment_method_id: None, - address: PaymentAddress::default(), - amount_captured: None, - access_token: None, - session_token: None, - reference_id: None, - payment_method_token: None, - connector_customer: None, - } -} - -#[actix_web::test] -#[ignore] -async fn payments_create_success() { - let conf = Settings::new().unwrap(); - let tx: oneshot::Sender<()> = oneshot::channel().0; - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx).await; - static CV: Authorizedotnet = Authorizedotnet; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Authorizedotnet, - get_token: types::api::GetToken::Connector, - }; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let request = construct_payment_router_data(); - - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); - - println!("{response:?}"); - - assert!( - response.status == enums::AttemptStatus::Charged, - "The payment failed" - ); -} - -#[actix_web::test] -#[ignore] -async fn payments_create_failure() { - { - let conf = Settings::new().unwrap(); - static CV: Authorizedotnet = Authorizedotnet; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), +#[derive(Clone, Copy)] +struct AuthorizedotnetTest; +impl ConnectorActions for AuthorizedotnetTest {} +impl utils::Connector for AuthorizedotnetTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Authorizedotnet; + types::api::ConnectorData { + connector: Box::new(&Authorizedotnet), connector_name: types::Connector::Authorizedotnet, get_token: types::api::GetToken::Connector, - }; - let tx: oneshot::Sender<()> = oneshot::channel().0; - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx).await; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > = connector.connector.get_connector_integration(); - let mut request = construct_payment_router_data(); + } + } - request.request.payment_method_data = - types::api::PaymentMethodData::Card(types::api::Card { - card_number: cards::CardNumber::from_str("5424000000000015").unwrap(), - card_exp_month: Secret::new("10".to_string()), - card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), - card_cvc: Secret::new("999".to_string()), - card_issuer: None, - card_network: None, - }); + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .authorizedotnet + .expect("Missing connector authentication configuration"), + ) + } - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, + fn get_name(&self) -> String { + "authorizedotnet".to_string() + } +} +static CONNECTOR: AuthorizedotnetTest = AuthorizedotnetTest {}; + +fn get_payment_method_data() -> api::Card { + api::Card { + card_number: cards::CardNumber::from_str("5424000000000015").unwrap(), + card_exp_month: Secret::new("02".to_string()), + card_exp_year: Secret::new("2035".to_string()), + card_holder_name: Secret::new("John Doe".to_string()), + card_cvc: Secret::new("123".to_string()), + ..Default::default() + } +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + Some(types::PaymentsAuthorizeData { + amount: 300, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .expect("Authorize payment response"); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = + utils::get_connector_transaction_id(authorize_response.response).unwrap_or_default(); + let psync_response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + + assert_eq!(psync_response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + Some(types::PaymentsAuthorizeData { + amount: 301, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .expect("Authorize payment response"); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = + utils::get_connector_transaction_id(authorize_response.response).unwrap_or_default(); + let psync_response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.clone(), + ), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(psync_response.status, enums::AttemptStatus::Authorized); + let cap_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + amount_to_capture: 301, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + assert_eq!(cap_response.status, enums::AttemptStatus::Pending); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::CaptureInitiated, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + Some(types::PaymentsAuthorizeData { + amount: 302, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .expect("Authorize payment response"); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = + utils::get_connector_transaction_id(authorize_response.response).unwrap_or_default(); + let psync_response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.clone(), + ), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(psync_response.status, enums::AttemptStatus::Authorized); + let cap_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + amount_to_capture: 150, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + assert_eq!(cap_response.status, enums::AttemptStatus::Pending); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::CaptureInitiated, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + Some(types::PaymentsAuthorizeData { + amount: 303, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .expect("Authorize payment response"); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = + utils::get_connector_transaction_id(authorize_response.response).unwrap_or_default(); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS).x +#[actix_web::test] +async fn should_void_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + Some(types::PaymentsAuthorizeData { + amount: 304, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .expect("Authorize payment response"); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = + utils::get_connector_transaction_id(authorize_response.response).unwrap_or_default(); + let psync_response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.clone(), + ), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + + assert_eq!(psync_response.status, enums::AttemptStatus::Authorized); + let void_response = CONNECTOR + .void_payment( + txn_id, + Some(types::PaymentsCancelData { + amount: Some(304), + ..utils::PaymentCancelType::default().0 + }), + None, + ) + .await + .expect("Void response"); + assert_eq!(void_response.status, enums::AttemptStatus::VoidInitiated) +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let cap_response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + amount: 310, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, ) .await .unwrap(); - - println!("{response:?}"); - - assert!( - response.status == enums::AttemptStatus::Failure, - "The payment was intended to fail but it passed" - ); - } + assert_eq!(cap_response.status, enums::AttemptStatus::Pending); + let txn_id = utils::get_connector_transaction_id(cap_response.response).unwrap_or_default(); + let psync_response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::CaptureInitiated, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.clone(), + ), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!( + psync_response.status, + enums::AttemptStatus::CaptureInitiated + ); } +// Synchronizes a payment using the automatic capture flow (Non 3DS). #[actix_web::test] -#[ignore] -async fn refunds_create_success() { - let conf = Settings::new().unwrap(); - static CV: Authorizedotnet = Authorizedotnet; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Authorizedotnet, - get_token: types::api::GetToken::Connector, - }; - let tx: oneshot::Sender<()> = oneshot::channel().0; - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx).await; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Execute, - types::RefundsData, - types::RefundsResponseData, - > = connector.connector.get_connector_integration(); +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + amount: 311, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Pending, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + ..Default::default() + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated); +} - let mut request = construct_refund_router_data(); - request.request.connector_transaction_id = "abfbc35c-4825-4dd4-ab2d-fae0acc22389".to_string(); +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + "60217566768".to_string(), + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); +// Creates a payment with empty card number. +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("").unwrap(), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + "The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.", + ); +} - println!("{response:?}"); +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardCode' element is invalid - The value XXXXXXX is invalid according to its datatype 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardCode' - The actual length is greater than the MaxLength value.".to_string(), + ); +} +// todo() - assert!( - response.response.unwrap().refund_status == enums::RefundStatus::Success, - "The refund transaction failed" +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Credit card expiration date is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "The credit card has expired.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + amount: 307, + payment_method_data: types::api::PaymentMethodData::Card(get_payment_method_data()), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, None) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:amount' element is invalid - The value '' is invalid according to its datatype 'http://www.w3.org/2001/XMLSchema:decimal' - The string '' is not a valid Decimal value." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, None) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + "The transaction cannot be found." ); } #[actix_web::test] -async fn refunds_create_failure() { - let conf = Settings::new().unwrap(); - static CV: Authorizedotnet = Authorizedotnet; - let connector = types::api::ConnectorData { - connector: Box::new(&CV), - connector_name: types::Connector::Authorizedotnet, - get_token: types::api::GetToken::Connector, - }; - let tx: oneshot::Sender<()> = oneshot::channel().0; - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx).await; - let connector_integration: services::BoxedConnectorIntegration< - '_, - types::api::Execute, - types::RefundsData, - types::RefundsResponseData, - > = connector.connector.get_connector_integration(); +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_partially_refund_manually_captured_payment() {} - let mut request = construct_refund_router_data(); - request.request.connector_transaction_id = "1234".to_string(); +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_refund_manually_captured_payment() {} - let response = services::api::execute_connector_processing_step( - &state, - connector_integration, - &request, - payments::CallConnectorAction::Trigger, - ) - .await - .unwrap(); +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_sync_manually_captured_refund() {} - println!("{response:?}"); +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_refund_auto_captured_payment() {} - assert!( - response.response.unwrap().refund_status == enums::RefundStatus::Failure, - "The test was intended to fail but it passed" - ); -} +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_partially_refund_succeeded_payment() {} + +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_refund_succeeded_payment_multiple_times() {} + +#[actix_web::test] +#[ignore = "refunds tests are ignored for this connector becuase it takes one day for a payment to be settled."] +async fn should_fail_for_refund_amount_higher_than_payment_amount() {} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests