feat(connector): enhance ACI connector with comprehensive 3DS support - DRAFT (#8986)

Co-authored-by: Ben Janecke <ben.janecke@peachpayments.com>
Co-authored-by: Sweta-Kumari-Sharma <ss6175983@gmail.com>
This commit is contained in:
Ben Janecke
2025-09-10 14:18:42 +02:00
committed by GitHub
parent c0e31d38ff
commit b014b1387a
10 changed files with 998 additions and 473 deletions

View File

@ -541,8 +541,8 @@ wallet.google_pay = { connector_list = "bankofamerica,authorizedotnet,cybersourc
bank_redirect.giropay = { connector_list = "globalpay" }
[mandates.supported_payment_methods]
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv"
wallet.paypal = { connector_list = "adyen,novalnet" } # Mandate supported payment method type and connector for wallets
pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later
bank_debit.ach = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit

View File

@ -231,8 +231,8 @@ bank_debit.ach = { connector_list = "gocardless,adyen,stripe" }
bank_debit.becs = { connector_list = "gocardless,stripe,adyen" }
bank_debit.bacs = { connector_list = "stripe,gocardless" }
bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" }
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
pay_later.klarna.connector_list = "adyen,aci"
wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet,nuvei,authorizedotnet,wellsfargo"
wallet.samsung_pay.connector_list = "cybersource"

View File

@ -231,8 +231,8 @@ bank_debit.ach = { connector_list = "gocardless,adyen,stripe" }
bank_debit.becs = { connector_list = "gocardless,stripe,adyen" }
bank_debit.bacs = { connector_list = "stripe,gocardless" }
bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" }
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
pay_later.klarna.connector_list = "adyen,aci"
wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet,nuvei,authorizedotnet,wellsfargo"
wallet.samsung_pay.connector_list = "cybersource"

View File

@ -238,8 +238,8 @@ bank_debit.ach = { connector_list = "gocardless,adyen,stripe" }
bank_debit.becs = { connector_list = "gocardless,stripe,adyen" }
bank_debit.bacs = { connector_list = "stripe,gocardless" }
bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" }
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
pay_later.klarna.connector_list = "adyen,aci"
wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet,nuvei,authorizedotnet,wellsfargo,worldpayvantiv"
wallet.samsung_pay.connector_list = "cybersource"

View File

@ -1081,8 +1081,8 @@ bank_debit.ach = { connector_list = "gocardless,adyen,stripe" }
bank_debit.becs = { connector_list = "gocardless,stripe,adyen" }
bank_debit.bacs = { connector_list = "stripe,gocardless" }
bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" }
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,archipel,worldpayvantiv,payload"
pay_later.klarna.connector_list = "adyen,aci"
wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nuvei,nexinets,novalnet,authorizedotnet,wellsfargo,worldpayvantiv"
wallet.samsung_pay.connector_list = "cybersource"

View File

@ -1009,8 +1009,8 @@ wallet.google_pay = { connector_list = "stripe,cybersource,adyen,bankofamerica,a
wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon,bankofamerica,authorizedotnet,novalnet,multisafepay,wellsfargo,nuvei" }
wallet.samsung_pay = { connector_list = "cybersource" }
wallet.paypal = { connector_list = "adyen,novalnet,authorizedotnet" }
card.credit = { connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,elavon,xendit,novalnet,bamboraapac,archipel,wellsfargo,worldpayvantiv,payload" }
card.debit = { connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,elavon,xendit,novalnet,bamboraapac,archipel,wellsfargo,worldpayvantiv,payload" }
card.credit = { connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,elavon,xendit,novalnet,bamboraapac,archipel,wellsfargo,worldpayvantiv,payload" }
card.debit = { connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,elavon,xendit,novalnet,bamboraapac,archipel,wellsfargo,worldpayvantiv,payload" }
bank_debit.ach = { connector_list = "gocardless,adyen" }
bank_debit.becs = { connector_list = "gocardless" }
bank_debit.bacs = { connector_list = "adyen" }

View File

@ -96,7 +96,7 @@ impl ConnectorCommon for Aci {
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(
headers::AUTHORIZATION.to_string(),
auth.api_key.into_masked(),
format!("Bearer {}", auth.api_key.peek()).into_masked(),
)])
}
@ -161,31 +161,133 @@ impl api::PaymentToken for Aci {}
impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
for Aci
{
// Not Implemented (R)
fn build_request(
&self,
_req: &RouterData<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>,
_connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Err(errors::ConnectorError::NotSupported {
message: "Payment method tokenization not supported".to_string(),
connector: "ACI",
}
.into())
}
}
impl ConnectorIntegration<Session, PaymentsSessionData, PaymentsResponseData> for Aci {
// Not Implemented (R)
fn build_request(
&self,
_req: &RouterData<Session, PaymentsSessionData, PaymentsResponseData>,
_connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Err(errors::ConnectorError::NotSupported {
message: "Payment sessions not supported".to_string(),
connector: "ACI",
}
.into())
}
}
impl ConnectorIntegration<AccessTokenAuth, AccessTokenRequestData, AccessToken> for Aci {
// Not Implemented (R)
fn build_request(
&self,
_req: &RouterData<AccessTokenAuth, AccessTokenRequestData, AccessToken>,
_connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Err(errors::ConnectorError::NotSupported {
message: "Access token authentication not supported".to_string(),
connector: "ACI",
}
.into())
}
}
impl api::MandateSetup for Aci {}
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData> for Aci {
// Issue: #173
fn build_request(
fn get_headers(
&self,
req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
_connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
self.common_get_content_type().to_string().into(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}v1/registrations", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = aci::AciMandateRequest::try_from(req)?;
Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
}
fn build_request(
&self,
req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Aci".to_string()).into())
Ok(Some(
RequestBuilder::new()
.method(Method::Post)
.url(&self.get_url(req, connectors)?)
.attach_default_headers()
.headers(self.get_headers(req, connectors)?)
.set_body(self.get_request_body(req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<
RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
errors::ConnectorError,
> {
let response: aci::AciMandateResponse = res
.response
.parse_struct("AciMandateResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
// TODO: Investigate unexplained error in capture flow from connector.
impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Aci {
fn get_headers(
&self,
@ -409,8 +511,6 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
// encode only for for urlencoded things.
let amount = convert_amount(
self.amount_converter,
req.request.minor_amount,
@ -724,14 +824,13 @@ fn decrypt_aci_webhook_payload(
Ok(ciphertext_and_tag)
}
// TODO: Test this webhook flow once dashboard access is available.
#[async_trait::async_trait]
impl IncomingWebhook for Aci {
fn get_webhook_source_verification_algorithm(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::NoAlgorithm))
Ok(Box::new(crypto::HmacSha256))
}
fn get_webhook_source_verification_signature(
@ -899,15 +998,15 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::CaptureMethod::Manual,
];
let supported_card_network = vec![
let supported_card_networks = vec![
common_enums::CardNetwork::Visa,
common_enums::CardNetwork::Mastercard,
common_enums::CardNetwork::AmericanExpress,
common_enums::CardNetwork::JCB,
common_enums::CardNetwork::DinersClub,
common_enums::CardNetwork::Discover,
common_enums::CardNetwork::JCB,
common_enums::CardNetwork::Maestro,
common_enums::CardNetwork::Mastercard,
common_enums::CardNetwork::UnionPay,
common_enums::CardNetwork::Visa,
common_enums::CardNetwork::Maestro,
];
let mut aci_supported_payment_methods = SupportedPaymentMethods::new();
@ -944,9 +1043,9 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
specific_features: Some(
api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({
api_models::feature_matrix::CardSpecificFeatures {
three_ds: common_enums::FeatureStatus::NotSupported,
three_ds: common_enums::FeatureStatus::Supported,
no_three_ds: common_enums::FeatureStatus::Supported,
supported_card_networks: supported_card_network.clone(),
supported_card_networks: supported_card_networks.clone(),
}
}),
),
@ -963,14 +1062,15 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
specific_features: Some(
api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({
api_models::feature_matrix::CardSpecificFeatures {
three_ds: common_enums::FeatureStatus::NotSupported,
three_ds: common_enums::FeatureStatus::Supported,
no_three_ds: common_enums::FeatureStatus::Supported,
supported_card_networks: supported_card_network.clone(),
supported_card_networks: supported_card_networks.clone(),
}
}),
),
},
);
aci_supported_payment_methods.add(
enums::PaymentMethod::BankRedirect,
enums::PaymentMethodType::Eps,
@ -1051,6 +1151,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
specific_features: None,
},
);
aci_supported_payment_methods.add(
enums::PaymentMethod::PayLater,
enums::PaymentMethodType::Klarna,
@ -1061,6 +1162,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
specific_features: None,
},
);
aci_supported_payment_methods
});

View File

@ -4,10 +4,15 @@ use common_enums::enums;
use common_utils::{id_type, pii::Email, request::Method, types::StringMajorUnit};
use error_stack::report;
use hyperswitch_domain_models::{
payment_method_data::{BankRedirectData, Card, PayLaterData, PaymentMethodData, WalletData},
network_tokenization::NetworkTokenNumber,
payment_method_data::{
BankRedirectData, Card, NetworkTokenData, PayLaterData, PaymentMethodData, WalletData,
},
router_data::{ConnectorAuthType, RouterData},
router_flow_types::SetupMandate,
router_request_types::{
PaymentsAuthorizeData, PaymentsCancelData, PaymentsSyncData, ResponseId,
SetupMandateRequestData,
},
router_response_types::{
MandateReference, PaymentsResponseData, RedirectForm, RefundsResponseData,
@ -25,7 +30,10 @@ use url::Url;
use super::aci_result_codes::{FAILURE_CODES, PENDING_CODES, SUCCESSFUL_CODES};
use crate::{
types::{RefundsResponseRouterData, ResponseRouterData},
utils::{self, PhoneDetailsData, RouterData as _},
utils::{
self, CardData, NetworkTokenData as NetworkTokenDataTrait, PaymentsAuthorizeRequestData,
PhoneDetailsData, RouterData as _,
},
};
type Error = error_stack::Report<errors::ConnectorError>;
@ -54,8 +62,8 @@ impl GetCaptureMethod for PaymentsCancelData {
#[derive(Debug, Serialize)]
pub struct AciRouterData<T> {
amount: StringMajorUnit,
router_data: T,
pub amount: StringMajorUnit,
pub router_data: T,
}
impl<T> From<(StringMajorUnit, T)> for AciRouterData<T> {
@ -114,6 +122,24 @@ pub struct AciCancelRequest {
pub payment_type: AciPaymentType,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciMandateRequest {
pub entity_id: Secret<String>,
pub payment_brand: PaymentBrand,
#[serde(flatten)]
pub payment_details: PaymentDetails,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciMandateResponse {
pub id: String,
pub result: ResultCode,
pub build_number: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum PaymentDetails {
@ -123,6 +149,7 @@ pub enum PaymentDetails {
Wallet(Box<WalletPMData>),
Klarna,
Mandate,
AciNetworkToken(Box<AciNetworkTokenData>),
}
impl TryFrom<(&WalletData, &PaymentsAuthorizeRouterData)> for PaymentDetails {
@ -321,21 +348,117 @@ impl
}
}
fn get_aci_payment_brand(
card_network: Option<common_enums::CardNetwork>,
is_network_token_flow: bool,
) -> Result<PaymentBrand, Error> {
match card_network {
Some(common_enums::CardNetwork::Visa) => Ok(PaymentBrand::Visa),
Some(common_enums::CardNetwork::Mastercard) => Ok(PaymentBrand::Mastercard),
Some(common_enums::CardNetwork::AmericanExpress) => Ok(PaymentBrand::AmericanExpress),
Some(common_enums::CardNetwork::JCB) => Ok(PaymentBrand::Jcb),
Some(common_enums::CardNetwork::DinersClub) => Ok(PaymentBrand::DinersClub),
Some(common_enums::CardNetwork::Discover) => Ok(PaymentBrand::Discover),
Some(common_enums::CardNetwork::UnionPay) => Ok(PaymentBrand::UnionPay),
Some(common_enums::CardNetwork::Maestro) => Ok(PaymentBrand::Maestro),
Some(unsupported_network) => Err(errors::ConnectorError::NotSupported {
message: format!(
"Card network {:?} is not supported by ACI",
unsupported_network
),
connector: "ACI",
})?,
None => {
if is_network_token_flow {
Ok(PaymentBrand::Visa)
} else {
Err(errors::ConnectorError::MissingRequiredField {
field_name: "card.card_network",
}
.into())
}
}
}
}
impl TryFrom<(Card, Option<Secret<String>>)> for PaymentDetails {
type Error = Error;
fn try_from(
(card_data, card_holder_name): (Card, Option<Secret<String>>),
) -> Result<Self, Self::Error> {
let card_expiry_year = card_data.get_expiry_year_4_digit();
let payment_brand = get_aci_payment_brand(card_data.card_network, false)?;
Ok(Self::AciCard(Box::new(CardDetails {
card_number: card_data.card_number,
card_holder: card_holder_name.unwrap_or(Secret::new("".to_string())),
card_expiry_month: card_data.card_exp_month,
card_expiry_year: card_data.card_exp_year,
card_holder: card_holder_name.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "card_holder_name",
})?,
card_expiry_month: card_data.card_exp_month.clone(),
card_expiry_year,
card_cvv: card_data.card_cvc,
payment_brand,
})))
}
}
impl
TryFrom<(
&AciRouterData<&PaymentsAuthorizeRouterData>,
&NetworkTokenData,
)> for PaymentDetails
{
type Error = Error;
fn try_from(
value: (
&AciRouterData<&PaymentsAuthorizeRouterData>,
&NetworkTokenData,
),
) -> Result<Self, Self::Error> {
let (_item, network_token_data) = value;
let token_number = network_token_data.get_network_token();
let payment_brand = get_aci_payment_brand(network_token_data.card_network.clone(), true)?;
let aci_network_token_data = AciNetworkTokenData {
token_type: AciTokenAccountType::Network,
token_number,
token_expiry_month: network_token_data.get_network_token_expiry_month(),
token_expiry_year: network_token_data.get_expiry_year_4_digit(),
token_cryptogram: Some(
network_token_data
.get_cryptogram()
.clone()
.unwrap_or_default(),
),
payment_brand,
};
Ok(Self::AciNetworkToken(Box::new(aci_network_token_data)))
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum AciTokenAccountType {
Network,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciNetworkTokenData {
#[serde(rename = "tokenAccount.type")]
pub token_type: AciTokenAccountType,
#[serde(rename = "tokenAccount.number")]
pub token_number: NetworkTokenNumber,
#[serde(rename = "tokenAccount.expiryMonth")]
pub token_expiry_month: Secret<String>,
#[serde(rename = "tokenAccount.expiryYear")]
pub token_expiry_year: Secret<String>,
#[serde(rename = "tokenAccount.cryptogram")]
pub token_cryptogram: Option<Secret<String>>,
#[serde(rename = "paymentBrand")]
pub payment_brand: PaymentBrand,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BankRedirectionPMData {
@ -365,7 +488,7 @@ pub struct WalletPMData {
account_id: Option<Secret<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentBrand {
Eps,
@ -379,6 +502,23 @@ pub enum PaymentBrand {
Mbway,
#[serde(rename = "ALIPAY")]
AliPay,
// Card network brands
#[serde(rename = "VISA")]
Visa,
#[serde(rename = "MASTER")]
Mastercard,
#[serde(rename = "AMEX")]
AmericanExpress,
#[serde(rename = "JCB")]
Jcb,
#[serde(rename = "DINERS")]
DinersClub,
#[serde(rename = "DISCOVER")]
Discover,
#[serde(rename = "UNIONPAY")]
UnionPay,
#[serde(rename = "MAESTRO")]
Maestro,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
@ -393,6 +533,8 @@ pub struct CardDetails {
pub card_expiry_year: Secret<String>,
#[serde(rename = "card.cvv")]
pub card_cvv: Secret<String>,
#[serde(rename = "paymentBrand")]
pub payment_brand: PaymentBrand,
}
#[derive(Debug, Clone, Serialize)]
@ -460,6 +602,9 @@ impl TryFrom<&AciRouterData<&PaymentsAuthorizeRouterData>> for AciPaymentsReques
fn try_from(item: &AciRouterData<&PaymentsAuthorizeRouterData>) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
PaymentMethodData::Card(ref card_data) => Self::try_from((item, card_data)),
PaymentMethodData::NetworkToken(ref network_token_data) => {
Self::try_from((item, network_token_data))
}
PaymentMethodData::Wallet(ref wallet_data) => Self::try_from((item, wallet_data)),
PaymentMethodData::PayLater(ref pay_later_data) => {
Self::try_from((item, pay_later_data))
@ -487,7 +632,6 @@ impl TryFrom<&AciRouterData<&PaymentsAuthorizeRouterData>> for AciPaymentsReques
| PaymentMethodData::Voucher(_)
| PaymentMethodData::OpenBanking(_)
| PaymentMethodData::CardToken(_)
| PaymentMethodData::NetworkToken(_)
| PaymentMethodData::CardDetailsForNetworkTransactionId(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Aci"),
@ -574,7 +718,34 @@ impl TryFrom<(&AciRouterData<&PaymentsAuthorizeRouterData>, &Card)> for AciPayme
txn_details,
payment_method,
instruction,
shopper_result_url: None,
shopper_result_url: item.router_data.request.router_return_url.clone(),
})
}
}
impl
TryFrom<(
&AciRouterData<&PaymentsAuthorizeRouterData>,
&NetworkTokenData,
)> for AciPaymentsRequest
{
type Error = Error;
fn try_from(
value: (
&AciRouterData<&PaymentsAuthorizeRouterData>,
&NetworkTokenData,
),
) -> Result<Self, Self::Error> {
let (item, network_token_data) = value;
let txn_details = get_transaction_details(item)?;
let payment_method = PaymentDetails::try_from((item, network_token_data))?;
let instruction = get_instruction_details(item);
Ok(Self {
txn_details,
payment_method,
instruction,
shopper_result_url: item.router_data.request.router_return_url.clone(),
})
}
}
@ -609,11 +780,16 @@ fn get_transaction_details(
item: &AciRouterData<&PaymentsAuthorizeRouterData>,
) -> Result<TransactionDetails, error_stack::Report<errors::ConnectorError>> {
let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?;
let payment_type = if item.router_data.request.is_auto_capture()? {
AciPaymentType::Debit
} else {
AciPaymentType::Preauthorization
};
Ok(TransactionDetails {
entity_id: auth.entity_id,
amount: item.amount.to_owned(),
currency: item.router_data.request.currency.to_string(),
payment_type: AciPaymentType::Debit,
payment_type,
})
}
@ -650,6 +826,61 @@ impl TryFrom<&PaymentsCancelRouterData> for AciCancelRequest {
}
}
impl TryFrom<&RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>>
for AciMandateRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let auth = AciAuthType::try_from(&item.connector_auth_type)?;
let (payment_brand, payment_details) = match &item.request.payment_method_data {
PaymentMethodData::Card(card_data) => {
let brand = get_aci_payment_brand(card_data.card_network.clone(), false)?;
match brand {
PaymentBrand::Visa
| PaymentBrand::Mastercard
| PaymentBrand::AmericanExpress => {}
_ => Err(errors::ConnectorError::NotSupported {
message: "Payment method not supported for mandate setup".to_string(),
connector: "ACI",
})?,
}
let details = PaymentDetails::AciCard(Box::new(CardDetails {
card_number: card_data.card_number.clone(),
card_expiry_month: card_data.card_exp_month.clone(),
card_expiry_year: card_data.get_expiry_year_4_digit(),
card_cvv: card_data.card_cvc.clone(),
card_holder: card_data.card_holder_name.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "card_holder_name",
},
)?,
payment_brand: brand.clone(),
}));
(brand, details)
}
_ => {
return Err(errors::ConnectorError::NotSupported {
message: "Payment method not supported for mandate setup".to_string(),
connector: "ACI",
}
.into());
}
};
Ok(Self {
entity_id: auth.entity_id,
payment_brand,
payment_details,
})
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AciPaymentStatus {
@ -674,6 +905,7 @@ fn map_aci_attempt_status(item: AciPaymentStatus, auto_capture: bool) -> enums::
AciPaymentStatus::RedirectShopper => enums::AttemptStatus::AuthenticationPending,
}
}
impl FromStr for AciPaymentStatus {
type Err = error_stack::Report<errors::ConnectorError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -696,10 +928,8 @@ impl FromStr for AciPaymentStatus {
pub struct AciPaymentsResponse {
id: String,
registration_id: Option<Secret<String>>,
// ndc is an internal unique identifier for the request.
ndc: String,
timestamp: String,
// Number useful for support purposes.
build_number: String,
pub(super) result: ResultCode,
pub(super) redirect: Option<AciRedirectionData>,
@ -717,15 +947,24 @@ pub struct AciErrorResponse {
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciRedirectionData {
method: Option<Method>,
parameters: Vec<Parameters>,
url: Url,
pub method: Option<Method>,
pub parameters: Vec<Parameters>,
pub url: Url,
pub preconditions: Option<Vec<PreconditionData>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PreconditionData {
pub method: Option<Method>,
pub parameters: Vec<Parameters>,
pub url: Url,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct Parameters {
name: String,
value: String,
pub name: String,
pub value: String,
}
#[derive(Default, Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
@ -863,12 +1102,45 @@ pub struct AciCaptureResultDetails {
extended_description: String,
#[serde(rename = "clearingInstituteName")]
clearing_institute_name: String,
connector_tx_id1: String,
connector_tx_id3: String,
connector_tx_id2: String,
connector_tx_i_d1: String,
connector_tx_i_d3: String,
connector_tx_i_d2: String,
acquirer_response: String,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub enum AciCaptureStatus {
Succeeded,
Failed,
#[default]
Pending,
}
impl FromStr for AciCaptureStatus {
type Err = error_stack::Report<errors::ConnectorError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if FAILURE_CODES.contains(&s) {
Ok(Self::Failed)
} else if PENDING_CODES.contains(&s) {
Ok(Self::Pending)
} else if SUCCESSFUL_CODES.contains(&s) {
Ok(Self::Succeeded)
} else {
Err(report!(errors::ConnectorError::UnexpectedResponseError(
bytes::Bytes::from(s.to_owned())
)))
}
}
}
fn map_aci_capture_status(item: AciCaptureStatus) -> enums::AttemptStatus {
match item {
AciCaptureStatus::Succeeded => enums::AttemptStatus::Charged,
AciCaptureStatus::Failed => enums::AttemptStatus::Failure,
AciCaptureStatus::Pending => enums::AttemptStatus::Pending,
}
}
impl<F, T> TryFrom<ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
{
@ -877,10 +1149,7 @@ impl<F, T> TryFrom<ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponse
item: ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: map_aci_attempt_status(
AciPaymentStatus::from_str(&item.response.result.code)?,
false,
),
status: map_aci_capture_status(AciCaptureStatus::from_str(&item.response.result.code)?),
reference_id: Some(item.response.referenced_id.clone()),
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()),
@ -963,7 +1232,6 @@ impl From<AciRefundStatus> for enums::RefundStatus {
#[serde(rename_all = "camelCase")]
pub struct AciRefundResponse {
id: String,
//ndc is an internal unique identifier for the request.
ndc: String,
timestamp: String,
build_number: String,
@ -987,6 +1255,58 @@ impl<F> TryFrom<RefundsResponseRouterData<F, AciRefundResponse>> for RefundsRout
}
}
impl
TryFrom<
ResponseRouterData<
SetupMandate,
AciMandateResponse,
SetupMandateRequestData,
PaymentsResponseData,
>,
> for RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<
SetupMandate,
AciMandateResponse,
SetupMandateRequestData,
PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let mandate_reference = Some(MandateReference {
connector_mandate_id: Some(item.response.id.clone()),
payment_method_id: None,
mandate_metadata: None,
connector_mandate_request_reference_id: None,
});
let status = if SUCCESSFUL_CODES.contains(&item.response.result.code.as_str()) {
enums::AttemptStatus::Charged
} else if FAILURE_CODES.contains(&item.response.result.code.as_str()) {
enums::AttemptStatus::Failure
} else {
enums::AttemptStatus::Pending
};
Ok(Self {
status,
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()),
redirection_data: Box::new(None),
mandate_reference: Box::new(mandate_reference),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(item.response.id),
incremental_authorization_allowed: None,
charges: None,
}),
..item.data
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum AciWebhookEventType {
Payment,

View File

@ -1,456 +1,559 @@
#![allow(clippy::print_stdout)]
use std::str::FromStr;
use std::{borrow::Cow, marker::PhantomData, str::FromStr, sync::Arc};
use common_utils::id_type;
use hyperswitch_domain_models::address::{Address, AddressDetails, PhoneDetails};
use masking::Secret;
use router::{
configs::settings::Settings,
core::payments,
db::StorageImpl,
routes, services,
types::{self, storage::enums, PaymentAddress},
use hyperswitch_domain_models::{
address::{Address, AddressDetails, PhoneDetails},
payment_method_data::{Card, PaymentMethodData},
router_request_types::AuthenticationData,
};
use tokio::sync::oneshot;
use masking::Secret;
use router::types::{self, storage::enums, PaymentAddress};
use crate::{connector_auth::ConnectorAuthentication, utils};
use crate::{
connector_auth,
utils::{self, ConnectorActions, PaymentInfo},
};
fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
let auth = ConnectorAuthentication::new()
.aci
.expect("Missing ACI connector authentication configuration");
let merchant_id = id_type::MerchantId::try_from(Cow::from("aci")).unwrap();
types::RouterData {
flow: PhantomData,
merchant_id,
customer_id: Some(id_type::CustomerId::try_from(Cow::from("aci")).unwrap()),
tenant_id: id_type::TenantId::try_from_string("public".to_string()).unwrap(),
connector: "aci".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: utils::to_connector_auth_type(auth.into()),
description: Some("This is a test".to_string()),
payment_method_status: None,
request: types::PaymentsAuthorizeData {
amount: 1000,
currency: enums::Currency::USD,
payment_method_data: types::domain::PaymentMethodData::Card(types::domain::Card {
card_number: cards::CardNumber::from_str("4200000000000000").unwrap(),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_cvc: Secret::new("999".to_string()),
card_issuer: None,
card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(Secret::new("nick_name".into())),
card_holder_name: Some(Secret::new("card holder name".into())),
co_badged_card_data: 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,
order_category: None,
email: None,
customer_name: 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,
customer_id: None,
surcharge_details: None,
request_incremental_authorization: false,
metadata: None,
authentication_data: None,
customer_acceptance: None,
locale: None,
enable_partial_authorization: None,
..utils::PaymentAuthorizeType::default().0
},
response: Err(types::ErrorResponse::default()),
address: PaymentAddress::new(
#[derive(Clone, Copy)]
struct AciTest;
impl ConnectorActions for AciTest {}
impl utils::Connector for AciTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Aci;
utils::construct_connector_data_old(
Box::new(Aci::new()),
types::Connector::Aci,
types::api::GetToken::Connector,
None,
)
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
utils::to_connector_auth_type(
connector_auth::ConnectorAuthentication::new()
.aci
.expect("Missing connector authentication configuration")
.into(),
)
}
fn get_name(&self) -> String {
"aci".to_string()
}
}
static CONNECTOR: AciTest = AciTest {};
fn get_default_payment_info() -> Option<PaymentInfo> {
Some(PaymentInfo {
address: Some(PaymentAddress::new(
None,
Some(Address {
address: Some(AddressDetails {
first_name: Some(Secret::new("John".to_string())),
last_name: Some(Secret::new("Doe".to_string())),
line1: Some(Secret::new("123 Main St".to_string())),
city: Some("New York".to_string()),
state: Some(Secret::new("NY".to_string())),
zip: Some(Secret::new("10001".to_string())),
country: Some(enums::CountryAlpha2::US),
..Default::default()
}),
phone: Some(PhoneDetails {
number: Some(Secret::new("9123456789".to_string())),
country_code: Some("+351".to_string()),
number: Some(Secret::new("+1234567890".to_string())),
country_code: Some("+1".to_string()),
}),
email: None,
}),
None,
),
connector_meta_data: None,
connector_wallets_details: None,
amount_captured: None,
minor_amount_captured: None,
access_token: None,
session_token: None,
reference_id: None,
payment_method_token: None,
connector_customer: None,
recurring_mandate_payment_data: None,
connector_response: None,
preprocessing_id: None,
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
#[cfg(feature = "payouts")]
payout_method_data: None,
#[cfg(feature = "payouts")]
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: None,
apple_pay_flow: None,
external_latency: None,
frm_metadata: None,
refund_id: None,
dispute_id: None,
integrity_check: Ok(()),
additional_merchant_data: None,
header_payload: None,
connector_mandate_request_reference_id: None,
authentication_id: None,
psd2_sca_exemption_type: None,
raw_connector_response: None,
is_payment_id_from_merchant: None,
l2_l3_data: None,
minor_amount_capturable: None,
}
None,
)),
..Default::default()
})
}
fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
let auth = ConnectorAuthentication::new()
.aci
.expect("Missing ACI connector authentication configuration");
fn get_payment_authorize_data() -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(Card {
card_number: cards::CardNumber::from_str("4200000000000000").unwrap(),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_cvc: Secret::new("999".to_string()),
card_holder_name: Some(Secret::new("John Doe".to_string())),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
})
}
let merchant_id = id_type::MerchantId::try_from(Cow::from("aci")).unwrap();
types::RouterData {
flow: PhantomData,
merchant_id,
customer_id: Some(id_type::CustomerId::try_from(Cow::from("aci")).unwrap()),
tenant_id: id_type::TenantId::try_from_string("public".to_string()).unwrap(),
connector: "aci".to_string(),
payment_id: uuid::Uuid::new_v4().to_string(),
attempt_id: uuid::Uuid::new_v4().to_string(),
payment_method_status: None,
status: enums::AttemptStatus::default(),
payment_method: enums::PaymentMethod::Card,
auth_type: enums::AuthenticationType::NoThreeDs,
connector_auth_type: utils::to_connector_auth_type(auth.into()),
description: Some("This is a test".to_string()),
request: types::RefundsData {
payment_amount: 1000,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
connector_transaction_id: String::new(),
refund_amount: 100,
webhook_url: None,
connector_metadata: None,
reason: None,
connector_refund_id: None,
browser_info: None,
..utils::PaymentRefundType::default().0
},
response: Err(types::ErrorResponse::default()),
address: PaymentAddress::default(),
connector_meta_data: None,
connector_wallets_details: None,
amount_captured: None,
minor_amount_captured: None,
access_token: None,
session_token: None,
reference_id: None,
payment_method_token: None,
connector_customer: None,
recurring_mandate_payment_data: None,
connector_response: None,
preprocessing_id: None,
connector_request_reference_id: uuid::Uuid::new_v4().to_string(),
#[cfg(feature = "payouts")]
payout_method_data: None,
#[cfg(feature = "payouts")]
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: None,
apple_pay_flow: None,
external_latency: None,
frm_metadata: None,
refund_id: None,
dispute_id: None,
integrity_check: Ok(()),
additional_merchant_data: None,
header_payload: None,
connector_mandate_request_reference_id: None,
authentication_id: None,
psd2_sca_exemption_type: None,
raw_connector_response: None,
is_payment_id_from_merchant: None,
l2_l3_data: None,
minor_amount_capturable: None,
}
fn get_threeds_payment_authorize_data() -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(Card {
card_number: cards::CardNumber::from_str("4200000000000000").unwrap(),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_cvc: Secret::new("999".to_string()),
card_holder_name: Some(Secret::new("John Doe".to_string())),
..utils::CCardType::default().0
}),
enrolled_for_3ds: true,
authentication_data: Some(AuthenticationData {
eci: Some("05".to_string()),
cavv: Secret::new("jJ81HADVRtXfCBATEp01CJUAAAA".to_string()),
threeds_server_transaction_id: Some("9458d8d4-f19f-4c28-b5c7-421b1dd2e1aa".to_string()),
message_version: Some(common_utils::types::SemanticVersion::new(2, 1, 0)),
ds_trans_id: Some("97267598FAE648F28083B2D2AF7B1234".to_string()),
created_at: common_utils::date_time::now(),
challenge_code: Some("01".to_string()),
challenge_cancel: None,
challenge_code_reason: Some("01".to_string()),
message_extension: None,
acs_trans_id: None,
authentication_type: None,
}),
..utils::PaymentAuthorizeType::default().0
})
}
#[actix_web::test]
async fn payments_create_success() {
let conf = Settings::new().unwrap();
let tx: oneshot::Sender<()> = oneshot::channel().0;
let app_state = Box::pin(routes::AppState::with_storage(
conf,
StorageImpl::PostgresqlTest,
tx,
Box::new(services::MockApiClient),
))
.await;
let state = Arc::new(app_state)
.get_session_state(
&id_type::TenantId::try_from_string("public".to_string()).unwrap(),
None,
|| {},
)
.unwrap();
use router::connector::Aci;
let connector = utils::construct_connector_data_old(
Box::new(Aci::new()),
types::Connector::Aci,
types::api::GetToken::Connector,
None,
);
let connector_integration: services::BoxedPaymentConnectorIntegrationInterface<
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,
None,
None,
)
.await
.unwrap();
assert!(
response.status == enums::AttemptStatus::Charged,
"The payment failed"
);
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(get_payment_authorize_data(), get_default_payment_info())
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
#[actix_web::test]
#[ignore]
async fn payments_create_failure() {
{
let conf = Settings::new().unwrap();
use router::connector::Aci;
let tx: oneshot::Sender<()> = oneshot::channel().0;
let app_state = Box::pin(routes::AppState::with_storage(
conf,
StorageImpl::PostgresqlTest,
tx,
Box::new(services::MockApiClient),
))
.await;
let state = Arc::new(app_state)
.get_session_state(
&id_type::TenantId::try_from_string("public".to_string()).unwrap(),
None,
|| {},
)
.unwrap();
let connector = utils::construct_connector_data_old(
Box::new(Aci::new()),
types::Connector::Aci,
types::api::GetToken::Connector,
None,
);
let connector_integration: services::BoxedPaymentConnectorIntegrationInterface<
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::domain::PaymentMethodData::Card(types::domain::Card {
card_number: cards::CardNumber::from_str("4200000000000000").unwrap(),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_cvc: Secret::new("99".to_string()),
card_issuer: None,
card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(Secret::new("nick_name".into())),
card_holder_name: Some(Secret::new("card holder name".into())),
co_badged_card_data: None,
});
let response = services::api::execute_connector_processing_step(
&state,
connector_integration,
&request,
payments::CallConnectorAction::Trigger,
None,
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
get_payment_authorize_data(),
None,
get_default_payment_info(),
)
.await
.is_err();
println!("{response:?}");
assert!(response, "The payment was intended to fail but it passed");
}
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn refund_for_successful_payments() {
let conf = Settings::new().unwrap();
use router::connector::Aci;
let connector = utils::construct_connector_data_old(
Box::new(Aci::new()),
types::Connector::Aci,
types::api::GetToken::Connector,
None,
);
let tx: oneshot::Sender<()> = oneshot::channel().0;
let app_state = Box::pin(routes::AppState::with_storage(
conf,
StorageImpl::PostgresqlTest,
tx,
Box::new(services::MockApiClient),
))
.await;
let state = Arc::new(app_state)
.get_session_state(
&id_type::TenantId::try_from_string("public".to_string()).unwrap(),
None,
|| {},
async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
get_payment_authorize_data(),
Some(types::PaymentsCaptureData {
amount_to_capture: 50,
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(),
)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(get_payment_authorize_data(), get_default_payment_info())
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
..Default::default()
}),
get_default_payment_info(),
)
.await
.expect("PSync response");
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
}
#[actix_web::test]
async fn should_void_authorized_payment() {
let response = CONNECTOR
.authorize_and_void_payment(
get_payment_authorize_data(),
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
}),
get_default_payment_info(),
)
.await
.expect("Void payment response");
assert_eq!(response.status, enums::AttemptStatus::Voided);
}
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
get_payment_authorize_data(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
let connector_integration: services::BoxedPaymentConnectorIntegrationInterface<
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,
None,
None,
)
.await
.unwrap();
assert!(
response.status == enums::AttemptStatus::Charged,
"The payment failed"
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
types::api::Execute,
types::RefundsData,
types::RefundsResponseData,
> = connector.connector.get_connector_integration();
let mut refund_request = construct_refund_router_data();
refund_request.request.connector_transaction_id = match response.response.unwrap() {
types::PaymentsResponseData::TransactionResponse { resource_id, .. } => {
resource_id.get_connector_transaction_id().unwrap()
}
_ => panic!("Connector transaction id not found"),
};
let response = services::api::execute_connector_processing_step(
&state,
connector_integration,
&refund_request,
payments::CallConnectorAction::Trigger,
None,
None,
)
.await
.unwrap();
println!("{response:?}");
}
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
get_payment_authorize_data(),
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
let refund_response = CONNECTOR
.capture_payment_and_refund(
get_payment_authorize_data(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(get_payment_authorize_data(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(get_payment_authorize_data(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
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::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
capture_method: Some(enums::CaptureMethod::Automatic),
..Default::default()
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Charged,);
}
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let response = CONNECTOR
.make_payment_and_refund(
get_payment_authorize_data(),
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let refund_response = CONNECTOR
.make_payment_and_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
CONNECTOR
.make_payment_and_multiple_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await;
}
#[actix_web::test]
async fn should_sync_refund() {
let refund_response = CONNECTOR
.make_payment_and_refund(
get_payment_authorize_data(),
None,
get_default_payment_info(),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(Card {
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert!(
response.response.unwrap().refund_status == enums::RefundStatus::Success,
"The refund transaction failed"
response.response.is_err(),
"Payment should fail with incorrect CVC"
);
}
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(Card {
card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert!(
response.response.is_err(),
"Payment should fail with invalid expiry month"
);
}
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: PaymentMethodData::Card(Card {
card_exp_year: Secret::new("2000".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert!(
response.response.is_err(),
"Payment should fail with incorrect expiry year"
);
}
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(get_payment_authorize_data(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
.await
.unwrap();
assert!(
void_response.response.is_err(),
"Void should fail for already captured payment"
);
}
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, get_default_payment_info())
.await
.unwrap();
assert!(
capture_response.response.is_err(),
"Capture should fail for invalid payment ID"
);
}
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR
.make_payment_and_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert!(
response.response.is_err(),
"Refund should fail when amount exceeds payment amount"
);
}
#[actix_web::test]
#[ignore]
async fn refunds_create_failure() {
let conf = Settings::new().unwrap();
use router::connector::Aci;
let connector = utils::construct_connector_data_old(
Box::new(Aci::new()),
types::Connector::Aci,
types::api::GetToken::Connector,
None,
);
let tx: oneshot::Sender<()> = oneshot::channel().0;
let app_state = Box::pin(routes::AppState::with_storage(
conf,
StorageImpl::PostgresqlTest,
tx,
Box::new(services::MockApiClient),
))
.await;
let state = Arc::new(app_state)
.get_session_state(
&id_type::TenantId::try_from_string("public".to_string()).unwrap(),
None,
|| {},
async fn should_make_threeds_payment() {
let authorize_response = CONNECTOR
.make_payment(
get_threeds_payment_authorize_data(),
get_default_payment_info(),
)
.await
.unwrap();
let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
types::api::Execute,
types::RefundsData,
types::RefundsResponseData,
> = connector.connector.get_connector_integration();
let mut request = construct_refund_router_data();
request.request.connector_transaction_id = "1234".to_string();
let response = services::api::execute_connector_processing_step(
&state,
connector_integration,
&request,
payments::CallConnectorAction::Trigger,
None,
None,
)
.await
.is_err();
println!("{response:?}");
assert!(response, "The refund was intended to fail but it passed");
assert!(
authorize_response.status == enums::AttemptStatus::AuthenticationPending
|| authorize_response.status == enums::AttemptStatus::Charged,
"3DS payment should result in AuthenticationPending or Charged status, got: {:?}",
authorize_response.status
);
if let Ok(types::PaymentsResponseData::TransactionResponse {
redirection_data, ..
}) = &authorize_response.response
{
if authorize_response.status == enums::AttemptStatus::AuthenticationPending {
assert!(
redirection_data.is_some(),
"3DS flow should include redirection data for authentication"
);
}
}
}
#[actix_web::test]
#[ignore]
async fn should_authorize_threeds_payment() {
let response = CONNECTOR
.authorize_payment(
get_threeds_payment_authorize_data(),
get_default_payment_info(),
)
.await
.expect("Authorize 3DS payment response");
assert!(
response.status == enums::AttemptStatus::AuthenticationPending
|| response.status == enums::AttemptStatus::Authorized,
"3DS authorization should result in AuthenticationPending or Authorized status, got: {:?}",
response.status
);
}
#[actix_web::test]
#[ignore]
async fn should_sync_threeds_payment() {
let authorize_response = CONNECTOR
.authorize_payment(
get_threeds_payment_authorize_data(),
get_default_payment_info(),
)
.await
.expect("Authorize 3DS payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::AuthenticationPending,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
..Default::default()
}),
get_default_payment_info(),
)
.await
.expect("PSync 3DS response");
assert!(
response.status == enums::AttemptStatus::AuthenticationPending
|| response.status == enums::AttemptStatus::Authorized,
"3DS sync should maintain AuthenticationPending or Authorized status"
);
}

View File

@ -721,8 +721,8 @@ bank_debit.ach = { connector_list = "gocardless,adyen,stripe" }
bank_debit.becs = { connector_list = "gocardless,stripe,adyen" }
bank_debit.bacs = { connector_list = "stripe,gocardless" }
bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" }
card.credit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,worldpayvantiv,payload"
card.debit.connector_list = "checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,worldpayvantiv,payload"
card.credit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,worldpayvantiv,payload"
card.debit.connector_list = "aci,checkout,stripe,adyen,authorizedotnet,cybersource,datatrans,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal,xendit,moneris,worldpayvantiv,payload"
pay_later.klarna.connector_list = "adyen,aci"
wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet,authorizedotnet"
wallet.samsung_pay.connector_list = "cybersource"