feat(connector): add 3DS flow for Worldpay (#6374)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-10-21 19:19:44 +05:30
committed by GitHub
parent b3ce373f8e
commit b93c849623
8 changed files with 583 additions and 112 deletions

View File

@ -624,6 +624,113 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
}
}
impl api::PaymentsCompleteAuthorize for Worldpay {}
impl
ConnectorIntegration<
api::CompleteAuthorize,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
> for Worldpay
{
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> {
let connector_payment_id = req
.request
.connector_transaction_id
.clone()
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
let stage = match req.status {
enums::AttemptStatus::DeviceDataCollectionPending => "3dsDeviceData".to_string(),
_ => "3dsChallenges".to_string(),
};
Ok(format!(
"{}api/payments/{connector_payment_id}/{stage}",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let req_obj = WorldpayCompleteAuthorizationRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(req_obj)))
}
fn build_request(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCompleteAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsCompleteAuthorizeType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsCompleteAuthorizeType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PaymentsCompleteAuthorizeRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
let response: WorldpayPaymentsResponse = res
.response
.parse_struct("WorldpayPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
let optional_correlation_id = res.headers.and_then(|headers| {
headers
.get("WP-CorrelationId")
.and_then(|header_value| header_value.to_str().ok())
.map(|id| id.to_string())
});
types::RouterData::foreign_try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
optional_correlation_id,
))
.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)
}
}
impl api::Refund for Worldpay {}
impl api::RefundExecute for Worldpay {}
impl api::RefundSync for Worldpay {}
@ -900,3 +1007,20 @@ impl api::IncomingWebhook for Worldpay {
Ok(Box::new(psync_body))
}
}
impl services::ConnectorRedirectResponse for Worldpay {
fn get_flow_type(
&self,
_query_params: &str,
_json_payload: Option<serde_json::Value>,
action: services::PaymentAction,
) -> CustomResult<enums::CallConnectorAction, errors::ConnectorError> {
match action {
services::PaymentAction::CompleteAuthorize => Ok(enums::CallConnectorAction::Trigger),
services::PaymentAction::PSync
| services::PaymentAction::PaymentAuthenticateCompleteAuthorize => {
Ok(enums::CallConnectorAction::Avoid)
}
}
}
}

View File

