mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
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:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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()
|
||||
)
|
||||
{}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user