feat(connector): [Cybersource] added 3ds support to setup mandate flow in Cybersource (#11419)

This commit is contained in:
Sagnik Mitra
2026-03-06 20:51:22 +05:30
committed by GitHub
parent 8bc500845c
commit 0d538e1af2
4 changed files with 137 additions and 22 deletions

View File

@@ -132,6 +132,8 @@ pub struct CybersourceZeroMandateRequest {
payment_information: PaymentInformation,
order_information: OrderInformationWithBill,
client_reference_information: ClientReferenceInformation,
#[serde(skip_serializing_if = "Option::is_none")]
consumer_authentication_information: Option<CybersourceConsumerAuthInformation>,
}
impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
@@ -191,7 +193,11 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
code: Some(item.connector_request_reference_id.clone()),
};
let (payment_information, solution) = match item.request.payment_method_data.clone() {
let (payment_information, solution, consumer_authentication_information) = match item
.request
.payment_method_data
.clone()
{
PaymentMethodData::Card(ccard) => {
let card_type = match ccard
.card_network
@@ -202,6 +208,70 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
None => ccard.get_card_issuer().ok().map(String::from),
};
// For all card payments, we are explicitly setting `pares_status` to `AuthenticationSuccessful`
// to indicate that the Payer Authentication was successful, regardless of actual ACS response.
// This is a default behavior and may be adjusted based on future integration requirements.
let pares_status = Some(CybersourceParesStatus::AuthenticationSuccessful);
let consumer_authentication_information =
item.request.authentication_data.as_ref().map(|authn_data| {
let effective_authentication_type =
authn_data.authentication_type.map(Into::into);
let (ucaf_authentication_data, cavv, ucaf_collection_indicator) =
if ccard.card_network == Some(common_enums::CardNetwork::Mastercard) {
(Some(authn_data.cavv.clone()), None, Some("2".to_string()))
} else {
(None, Some(authn_data.cavv.clone()), None)
};
let authentication_date = date_time::format_date(
authn_data.created_at,
date_time::DateFormat::YYYYMMDDHHmmss,
)
.ok();
let network_score = (ccard.card_network
== Some(common_enums::CardNetwork::CartesBancaires))
.then_some(authn_data.message_extension.as_ref())
.flatten()
.map(|secret| secret.clone().expose())
.and_then(|exposed| {
serde_json::from_value::<Vec<MessageExtensionAttribute>>(exposed)
.map_err(|err| {
router_env::logger::error!(
"Failed to deserialize message_extension: {:?}",
err
);
})
.ok()
.and_then(|exts| extract_score_id(&exts))
});
let cavv_algorithm = Some("2".to_string());
CybersourceConsumerAuthInformation {
pares_status,
ucaf_collection_indicator,
cavv,
ucaf_authentication_data,
xid: None,
directory_server_transaction_id: authn_data
.ds_trans_id
.clone()
.map(Secret::new),
specification_version: authn_data.message_version.clone(),
pa_specification_version: authn_data.message_version.clone(),
veres_enrolled: Some("Y".to_string()),
eci_raw: authn_data.eci.clone(),
authentication_date,
effective_authentication_type,
challenge_code: authn_data.challenge_code.clone(),
signed_pares_status_reason: authn_data.challenge_code_reason.clone(),
challenge_cancel_code: authn_data.challenge_cancel.clone(),
network_score,
acs_transaction_id: authn_data.acs_trans_id.clone(),
cavv_algorithm,
}
});
(
PaymentInformation::Cards(Box::new(CardPaymentInformation {
card: Card {
@@ -214,6 +284,7 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
},
})),
None,
consumer_authentication_information,
)
}
PaymentMethodData::Wallet(wallet_data) => match wallet_data {
@@ -241,6 +312,7 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
},
)),
Some(PaymentSolution::ApplePay),
None,
)
}
PaymentMethodToken::Token(_) => Err(unimplemented_payment_method!(
@@ -275,6 +347,7 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
},
)),
Some(PaymentSolution::ApplePay),
None,
)
}
},
@@ -302,11 +375,13 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
},
)),
Some(PaymentSolution::GooglePay),
None,
),
WalletData::SamsungPay(samsung_pay_data) => (
(get_samsung_pay_payment_information(&samsung_pay_data)
.attach_printable("Failed to get samsung pay payment information")?),
Some(PaymentSolution::SamsungPay),
None,
),
WalletData::AliPayQr(_)
| WalletData::AliPayRedirect(_)
@@ -382,6 +457,7 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest {
payment_information,
order_information,
client_reference_information,
consumer_authentication_information,
})
}
}

View File

@@ -1705,6 +1705,7 @@ pub struct SetupMandateRequestData {
pub tokenization: Option<common_enums::Tokenization>,
pub partner_merchant_identifier_details:
Option<common_types::payments::PartnerMerchantIdentifierDetails>,
pub authentication_data: Option<AuthenticationData>,
}
#[derive(Debug, Clone)]

View File

@@ -4144,27 +4144,54 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize {
}),
..Default::default()
};
Box::pin(payments_core::<
api::Authorize,
api::PaymentsResponse,
_,
_,
_,
_,
>(
state.clone(),
req_state,
platform.clone(),
None,
PaymentConfirm,
payment_confirm_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
None,
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
))
.await?
let is_setup_mandate = payment_intent.amount == MinorUnit::zero()
&& payment_intent.setup_future_usage
== Some(storage_enums::FutureUsage::OffSession);
if is_setup_mandate {
Box::pin(payments_core::<
api::SetupMandate,
api::PaymentsResponse,
_,
_,
_,
_,
>(
state.clone(),
req_state,
platform.clone(),
None,
PaymentConfirm,
payment_confirm_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
None,
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
))
.await?
} else {
Box::pin(payments_core::<
api::Authorize,
api::PaymentsResponse,
_,
_,
_,
_,
>(
state.clone(),
req_state,
platform.clone(),
None,
PaymentConfirm,
payment_confirm_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
None,
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
))
.await?
}
} else {
let payment_sync_req = api::PaymentsRetrieveRequest {
resource_id: req.resource_id,

View File

@@ -1577,6 +1577,7 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>(
billing_descriptor: None,
split_payments: None,
partner_merchant_identifier_details: None,
authentication_data: None,
};
let connector_mandate_request_reference_id = payment_data
.payment_attempt
@@ -6152,6 +6153,16 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::SetupMandateRequ
partner_merchant_identifier_details: payment_data
.payment_intent
.partner_merchant_identifier_details,
authentication_data: payment_data
.authentication
.as_ref()
.map(AuthenticationData::foreign_try_from)
.transpose()?
.or(payment_data
.external_authentication_data
.as_ref()
.map(AuthenticationData::foreign_try_from)
.transpose()?),
})
}
}