From ed82af81f9316c266adc7ee8273b0e33e1c83ccf Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Mon, 27 May 2024 22:27:07 +0530 Subject: [PATCH] feat(connector): [AUTHORIZEDOTNET] Implement non-zero mandates (#4758) --- .../router/src/connector/authorizedotnet.rs | 10 + .../connector/authorizedotnet/transformers.rs | 422 +++++++++++++----- 2 files changed, 317 insertions(+), 115 deletions(-) diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index a9e000b82b99..8d0c1db37a22 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -79,6 +79,16 @@ impl ConnectorValidation for Authorizedotnet { ), } } + + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: types::domain::payments::PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = + std::collections::HashSet::from([crate::connector::utils::PaymentMethodDataType::Card]); + connector_utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id()) + } } impl api::Payment for Authorizedotnet {} diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 3e4d0a382e61..de68438edc46 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -133,20 +133,36 @@ pub enum WalletMethod { struct TransactionRequest { transaction_type: TransactionType, amount: f64, - currency_code: String, - #[serde(skip_serializing_if = "Option::is_none")] - profile: Option, + currency_code: common_enums::Currency, #[serde(skip_serializing_if = "Option::is_none")] payment: Option, + #[serde(skip_serializing_if = "Option::is_none")] + profile: Option, order: Order, #[serde(skip_serializing_if = "Option::is_none")] + customer: Option, + #[serde(skip_serializing_if = "Option::is_none")] bill_to: Option, + #[serde(skip_serializing_if = "Option::is_none")] processing_options: Option, #[serde(skip_serializing_if = "Option::is_none")] subsequent_auth_information: Option, authorization_indicator_type: Option, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +enum ProfileDetails { + CreateProfileDetails(CreateProfileDetails), + CustomerProfileDetails(CustomerProfileDetails), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CreateProfileDetails { + create_profile: bool, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct CustomerProfileDetails { @@ -160,6 +176,12 @@ struct PaymentProfileDetails { payment_profile_id: Secret, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomerDetails { + id: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingOptions { @@ -461,37 +483,37 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - let (payment_details, processing_options, subsequent_auth_information, profile) = match item + let merchant_authentication = + AuthorizedotnetAuthType::try_from(&item.router_data.connector_auth_type)?; + + let ref_id = if item.router_data.connector_request_reference_id.len() <= 20 { + Some(item.router_data.connector_request_reference_id.clone()) + } else { + None + }; + + let transaction_request = match item .router_data .request .mandate_id - .to_owned() + .clone() .and_then(|mandate_ids| mandate_ids.mandate_reference_id) { Some(api_models::payments::MandateReferenceId::NetworkMandateId(network_trans_id)) => { - let processing_options = Some(ProcessingOptions { - is_subsequent_auth: true, - }); - let subsequent_auth_info = Some(SubsequentAuthInformation { - original_network_trans_id: Secret::new(network_trans_id), - reason: Reason::Resubmission, - }); - match item.router_data.request.payment_method_data { - domain::PaymentMethodData::Card(ref ccard) => { - let payment_details = PaymentDetails::CreditCard(CreditCardDetails { - card_number: (*ccard.card_number).clone(), - expiration_date: ccard.get_expiry_date_as_yyyymm("-"), - card_code: None, - }); - ( - Some(payment_details), - processing_options, - subsequent_auth_info, - None, - ) + TransactionRequest::try_from((item, network_trans_id))? + } + Some(api_models::payments::MandateReferenceId::ConnectorMandateId( + connector_mandate_id, + )) => TransactionRequest::try_from((item, connector_mandate_id))?, + None => { + match &item.router_data.request.payment_method_data { + domain::PaymentMethodData::Card(ccard) => { + TransactionRequest::try_from((item, ccard)) + } + domain::PaymentMethodData::Wallet(wallet_data) => { + TransactionRequest::try_from((item, wallet_data)) } domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::Wallet(_) | domain::PaymentMethodData::PayLater(_) | domain::PaymentMethodData::BankRedirect(_) | domain::PaymentMethodData::BankDebit(_) @@ -510,16 +532,116 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>> ))? } } - } - Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - connector_mandate_id, - )) => ( - None, - Some(ProcessingOptions { - is_subsequent_auth: true, + }?, + }; + Ok(Self { + create_transaction_request: AuthorizedotnetPaymentsRequest { + merchant_authentication, + ref_id, + transaction_request, + }, + }) + } +} + +impl + TryFrom<( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + String, + )> for TransactionRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, network_trans_id): ( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + String, + ), + ) -> Result { + Ok(Self { + transaction_type: TransactionType::try_from(item.router_data.request.capture_method)?, + amount: item.amount, + currency_code: item.router_data.request.currency, + payment: Some(match item.router_data.request.payment_method_data { + domain::PaymentMethodData::Card(ref ccard) => { + PaymentDetails::CreditCard(CreditCardDetails { + card_number: (*ccard.card_number).clone(), + expiration_date: ccard.get_expiry_date_as_yyyymm("-"), + card_code: None, + }) + } + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("authorizedotnet"), + ))? + } + }), + profile: None, + order: Order { + description: item.router_data.connector_request_reference_id.clone(), + }, + customer: None, + bill_to: item + .router_data + .get_optional_billing() + .and_then(|billing_address| billing_address.address.as_ref()) + .map(|address| BillTo { + first_name: address.first_name.clone(), + last_name: address.last_name.clone(), + address: address.line1.clone(), + city: address.city.clone(), + state: address.state.clone(), + zip: address.zip.clone(), + country: address.country, + }), + processing_options: Some(ProcessingOptions { + is_subsequent_auth: true, + }), + subsequent_auth_information: Some(SubsequentAuthInformation { + original_network_trans_id: Secret::new(network_trans_id), + reason: Reason::Resubmission, + }), + authorization_indicator_type: match item.router_data.request.capture_method { + Some(capture_method) => Some(AuthorizationIndicator { + authorization_indicator: capture_method.try_into()?, }), - None, - Some(CustomerProfileDetails { + None => None, + }, + }) + } +} + +impl + TryFrom<( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + api_models::payments::ConnectorMandateReferenceId, + )> for TransactionRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, connector_mandate_id): ( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + api_models::payments::ConnectorMandateReferenceId, + ), + ) -> Result { + Ok(Self { + transaction_type: TransactionType::try_from(item.router_data.request.capture_method)?, + amount: item.amount, + currency_code: item.router_data.request.currency, + payment: None, + profile: Some(ProfileDetails::CustomerProfileDetails( + CustomerProfileDetails { customer_profile_id: Secret::from( connector_mandate_id .connector_mandate_id @@ -532,64 +654,86 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>> .ok_or(errors::ConnectorError::MissingConnectorMandateID)?, ), }, - }), - ), - None => { - match item.router_data.request.payment_method_data { - domain::PaymentMethodData::Card(ref ccard) => { - ( - Some(PaymentDetails::CreditCard(CreditCardDetails { - card_number: (*ccard.card_number).clone(), - // expiration_date: format!("{expiry_year}-{expiry_month}").into(), - expiration_date: ccard.get_expiry_date_as_yyyymm("-"), - card_code: Some(ccard.card_cvc.clone()), - })), - Some(ProcessingOptions { - is_subsequent_auth: true, - }), - None, - None, - ) - } - domain::PaymentMethodData::Wallet(ref wallet_data) => ( - Some(get_wallet_data( - wallet_data, - &item.router_data.request.complete_authorize_url, - )?), - None, - None, - None, - ), - domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::PayLater(_) - | domain::PaymentMethodData::BankRedirect(_) - | domain::PaymentMethodData::BankDebit(_) - | domain::PaymentMethodData::BankTransfer(_) - | domain::PaymentMethodData::Crypto(_) - | domain::PaymentMethodData::MandatePayment - | domain::PaymentMethodData::Reward - | domain::PaymentMethodData::Upi(_) - | domain::PaymentMethodData::Voucher(_) - | domain::PaymentMethodData::GiftCard(_) - | domain::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message( - "authorizedotnet", - ), - ))? - } - } - } - }; - let authorization_indicator_type = match item.router_data.request.capture_method { - Some(capture_method) => Some(AuthorizationIndicator { - authorization_indicator: capture_method.try_into()?, + }, + )), + order: Order { + description: item.router_data.connector_request_reference_id.clone(), + }, + customer: None, + bill_to: None, + processing_options: Some(ProcessingOptions { + is_subsequent_auth: true, }), - None => None, + subsequent_auth_information: None, + authorization_indicator_type: match item.router_data.request.capture_method { + Some(capture_method) => Some(AuthorizationIndicator { + authorization_indicator: capture_method.try_into()?, + }), + None => None, + }, + }) + } +} + +impl + TryFrom<( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::Card, + )> for TransactionRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::Card, + ), + ) -> Result { + let (profile, customer) = if item + .router_data + .request + .setup_future_usage + .map_or(false, |future_usage| { + matches!(future_usage, common_enums::FutureUsage::OffSession) + }) + && (item.router_data.request.customer_acceptance.is_some() + || item + .router_data + .request + .setup_mandate_details + .clone() + .map_or(false, |mandate_details| { + mandate_details.customer_acceptance.is_some() + })) { + ( + Some(ProfileDetails::CreateProfileDetails(CreateProfileDetails { + create_profile: true, + })), + Some(CustomerDetails { + id: item + .router_data + .customer_id + .clone() + .ok_or_else(missing_field_err("customer_id"))?, + }), + ) + } else { + (None, None) }; - let bill_to = match profile { - Some(_) => None, - None => item + Ok(Self { + transaction_type: TransactionType::try_from(item.router_data.request.capture_method)?, + amount: item.amount, + currency_code: item.router_data.request.currency, + payment: Some(PaymentDetails::CreditCard(CreditCardDetails { + card_number: (*ccard.card_number).clone(), + expiration_date: ccard.get_expiry_date_as_yyyymm("-"), + card_code: Some(ccard.card_cvc.clone()), + })), + profile, + order: Order { + description: item.router_data.connector_request_reference_id.clone(), + }, + customer, + bill_to: item .router_data .get_optional_billing() .and_then(|billing_address| billing_address.address.as_ref()) @@ -602,34 +746,64 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>> zip: address.zip.clone(), country: address.country, }), - }; - let transaction_request = TransactionRequest { + processing_options: None, + subsequent_auth_information: None, + authorization_indicator_type: match item.router_data.request.capture_method { + Some(capture_method) => Some(AuthorizationIndicator { + authorization_indicator: capture_method.try_into()?, + }), + None => None, + }, + }) + } +} + +impl + TryFrom<( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::WalletData, + )> for TransactionRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, wallet_data): ( + &AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::WalletData, + ), + ) -> Result { + Ok(Self { transaction_type: TransactionType::try_from(item.router_data.request.capture_method)?, amount: item.amount, - currency_code: item.router_data.request.currency.to_string(), - profile, - payment: payment_details, + currency_code: item.router_data.request.currency, + payment: Some(get_wallet_data( + wallet_data, + &item.router_data.request.complete_authorize_url, + )?), + profile: None, order: Order { description: item.router_data.connector_request_reference_id.clone(), }, - bill_to, - processing_options, - subsequent_auth_information, - authorization_indicator_type, - }; - - let merchant_authentication = - AuthorizedotnetAuthType::try_from(&item.router_data.connector_auth_type)?; - let ref_id = if item.router_data.connector_request_reference_id.len() <= 20 { - Some(item.router_data.connector_request_reference_id.clone()) - } else { - None - }; - Ok(Self { - create_transaction_request: AuthorizedotnetPaymentsRequest { - merchant_authentication, - ref_id, - transaction_request, + customer: None, + bill_to: item + .router_data + .get_optional_billing() + .and_then(|billing_address| billing_address.address.as_ref()) + .map(|address| BillTo { + first_name: address.first_name.clone(), + last_name: address.last_name.clone(), + address: address.line1.clone(), + city: address.city.clone(), + state: address.state.clone(), + zip: address.zip.clone(), + country: address.country, + }), + processing_options: None, + subsequent_auth_information: None, + authorization_indicator_type: match item.router_data.request.capture_method { + Some(capture_method) => Some(AuthorizationIndicator { + authorization_indicator: capture_method.try_into()?, + }), + None => None, }, }) } @@ -804,6 +978,15 @@ pub struct SecureAcceptance { #[serde(rename_all = "camelCase")] pub struct AuthorizedotnetPaymentsResponse { pub transaction_response: Option, + pub profile_response: Option, + pub messages: ResponseMessages, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizedotnetNonZeroMandateResponse { + customer_profile_id: Option, + customer_payment_profile_id_list: Option>, pub messages: ResponseMessages, } @@ -913,7 +1096,16 @@ impl transaction_response.transaction_id.clone(), ), redirection_data, - mandate_reference: None, + mandate_reference: item.response.profile_response.map( + |profile_response| types::MandateReference { + connector_mandate_id: profile_response.customer_profile_id, + payment_method_id: profile_response + .customer_payment_profile_id_list + .and_then(|customer_payment_profile_id_list| { + customer_payment_profile_id_list.first().cloned() + }), + }, + ), connector_metadata: metadata, network_txn_id: transaction_response .network_trans_id