mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
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:
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user