feat(connector): [cybersource] Implement 3DS flow for cards (#3290)

Co-authored-by: DEEPANSHU BANSAL <deepanshubansal515@gmail.com>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com>
This commit is contained in:
SamraatBansal
2024-01-11 20:50:45 +05:30
committed by GitHub
parent 9f6ef3f224
commit 6fb3b00e82
13 changed files with 1175 additions and 44 deletions

View File

@ -351,6 +351,7 @@ stripe = { payment_method = "bank_transfer" }
nuvei = { payment_method = "card" }
shift4 = { payment_method = "card" }
bluesnap = { payment_method = "card" }
cybersource = {payment_method = "card"}
nmi = {payment_method = "card"}
[dummy_connector]

View File

@ -428,6 +428,7 @@ stripe = {payment_method = "bank_transfer"}
nuvei = {payment_method = "card"}
shift4 = {payment_method = "card"}
bluesnap = {payment_method = "card"}
cybersource = {payment_method = "card"}
nmi = {payment_method = "card"}
[connector_customer]

View File

@ -241,6 +241,7 @@ stripe = {payment_method = "bank_transfer"}
nuvei = {payment_method = "card"}
shift4 = {payment_method = "card"}
bluesnap = {payment_method = "card"}
cybersource = {payment_method = "card"}
nmi = {payment_method = "card"}
[dummy_connector]

View File

@ -12,6 +12,7 @@ use time::OffsetDateTime;
use transformers as cybersource;
use url::Url;
use super::utils::{PaymentsAuthorizeRequestData, RouterData};
use crate::{
configs::settings,
connector::{utils as connector_utils, utils::RefundsRequestData},
@ -286,6 +287,8 @@ impl api::PaymentIncrementalAuthorization for Cybersource {}
impl api::MandateSetup for Cybersource {}
impl api::ConnectorAccessToken for Cybersource {}
impl api::PaymentToken for Cybersource {}
impl api::PaymentsPreProcessing for Cybersource {}
impl api::PaymentsCompleteAuthorize for Cybersource {}
impl api::ConnectorMandateRevoke for Cybersource {}
impl
@ -472,6 +475,113 @@ impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::Payme
{
}
impl
ConnectorIntegration<
api::PreProcessing,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
> for Cybersource
{
fn get_headers(
&self,
req: &types::PaymentsPreProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
req: &types::PaymentsPreProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let redirect_response = req.request.redirect_response.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "redirect_response",
},
)?;
match redirect_response.params {
Some(param) if !param.clone().peek().is_empty() => Ok(format!(
"{}risk/v1/authentications",
self.base_url(connectors)
)),
Some(_) | None => Ok(format!(
"{}risk/v1/authentication-results",
self.base_url(connectors)
)),
}
}
fn get_request_body(
&self,
req: &types::PaymentsPreProcessingRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_router_data = cybersource::CybersourceRouterData::try_from((
&self.get_currency_unit(),
req.request
.currency
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "currency",
})?,
req.request
.amount
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "amount",
})?,
req,
))?;
let connector_req =
cybersource::CybersourcePreProcessingRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PaymentsPreProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsPreProcessingType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsPreProcessingType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsPreProcessingType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsPreProcessingRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsPreProcessingRouterData, errors::ConnectorError> {
let response: cybersource::CybersourcePreProcessingResponse = res
.response
.parse_struct("Cybersource AuthEnrollmentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Cybersource
{
@ -672,14 +782,21 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
if req.is_three_ds() && req.request.is_card() {
Ok(format!(
"{}risk/v1/authentication-setups",
api::ConnectorCommon::base_url(self, connectors)
))
} else {
Ok(format!(
"{}pts/v2/payments/",
api::ConnectorCommon::base_url(self, connectors)
))
}
}
fn get_request_body(
&self,
@ -692,10 +809,16 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
req.request.amount,
req,
))?;
if req.is_three_ds() && req.request.is_card() {
let connector_req =
cybersource::CybersourceAuthSetupRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
} else {
let connector_req =
cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
}
fn build_request(
&self,
@ -722,6 +845,106 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
if data.is_three_ds() && data.request.is_card() {
let response: cybersource::CybersourceAuthSetupResponse = res
.response
.parse_struct("Cybersource AuthSetupResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
} else {
let response: cybersource::CybersourcePaymentsResponse = res
.response
.parse_struct("Cybersource PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl
ConnectorIntegration<
api::CompleteAuthorize,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
> for Cybersource
{
fn get_headers(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}pts/v2/payments/",
api::ConnectorCommon::base_url(self, connectors)
))
}
fn get_request_body(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_router_data = cybersource::CybersourceRouterData::try_from((
&self.get_currency_unit(),
req.request.currency,
req.request.amount,
req,
))?;
let connector_req =
cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCompleteAuthorizeRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
let response: cybersource::CybersourcePaymentsResponse = res
.response
.parse_struct("Cybersource PaymentResponse")

View File

@ -1,6 +1,7 @@
use api_models::payments;
use base64::Engine;
use common_utils::pii;
use common_utils::{ext_traits::ValueExt, pii};
use error_stack::{IntoReport, ResultExt};
use masking::{PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -8,10 +9,12 @@ use serde_json::Value;
use crate::{
connector::utils::{
self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData,
PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData,
PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData,
},
consts,
core::errors,
services,
types::{
self,
api::{self, enums as api_enums},
@ -200,7 +203,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest {
action_list,
action_token_types,
authorization_options,
commerce_indicator: CybersourceCommerceIndicator::Internet,
commerce_indicator: String::from("internet"),
payment_solution: solution.map(String::from),
};
Ok(Self {
@ -220,6 +223,8 @@ pub struct CybersourcePaymentsRequest {
order_information: OrderInformationWithBill,
client_reference_information: ClientReferenceInformation,
#[serde(skip_serializing_if = "Option::is_none")]
consumer_authentication_information: Option<CybersourceConsumerAuthInformation>,
#[serde(skip_serializing_if = "Option::is_none")]
merchant_defined_information: Option<Vec<MerchantDefinedInformation>>,
}
@ -229,12 +234,22 @@ pub struct ProcessingInformation {
action_list: Option<Vec<CybersourceActionsList>>,
action_token_types: Option<Vec<CybersourceActionsTokenType>>,
authorization_options: Option<CybersourceAuthorizationOptions>,
commerce_indicator: CybersourceCommerceIndicator,
commerce_indicator: String,
capture: Option<bool>,
capture_options: Option<CaptureOptions>,
payment_solution: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthInformation {
ucaf_collection_indicator: Option<String>,
cavv: Option<String>,
ucaf_authentication_data: Option<String>,
xid: Option<String>,
directory_server_transaction_id: Option<String>,
specification_version: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MerchantDefinedInformation {
@ -282,12 +297,6 @@ pub enum CybersourcePaymentInitiatorTypes {
Customer,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CybersourceCommerceIndicator {
Internet,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureOptions {
@ -450,6 +459,16 @@ impl From<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
}
}
impl From<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
for ClientReferenceInformation
{
fn from(item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>) -> Self {
Self {
code: Some(item.router_data.connector_request_reference_id.clone()),
}
}
}
impl
From<(
&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>,
@ -489,7 +508,56 @@ impl
action_token_types,
authorization_options,
capture_options: None,
commerce_indicator: CybersourceCommerceIndicator::Internet,
commerce_indicator: String::from("internet"),
}
}
}
impl
From<(
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
Option<PaymentSolution>,
&CybersourceConsumerAuthValidateResponse,
)> for ProcessingInformation
{
fn from(
(item, solution, three_ds_data): (
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
Option<PaymentSolution>,
&CybersourceConsumerAuthValidateResponse,
),
) -> Self {
let (action_list, action_token_types, authorization_options) =
if item.router_data.request.setup_future_usage.is_some() {
(
Some(vec![CybersourceActionsList::TokenCreate]),
Some(vec![CybersourceActionsTokenType::PaymentInstrument]),
Some(CybersourceAuthorizationOptions {
initiator: CybersourcePaymentInitiator {
initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer),
credential_stored_on_file: Some(true),
stored_credential_used: None,
},
merchant_intitiated_transaction: None,
}),
)
} else {
(None, None, None)
};
Self {
capture: Some(matches!(
item.router_data.request.capture_method,
Some(enums::CaptureMethod::Automatic) | None
)),
payment_solution: solution.map(String::from),
action_list,
action_token_types,
authorization_options,
capture_options: None,
commerce_indicator: three_ds_data
.indicator
.to_owned()
.unwrap_or(String::from("internet")),
}
}
}
@ -516,6 +584,28 @@ impl
}
}
impl
From<(
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
BillTo,
)> for OrderInformationWithBill
{
fn from(
(item, bill_to): (
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
BillTo,
),
) -> Self {
Self {
amount_details: Amount {
total_amount: item.amount.to_owned(),
currency: item.router_data.request.currency,
},
bill_to: Some(bill_to),
}
}
}
// for cybersource each item in Billing is mandatory
fn build_bill_to(
address_details: &payments::Address,
@ -602,6 +692,84 @@ impl
payment_information,
order_information,
client_reference_information,
consumer_authentication_information: None,
merchant_defined_information,
})
}
}
impl
TryFrom<(
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
payments::Card,
)> for CybersourcePaymentsRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(item, ccard): (
&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
payments::Card,
),
) -> Result<Self, Self::Error> {
let email = item.router_data.request.get_email()?;
let bill_to = build_bill_to(item.router_data.get_billing()?, email)?;
let order_information = OrderInformationWithBill::from((item, bill_to));
let card_issuer = ccard.get_card_issuer();
let card_type = match card_issuer {
Ok(issuer) => Some(String::from(issuer)),
Err(_) => None,
};
let payment_information = PaymentInformation::Cards(CardPaymentInformation {
card: Card {
number: ccard.card_number,
expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc,
card_type,
},
});
let client_reference_information = ClientReferenceInformation::from(item);
let three_ds_info: CybersourceThreeDSMetadata = item
.router_data
.request
.connector_meta
.clone()
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "connector_meta",
})?
.parse_value("CybersourceThreeDSMetadata")
.change_context(errors::ConnectorError::InvalidConnectorConfig {
config: "Merchant connector account metadata",
})?;
let processing_information =
ProcessingInformation::from((item, None, &three_ds_info.three_ds_data));
let consumer_authentication_information = Some(CybersourceConsumerAuthInformation {
ucaf_collection_indicator: three_ds_info.three_ds_data.ucaf_collection_indicator,
cavv: three_ds_info.three_ds_data.cavv,
ucaf_authentication_data: three_ds_info.three_ds_data.ucaf_authentication_data,
xid: three_ds_info.three_ds_data.xid,
directory_server_transaction_id: three_ds_info
.three_ds_data
.directory_server_transaction_id,
specification_version: three_ds_info.three_ds_data.specification_version,
});
let merchant_defined_information =
item.router_data.request.metadata.clone().map(|metadata| {
Vec::<MerchantDefinedInformation>::foreign_from(metadata.peek().to_owned())
});
Ok(Self {
processing_information,
payment_information,
order_information,
client_reference_information,
consumer_authentication_information,
merchant_defined_information,
})
}
@ -647,6 +815,7 @@ impl
payment_information,
order_information,
client_reference_information,
consumer_authentication_information: None,
merchant_defined_information,
})
}
@ -689,6 +858,7 @@ impl
payment_information,
order_information,
client_reference_information,
consumer_authentication_information: None,
merchant_defined_information,
})
}
@ -747,6 +917,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
order_information,
client_reference_information,
merchant_defined_information,
consumer_authentication_information: None,
})
}
}
@ -810,6 +981,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
order_information,
client_reference_information,
merchant_defined_information,
consumer_authentication_information: None,
})
}
payments::PaymentMethodData::CardRedirect(_)
@ -832,6 +1004,64 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceAuthSetupRequest {
payment_information: PaymentInformation,
client_reference_information: ClientReferenceInformation,
}
impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
for CybersourceAuthSetupRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
payments::PaymentMethodData::Card(ccard) => {
let card_issuer = ccard.get_card_issuer();
let card_type = match card_issuer {
Ok(issuer) => Some(String::from(issuer)),
Err(_) => None,
};
let payment_information = PaymentInformation::Cards(CardPaymentInformation {
card: Card {
number: ccard.card_number,
expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc,
card_type,
},
});
let client_reference_information = ClientReferenceInformation::from(item);
Ok(Self {
payment_information,
client_reference_information,
})
}
payments::PaymentMethodData::Wallet(_)
| payments::PaymentMethodData::CardRedirect(_)
| payments::PaymentMethodData::PayLater(_)
| payments::PaymentMethodData::BankRedirect(_)
| payments::PaymentMethodData::BankDebit(_)
| payments::PaymentMethodData::BankTransfer(_)
| payments::PaymentMethodData::Crypto(_)
| payments::PaymentMethodData::MandatePayment
| payments::PaymentMethodData::Reward
| payments::PaymentMethodData::Upi(_)
| payments::PaymentMethodData::Voucher(_)
| payments::PaymentMethodData::GiftCard(_)
| payments::PaymentMethodData::CardToken(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Cybersource"),
)
.into())
}
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourcePaymentsCaptureRequest {
@ -870,7 +1100,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>>
action_token_types: None,
authorization_options: None,
capture: None,
commerce_indicator: CybersourceCommerceIndicator::Internet,
commerce_indicator: String::from("internet"),
payment_solution: None,
},
order_information: OrderInformationWithBill {
@ -909,7 +1139,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout
reason: "5".to_owned(),
}),
}),
commerce_indicator: CybersourceCommerceIndicator::Internet,
commerce_indicator: String::from("internet"),
capture: None,
capture_options: None,
payment_solution: None,
@ -1118,6 +1348,29 @@ pub struct CybersourceErrorInformationResponse {
error_information: CybersourceErrorInformation,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthInformationResponse {
access_token: String,
device_data_collection_url: String,
reference_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientAuthSetupInfoResponse {
id: String,
client_reference_information: ClientReferenceInformation,
consumer_authentication_information: CybersourceConsumerAuthInformationResponse,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CybersourceAuthSetupResponse {
ClientAuthSetupInfo(ClientAuthSetupInfoResponse),
ErrorInformation(CybersourceErrorInformationResponse),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourcePaymentsIncrementalAuthorizationResponse {
@ -1326,6 +1579,492 @@ impl<F>
}
}
impl<F>
TryFrom<
types::ResponseRouterData<
F,
CybersourceAuthSetupResponse,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
> for types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
CybersourceAuthSetupResponse,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match item.response {
CybersourceAuthSetupResponse::ClientAuthSetupInfo(info_response) => Ok(Self {
status: enums::AttemptStatus::AuthenticationPending,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirection_data: Some(services::RedirectForm::CybersourceAuthSetup {
access_token: info_response
.consumer_authentication_information
.access_token,
ddc_url: info_response
.consumer_authentication_information
.device_data_collection_url,
reference_id: info_response
.consumer_authentication_information
.reference_id,
}),
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(
info_response
.client_reference_information
.code
.unwrap_or(info_response.id.clone()),
),
incremental_authorization_allowed: None,
}),
..item.data
}),
CybersourceAuthSetupResponse::ErrorInformation(error_response) => {
let error_reason = error_response
.error_information
.message
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string());
let error_message = error_response.error_information.reason;
Ok(Self {
response: Err(types::ErrorResponse {
code: error_message
.clone()
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: Some(error_reason),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(error_response.id.clone()),
}),
status: enums::AttemptStatus::AuthenticationFailed,
..item.data
})
}
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthInformationRequest {
return_url: String,
reference_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceAuthEnrollmentRequest {
payment_information: PaymentInformation,
client_reference_information: ClientReferenceInformation,
consumer_authentication_information: CybersourceConsumerAuthInformationRequest,
order_information: OrderInformationWithBill,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CybersourceRedirectionAuthResponse {
pub transaction_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthInformationValidateRequest {
authentication_transaction_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceAuthValidateRequest {
payment_information: PaymentInformation,
client_reference_information: ClientReferenceInformation,
consumer_authentication_information: CybersourceConsumerAuthInformationValidateRequest,
order_information: OrderInformation,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum CybersourcePreProcessingRequest {
AuthEnrollment(CybersourceAuthEnrollmentRequest),
AuthValidate(CybersourceAuthValidateRequest),
}
impl TryFrom<&CybersourceRouterData<&types::PaymentsPreProcessingRouterData>>
for CybersourcePreProcessingRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &CybersourceRouterData<&types::PaymentsPreProcessingRouterData>,
) -> Result<Self, Self::Error> {
let client_reference_information = ClientReferenceInformation {
code: Some(item.router_data.connector_request_reference_id.clone()),
};
let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or(
errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "payment_method_data",
},
)?;
let payment_information = match payment_method_data {
payments::PaymentMethodData::Card(ccard) => {
let card_issuer = ccard.get_card_issuer();
let card_type = match card_issuer {
Ok(issuer) => Some(String::from(issuer)),
Err(_) => None,
};
Ok(PaymentInformation::Cards(CardPaymentInformation {
card: Card {
number: ccard.card_number,
expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc,
card_type,
},
}))
}
payments::PaymentMethodData::Wallet(_)
| payments::PaymentMethodData::CardRedirect(_)
| payments::PaymentMethodData::PayLater(_)
| payments::PaymentMethodData::BankRedirect(_)
| payments::PaymentMethodData::BankDebit(_)
| payments::PaymentMethodData::BankTransfer(_)
| payments::PaymentMethodData::Crypto(_)
| payments::PaymentMethodData::MandatePayment
| payments::PaymentMethodData::Reward
| payments::PaymentMethodData::Upi(_)
| payments::PaymentMethodData::Voucher(_)
| payments::PaymentMethodData::GiftCard(_)
| payments::PaymentMethodData::CardToken(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Cybersource"),
))
}
}?;
let redirect_response = item.router_data.request.redirect_response.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "redirect_response",
},
)?;
let amount_details = Amount {
total_amount: item.amount.clone(),
currency: item.router_data.request.currency.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "currency",
},
)?,
};
match redirect_response.params {
Some(param) if !param.clone().peek().is_empty() => {
let reference_id = param
.clone()
.peek()
.split_once('=')
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.redirect_response.params.reference_id",
})?
.1
.to_string();
let email = item.router_data.request.get_email()?;
let bill_to = build_bill_to(item.router_data.get_billing()?, email)?;
let order_information = OrderInformationWithBill {
amount_details,
bill_to: Some(bill_to),
};
Ok(Self::AuthEnrollment(CybersourceAuthEnrollmentRequest {
payment_information,
client_reference_information,
consumer_authentication_information:
CybersourceConsumerAuthInformationRequest {
return_url: item.router_data.request.get_complete_authorize_url()?,
reference_id,
},
order_information,
}))
}
Some(_) | None => {
let redirect_payload: CybersourceRedirectionAuthResponse = redirect_response
.payload
.ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload {
field_name: "request.redirect_response.payload",
})?
.peek()
.clone()
.parse_value("CybersourceRedirectionAuthResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let order_information = OrderInformation { amount_details };
Ok(Self::AuthValidate(CybersourceAuthValidateRequest {
payment_information,
client_reference_information,
consumer_authentication_information:
CybersourceConsumerAuthInformationValidateRequest {
authentication_transaction_id: redirect_payload.transaction_id,
},
order_information,
}))
}
}
}
}
impl TryFrom<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
for CybersourcePaymentsRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "payment_method_data",
},
)?;
match payment_method_data {
payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)),
payments::PaymentMethodData::Wallet(_)
| payments::PaymentMethodData::CardRedirect(_)
| payments::PaymentMethodData::PayLater(_)
| payments::PaymentMethodData::BankRedirect(_)
| payments::PaymentMethodData::BankDebit(_)
| payments::PaymentMethodData::BankTransfer(_)
| payments::PaymentMethodData::Crypto(_)
| payments::PaymentMethodData::MandatePayment
| payments::PaymentMethodData::Reward
| payments::PaymentMethodData::Upi(_)
| payments::PaymentMethodData::Voucher(_)
| payments::PaymentMethodData::GiftCard(_)
| payments::PaymentMethodData::CardToken(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Cybersource"),
)
.into())
}
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CybersourceAuthEnrollmentStatus {
PendingAuthentication,
AuthenticationSuccessful,
AuthenticationFailed,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthValidateResponse {
ucaf_collection_indicator: Option<String>,
cavv: Option<String>,
ucaf_authentication_data: Option<String>,
xid: Option<String>,
specification_version: Option<String>,
directory_server_transaction_id: Option<String>,
indicator: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CybersourceThreeDSMetadata {
three_ds_data: CybersourceConsumerAuthValidateResponse,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceConsumerAuthInformationEnrollmentResponse {
access_token: Option<String>,
step_up_url: Option<String>,
//Added to segregate the three_ds_data in a separate struct
#[serde(flatten)]
validate_response: CybersourceConsumerAuthValidateResponse,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientAuthCheckInfoResponse {
id: String,
client_reference_information: ClientReferenceInformation,
consumer_authentication_information: CybersourceConsumerAuthInformationEnrollmentResponse,
status: CybersourceAuthEnrollmentStatus,
error_information: Option<CybersourceErrorInformation>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CybersourcePreProcessingResponse {
ClientAuthCheckInfo(Box<ClientAuthCheckInfoResponse>),
ErrorInformation(CybersourceErrorInformationResponse),
}
impl From<CybersourceAuthEnrollmentStatus> for enums::AttemptStatus {
fn from(item: CybersourceAuthEnrollmentStatus) -> Self {
match item {
CybersourceAuthEnrollmentStatus::PendingAuthentication => Self::AuthenticationPending,
CybersourceAuthEnrollmentStatus::AuthenticationSuccessful => {
Self::AuthenticationSuccessful
}
CybersourceAuthEnrollmentStatus::AuthenticationFailed => Self::AuthenticationFailed,
}
}
}
impl<F>
TryFrom<
types::ResponseRouterData<
F,
CybersourcePreProcessingResponse,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
>,
> for types::RouterData<F, types::PaymentsPreProcessingData, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
CybersourcePreProcessingResponse,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match item.response {
CybersourcePreProcessingResponse::ClientAuthCheckInfo(info_response) => {
let status = enums::AttemptStatus::from(info_response.status);
let risk_info: Option<ClientRiskInformation> = None;
if utils::is_payment_failure(status) {
let response = Err(types::ErrorResponse::from((
&info_response.error_information,
&risk_info,
item.http_code,
info_response.id.clone(),
)));
Ok(Self {
status,
response,
..item.data
})
} else {
let connector_response_reference_id = Some(
info_response
.client_reference_information
.code
.unwrap_or(info_response.id.clone()),
);
let redirection_data = match (
info_response
.consumer_authentication_information
.access_token,
info_response
.consumer_authentication_information
.step_up_url,
) {
(Some(access_token), Some(step_up_url)) => {
Some(services::RedirectForm::CybersourceConsumerAuth {
access_token,
step_up_url,
})
}
_ => None,
};
let three_ds_data = serde_json::to_value(
info_response
.consumer_authentication_information
.validate_response,
)
.into_report()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
Ok(Self {
status,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirection_data,
mandate_reference: None,
connector_metadata: Some(
serde_json::json!({"three_ds_data":three_ds_data}),
),
network_txn_id: None,
connector_response_reference_id,
incremental_authorization_allowed: None,
}),
..item.data
})
}
}
CybersourcePreProcessingResponse::ErrorInformation(ref error_response) => {
let error_reason = error_response
.error_information
.message
.to_owned()
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string());
let error_message = error_response.error_information.reason.to_owned();
let response = Err(types::ErrorResponse {
code: error_message
.clone()
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: Some(error_reason),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(error_response.id.clone()),
});
Ok(Self {
response,
status: enums::AttemptStatus::AuthenticationFailed,
..item.data
})
}
}
}
}
impl<F>
TryFrom<
types::ResponseRouterData<
F,
CybersourcePaymentsResponse,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
>,
> for types::RouterData<F, types::CompleteAuthorizeData, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
CybersourcePaymentsResponse,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match item.response {
CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => {
let status = enums::AttemptStatus::foreign_from((
info_response.status.clone(),
item.data.request.is_auto_capture()?,
));
let response = get_payment_response((&info_response, status, item.http_code));
Ok(Self {
status,
response,
..item.data
})
}
CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from((
&error_response.clone(),
item,
Some(enums::AttemptStatus::Failure),
))),
}
}
}
impl<F>
TryFrom<
types::ResponseRouterData<
@ -1463,25 +2202,29 @@ impl<F, T>
..item.data
})
}
CybersourceSetupMandatesResponse::ErrorInformation(error_response) => Ok(Self {
response: {
let error_reason = &error_response.error_information.reason;
Err(types::ErrorResponse {
code: error_reason
CybersourceSetupMandatesResponse::ErrorInformation(ref error_response) => {
let error_reason = error_response
.error_information
.message
.to_owned()
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string());
let error_message = error_response.error_information.reason.to_owned();
let response = Err(types::ErrorResponse {
code: error_message
.clone()
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: error_reason
.clone()
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: error_response.error_information.message,
message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: Some(error_reason),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(error_response.id.clone()),
})
},
});
Ok(Self {
response,
status: enums::AttemptStatus::Failure,
..item.data
}),
})
}
}
}
}

