diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 85c698248924..0627fa0048a2 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -171,11 +171,23 @@ impl PaymentAttempt { merchant_id: &common_utils::id_type::MerchantId, connector_txn_id: &str, ) -> StorageResult { + let (txn_id, txn_data) = common_utils::types::ConnectorTransactionId::form_id_and_data( + connector_txn_id.to_string(), + ); + let connector_transaction_id = txn_id + .get_txn_id(txn_data.as_ref()) + .change_context(DatabaseError::Others) + .attach_printable_lazy(|| { + format!( + "Failed to retrieve txn_id for ({:?}, {:?})", + txn_id, txn_data + ) + })?; generics::generic_find_one::<::Table, _, _>( conn, dsl::merchant_id .eq(merchant_id.to_owned()) - .and(dsl::connector_transaction_id.eq(connector_txn_id.to_owned())), + .and(dsl::connector_transaction_id.eq(connector_transaction_id.to_owned())), ) .await } diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index eca56b8c866f..6682ac1ad44c 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -163,6 +163,12 @@ pub enum RedirectForm { Mifinity { initialization_token: String, }, + WorldpayDDCForm { + endpoint: url::Url, + method: Method, + form_fields: HashMap, + collection_id: Option, + }, } impl From<(url::Url, Method)> for RedirectForm { diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 7a4c4c5da97b..97da36f39b35 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -624,6 +624,113 @@ impl ConnectorIntegration for Worldpay +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, 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 { + 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 { + 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, 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 { + 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 { + 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, + action: services::PaymentAction, + ) -> CustomResult { + match action { + services::PaymentAction::CompleteAuthorize => Ok(enums::CallConnectorAction::Trigger), + services::PaymentAction::PSync + | services::PaymentAction::PaymentAuthenticateCompleteAuthorize => { + Ok(enums::CallConnectorAction::Avoid) + } + } + } +} diff --git a/crates/router/src/connector/worldpay/requests.rs b/crates/router/src/connector/worldpay/requests.rs index 3d0be891ebb9..b0fa85a64c36 100644 --- a/crates/router/src/connector/worldpay/requests.rs +++ b/crates/router/src/connector/worldpay/requests.rs @@ -31,6 +31,8 @@ pub struct Instruction { pub value: PaymentValue, #[serde(skip_serializing_if = "Option::is_none")] pub debt_repayment: Option, + #[serde(rename = "threeDS")] + pub three_ds: Option, } #[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, + pub browser_screen_width: Option, + pub browser_screen_height: Option, + pub browser_color_depth: Option, + pub time_zone: Option, + pub browser_java_enabled: Option, + pub browser_javascript_enabled: Option, + pub channel: Option, +} + +#[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, +} diff --git a/crates/router/src/connector/worldpay/response.rs b/crates/router/src/connector/worldpay/response.rs index 0a7f690c3aa9..edc3c26948fa 100644 --- a/crates/router/src/connector/worldpay/response.rs +++ b/crates/router/src/connector/worldpay/response.rs @@ -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, #[serde(flatten)] - pub other_fields: WorldpayPaymentResponseFields, + pub other_fields: Option, } #[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, + pub payment_instrument: PaymentsResPaymentInstrument, #[serde(skip_serializing_if = "Option::is_none")] pub issuer: Option, #[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, + pub payload: Secret, +} + +#[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, + pub url: Url, + pub bin: Secret, } #[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( 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, + #[serde(rename = "type")] + pub payment_instrument_type: String, pub card_bin: Option, pub last_four: Option, pub expiry_date: Option, @@ -268,22 +320,6 @@ pub struct PaymentsResPaymentInstrument { pub payment_account_reference: Option, } -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 { diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index a0f2bfd2508b..a28d3bff7ed4 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -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> for WorldpayConnectorMetadataObject fn fetch_payment_instrument( payment_method: domain::PaymentMethodData, billing_address: Option<&Address>, - auth_type: enums::AuthenticationType, ) -> CustomResult { 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 for enums::AttemptStatus { Self::AutoRefunded } PaymentOutcome::Refused | PaymentOutcome::FraudHighRisk => Self::Failure, + PaymentOutcome::ThreeDsUnavailable => Self::AuthenticationFailed, } } } @@ -363,42 +405,105 @@ impl From for enums::RefundStatus { } } -impl +impl ForeignTryFrom<( - types::PaymentsResponseRouterData, + types::ResponseRouterData, Option, - )> for types::PaymentsAuthorizeRouterData + )> for types::RouterData { type Error = error_stack::Report; fn foreign_try_from( item: ( - types::PaymentsResponseRouterData, + types::ResponseRouterData, Option, ), ) -> Result { 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)> for types::Respo get_resource_id(item.0, item.1, Self::ConnectorTransactionId) } } + +impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for WorldpayCompleteAuthorizationRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result { + 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::(params.peek()) + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + } +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index babf393bb3a9..700fff16cf58 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -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 ); diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 579a4ac4ffd0..0d84ba9c30f4 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -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##" + + "##)) + } + + 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#""#)) + (PreEscaped(r#" + + "#)) + 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::>().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#" + + "#))) + } + } + }.into_string() + ) + {} + } + } + }, } }