From 15c2a70b427df1c7ec719c2e738f83be1b6a5662 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:11:17 +0530 Subject: [PATCH] feat(connector): [ACI] implement Card Mandates for ACI (#1174) --- crates/router/src/connector/aci.rs | 12 +- .../router/src/connector/aci/transformers.rs | 569 ++++++++++++------ crates/router/src/core/errors.rs | 2 + 3 files changed, 407 insertions(+), 176 deletions(-) diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index c956a8e0ae..b6e37ca78e 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -5,6 +5,7 @@ use std::fmt::Debug; use error_stack::{IntoReport, ResultExt}; use transformers as aci; +use super::utils::PaymentsAuthorizeRequestData; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -245,10 +246,17 @@ impl fn get_url( &self, - _req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!("{}{}", self.base_url(connectors), "v1/payments")) + match req.request.connector_mandate_id() { + Some(mandate_id) => Ok(format!( + "{}v1/registrations/{}/payments", + self.base_url(connectors), + mandate_id + )), + _ => Ok(format!("{}{}", self.base_url(connectors), "v1/payments")), + } } fn get_request_body( diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 69fc8f47a0..2666b5c7b3 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use api_models::enums::BankNames; use common_utils::pii::Email; use error_stack::report; use masking::Secret; @@ -8,12 +9,14 @@ use serde::{Deserialize, Serialize}; use super::result_codes::{FAILURE_CODES, PENDING_CODES, SUCCESSFUL_CODES}; use crate::{ - connector::utils, + connector::utils::{self, RouterData}, core::errors, services, types::{self, api, storage::enums}, }; +type Error = error_stack::Report; + pub struct AciAuthType { pub api_key: String, pub entity_id: String, @@ -36,12 +39,22 @@ impl TryFrom<&types::ConnectorAuthType> for AciAuthType { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct AciPaymentsRequest { + #[serde(flatten)] + pub txn_details: TransactionDetails, + #[serde(flatten)] + pub payment_method: PaymentDetails, + #[serde(flatten)] + pub instruction: Option, + pub shopper_result_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionDetails { pub entity_id: String, pub amount: String, pub currency: String, pub payment_type: AciPaymentType, - #[serde(flatten)] - pub payment_method: PaymentDetails, } #[derive(Debug, Serialize)] @@ -59,6 +72,160 @@ pub enum PaymentDetails { BankRedirect(Box), Wallet(Box), Klarna, + Mandate, +} + +impl TryFrom<&api_models::payments::WalletData> for PaymentDetails { + type Error = Error; + fn try_from(wallet_data: &api_models::payments::WalletData) -> Result { + let payment_data = match wallet_data { + api_models::payments::WalletData::MbWayRedirect(data) => { + Self::Wallet(Box::new(WalletPMData { + payment_brand: PaymentBrand::Mbway, + account_id: Some(data.telephone_number.clone()), + })) + } + api_models::payments::WalletData::AliPayRedirect { .. } => { + Self::Wallet(Box::new(WalletPMData { + payment_brand: PaymentBrand::AliPay, + account_id: None, + })) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment method".to_string(), + ))?, + }; + Ok(payment_data) + } +} + +impl + TryFrom<( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::BankRedirectData, + )> for PaymentDetails +{ + type Error = Error; + fn try_from( + value: ( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::BankRedirectData, + ), + ) -> Result { + let (item, bank_redirect_data) = value; + let payment_data = match bank_redirect_data { + api_models::payments::BankRedirectData::Eps { .. } => { + Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Eps, + bank_account_country: Some(api_models::enums::CountryAlpha2::AT), + bank_account_bank_name: None, + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: None, + })) + } + api_models::payments::BankRedirectData::Giropay { + bank_account_bic, + bank_account_iban, + .. + } => Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Giropay, + bank_account_country: Some(api_models::enums::CountryAlpha2::DE), + bank_account_bank_name: None, + bank_account_bic: bank_account_bic.clone(), + bank_account_iban: bank_account_iban.clone(), + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: None, + })), + api_models::payments::BankRedirectData::Ideal { bank_name, .. } => { + Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Ideal, + bank_account_country: Some(api_models::enums::CountryAlpha2::NL), + bank_account_bank_name: bank_name.to_owned(), + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: None, + })) + } + api_models::payments::BankRedirectData::Sofort { country, .. } => { + Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Sofortueberweisung, + bank_account_country: Some(country.to_owned()), + bank_account_bank_name: None, + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: None, + })) + } + api_models::payments::BankRedirectData::Przelewy24 { + billing_details, .. + } => Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Przelewy, + bank_account_country: None, + bank_account_bank_name: None, + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: billing_details.email.to_owned(), + })), + api_models::payments::BankRedirectData::Interac { email, country } => { + Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::InteracOnline, + bank_account_country: Some(country.to_owned()), + bank_account_bank_name: None, + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: Some(email.to_owned()), + })) + } + api_models::payments::BankRedirectData::Trustly { country } => { + Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Trustly, + bank_account_country: None, + bank_account_bank_name: None, + bank_account_bic: None, + bank_account_iban: None, + billing_country: Some(country.to_owned()), + merchant_customer_id: Some(Secret::new(item.get_customer_id()?)), + merchant_transaction_id: Some(Secret::new(item.payment_id.clone())), + customer_email: None, + })) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment method".to_string(), + ))?, + }; + Ok(payment_data) + } +} + +impl TryFrom for PaymentDetails { + type Error = Error; + fn try_from(card_data: api_models::payments::Card) -> Result { + Ok(Self::AciCard(Box::new(CardDetails { + card_number: card_data.card_number, + card_holder: card_data.card_holder_name, + card_expiry_month: card_data.card_exp_month, + card_expiry_year: card_data.card_exp_year, + card_cvv: card_data.card_cvc, + }))) + } } #[derive(Debug, Clone, Serialize)] @@ -68,7 +235,7 @@ pub struct BankRedirectionPMData { #[serde(rename = "bankAccount.country")] bank_account_country: Option, #[serde(rename = "bankAccount.bankName")] - bank_account_bank_name: Option, + bank_account_bank_name: Option, #[serde(rename = "bankAccount.bic")] bank_account_bic: Option>, #[serde(rename = "bankAccount.iban")] @@ -80,7 +247,6 @@ pub struct BankRedirectionPMData { #[serde(rename = "customer.merchantCustomerId")] merchant_customer_id: Option>, merchant_transaction_id: Option>, - shopper_result_url: Option, } #[derive(Debug, Clone, Serialize)] @@ -89,7 +255,6 @@ pub struct WalletPMData { payment_brand: PaymentBrand, #[serde(rename = "virtualAccount.accountId")] account_id: Option>, - shopper_result_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -121,6 +286,43 @@ pub struct CardDetails { pub card_cvv: Secret, } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum InstructionMode { + Initial, + Repeated, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum InstructionType { + Unscheduled, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum InstructionSource { + // Cardholder initiated transaction + Cit, + // Merchant initiated transaction + Mit, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Instruction { + #[serde(rename = "standingInstruction.mode")] + mode: InstructionMode, + + #[serde(rename = "standingInstruction.type")] + transaction_type: InstructionType, + + #[serde(rename = "standingInstruction.source")] + source: InstructionSource, + + create_registration: Option, +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub struct BankDetails { #[serde(rename = "bankAccount.holder")] @@ -128,7 +330,7 @@ pub struct BankDetails { } #[allow(dead_code)] -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum AciPaymentType { #[serde(rename = "PA")] Preauthorization, @@ -148,179 +350,189 @@ pub enum AciPaymentType { impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let payment_details: PaymentDetails = match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => PaymentDetails::AciCard(Box::new(CardDetails { - card_number: ccard.card_number, - card_holder: ccard.card_holder_name, - card_expiry_month: ccard.card_exp_month, - card_expiry_year: ccard.card_exp_year, - card_cvv: ccard.card_cvc, - })), - api::PaymentMethodData::PayLater(_) => PaymentDetails::Klarna, - api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { - api_models::payments::WalletData::MbWayRedirect(data) => { - PaymentDetails::Wallet(Box::new(WalletPMData { - payment_brand: PaymentBrand::Mbway, - account_id: Some(data.telephone_number.clone()), - shopper_result_url: item.request.router_return_url.clone(), - })) - } - api_models::payments::WalletData::AliPayRedirect { .. } => { - PaymentDetails::Wallet(Box::new(WalletPMData { - payment_brand: PaymentBrand::AliPay, - account_id: None, - shopper_result_url: item.request.router_return_url.clone(), - })) - } - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), - ))?, - }, - api::PaymentMethodData::BankRedirect(ref redirect_banking_data) => { - match redirect_banking_data { - api_models::payments::BankRedirectData::Eps { .. } => { - PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Eps, - bank_account_country: Some(api_models::enums::CountryAlpha2::AT), - bank_account_bank_name: None, - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: None, - shopper_result_url: item.request.router_return_url.clone(), - })) - } - api_models::payments::BankRedirectData::Giropay { - bank_account_bic, - bank_account_iban, - .. - } => PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Giropay, - bank_account_country: Some(api_models::enums::CountryAlpha2::DE), - bank_account_bank_name: None, - bank_account_bic: bank_account_bic.clone(), - bank_account_iban: bank_account_iban.clone(), - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: None, - shopper_result_url: item.request.router_return_url.clone(), - })), - api_models::payments::BankRedirectData::Ideal { bank_name, .. } => { - PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Ideal, - bank_account_country: Some(api_models::enums::CountryAlpha2::NL), - bank_account_bank_name: bank_name - .map(|bank_name| bank_name.to_string()), - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: None, - shopper_result_url: item.request.router_return_url.clone(), - })) - } - api_models::payments::BankRedirectData::Sofort { country, .. } => { - PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Sofortueberweisung, - bank_account_country: Some(*country), - bank_account_bank_name: None, - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: None, - shopper_result_url: item.request.router_return_url.clone(), - })) - } - - api_models::payments::BankRedirectData::Przelewy24 { - billing_details, .. - } => PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Przelewy, - bank_account_country: None, - bank_account_bank_name: None, - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: billing_details.email.clone(), - shopper_result_url: item.request.router_return_url.clone(), - })), - - api_models::payments::BankRedirectData::Interac { email, country } => { - PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::InteracOnline, - bank_account_country: Some(*country), - bank_account_bank_name: None, - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: Some(email.to_owned()), - shopper_result_url: item.request.router_return_url.clone(), - })) - } - - api_models::payments::BankRedirectData::Trustly { country } => { - PaymentDetails::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Trustly, - bank_account_country: None, - bank_account_bank_name: None, - bank_account_bic: None, - bank_account_iban: None, - billing_country: Some(*country), - merchant_customer_id: Some(Secret::new( - item.customer_id.clone().ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "customer_id", - }, - )?, - )), - merchant_transaction_id: Some(Secret::new(item.payment_id.clone())), - customer_email: None, - shopper_result_url: item.request.router_return_url.clone(), - })) - } - - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), - ))?, - } + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ref card_data) => Self::try_from((item, card_data)), + api::PaymentMethodData::Wallet(ref wallet_data) => Self::try_from((item, wallet_data)), + api::PaymentMethodData::PayLater(ref pay_later_data) => { + Self::try_from((item, pay_later_data)) + } + api::PaymentMethodData::BankRedirect(ref bank_redirect_data) => { + Self::try_from((item, bank_redirect_data)) + } + api::PaymentMethodData::MandatePayment => { + let mandate_id = item.request.mandate_id.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "mandate_id", + }, + )?; + Self::try_from((item, mandate_id)) } - api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) | api::PaymentMethodData::BankTransfer(_) - | api::PaymentMethodData::Reward(_) - | api::PaymentMethodData::MandatePayment => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.payment_method), - connector: "Aci", - payment_experience: api_models::enums::PaymentExperience::RedirectToUrl - .to_string(), - })? - } - }; - - let auth = AciAuthType::try_from(&item.connector_auth_type)?; - let aci_payment_request = Self { - payment_method: payment_details, - entity_id: auth.entity_id, - amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, - currency: item.request.currency.to_string(), - payment_type: AciPaymentType::Debit, - }; - Ok(aci_payment_request) + | api::PaymentMethodData::Reward(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.payment_method), + connector: "Aci", + payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(), + })?, + } } } +impl + TryFrom<( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::WalletData, + )> for AciPaymentsRequest +{ + type Error = Error; + fn try_from( + value: ( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::WalletData, + ), + ) -> Result { + let (item, wallet_data) = value; + let txn_details = get_transaction_details(item)?; + let payment_method = PaymentDetails::try_from(wallet_data)?; + + Ok(Self { + txn_details, + payment_method, + instruction: None, + shopper_result_url: item.request.router_return_url.clone(), + }) + } +} + +impl + TryFrom<( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::BankRedirectData, + )> for AciPaymentsRequest +{ + type Error = Error; + fn try_from( + value: ( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::BankRedirectData, + ), + ) -> Result { + let (item, bank_redirect_data) = value; + let txn_details = get_transaction_details(item)?; + let payment_method = PaymentDetails::try_from((item, bank_redirect_data))?; + + Ok(Self { + txn_details, + payment_method, + instruction: None, + shopper_result_url: item.request.router_return_url.clone(), + }) + } +} + +impl + TryFrom<( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::PayLaterData, + )> for AciPaymentsRequest +{ + type Error = Error; + fn try_from( + value: ( + &types::PaymentsAuthorizeRouterData, + &api_models::payments::PayLaterData, + ), + ) -> Result { + let (item, _pay_later_data) = value; + let txn_details = get_transaction_details(item)?; + let payment_method = PaymentDetails::Klarna; + + Ok(Self { + txn_details, + payment_method, + instruction: None, + shopper_result_url: item.request.router_return_url.clone(), + }) + } +} + +impl TryFrom<(&types::PaymentsAuthorizeRouterData, &api::Card)> for AciPaymentsRequest { + type Error = Error; + fn try_from( + value: (&types::PaymentsAuthorizeRouterData, &api::Card), + ) -> Result { + let (item, card_data) = value; + let txn_details = get_transaction_details(item)?; + let payment_method = PaymentDetails::try_from(card_data.clone())?; + let instruction = get_instruction_details(item); + + Ok(Self { + txn_details, + payment_method, + instruction, + shopper_result_url: None, + }) + } +} + +impl + TryFrom<( + &types::PaymentsAuthorizeRouterData, + api_models::payments::MandateIds, + )> for AciPaymentsRequest +{ + type Error = Error; + fn try_from( + value: ( + &types::PaymentsAuthorizeRouterData, + api_models::payments::MandateIds, + ), + ) -> Result { + let (item, _mandate_data) = value; + let instruction = get_instruction_details(item); + let txn_details = get_transaction_details(item)?; + + Ok(Self { + txn_details, + payment_method: PaymentDetails::Mandate, + instruction, + shopper_result_url: item.request.router_return_url.clone(), + }) + } +} + +fn get_transaction_details( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + let auth = AciAuthType::try_from(&item.connector_auth_type)?; + Ok(TransactionDetails { + entity_id: auth.entity_id, + amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + currency: item.request.currency.to_string(), + payment_type: AciPaymentType::Debit, + }) +} + +fn get_instruction_details(item: &types::PaymentsAuthorizeRouterData) -> Option { + if item.request.setup_mandate_details.is_some() { + return Some(Instruction { + mode: InstructionMode::Initial, + transaction_type: InstructionType::Unscheduled, + source: InstructionSource::Cit, + create_registration: Some(true), + }); + } else if item.request.mandate_id.is_some() { + return Some(Instruction { + mode: InstructionMode::Repeated, + transaction_type: InstructionType::Unscheduled, + source: InstructionSource::Mit, + create_registration: None, + }); + } + None +} + impl TryFrom<&types::PaymentsCancelRouterData> for AciCancelRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCancelRouterData) -> Result { @@ -374,6 +586,7 @@ impl FromStr for AciPaymentStatus { #[serde(rename_all = "camelCase")] pub struct AciPaymentsResponse { id: String, + registration_id: Option, // ndc is an internal unique identifier for the request. ndc: String, timestamp: String, @@ -437,6 +650,14 @@ impl } }); + let mandate_reference = item + .response + .registration_id + .map(|id| types::MandateReference { + connector_mandate_id: Some(id), + payment_method_id: None, + }); + Ok(Self { status: { if redirection_data.is_some() { @@ -450,7 +671,7 @@ impl response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), redirection_data, - mandate_reference: None, + mandate_reference, connector_metadata: None, network_txn_id: None, }), diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index ffebb823f0..d41fe74a4b 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -267,6 +267,8 @@ pub enum ConnectorError { FlowNotSupported { flow: String, connector: String }, #[error("Capture method not supported")] CaptureMethodNotSupported, + #[error("Missing connector mandate ID")] + MissingConnectorMandateID, #[error("Missing connector transaction ID")] MissingConnectorTransactionID, #[error("Missing connector refund ID")]