View File

@ -273,6 +273,7 @@ pub trait PaymentsPreProcessingData {
fn get_webhook_url(&self) -> Result<String, Error>;
fn get_return_url(&self) -> Result<String, Error>;
fn get_browser_info(&self) -> Result<BrowserInformation, Error>;
fn get_complete_authorize_url(&self) -> Result<String, Error>;
}
impl PaymentsPreProcessingData for types::PaymentsPreProcessingData {
@ -317,6 +318,11 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData {
.clone()
.ok_or_else(missing_field_err("browser_info"))
}
fn get_complete_authorize_url(&self) -> Result<String, Error> {
self.complete_authorize_url
.clone()
.ok_or_else(missing_field_err("complete_authorize_url"))
}
}
pub trait PaymentsCaptureRequestData {
@ -592,6 +598,7 @@ pub trait PaymentsCompleteAuthorizeRequestData {
fn is_auto_capture(&self) -> Result<bool, Error>;
fn get_email(&self) -> Result<Email, Error>;
fn get_redirect_response_payload(&self) -> Result<pii::SecretSerdeValue, Error>;
fn get_complete_authorize_url(&self) -> Result<String, Error>;
}
impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
@ -616,6 +623,11 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
.into(),
)
}
fn get_complete_authorize_url(&self) -> Result<String, Error> {
self.complete_authorize_url
.clone()
.ok_or_else(missing_field_err("complete_authorize_url"))
}
}
pub trait PaymentsSyncRequestData {

View File

@ -1489,6 +1489,22 @@ where
router_data = router_data.preprocessing_steps(state, connector).await?;
(router_data, false)
} else if connector.connector_name == router_types::Connector::Cybersource
&& is_operation_complete_authorize(&operation)
&& router_data.auth_type == storage_enums::AuthenticationType::ThreeDs
{
router_data = router_data.preprocessing_steps(state, connector).await?;
// Should continue the flow only if no redirection_data is returned else a response with redirection form shall be returned
let should_continue = matches!(
router_data.response,
Ok(router_types::PaymentsResponseData::TransactionResponse {
redirection_data: None,
..
})
) && router_data.status
!= common_enums::AttemptStatus::AuthenticationFailed;
(router_data, should_continue)
} else {
(router_data, should_continue_payment)
}
@ -2106,6 +2122,10 @@ pub fn is_operation_confirm<Op: Debug>(operation: &Op) -> bool {
matches!(format!("{operation:?}").as_str(), "PaymentConfirm")
}
pub fn is_operation_complete_authorize<Op: Debug>(operation: &Op) -> bool {
matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
}
#[cfg(feature = "olap")]
pub async fn list_payments(
state: AppState,

View File

@ -154,7 +154,6 @@ default_imp_for_complete_authorize!(
connector::Checkout,
connector::Coinbase,
connector::Cryptopay,
connector::Cybersource,
connector::Dlocal,
connector::Fiserv,
connector::Forte,
@ -873,7 +872,6 @@ default_imp_for_pre_processing_steps!(
connector::Checkout,
connector::Coinbase,
connector::Cryptopay,
connector::Cybersource,
connector::Dlocal,
connector::Iatapay,
connector::Fiserv,

View File

@ -412,6 +412,7 @@ impl TryFrom<types::PaymentsAuthorizeData> for types::PaymentsPreProcessingData
browser_info: data.browser_info,
surcharge_details: data.surcharge_details,
connector_transaction_id: None,
redirect_response: None,
})
}
}
@ -431,10 +432,11 @@ impl TryFrom<types::CompleteAuthorizeData> for types::PaymentsPreProcessingData
order_details: None,
router_return_url: None,
webhook_url: None,
complete_authorize_url: None,
complete_authorize_url: data.complete_authorize_url,
browser_info: data.browser_info,
surcharge_details: None,
connector_transaction_id: data.connector_transaction_id,
redirect_response: data.redirect_response,
})
}
}

