diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index aaddae176667..fd67bb60128a 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -13,6 +13,7 @@ use common_utils::{ }; use error_stack::ResultExt; use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, @@ -29,7 +30,7 @@ use hyperswitch_domain_models::{ types::{ PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundExecuteRouterData, - RefundSyncRouterData, RefundsRouterData, + RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; use hyperswitch_interfaces::{ @@ -50,15 +51,17 @@ use requests::{ use response::{ EventType, ResponseIdStr, WorldpayErrorResponse, WorldpayEventResponse, WorldpayPaymentsResponse, WorldpayWebhookEventType, WorldpayWebhookTransactionId, + WP_CORRELATION_ID, }; -use transformers::{self as worldpay, WP_CORRELATION_ID}; +use ring::hmac; +use transformers::{self as worldpay}; use crate::{ constants::headers, types::ResponseRouterData, utils::{ construct_not_implemented_error_report, convert_amount, get_header_key_value, - ForeignTryFrom, RefundsRequestData, + is_mandate_supported, ForeignTryFrom, PaymentMethodDataType, RefundsRequestData, }, }; @@ -171,6 +174,19 @@ impl ConnectorValidation for Worldpay { ), } } + + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]); + is_mandate_supported(pm_data.clone(), pm_type, mandate_supported_pmd, self.id()) + } + + fn is_webhook_source_verification_mandatory(&self) -> bool { + true + } } impl api::Payment for Worldpay {} @@ -179,15 +195,108 @@ impl api::MandateSetup for Worldpay {} impl ConnectorIntegration for Worldpay { - fn build_request( + fn get_headers( + &self, + req: &SetupMandateRouterData, + connectors: &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: &SetupMandateRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!("{}api/payments", self.base_url(connectors))) + } + + fn get_request_body( &self, - _req: &RouterData, + req: &SetupMandateRouterData, _connectors: &Connectors, + ) -> CustomResult { + let auth = worldpay::WorldpayAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let connector_router_data = worldpay::WorldpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.minor_amount.unwrap_or_default(), + req, + ))?; + let connector_req = + WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?; + + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &SetupMandateRouterData, + connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - Err( - errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string()) - .into(), - ) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .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_CORRELATION_ID) + .and_then(|header_value| header_value.to_str().ok()) + .map(|id| id.to_string()) + }); + + RouterData::foreign_try_from(( + 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) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) } } @@ -401,6 +510,7 @@ impl ConnectorIntegration for Wor enums::AttemptStatus::Authorizing | enums::AttemptStatus::Authorized | enums::AttemptStatus::CaptureInitiated + | enums::AttemptStatus::Charged | enums::AttemptStatus::Pending | enums::AttemptStatus::VoidInitiated, EventType::Authorized, @@ -587,6 +697,7 @@ impl ConnectorIntegration, _merchant_id: &common_utils::id_type::MerchantId, - connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let to_sign = format!( - "{}{}", - secret_str, - std::str::from_utf8(request.body) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? - ); - Ok(to_sign.into_bytes()) + Ok(request.body.to_vec()) + } + + async fn verify_webhook_source( + &self, + request: &IncomingWebhookRequestDetails<'_>, + merchant_id: &common_utils::id_type::MerchantId, + connector_webhook_details: Option, + _connector_account_details: crypto::Encryptable>, + connector_label: &str, + ) -> CustomResult { + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_id, + connector_label, + connector_webhook_details, + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self + .get_webhook_source_verification_message( + request, + merchant_id, + &connector_webhook_secrets, + ) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let secret_key = hex::decode(connector_webhook_secrets.secret) + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)?; + + let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_key); + let signed_message = hmac::sign(&signing_key, &message); + let computed_signature = hex::encode(signed_message.as_ref()); + + Ok(computed_signature.as_bytes() == hex::encode(signature).as_bytes()) } fn get_webhook_object_reference_id( diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs index b0fa85a64c36..884caa9e840b 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs @@ -24,6 +24,7 @@ pub struct Merchant { #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Instruction { + #[serde(skip_serializing_if = "Option::is_none")] pub settlement: Option, pub method: PaymentMethod, pub payment_instrument: PaymentInstrument, @@ -33,6 +34,43 @@ pub struct Instruction { pub debt_repayment: Option, #[serde(rename = "threeDS")] pub three_ds: Option, + /// For setting up mandates + pub token_creation: Option, + /// For specifying CIT vs MIT + pub customer_agreement: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct TokenCreation { + #[serde(rename = "type")] + pub token_type: TokenCreationType, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum TokenCreationType { + Worldpay, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomerAgreement { + #[serde(rename = "type")] + pub agreement_type: CustomerAgreementType, + pub stored_card_usage: StoredCardUsageType, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CustomerAgreementType { + Subscription, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum StoredCardUsageType { + First, + Subsequent, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -225,6 +263,14 @@ pub enum ThreeDSRequestChannel { #[serde(rename_all = "camelCase")] pub struct ThreeDSRequestChallenge { pub return_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub preference: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ThreeDsPreference { + ChallengeMandated, } #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] @@ -284,3 +330,6 @@ pub struct WorldpayCompleteAuthorizationRequest { #[serde(skip_serializing_if = "Option::is_none")] pub collection_reference: Option, } + +pub(super) const THREE_DS_MODE: &str = "always"; +pub(super) const THREE_DS_TYPE: &str = "integrated"; diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs index 2b090bf02ac3..5e9fb0304243 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs @@ -41,6 +41,16 @@ pub struct AuthorizedResponse { pub description: Option, pub risk_factors: Option>, pub fraud: Option, + /// Mandate's token + pub token: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MandateToken { + pub href: Secret, + pub token_id: String, + pub token_expiry_date_time: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -445,3 +455,6 @@ pub enum WorldpayWebhookStatus { SentForRefund, RefundFailed, } + +/// Worldpay's unique reference ID for a request +pub(super) const WP_CORRELATION_ID: &str = "WP-CorrelationId"; diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index daaa095c3149..b69e4bcd97b6 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use api_models::payments::Address; +use api_models::payments::{Address, MandateIds, MandateReferenceId}; use base64::Engine; use common_enums::enums; use common_utils::{ @@ -10,9 +10,11 @@ use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::{PaymentMethodData, WalletData}, router_data::{ConnectorAuthType, ErrorResponse, RouterData}, - router_flow_types::Authorize, - router_request_types::{PaymentsAuthorizeData, ResponseId}, - router_response_types::{PaymentsResponseData, RedirectForm}, + router_flow_types::{Authorize, SetupMandate}, + router_request_types::{ + BrowserInformation, PaymentsAuthorizeData, ResponseId, SetupMandateRequestData, + }, + router_response_types::{MandateReference, PaymentsResponseData, RedirectForm}, types, }; use hyperswitch_interfaces::{api, errors}; @@ -22,7 +24,10 @@ use serde::{Deserialize, Serialize}; use super::{requests::*, response::*}; use crate::{ types::ResponseRouterData, - utils::{self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, RouterData as _}, + utils::{ + self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, + PaymentsSetupMandateRequestData, RouterData as RouterDataTrait, + }, }; #[derive(Debug, Serialize)] @@ -47,18 +52,15 @@ impl TryFrom<(&api::CurrencyUnit, enums::Currency, MinorUnit, T)> for Worldpa } } -/// Worldpay's unique reference ID for a request -pub const WP_CORRELATION_ID: &str = "WP-CorrelationId"; - #[derive(Debug, Default, Serialize, Deserialize)] pub struct WorldpayConnectorMetadataObject { pub merchant_name: Option>, } -impl TryFrom<&Option> for WorldpayConnectorMetadataObject { +impl TryFrom> for WorldpayConnectorMetadataObject { type Error = error_stack::Report; - fn try_from(meta_data: &Option) -> Result { - let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + fn try_from(meta_data: Option<&pii::SecretSerdeValue>) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.cloned()) .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata", })?; @@ -69,6 +71,7 @@ impl TryFrom<&Option> for WorldpayConnectorMetadataObject fn fetch_payment_instrument( payment_method: PaymentMethodData, billing_address: Option<&Address>, + mandate_ids: Option, ) -> CustomResult { match payment_method { PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment { @@ -103,6 +106,29 @@ fn fetch_payment_instrument( None }, })), + PaymentMethodData::MandatePayment => mandate_ids + .and_then(|mandate_ids| { + mandate_ids + .mandate_reference_id + .and_then(|mandate_id| match mandate_id { + MandateReferenceId::ConnectorMandateId(connector_mandate_id) => { + connector_mandate_id.get_connector_mandate_id().map(|href| { + PaymentInstrument::CardToken(CardToken { + payment_type: PaymentType::Token, + href, + cvc: None, + }) + }) + } + _ => None, + }) + }) + .ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + } + .into(), + ), PaymentMethodData::Wallet(wallet) => match wallet { WalletData::GooglePay(data) => Ok(PaymentInstrument::Googlepay(WalletPayment { payment_type: PaymentType::Encrypted, @@ -149,7 +175,6 @@ fn fetch_payment_instrument( | PaymentMethodData::BankDebit(_) | PaymentMethodData::BankTransfer(_) | PaymentMethodData::Crypto(_) - | PaymentMethodData::MandatePayment | PaymentMethodData::Reward | PaymentMethodData::RealTimePayment(_) | PaymentMethodData::Upi(_) @@ -196,109 +221,300 @@ impl TryFrom<(enums::PaymentMethod, Option)> for Payme } } -impl - TryFrom<( - &WorldpayRouterData<&RouterData>, - &Secret, - )> for WorldpayPaymentsRequest +// Trait to abstract common functionality between Authorize and SetupMandate +trait WorldpayPaymentsRequestData { + fn get_return_url(&self) -> Result>; + fn get_auth_type(&self) -> &enums::AuthenticationType; + fn get_browser_info(&self) -> Option<&BrowserInformation>; + fn get_payment_method_data(&self) -> &PaymentMethodData; + fn get_setup_future_usage(&self) -> Option; + fn get_off_session(&self) -> Option; + fn get_mandate_id(&self) -> Option; + fn get_currency(&self) -> enums::Currency; + fn get_optional_billing_address(&self) -> Option<&Address>; + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue>; + fn get_payment_method(&self) -> enums::PaymentMethod; + fn get_payment_method_type(&self) -> Option; + fn get_connector_request_reference_id(&self) -> String; + fn get_is_mandate_payment(&self) -> bool; + fn get_settlement_info(&self, _amount: i64) -> Option { + None + } +} + +impl WorldpayPaymentsRequestData + for RouterData { - type Error = error_stack::Report; + fn get_return_url(&self) -> Result> { + self.request.get_router_return_url() + } - fn try_from( - req: ( - &WorldpayRouterData< - &RouterData, - >, - &Secret, + fn get_auth_type(&self) -> &enums::AuthenticationType { + &self.auth_type + } + + fn get_browser_info(&self) -> Option<&BrowserInformation> { + self.request.browser_info.as_ref() + } + + fn get_payment_method_data(&self) -> &PaymentMethodData { + &self.request.payment_method_data + } + + fn get_setup_future_usage(&self) -> Option { + self.request.setup_future_usage + } + + fn get_off_session(&self) -> Option { + self.request.off_session + } + + fn get_mandate_id(&self) -> Option { + self.request.mandate_id.clone() + } + + fn get_currency(&self) -> enums::Currency { + self.request.currency + } + + fn get_optional_billing_address(&self) -> Option<&Address> { + self.get_optional_billing() + } + + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> { + self.connector_meta_data.as_ref() + } + + fn get_payment_method(&self) -> enums::PaymentMethod { + self.payment_method + } + + fn get_payment_method_type(&self) -> Option { + self.request.payment_method_type + } + + fn get_connector_request_reference_id(&self) -> String { + self.connector_request_reference_id.clone() + } + + fn get_is_mandate_payment(&self) -> bool { + true + } +} + +impl WorldpayPaymentsRequestData + for RouterData +{ + fn get_return_url(&self) -> Result> { + self.request.get_complete_authorize_url() + } + + fn get_auth_type(&self) -> &enums::AuthenticationType { + &self.auth_type + } + + fn get_browser_info(&self) -> Option<&BrowserInformation> { + self.request.browser_info.as_ref() + } + + fn get_payment_method_data(&self) -> &PaymentMethodData { + &self.request.payment_method_data + } + + fn get_setup_future_usage(&self) -> Option { + self.request.setup_future_usage + } + + fn get_off_session(&self) -> Option { + self.request.off_session + } + + fn get_mandate_id(&self) -> Option { + self.request.mandate_id.clone() + } + + fn get_currency(&self) -> enums::Currency { + self.request.currency + } + + fn get_optional_billing_address(&self) -> Option<&Address> { + self.get_optional_billing() + } + + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> { + self.connector_meta_data.as_ref() + } + + fn get_payment_method(&self) -> enums::PaymentMethod { + self.payment_method + } + + fn get_payment_method_type(&self) -> Option { + self.request.payment_method_type + } + + fn get_connector_request_reference_id(&self) -> String { + self.connector_request_reference_id.clone() + } + + fn get_is_mandate_payment(&self) -> bool { + self.request.is_mandate_payment() + } + + fn get_settlement_info(&self, amount: i64) -> Option { + match (self.request.capture_method.unwrap_or_default(), amount) { + (_, 0) => None, + (enums::CaptureMethod::Automatic, _) => Some(AutoSettlement { auto: true }), + (enums::CaptureMethod::Manual, _) | (enums::CaptureMethod::ManualMultiple, _) => { + Some(AutoSettlement { auto: false }) + } + _ => None, + } + } +} + +// Dangling helper function to create ThreeDS request +fn create_three_ds_request( + router_data: &T, + is_mandate_payment: bool, +) -> Result, error_stack::Report> { + match router_data.get_auth_type() { + enums::AuthenticationType::ThreeDs => { + let browser_info = router_data.get_browser_info().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "browser_info", + }, + )?; + + let accept_header = browser_info + .accept_header + .clone() + .get_required_value("accept_header") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "accept_header", + })?; + + let user_agent_header = browser_info + .user_agent + .clone() + .get_required_value("user_agent") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "user_agent", + })?; + + Ok(Some(ThreeDSRequest { + three_ds_type: THREE_DS_TYPE.to_string(), + mode: THREE_DS_MODE.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: router_data.get_return_url()?, + preference: if is_mandate_payment { + Some(ThreeDsPreference::ChallengeMandated) + } else { + None + }, + }, + })) + } + _ => Ok(None), + } +} + +// Dangling helper function to determine token and agreement settings +fn get_token_and_agreement( + payment_method_data: &PaymentMethodData, + setup_future_usage: Option, + off_session: Option, +) -> (Option, Option) { + match (payment_method_data, setup_future_usage, off_session) { + // CIT + (PaymentMethodData::Card(_), Some(enums::FutureUsage::OffSession), _) => ( + Some(TokenCreation { + token_type: TokenCreationType::Worldpay, + }), + Some(CustomerAgreement { + agreement_type: CustomerAgreementType::Subscription, + stored_card_usage: StoredCardUsageType::First, + }), ), - ) -> Result { + // MIT + (PaymentMethodData::Card(_), _, Some(true)) => ( + None, + Some(CustomerAgreement { + agreement_type: CustomerAgreementType::Subscription, + stored_card_usage: StoredCardUsageType::Subsequent, + }), + ), + _ => (None, None), + } +} + +// Implementation for WorldpayPaymentsRequest using abstracted request +impl TryFrom<(&WorldpayRouterData<&T>, &Secret)> + for WorldpayPaymentsRequest +{ + type Error = error_stack::Report; + + fn try_from(req: (&WorldpayRouterData<&T>, &Secret)) -> Result { let (item, entity_id) = req; let worldpay_connector_metadata_object: WorldpayConnectorMetadataObject = - WorldpayConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?; + WorldpayConnectorMetadataObject::try_from(item.router_data.get_connector_meta_data())?; + let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or( errors::ConnectorError::InvalidConnectorConfig { 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, - }; + + let is_mandate_payment = item.router_data.get_is_mandate_payment(); + let three_ds = create_three_ds_request(item.router_data, is_mandate_payment)?; + + let (token_creation, customer_agreement) = get_token_and_agreement( + item.router_data.get_payment_method_data(), + item.router_data.get_setup_future_usage(), + item.router_data.get_off_session(), + ); + Ok(Self { instruction: Instruction { - settlement: item - .router_data - .request - .capture_method - .map(|capture_method| AutoSettlement { - auto: capture_method == enums::CaptureMethod::Automatic, - }), + settlement: item.router_data.get_settlement_info(item.amount), method: PaymentMethod::try_from(( - item.router_data.payment_method, - item.router_data.request.payment_method_type, + item.router_data.get_payment_method(), + item.router_data.get_payment_method_type(), ))?, payment_instrument: fetch_payment_instrument( - item.router_data.request.payment_method_data.clone(), - item.router_data.get_optional_billing(), + item.router_data.get_payment_method_data().clone(), + item.router_data.get_optional_billing_address(), + item.router_data.get_mandate_id(), )?, narrative: InstructionNarrative { line1: merchant_name.expose(), }, value: PaymentValue { amount: item.amount, - currency: item.router_data.request.currency, + currency: item.router_data.get_currency(), }, debt_repayment: None, three_ds, + token_creation, + customer_agreement, }, merchant: Merchant { entity: entity_id.clone(), ..Default::default() }, - transaction_reference: item.router_data.connector_request_reference_id.clone(), + transaction_reference: item.router_data.get_connector_request_reference_id(), customer: None, }) } @@ -409,14 +625,22 @@ impl ), ) -> Result { let (router_data, optional_correlation_id) = item; - let (description, redirection_data, error) = router_data + let (description, redirection_data, mandate_reference, error) = router_data .response .other_fields .as_ref() .map(|other_fields| match other_fields { - WorldpayPaymentResponseFields::AuthorizedResponse(res) => { - (res.description.clone(), None, None) - } + WorldpayPaymentResponseFields::AuthorizedResponse(res) => ( + res.description.clone(), + None, + res.token.as_ref().map(|mandate_token| MandateReference { + connector_mandate_id: Some(mandate_token.href.clone().expose()), + payment_method_id: Some(mandate_token.token_id.clone()), + mandate_metadata: None, + connector_mandate_request_reference_id: None, + }), + None, + ), WorldpayPaymentResponseFields::DDCResponse(res) => ( None, Some(RedirectForm::WorldpayDDCForm { @@ -435,6 +659,7 @@ impl ]), }), None, + None, ), WorldpayPaymentResponseFields::ThreeDsChallenged(res) => ( None, @@ -447,15 +672,17 @@ impl )]), }), None, + None, ), WorldpayPaymentResponseFields::RefusedResponse(res) => ( + None, None, None, Some((res.refusal_code.clone(), res.refusal_description.clone())), ), - WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None), + WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None), }) - .unwrap_or((None, None, None)); + .unwrap_or((None, None, None, None)); let worldpay_status = router_data.response.outcome.clone(); let optional_error_message = match worldpay_status { PaymentOutcome::ThreeDsAuthenticationFailed => { @@ -475,7 +702,7 @@ impl optional_correlation_id.clone(), ))?, redirection_data: Box::new(redirection_data), - mandate_reference: Box::new(None), + mandate_reference: Box::new(mandate_reference), connector_metadata: None, network_txn_id: None, connector_response_reference_id: optional_correlation_id.clone(), diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index a0daab47a24e..74333be1a1c1 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1433,6 +1433,7 @@ impl RefundsRequestData for RefundsData { pub trait PaymentsSetupMandateRequestData { fn get_browser_info(&self) -> Result; fn get_email(&self) -> Result; + fn get_router_return_url(&self) -> Result; fn is_card(&self) -> bool; } @@ -1445,6 +1446,11 @@ impl PaymentsSetupMandateRequestData for SetupMandateRequestData { fn get_email(&self) -> Result { self.email.clone().ok_or_else(missing_field_err("email")) } + fn get_router_return_url(&self) -> Result { + self.router_return_url + .clone() + .ok_or_else(missing_field_err("router_return_url")) + } fn is_card(&self) -> bool { matches!(self.payment_method_data, PaymentMethodData::Card(_)) } diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index 75bd7740026f..b02da3960756 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -644,7 +644,7 @@ impl ErrorSwitch for ApiErrorRespon AER::Unprocessable(ApiError::new("WE", 5, "There was an issue processing the webhook body", None)) }, Self::WebhookInvalidMerchantSecret => { - AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verificartion is invalid", None)) + AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verification is invalid", None)) } Self::IntegrityCheckFailed { reason, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 090ddca961ba..3f4faedff81b 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -180,6 +180,3 @@ pub const VAULT_DELETE_FLOW_TYPE: &str = "delete_from_vault"; /// Vault Fingerprint fetch flow type #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub const VAULT_GET_FINGERPRINT_FLOW_TYPE: &str = "get_fingerprint_vault"; - -/// Worldpay's unique reference ID for a request TODO: Move to hyperswitch_connectors/constants once Worldpay is moved to connectors crate -pub const WP_CORRELATION_ID: &str = "WP-CorrelationId"; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 0234b97032c7..1fa28b81414d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2364,7 +2364,7 @@ fn update_connector_mandate_details_for_the_flow( )) } } else { - None + original_connector_mandate_reference_id }; payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 06ab971925bc..215a8b209cf0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -89,7 +89,6 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; - pub const X_WP_API_VERSION: &str = "WP-Api-Version"; } pub mod pii {