feat(connector): [ACI] implement Card Mandates for ACI (#1174)

This commit is contained in:
Sakil Mostak
2023-06-30 13:11:17 +05:30
committed by GitHub
parent cd4dbcb3f6
commit 15c2a70b42
3 changed files with 407 additions and 176 deletions

View File

@ -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<String, errors::ConnectorError> {
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(

View File

@ -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<errors::ConnectorError>;
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<Instruction>,
pub shopper_result_url: Option<String>,
}
#[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<BankRedirectionPMData>),
Wallet(Box<WalletPMData>),
Klarna,
Mandate,
}
impl TryFrom<&api_models::payments::WalletData> for PaymentDetails {
type Error = Error;
fn try_from(wallet_data: &api_models::payments::WalletData) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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<api_models::payments::Card> for PaymentDetails {
type Error = Error;
fn try_from(card_data: api_models::payments::Card) -> Result<Self, Self::Error> {
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<api_models::enums::CountryAlpha2>,
#[serde(rename = "bankAccount.bankName")]
bank_account_bank_name: Option<String>,
bank_account_bank_name: Option<BankNames>,
#[serde(rename = "bankAccount.bic")]
bank_account_bic: Option<Secret<String>>,
#[serde(rename = "bankAccount.iban")]
@ -80,7 +247,6 @@ pub struct BankRedirectionPMData {
#[serde(rename = "customer.merchantCustomerId")]
merchant_customer_id: Option<Secret<String>>,
merchant_transaction_id: Option<Secret<String>>,
shopper_result_url: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
@ -89,7 +255,6 @@ pub struct WalletPMData {
payment_brand: PaymentBrand,
#[serde(rename = "virtualAccount.accountId")]
account_id: Option<Secret<String>>,
shopper_result_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -121,6 +286,43 @@ pub struct CardDetails {
pub card_cvv: Secret<String>,
}
#[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<bool>,
}
#[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<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<TransactionDetails, error_stack::Report<errors::ConnectorError>> {
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<Instruction> {
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<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
@ -374,6 +586,7 @@ impl FromStr for AciPaymentStatus {
#[serde(rename_all = "camelCase")]
pub struct AciPaymentsResponse {
id: String,
registration_id: Option<String>,
// ndc is an internal unique identifier for the request.
ndc: String,
timestamp: String,
@ -437,6 +650,14 @@ impl<F, T>
}
});
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<F, T>
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,
}),

View File

@ -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")]