View File

@ -203,10 +203,19 @@ pub async fn complete_authorize_preprocessing_steps<F: Clone>(
],
);
let mut router_data_request = router_data.request.to_owned();
if let Ok(types::PaymentsResponseData::TransactionResponse {
connector_metadata, ..
}) = &resp.response
{
router_data_request.connector_meta = connector_metadata.to_owned();
};
let authorize_router_data =
payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>(
resp.clone(),
router_data.request.to_owned(),
router_data_request,
resp.response,
);

View File

@ -1425,6 +1425,9 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let router_base_url = &additional_data.router_base_url;
let connector_name = &additional_data.connector_name;
let attempt = &payment_data.payment_attempt;
let browser_info: Option<types::BrowserInformation> = payment_data
.payment_attempt
.browser_info
@ -1446,7 +1449,11 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
.as_ref()
.map(|surcharge_details| surcharge_details.final_amount)
.unwrap_or(payment_data.amount.into());
let complete_authorize_url = Some(helpers::create_complete_authorize_url(
router_base_url,
attempt,
connector_name,
));
Ok(Self {
setup_future_usage: payment_data.payment_intent.setup_future_usage,
mandate_id: payment_data.mandate_id.clone(),
@ -1463,6 +1470,8 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
connector_transaction_id: payment_data.payment_attempt.connector_transaction_id,
redirect_response,
connector_meta: payment_data.payment_attempt.connector_metadata,
complete_authorize_url,
metadata: payment_data.payment_intent.metadata,
})
}
}
@ -1541,6 +1550,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce
browser_info,
surcharge_details: payment_data.surcharge_details,
connector_transaction_id: payment_data.payment_attempt.connector_transaction_id,
redirect_response: None,
})
}
}