@ -31,6 +31,8 @@ pub struct Instruction {
pub value: PaymentValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub debt_repayment: Option<bool>,
#[serde(rename = "threeDS")]
pub three_ds: Option<ThreeDSRequest>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -187,6 +189,44 @@ pub struct AutoSettlement {
pub auto: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDSRequest {
#[serde(rename = "type")]
pub three_ds_type: String,
pub mode: String,
pub device_data: ThreeDSRequestDeviceData,
pub challenge: ThreeDSRequestChallenge,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDSRequestDeviceData {
pub accept_header: String,
pub user_agent_header: String,
pub browser_language: Option<String>,
pub browser_screen_width: Option<u32>,
pub browser_screen_height: Option<u32>,
pub browser_color_depth: Option<String>,
pub time_zone: Option<String>,
pub browser_java_enabled: Option<bool>,
pub browser_javascript_enabled: Option<bool>,
pub channel: Option<ThreeDSRequestChannel>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThreeDSRequestChannel {
Browser,
Native,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDSRequestChallenge {
pub return_url: String,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentMethod {
@ -237,3 +277,10 @@ pub struct WorldpayPartialRequest {
pub value: PaymentValue,
pub reference: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayCompleteAuthorizationRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_reference: Option<String>,
}

View File

@ -1,6 +1,7 @@
use error_stack::ResultExt;
use masking::Secret;
use serde::{Deserialize, Serialize};
use url::Url;
use super::requests::*;
use crate::core::errors;
@ -10,7 +11,7 @@ pub struct WorldpayPaymentsResponse {
pub outcome: PaymentOutcome,
pub transaction_reference: Option<String>,
#[serde(flatten)]
pub other_fields: WorldpayPaymentResponseFields,
pub other_fields: Option<WorldpayPaymentResponseFields>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -20,13 +21,13 @@ pub enum WorldpayPaymentResponseFields {
DDCResponse(DDCResponse),
FraudHighRisk(FraudHighRiskResponse),
RefusedResponse(RefusedResponse),
ThreeDsChallenged(ThreeDsChallengedResponse),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_instrument: Option<PaymentsResPaymentInstrument>,
pub payment_instrument: PaymentsResPaymentInstrument,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<Issuer>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -67,6 +68,34 @@ pub struct ThreeDsResponse {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeDsChallengedResponse {
pub authentication: AuthenticationResponse,
pub challenge: ThreeDsChallenge,
#[serde(rename = "_actions")]
pub actions: CompleteThreeDsActionLink,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AuthenticationResponse {
pub version: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ThreeDsChallenge {
pub reference: String,
pub url: Url,
pub jwt: Secret<String>,
pub payload: Secret<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CompleteThreeDsActionLink {
#[serde(rename = "complete3dsChallenge")]
pub complete_three_ds_challenge: ActionLink,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IssuerResponse {
Challenged,
Frictionless,
@ -82,16 +111,15 @@ pub struct DDCResponse {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DDCToken {
pub jwt: String,
pub url: String,
pub bin: String,
pub jwt: Secret<String>,
pub url: Url,
pub bin: Secret<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DDCActionLink {
#[serde(rename = "supply3dsDeviceData")]
supply_ddc_data: ActionLink,
method: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -105,11 +133,32 @@ pub enum PaymentOutcome {
FraudHighRisk,
#[serde(alias = "3dsDeviceDataRequired")]
ThreeDsDeviceDataRequired,
ThreeDsChallenged,
SentForCancellation,
#[serde(alias = "3dsAuthenticationFailed")]
ThreeDsAuthenticationFailed,
SentForPartialRefund,
#[serde(alias = "3dsChallenged")]
ThreeDsChallenged,
#[serde(alias = "3dsUnavailable")]
ThreeDsUnavailable,
}
impl std::fmt::Display for PaymentOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Authorized => write!(f, "authorized"),
Self::Refused => write!(f, "refused"),
Self::SentForSettlement => write!(f, "sentForSettlement"),
Self::SentForRefund => write!(f, "sentForRefund"),
Self::FraudHighRisk => write!(f, "fraudHighRisk"),
Self::ThreeDsDeviceDataRequired => write!(f, "3dsDeviceDataRequired"),
Self::SentForCancellation => write!(f, "sentForCancellation"),
Self::ThreeDsAuthenticationFailed => write!(f, "3dsAuthenticationFailed"),
Self::SentForPartialRefund => write!(f, "sentForPartialRefund"),
Self::ThreeDsChallenged => write!(f, "3dsChallenged"),
Self::ThreeDsUnavailable => write!(f, "3dsUnavailable"),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -202,30 +251,33 @@ pub fn get_resource_id<T, F>(
where
F: Fn(String) -> T,
{
let reference_id = match response.other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(res) => res
.links
.as_ref()
.and_then(|link| link.self_link.href.rsplit_once('/'))
.map(|(_, h)| urlencoding::decode(h))
.transpose()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?
.map(|s| transform_fn(s.into_owned())),
WorldpayPaymentResponseFields::DDCResponse(res) => res
.actions
.supply_ddc_data
.href
.split('/')
.rev()
.nth(1)
.map(urlencoding::decode)
.transpose()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?
.map(|s| transform_fn(s.into_owned())),
WorldpayPaymentResponseFields::FraudHighRisk(_) => None,
WorldpayPaymentResponseFields::RefusedResponse(_) => None,
};
reference_id
let optional_reference_id = response
.other_fields
.as_ref()
.and_then(|other_fields| match other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(res) => res
.links
.as_ref()
.and_then(|link| link.self_link.href.rsplit_once('/').map(|(_, h)| h)),
WorldpayPaymentResponseFields::DDCResponse(res) => {
res.actions.supply_ddc_data.href.split('/').nth_back(1)
}
WorldpayPaymentResponseFields::ThreeDsChallenged(res) => res
.actions
.complete_three_ds_challenge
.href
.split('/')
.nth_back(1),
WorldpayPaymentResponseFields::FraudHighRisk(_)
| WorldpayPaymentResponseFields::RefusedResponse(_) => None,
})
.map(|href| {
urlencoding::decode(href)
.map(|s| transform_fn(s.into_owned()))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
})
.transpose()?;
optional_reference_id
.or_else(|| connector_transaction_id.map(transform_fn))
.ok_or_else(|| {
errors::ConnectorError::MissingRequiredField {
@ -256,8 +308,8 @@ impl Issuer {
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentsResPaymentInstrument {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub payment_instrument_type: Option<String>,
#[serde(rename = "type")]
pub payment_instrument_type: String,
pub card_bin: Option<String>,
pub last_four: Option<String>,
pub expiry_date: Option<ExpiryDate>,
@ -268,22 +320,6 @@ pub struct PaymentsResPaymentInstrument {
pub payment_account_reference: Option<String>,
}
impl PaymentsResPaymentInstrument {
pub fn new() -> Self {
Self {
payment_instrument_type: None,
card_bin: None,
last_four: None,
category: None,
expiry_date: None,
card_brand: None,
funding_type: None,
issuer_name: None,
payment_account_reference: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiskFactorsInner {

View File

@ -1,17 +1,20 @@
use std::collections::HashMap;
use api_models::payments::Address;
use base64::Engine;
use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii, types::MinorUnit};
use diesel_models::enums;
use error_stack::ResultExt;
use hyperswitch_connectors::utils::RouterData;
use hyperswitch_connectors::utils::{PaymentsAuthorizeRequestData, RouterData};
use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use super::{requests::*, response::*};
use crate::{
connector::utils,
connector::utils::{self, AddressData},
consts,
core::errors,
services,
types::{
self, domain, transformers::ForeignTryFrom, PaymentsAuthorizeData, PaymentsResponseData,
},
@ -65,49 +68,40 @@ impl TryFrom<&Option<pii::SecretSerdeValue>> for WorldpayConnectorMetadataObject
fn fetch_payment_instrument(
payment_method: domain::PaymentMethodData,
billing_address: Option<&Address>,
auth_type: enums::AuthenticationType,
) -> CustomResult<PaymentInstrument, errors::ConnectorError> {
match payment_method {
domain::PaymentMethodData::Card(card) => {
if auth_type == enums::AuthenticationType::ThreeDs {
return Err(errors::ConnectorError::NotImplemented(
"ThreeDS flow through worldpay".to_string(),
)
.into());
}
Ok(PaymentInstrument::Card(CardPayment {
payment_type: PaymentType::Plain,
expiry_date: ExpiryDate {
month: utils::CardData::get_expiry_month_as_i8(&card)?,
year: utils::CardData::get_expiry_year_as_i32(&card)?,
},
card_number: card.card_number,
cvc: card.card_cvc,
card_holder_name: card.nick_name,
billing_address: if let Some(address) =
billing_address.and_then(|addr| addr.address.clone())
{
Some(BillingAddress {
address1: address.line1,
address2: address.line2,
address3: address.line3,
city: address.city,
state: address.state,
postal_code: address.zip.get_required_value("zip").change_context(
errors::ConnectorError::MissingRequiredField { field_name: "zip" },
)?,
country_code: address
.country
.get_required_value("country_code")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "country_code",
})?,
})
} else {
None
},
}))
}
domain::PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
payment_type: PaymentType::Plain,
expiry_date: ExpiryDate {
month: utils::CardData::get_expiry_month_as_i8(&card)?,
year: utils::CardData::get_expiry_year_as_i32(&card)?,
},
card_number: card.card_number,
cvc: card.card_cvc,
card_holder_name: billing_address.and_then(|address| address.get_optional_full_name()),
billing_address: if let Some(address) =
billing_address.and_then(|addr| addr.address.clone())
{
Some(BillingAddress {
address1: address.line1,
address2: address.line2,
address3: address.line3,
city: address.city,
state: address.state,
postal_code: address.zip.get_required_value("zip").change_context(
errors::ConnectorError::MissingRequiredField { field_name: "zip" },
)?,
country_code: address
.country
.get_required_value("country_code")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "country_code",
})?,
})
} else {
None
},
})),
domain::PaymentMethodData::Wallet(wallet) => match wallet {
domain::WalletData::GooglePay(data) => {
Ok(PaymentInstrument::Googlepay(WalletPayment {
@ -230,6 +224,53 @@ impl
config: "metadata.merchant_name",
},
)?;
let three_ds = match item.router_data.auth_type {
enums::AuthenticationType::ThreeDs => {
let browser_info = item
.router_data
.request
.browser_info
.clone()
.get_required_value("browser_info")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "browser_info",
})?;
let accept_header = browser_info
.accept_header
.get_required_value("accept_header")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "accept_header",
})?;
let user_agent_header = browser_info
.user_agent
.get_required_value("user_agent")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "user_agent",
})?;
Some(ThreeDSRequest {
three_ds_type: "integrated".to_string(),
mode: "always".to_string(),
device_data: ThreeDSRequestDeviceData {
accept_header,
user_agent_header,
browser_language: browser_info.language.clone(),
browser_screen_width: browser_info.screen_width,
browser_screen_height: browser_info.screen_height,
browser_color_depth: browser_info
.color_depth
.map(|depth| depth.to_string()),
time_zone: browser_info.time_zone.map(|tz| tz.to_string()),
browser_java_enabled: browser_info.java_enabled,
browser_javascript_enabled: browser_info.java_script_enabled,
channel: Some(ThreeDSRequestChannel::Browser),
},
challenge: ThreeDSRequestChallenge {
return_url: item.router_data.request.get_complete_authorize_url()?,
},
})
}
_ => None,
};
Ok(Self {
instruction: Instruction {
settlement: item
@ -252,7 +293,6 @@ impl
payment_instrument: fetch_payment_instrument(
item.router_data.request.payment_method_data.clone(),
item.router_data.get_optional_billing(),
item.router_data.auth_type,
)?,
narrative: InstructionNarrative {
line1: merchant_name.expose(),
@ -262,6 +302,7 @@ impl
currency: item.router_data.request.currency,
},
debt_repayment: None,
three_ds,
},
merchant: Merchant {
entity: entity_id.clone(),
@ -321,6 +362,7 @@ impl From<PaymentOutcome> for enums::AttemptStatus {
Self::AutoRefunded
}
PaymentOutcome::Refused | PaymentOutcome::FraudHighRisk => Self::Failure,
PaymentOutcome::ThreeDsUnavailable => Self::AuthenticationFailed,
}
}
}
@ -363,42 +405,105 @@ impl From<EventType> for enums::RefundStatus {
}
}
impl
impl<F, T>
ForeignTryFrom<(
types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
types::ResponseRouterData<F, WorldpayPaymentsResponse, T, PaymentsResponseData>,
Option<String>,
)> for types::PaymentsAuthorizeRouterData
)> for types::RouterData<F, T, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn foreign_try_from(
item: (
types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
types::ResponseRouterData<F, WorldpayPaymentsResponse, T, PaymentsResponseData>,
Option<String>,
),
) -> Result<Self, Self::Error> {
let (router_data, optional_correlation_id) = item;
let description = match router_data.response.other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(ref res) => res.description.clone(),
WorldpayPaymentResponseFields::DDCResponse(_)
| WorldpayPaymentResponseFields::FraudHighRisk(_)
| WorldpayPaymentResponseFields::RefusedResponse(_) => None,
let (description, redirection_data) = router_data
.response
.other_fields
.as_ref()
.map(|other_fields| match other_fields {
WorldpayPaymentResponseFields::AuthorizedResponse(res) => {
(res.description.clone(), None)
}
WorldpayPaymentResponseFields::DDCResponse(res) => (
None,
Some(services::RedirectForm::WorldpayDDCForm {
endpoint: res.device_data_collection.url.clone(),
method: common_utils::request::Method::Post,
collection_id: Some("SessionId".to_string()),
form_fields: HashMap::from([
(
"Bin".to_string(),
res.device_data_collection.bin.clone().expose(),
),
(
"JWT".to_string(),
res.device_data_collection.jwt.clone().expose(),
),
]),
}),
),
WorldpayPaymentResponseFields::ThreeDsChallenged(res) => (
None,
Some(services::RedirectForm::Form {
endpoint: res.challenge.url.to_string(),
method: common_utils::request::Method::Post,
form_fields: HashMap::from([(
"JWT".to_string(),
res.challenge.jwt.clone().expose(),
)]),
}),
),
WorldpayPaymentResponseFields::FraudHighRisk(_)
| WorldpayPaymentResponseFields::RefusedResponse(_) => (None, None),
})
.unwrap_or((None, None));
let worldpay_status = router_data.response.outcome.clone();
let optional_reason = match worldpay_status {
PaymentOutcome::ThreeDsAuthenticationFailed => {
Some("3DS authentication failed from issuer".to_string())
}
PaymentOutcome::ThreeDsUnavailable => {
Some("3DS authentication unavailable from issuer".to_string())
}
PaymentOutcome::FraudHighRisk => {
Some("Transaction marked as high risk by Worldpay".to_string())
}
PaymentOutcome::Refused => Some("Transaction refused by issuer".to_string()),
_ => None,
};
Ok(Self {
status: enums::AttemptStatus::from(router_data.response.outcome.clone()),
description,
response: Ok(PaymentsResponseData::TransactionResponse {
let status = enums::AttemptStatus::from(worldpay_status.clone());
let response = optional_reason.map_or(
Ok(PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::foreign_try_from((
router_data.response,
optional_correlation_id.clone(),
))?,
redirection_data: None,
redirection_data,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: optional_correlation_id,
connector_response_reference_id: optional_correlation_id.clone(),
incremental_authorization_allowed: None,
charge_id: None,
}),
|reason| {
Err(types::ErrorResponse {
code: worldpay_status.to_string(),
message: reason.clone(),
reason: Some(reason),
status_code: router_data.http_code,
attempt_status: Some(status),
connector_transaction_id: optional_correlation_id,
})
},
);
Ok(Self {
status,
description,
response,
..router_data.data
})
}
@ -459,3 +564,17 @@ impl ForeignTryFrom<(WorldpayPaymentsResponse, Option<String>)> for types::Respo
get_resource_id(item.0, item.1, Self::ConnectorTransactionId)
}
}
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for WorldpayCompleteAuthorizationRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
let params = item
.request
.redirect_response
.as_ref()
.and_then(|redirect_response| redirect_response.params.as_ref())
.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?;
serde_urlencoded::from_str::<Self>(params.peek())
.change_context(errors::ConnectorError::ResponseDeserializationFailed)
}
}

View File

@ -240,7 +240,6 @@ default_imp_for_complete_authorize!(
connector::Wise,
connector::Wellsfargo,
connector::Wellsfargopayout,
connector::Worldpay,
connector::Zen,
connector::Zsl
);
@ -472,7 +471,6 @@ default_imp_for_connector_redirect_response!(
connector::Wellsfargo,
connector::Wellsfargopayout,
connector::Wise,
connector::Worldpay,
connector::Zsl
);

View File

@ -1809,6 +1809,135 @@ pub fn build_redirection_form(
}
}
RedirectForm::WorldpayDDCForm {
endpoint,
method,
form_fields,
collection_id,
} => maud::html! {
(maud::DOCTYPE)
html {
meta name="viewport" content="width=device-width, initial-scale=1";
head {
(PreEscaped(r##"
<style>
#loader1 {
width: 500px;
}
@media max-width: 600px {
#loader1 {
width: 200px;
}
}
</style>
"##))
}
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-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..." }
script {
(PreEscaped(format!(
r#"
function submitCollectionReference(collectionReference) {{
var redirectPathname = window.location.pathname.replace(/payments\/redirect\/(\w+)\/(\w+)\/\w+/, "payments/$1/$2/redirect/complete/worldpay");
var redirectUrl = window.location.origin + redirectPathname;
try {{
if (typeof collectionReference === "string" && collectionReference.length > 0) {{
var form = document.createElement("form");
form.action = redirectPathname;
form.method = "GET";
var input = document.createElement("input");
input.type = "hidden";
input.name = "collectionReference";
input.value = collectionReference;
form.appendChild(input);
document.body.appendChild(form);
form.submit();;
}} else {{
window.location.replace(redirectUrl);
}}
}} catch (error) {{
window.location.replace(redirectUrl);
}}
}}
var allowedHost = "{}";
var collectionField = "{}";
window.addEventListener("message", function(event) {{
if (event.origin === allowedHost) {{
try {{
var data = JSON.parse(event.data);
if (collectionField.length > 0) {{
var collectionReference = data[collectionField];
return submitCollectionReference(collectionReference);
}} else {{
console.error("Collection field not found in event data (" + collectionField + ")");
}}
}} catch (error) {{
console.error("Error parsing event data: ", error);
}}
}} else {{
console.error("Invalid origin: " + event.origin, "Expected origin: " + allowedHost);
}}
submitCollectionReference("");
}});
// Redirect within 8 seconds if no collection reference is received
window.setTimeout(submitCollectionReference, 8000);
"#,
endpoint.host_str().map_or(endpoint.as_ref().split('/').take(3).collect::<Vec<&str>>().join("/"), |host| format!("{}://{}", endpoint.scheme(), host)),
collection_id.clone().unwrap_or("".to_string())))
)
}
iframe
style="display: none;"
srcdoc=(
maud::html! {
(maud::DOCTYPE)
html {
body {
form action=(PreEscaped(endpoint.to_string())) method=(method.to_string()) #payment_form {
@for (field, value) in form_fields {
input type="hidden" name=(field) value=(value);
}
}
(PreEscaped(format!(r#"
<script type="text/javascript"> {logging_template}
var form = document.getElementById("payment_form");
var formFields = form.querySelectorAll("input");
window.setTimeout(function () {{
if (form.method.toUpperCase() === "GET" && formFields.length === 0) {{
window.location.href = form.action;
}} else {{
form.submit();
}}
}}, 300);
</script>
"#)))
}
}
}.into_string()
)
{}
}
}
},
}
}