diff --git a/crates/hyperswitch_connectors/src/connectors/aci/aci_result_codes.rs b/crates/hyperswitch_connectors/src/connectors/aci/aci_result_codes.rs index 84502c46ee..dd7e07b814 100644 --- a/crates/hyperswitch_connectors/src/connectors/aci/aci_result_codes.rs +++ b/crates/hyperswitch_connectors/src/connectors/aci/aci_result_codes.rs @@ -1,4 +1,4 @@ -pub(super) const FAILURE_CODES: [&str; 501] = [ +pub(super) const FAILURE_CODES: [&str; 502] = [ "100.370.100", "100.370.110", "100.370.111", @@ -500,6 +500,7 @@ pub(super) const FAILURE_CODES: [&str; 501] = [ "800.800.102", "800.800.202", "800.800.302", + "100.390.100", ]; pub(super) const SUCCESSFUL_CODES: [&str; 16] = [ diff --git a/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs b/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs index d8c3e20d6b..eb659a8204 100644 --- a/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs @@ -136,7 +136,8 @@ pub struct AciCancelRequest { #[serde(rename_all = "camelCase")] pub struct AciMandateRequest { pub entity_id: Secret, - pub payment_brand: PaymentBrand, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_brand: Option, #[serde(flatten)] pub payment_details: PaymentDetails, } @@ -395,7 +396,7 @@ impl TryFrom<(Card, Option>)> for PaymentDetails { ) -> Result { let card_expiry_year = card_data.get_expiry_year_4_digit(); - let payment_brand = get_aci_payment_brand(card_data.card_network, false)?; + let payment_brand = get_aci_payment_brand(card_data.card_network, false).ok(); Ok(Self::AciCard(Box::new(CardDetails { card_number: card_data.card_number, @@ -541,7 +542,8 @@ pub struct CardDetails { #[serde(rename = "card.cvv")] pub card_cvv: Secret, #[serde(rename = "paymentBrand")] - pub payment_brand: PaymentBrand, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_brand: Option, } #[derive(Debug, Clone, Serialize)] @@ -879,16 +881,20 @@ impl TryFrom<&RouterData { - 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 brand = get_aci_payment_brand(card_data.card_network.clone(), false).ok(); + match brand.as_ref() { + Some(PaymentBrand::Visa) + | Some(PaymentBrand::Mastercard) + | Some(PaymentBrand::AmericanExpress) => (), + Some(_) => { + return Err(errors::ConnectorError::NotSupported { + message: "Payment method not supported for mandate setup".to_string(), + connector: "ACI", + } + .into()); + } + None => (), + }; let details = PaymentDetails::AciCard(Box::new(CardDetails { card_number: card_data.card_number.clone(), @@ -1150,13 +1156,13 @@ pub struct AciCaptureResponse { currency: String, descriptor: String, result: AciCaptureResult, - result_details: AciCaptureResultDetails, + result_details: Option, build_number: String, timestamp: String, ndc: Secret, - source: Secret, - payment_method: String, - short_id: String, + source: Option>, + payment_method: Option, + short_id: Option, } #[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)] @@ -1171,22 +1177,22 @@ pub struct AciCaptureResult { pub struct AciCaptureResultDetails { extended_description: String, #[serde(rename = "clearingInstituteName")] - clearing_institute_name: String, - connector_tx_i_d1: String, - connector_tx_i_d3: String, - connector_tx_i_d2: String, - acquirer_response: String, + clearing_institute_name: Option, + connector_tx_i_d1: Option, + connector_tx_i_d3: Option, + connector_tx_i_d2: Option, + acquirer_response: Option, } #[derive(Debug, Default, Clone, Deserialize)] -pub enum AciCaptureStatus { +pub enum AciStatus { Succeeded, Failed, #[default] Pending, } -impl FromStr for AciCaptureStatus { +impl FromStr for AciStatus { type Err = error_stack::Report; fn from_str(s: &str) -> Result { if FAILURE_CODES.contains(&s) { @@ -1203,11 +1209,11 @@ impl FromStr for AciCaptureStatus { } } -fn map_aci_capture_status(item: AciCaptureStatus) -> enums::AttemptStatus { +fn map_aci_capture_status(item: AciStatus) -> enums::AttemptStatus { match item { - AciCaptureStatus::Succeeded => enums::AttemptStatus::Charged, - AciCaptureStatus::Failed => enums::AttemptStatus::Failure, - AciCaptureStatus::Pending => enums::AttemptStatus::Pending, + AciStatus::Succeeded => enums::AttemptStatus::Charged, + AciStatus::Failed => enums::AttemptStatus::Failure, + AciStatus::Pending => enums::AttemptStatus::Pending, } } @@ -1218,8 +1224,7 @@ impl TryFrom, ) -> Result { - let status = - map_aci_capture_status(AciCaptureStatus::from_str(&item.response.result.code)?); + let status = map_aci_capture_status(AciStatus::from_str(&item.response.result.code)?); let response = if status == enums::AttemptStatus::Failure { Err(ErrorResponse { code: item.response.result.code.clone(), @@ -1254,6 +1259,69 @@ impl TryFrom, + build_number: String, + timestamp: String, + ndc: Secret, +} + +fn map_aci_void_status(item: AciStatus) -> enums::AttemptStatus { + match item { + AciStatus::Succeeded => enums::AttemptStatus::Voided, + AciStatus::Failed => enums::AttemptStatus::VoidFailed, + AciStatus::Pending => enums::AttemptStatus::VoidInitiated, + } +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = map_aci_void_status(AciStatus::from_str(&item.response.result.code)?); + let response = if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: item.response.result.code.clone(), + message: item.response.result.description.clone(), + reason: Some(item.response.result.description), + status_code: item.http_code, + attempt_status: Some(status), + connector_transaction_id: Some(item.response.id.clone()), + ..Default::default() + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.referenced_id.clone()), + incremental_authorization_allowed: None, + charges: None, + }) + }; + Ok(Self { + status, + response, + reference_id: Some(item.response.referenced_id), + ..item.data + }) + } +} + #[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct AciRefundRequest { diff --git a/cypress-tests/cypress/e2e/configs/Payment/Aci.js b/cypress-tests/cypress/e2e/configs/Payment/Aci.js index 8395fad63b..eacfd2f4b0 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Aci.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Aci.js @@ -2,27 +2,27 @@ import { customerAcceptance } from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4242424242424242", - card_exp_month: "10", - card_exp_year: "2050", + card_exp_month: "01", + card_exp_year: "2045", card_holder_name: "morino", card_cvc: "737", }; const successfulThreeDSTestCardDetails = { - card_number: "4111111111111111", - card_exp_month: "10", - card_exp_year: "2050", + card_number: "5386024192625914", + card_exp_month: "01", + card_exp_year: "2045", card_holder_name: "morino", card_cvc: "737", }; // This card details will fail because of card expiryYear const failedNo3DSCardDetails = { - card_number: "4242424242424242", + card_number: "4012001037461114", card_exp_month: "01", card_exp_year: "35", card_holder_name: "joseph Doe", - card_cvc: "123", + card_cvc: "737", }; const singleUseMandateData = { @@ -30,7 +30,7 @@ const singleUseMandateData = { mandate_type: { single_use: { amount: 8000, - currency: "USD", + currency: "EUR", }, }, }; @@ -40,7 +40,7 @@ const multiUseMandateData = { mandate_type: { multi_use: { amount: 8000, - currency: "USD", + currency: "EUR", }, }, }; @@ -67,7 +67,7 @@ export const connectorDetails = { card_pm: { PaymentIntent: { Request: { - currency: "USD", + currency: "EUR", customer_acceptance: null, setup_future_usage: "on_session", }, @@ -79,13 +79,28 @@ export const connectorDetails = { }, }, }, + PaymentIntentOffSession: { + Request: { + amount: 6000, + authentication_type: "no_three_ds", + currency: "EUR", + customer_acceptance: null, + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, No3DSAutoCapture: { Request: { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, }, - currency: "USD", + currency: "EUR", customer_acceptance: null, setup_future_usage: "on_session", }, @@ -98,31 +113,62 @@ export const connectorDetails = { }, }, }, + No3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + payment_method: "card", + attempt_count: 1, + }, + }, + }, "3DSAutoCapture": { - Configs: { - TRIGGER_SKIP: true, - }, Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulThreeDSTestCardDetails, }, - currency: "USD", + currency: "EUR", customer_acceptance: null, setup_future_usage: "on_session", }, Response: { status: 200, body: { - status: "succeeded", - payment_method: "card", - attempt_count: 1, + status: "requires_customer_action", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", }, }, }, PaymentIntentWithShippingCost: { Request: { - currency: "USD", + currency: "EUR", shipping_cost: 50, amount: 6000, }, @@ -139,7 +185,7 @@ export const connectorDetails = { payment_method_data: { card: successfulNo3DSCardDetails, }, - currency: "USD", + currency: "EUR", customer_acceptance: null, setup_future_usage: "on_session", }, @@ -194,6 +240,17 @@ export const connectorDetails = { }, }, }, + PartialCapture: { + Request: { + amount_to_capture: 2000, + }, + Response: { + status: 200, + body: { + status: "partially_captured", + }, + }, + }, manualPaymentRefund: { Request: { amount: 6000, @@ -225,7 +282,7 @@ export const connectorDetails = { payment_method_data: { card: successfulThreeDSTestCardDetails, }, - currency: "USD", + currency: "EUR", mandate_data: singleUseMandateData, }, Response: { @@ -235,6 +292,25 @@ export const connectorDetails = { }, }, }, + MandateSingleUse3DSManualCapture: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, MandateSingleUseNo3DSAutoCapture: { Configs: { TRIGGER_SKIP: true, @@ -244,7 +320,7 @@ export const connectorDetails = { payment_method_data: { card: successfulNo3DSCardDetails, }, - currency: "USD", + currency: "EUR", mandate_data: singleUseMandateData, }, Response: { @@ -254,7 +330,45 @@ export const connectorDetails = { }, }, }, + MandateSingleUseNo3DSManualCapture: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, MandateMultiUseNo3DSAutoCapture: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { Configs: { TRIGGER_SKIP: true, }, @@ -269,7 +383,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "succeeded", + status: "requires_capture", }, }, }, @@ -282,7 +396,7 @@ export const connectorDetails = { payment_method_data: { card: successfulThreeDSTestCardDetails, }, - currency: "USD", + currency: "EUR", mandate_data: multiUseMandateData, }, Response: { @@ -292,15 +406,38 @@ export const connectorDetails = { }, }, }, - ZeroAuthMandate: { + MandateMultiUse3DSManualCapture: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Aci is not implemented", - code: "IR_00", - }, + status: "requires_capture", + }, + }, + }, + ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", }, }, }, @@ -308,7 +445,7 @@ export const connectorDetails = { Request: { amount: 0, setup_future_usage: "off_session", - currency: "USD", + currency: "EUR", }, Response: { status: 200, @@ -328,13 +465,9 @@ export const connectorDetails = { }, }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Aci is not implemented", - code: "IR_00", - }, + status: "succeeded", }, }, }, @@ -344,7 +477,7 @@ export const connectorDetails = { payment_method_data: { card: successfulNo3DSCardDetails, }, - currency: "USD", + currency: "EUR", setup_future_usage: "on_session", customer_acceptance: customerAcceptance, }, @@ -355,6 +488,23 @@ export const connectorDetails = { }, }, }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + setup_future_usage: "on_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, SaveCardUseNo3DSAutoCaptureOffSession: { Configs: { TRIGGER_SKIP: true, @@ -375,6 +525,25 @@ export const connectorDetails = { }, }, }, + SaveCardUseNo3DSManualCaptureOffSession: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + setup_future_usage: "off_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, SaveCardUse3DSAutoCaptureOffSession: { Configs: { TRIGGER_SKIP: true, @@ -407,15 +576,12 @@ export const connectorDetails = { }, }, PaymentMethodIdMandateNo3DSAutoCapture: { - Configs: { - TRIGGER_SKIP: true, - }, Request: { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, }, - currency: "USD", + currency: "EUR", mandate_data: null, customer_acceptance: customerAcceptance, }, @@ -426,20 +592,74 @@ export const connectorDetails = { }, }, }, - PaymentMethodIdMandate3DSAutoCapture: { - Configs: { - TRIGGER_SKIP: true, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: null, + customer_acceptance: customerAcceptance, }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandate3DSAutoCapture: { Request: { payment_method: "card", payment_method_data: { card: successfulThreeDSTestCardDetails, }, - currency: "USD", + currency: "EUR", mandate_data: null, authentication_type: "three_ds", customer_acceptance: customerAcceptance, }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + PaymentMethodIdMandate3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + MITManualCapture: { + Request: { + currency: "EUR", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MITAutoCapture: { + Request: { + currency: "EUR", + }, Response: { status: 200, body: { @@ -448,6 +668,9 @@ export const connectorDetails = { }, }, No3DSFailPayment: { + Configs: { + TRIGGER_SKIP: true, + }, Request: { payment_method: "card", payment_method_data: { @@ -460,9 +683,8 @@ export const connectorDetails = { status: 200, body: { status: "failed", - error_code: "200.300.404", - error_message: - "Field is card.expiryYear and the message is must match ^[0-9]{4}$", + error_code: "100.390.112", + error_message: "Technical Error in 3D system", unified_code: "UE_9000", unified_message: "Something went wrong", }, diff --git a/cypress-tests/cypress/support/redirectionHandler.js b/cypress-tests/cypress/support/redirectionHandler.js index e89cc9b43c..78970f5aae 100644 --- a/cypress-tests/cypress/support/redirectionHandler.js +++ b/cypress-tests/cypress/support/redirectionHandler.js @@ -900,6 +900,18 @@ function threeDsRedirection(redirectionUrl, expectedUrl, connectorId) { connectorId, ({ connectorId, constants, expectedUrl }) => { switch (connectorId) { + case "aci": + cy.get('form[name="challengeForm"]', { + timeout: constants.WAIT_TIME, + }) + .should("exist") + .then(() => { + cy.get("#outcomeSelect") + .select("Approve") + .should("have.value", "Y"); + cy.get('button[type="submit"]').click(); + }); + break; case "adyen": cy.get("iframe") .its("0.contentDocument.body")