View File

@ -789,6 +789,15 @@ pub enum RedirectForm {
BlueSnap {
payment_fields_token: String, // payment-field-token
},
CybersourceAuthSetup {
access_token: String,
ddc_url: String,
reference_id: String,
},
CybersourceConsumerAuth {
access_token: String,
step_up_url: String,
},
Payme,
Braintree {
client_token: String,
@ -1426,6 +1435,105 @@ pub fn build_redirection_form(
")))
}}
}
RedirectForm::CybersourceAuthSetup {
access_token,
ddc_url,
reference_id,
} => {
maud::html! {
(maud::DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1";
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" }
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
(PreEscaped(r#"
<script>
var anime = bodymovin.loadAnimation({
container: document.getElementById('loader1'),
renderer: 'svg',
loop: true,
autoplay: true,
name: 'hyperswitch loader',
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
})
</script>
"#))
h3 style="text-align: center;" { "Please wait while we process your payment..." }
}
(PreEscaped(r#"<iframe id="cardinal_collection_iframe" name="collectionIframe" height="10" width="10" style="display: none;"></iframe>"#))
(PreEscaped(format!("<form id=\"cardinal_collection_form\" method=\"POST\" target=\"collectionIframe\" action=\"{ddc_url}\">
<input id=\"cardinal_collection_form_input\" type=\"hidden\" name=\"JWT\" value=\"{access_token}\">
</form>")))
(PreEscaped(r#"<script>
window.onload = function() {
var cardinalCollectionForm = document.querySelector('#cardinal_collection_form'); if(cardinalCollectionForm) cardinalCollectionForm.submit();
}
</script>"#))
(PreEscaped(format!("<script>
window.addEventListener(\"message\", function(event) {{
if (event.origin === \"https://centinelapistag.cardinalcommerce.com\" || event.origin === \"https://centinelapi.cardinalcommerce.com\") {{
window.location.href = window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/cybersource?referenceId={reference_id}\");
}}
}}, false);
</script>
")))
}}
}
RedirectForm::CybersourceConsumerAuth {
access_token,
step_up_url,
} => {
maud::html! {
(maud::DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1";
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" }
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
(PreEscaped(r#"
<script>
var anime = bodymovin.loadAnimation({
container: document.getElementById('loader1'),
renderer: 'svg',
loop: true,
autoplay: true,
name: 'hyperswitch loader',
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
})
</script>
"#))
h3 style="text-align: center;" { "Please wait while we process your payment..." }
}
// This is the iframe recommended by cybersource but the redirection happens inside this iframe once otp
// is received and we lose control of the redirection on user client browser, so to avoid that we have removed this iframe and directly consumed it.
// (PreEscaped(r#"<iframe id="step_up_iframe" style="border: none; margin-left: auto; margin-right: auto; display: block" height="800px" width="400px" name="stepUpIframe"></iframe>"#))
(PreEscaped(format!("<form id=\"step_up_form\" method=\"POST\" action=\"{step_up_url}\">
<input id=\"step_up_form_jwt_input\" type=\"hidden\" name=\"JWT\" value=\"{access_token}\">
</form>")))
(PreEscaped(r#"<script>
window.onload = function() {
var stepUpForm = document.querySelector('#step_up_form'); if(stepUpForm) stepUpForm.submit();
}
</script>"#))
}}
}
RedirectForm::Payme => {
maud::html! {
(maud::DOCTYPE)

View File

@ -490,6 +490,7 @@ pub struct PaymentsPreProcessingData {
pub surcharge_details: Option<types::SurchargeDetails>,
pub browser_info: Option<BrowserInformation>,
pub connector_transaction_id: Option<String>,
pub redirect_response: Option<CompleteAuthorizeRedirectResponse>,
}
#[derive(Debug, Clone)]
@ -510,6 +511,8 @@ pub struct CompleteAuthorizeData {
pub browser_info: Option<BrowserInformation>,
pub connector_transaction_id: Option<String>,
pub connector_meta: Option<serde_json::Value>,
pub complete_authorize_url: Option<String>,
pub metadata: Option<pii::SecretSerdeValue>,
}
#[derive(Debug, Clone)]