From c245ece19fb1b3988c61e12c0faa37c527cabd71 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 7 Apr 2025 14:23:00 +0530 Subject: [PATCH] feat(core): added force_3ds_challenge for decoupled txns (#7484) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 10 +++++ api-reference/openapi_spec.json | 40 +++++++++++++++++++ crates/api_models/src/admin.rs | 6 +-- crates/api_models/src/payments.rs | 16 ++++++++ crates/diesel_models/src/payment_intent.rs | 30 ++++++++++++++ crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/schema_v2.rs | 2 + .../hyperswitch_domain_models/src/payments.rs | 8 ++++ .../src/payments/payment_intent.rs | 28 +++++++++++++ .../src/revenue_recovery.rs | 1 + crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + crates/router/src/core/payment_methods.rs | 1 + crates/router/src/core/payments.rs | 2 +- crates/router/src/core/payments/helpers.rs | 6 +++ .../payments/operations/payment_confirm.rs | 2 +- .../payments/operations/payment_create.rs | 7 ++++ .../payments/operations/payment_update.rs | 5 +++ .../operations/payment_update_intent.rs | 1 + .../router/src/core/payments/transformers.rs | 4 ++ crates/router/src/utils/user/sample_data.rs | 2 + crates/router/tests/payments.rs | 4 ++ crates/router/tests/payments2.rs | 4 ++ .../down.sql | 3 ++ .../up.sql | 3 ++ .../down.sql | 3 ++ .../up.sql | 3 ++ 27 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/down.sql create mode 100644 migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/up.sql create mode 100644 migrations/2025-03-20-085151_force-3ds-challenge-triggered/down.sql create mode 100644 migrations/2025-03-20-085151_force-3ds-challenge-triggered/up.sql diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index de2cd1b036..0dfd749ec5 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -16907,6 +16907,11 @@ } ], "nullable": true + }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true } }, "additionalProperties": false @@ -17687,6 +17692,11 @@ "type": "string", "description": "The payment_method_id to be associated with the payment", "nullable": true + }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index b1acc0a812..0389ef35c5 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -19168,6 +19168,11 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, "threeds_method_comp_ind": { "allOf": [ { @@ -19575,6 +19580,11 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, "threeds_method_comp_ind": { "allOf": [ { @@ -20141,6 +20151,16 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, + "force_3ds_challenge_trigger": { + "type": "boolean", + "description": "Indicates if 3ds challenge is triggered", + "nullable": true + }, "issuer_error_code": { "type": "string", "description": "Error code received from the issuer in case of failed payments", @@ -20840,6 +20860,11 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, "threeds_method_comp_ind": { "allOf": [ { @@ -21432,6 +21457,16 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, + "force_3ds_challenge_trigger": { + "type": "boolean", + "description": "Indicates if 3ds challenge is triggered", + "nullable": true + }, "issuer_error_code": { "type": "string", "description": "Error code received from the issuer in case of failed payments", @@ -21939,6 +21974,11 @@ ], "nullable": true }, + "force_3ds_challenge": { + "type": "boolean", + "description": "Indicates if 3ds challenge is forced", + "nullable": true + }, "threeds_method_comp_ind": { "allOf": [ { diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index df648f5013..5d3f7c5dc8 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -112,7 +112,7 @@ pub struct MerchantAccountCreate { pub pm_collect_link_config: Option, /// Product Type of this merchant account - #[schema(value_type = Option)] + #[schema(value_type = Option, example = "Orchestration")] pub product_type: Option, } @@ -563,7 +563,7 @@ pub struct MerchantAccountResponse { pub pm_collect_link_config: Option, /// Product Type of this merchant account - #[schema(value_type = Option)] + #[schema(value_type = Option, example = "Orchestration")] pub product_type: Option, } @@ -599,7 +599,7 @@ pub struct MerchantAccountResponse { pub recon_status: api_enums::ReconStatus, /// Product Type of this merchant account - #[schema(value_type = Option)] + #[schema(value_type = Option, example = "Orchestration")] pub product_type: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3cfadb0a57..724204022d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -263,6 +263,9 @@ pub struct PaymentsCreateIntentRequest { #[schema(value_type = Option)] pub request_external_three_ds_authentication: Option, + + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, } #[cfg(feature = "v2")] @@ -1149,6 +1152,9 @@ pub struct PaymentsRequest { #[schema(value_type = Option)] pub ctp_service_details: Option, + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, + /// Indicates if 3DS method data was successfully completed or not pub threeds_method_comp_ind: Option, } @@ -5086,6 +5092,12 @@ pub struct PaymentsResponse { #[schema(value_type = Option, example = "manual")] pub card_discovery: Option, + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, + + /// Indicates if 3ds challenge is triggered + pub force_3ds_challenge_trigger: Option, + /// Error code received from the issuer in case of failed payments pub issuer_error_code: Option, @@ -5430,6 +5442,9 @@ pub struct PaymentsRequest { /// The payment_method_id to be associated with the payment #[schema(value_type = Option)] pub payment_method_id: Option, + + /// Indicates if 3ds challenge is forced + pub force_3ds_challenge: Option, } #[cfg(feature = "v2")] @@ -5463,6 +5478,7 @@ impl From<&PaymentsRequest> for PaymentsCreateIntentRequest { request_external_three_ds_authentication: request .request_external_three_ds_authentication .clone(), + force_3ds_challenge: request.force_3ds_challenge, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 6753afc2d9..445cc6eab3 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -59,6 +59,8 @@ pub struct PaymentIntent { pub psd2_sca_exemption_type: Option, pub split_payments: Option, pub platform_merchant_id: Option, + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, pub merchant_reference_id: Option, pub billing_address: Option, pub shipping_address: Option, @@ -145,6 +147,8 @@ pub struct PaymentIntent { pub psd2_sca_exemption_type: Option, pub split_payments: Option, pub platform_merchant_id: Option, + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, diesel::AsExpression, PartialEq)] @@ -327,6 +331,8 @@ pub struct PaymentIntentNew { pub apply_mit_exemption: Option, pub id: common_utils::id_type::GlobalPaymentId, pub platform_merchant_id: Option, + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, } #[cfg(feature = "v1")] @@ -396,6 +402,8 @@ pub struct PaymentIntentNew { pub psd2_sca_exemption_type: Option, pub split_payments: Option, pub platform_merchant_id: Option, + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, } #[cfg(feature = "v2")] @@ -519,6 +527,7 @@ pub struct PaymentIntentUpdateFields { pub customer_details: Option, pub merchant_order_reference_id: Option, pub is_payment_processor_token_flow: Option, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -551,6 +560,7 @@ pub struct PaymentIntentUpdateFields { pub shipping_details: Option, pub is_payment_processor_token_flow: Option, pub tax_details: Option, + pub force_3ds_challenge: Option, } // TODO: uncomment fields as necessary @@ -593,6 +603,7 @@ pub struct PaymentIntentUpdateInternal { pub frm_metadata: Option, pub request_external_three_ds_authentication: Option, pub updated_by: String, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -636,6 +647,7 @@ pub struct PaymentIntentUpdateInternal { pub shipping_details: Option, pub is_payment_processor_token_flow: Option, pub tax_details: Option, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -678,6 +690,7 @@ impl PaymentIntentUpdate { shipping_details, is_payment_processor_token_flow, tax_details, + force_3ds_challenge, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -723,6 +736,7 @@ impl PaymentIntentUpdate { is_payment_processor_token_flow: is_payment_processor_token_flow .or(source.is_payment_processor_token_flow), tax_details: tax_details.or(source.tax_details), + force_3ds_challenge: force_3ds_challenge.or(source.force_3ds_challenge), ..source } } @@ -772,6 +786,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::Update(value) => Self { amount: Some(value.amount), @@ -811,6 +826,7 @@ impl From for PaymentIntentUpdateInternal { authorization_count: None, is_payment_processor_token_flow: value.is_payment_processor_token_flow, tax_details: None, + force_3ds_challenge: value.force_3ds_challenge, }, PaymentIntentUpdate::PaymentCreateUpdate { return_url, @@ -857,6 +873,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::PGStatusUpdate { status, @@ -899,6 +916,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::MerchantStatusUpdate { status, @@ -942,6 +960,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::ResponseUpdate { // amount, @@ -992,6 +1011,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { active_attempt_id, @@ -1034,6 +1054,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::StatusAndAttemptUpdate { status, @@ -1077,6 +1098,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::ApproveUpdate { status, @@ -1119,6 +1141,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::RejectUpdate { status, @@ -1161,6 +1184,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::SurchargeApplicableUpdate { surcharge_applicable, @@ -1202,6 +1226,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { amount: Some(amount), @@ -1240,6 +1265,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::AuthorizationCountUpdate { authorization_count, @@ -1280,6 +1306,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::CompleteAuthorizeUpdate { shipping_address_id, @@ -1320,6 +1347,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::ManualUpdate { status, updated_by } => Self { status, @@ -1358,6 +1386,7 @@ impl From for PaymentIntentUpdateInternal { shipping_details: None, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: None, }, PaymentIntentUpdate::SessionResponseUpdate { tax_details, @@ -1401,6 +1430,7 @@ impl From for PaymentIntentUpdateInternal { merchant_order_reference_id: None, shipping_details, is_payment_processor_token_flow: None, + force_3ds_challenge: None, }, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 7aabdeb26d..07f36be2bf 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1018,6 +1018,8 @@ diesel::table! { split_payments -> Nullable, #[max_length = 64] platform_merchant_id -> Nullable, + force_3ds_challenge -> Nullable, + force_3ds_challenge_trigger -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 4c07445f71..3481dc5e7f 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -953,6 +953,8 @@ diesel::table! { split_payments -> Nullable, #[max_length = 64] platform_merchant_id -> Nullable, + force_3ds_challenge -> Nullable, + force_3ds_challenge_trigger -> Nullable, #[max_length = 64] merchant_reference_id -> Nullable, billing_address -> Nullable, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index dedba889c6..f82480163c 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -116,6 +116,8 @@ pub struct PaymentIntent { pub request_extended_authorization: Option, pub psd2_sca_exemption_type: Option, pub platform_merchant_id: Option, + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, } impl PaymentIntent { @@ -470,6 +472,9 @@ pub struct PaymentIntent { pub platform_merchant_id: Option, /// Split Payment Data pub split_payments: Option, + + pub force_3ds_challenge: Option, + pub force_3ds_challenge_trigger: Option, } #[cfg(feature = "v2")] @@ -573,6 +578,7 @@ impl PaymentIntent { .map(|order_detail| Secret::new(OrderDetailsWithAmount::convert_from(order_detail))) .collect() }); + Ok(Self { id: payment_id.clone(), merchant_id: merchant_account.get_id().clone(), @@ -645,6 +651,8 @@ impl PaymentIntent { platform_merchant_id: platform_merchant_id .map(|merchant_account| merchant_account.get_id().to_owned()), split_payments: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, }) } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 1345264944..6ebea9eeec 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -208,6 +208,7 @@ pub struct PaymentIntentUpdateFields { pub active_attempt_id: Option>, // updated_by is set internally, field not present in request pub updated_by: String, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -240,6 +241,7 @@ pub struct PaymentIntentUpdateFields { pub shipping_details: Option>>, pub is_payment_processor_token_flow: Option, pub tax_details: Option, + pub force_3ds_challenge: Option, } #[cfg(feature = "v1")] @@ -407,6 +409,7 @@ pub struct PaymentIntentUpdateInternal { pub shipping_details: Option>>, pub is_payment_processor_token_flow: Option, pub tax_details: Option, + pub force_3ds_challenge: Option, } // This conversion is used in the `update_payment_intent` function @@ -455,6 +458,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), PaymentIntentUpdate::ConfirmIntentPostUpdate { @@ -498,6 +502,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), PaymentIntentUpdate::SyncUpdate { status, @@ -539,6 +544,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), PaymentIntentUpdate::CaptureUpdate { status, @@ -580,6 +586,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), PaymentIntentUpdate::SessionIntentUpdate { prerouting_algorithm, @@ -624,6 +631,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), PaymentIntentUpdate::UpdateIntent(boxed_intent) => { let PaymentIntentUpdateFields { @@ -658,6 +666,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal request_external_three_ds_authentication, active_attempt_id, updated_by, + force_3ds_challenge, } = *boxed_intent; Ok(Self { status: None, @@ -702,6 +711,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal request_external_three_ds_authentication.map(|val| val.as_bool()), updated_by, + force_3ds_challenge, }) } PaymentIntentUpdate::RecordUpdate { @@ -745,6 +755,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal frm_metadata: None, request_external_three_ds_authentication: None, updated_by, + force_3ds_challenge: None, }), } } @@ -1020,6 +1031,7 @@ impl From for DieselPaymentIntentUpdate { shipping_details: value.shipping_details.map(Encryption::from), is_payment_processor_token_flow: value.is_payment_processor_token_flow, tax_details: value.tax_details, + force_3ds_challenge: value.force_3ds_challenge, })) } PaymentIntentUpdate::PaymentCreateUpdate { @@ -1176,6 +1188,7 @@ impl From for diesel_models::PaymentIntentUpdateInt shipping_details, is_payment_processor_token_flow, tax_details, + force_3ds_challenge, } = value; Self { amount, @@ -1214,6 +1227,7 @@ impl From for diesel_models::PaymentIntentUpdateInt shipping_details: shipping_details.map(Encryption::from), is_payment_processor_token_flow, tax_details, + force_3ds_challenge, } } } @@ -1593,6 +1607,8 @@ impl behaviour::Conversion for PaymentIntent { payment_link_config, platform_merchant_id, split_payments, + force_3ds_challenge, + force_3ds_challenge_trigger, } = self; Ok(DieselPaymentIntent { skip_external_tax_calculation: Some(amount_details.get_external_tax_action_as_bool()), @@ -1674,6 +1690,8 @@ impl behaviour::Conversion for PaymentIntent { request_extended_authorization: None, platform_merchant_id, split_payments, + force_3ds_challenge, + force_3ds_challenge_trigger, }) } async fn convert_back( @@ -1808,6 +1826,8 @@ impl behaviour::Conversion for PaymentIntent { routing_algorithm_id: storage_model.routing_algorithm_id, platform_merchant_id: storage_model.platform_merchant_id, split_payments: storage_model.split_payments, + force_3ds_challenge: storage_model.force_3ds_challenge, + force_3ds_challenge_trigger: storage_model.force_3ds_challenge_trigger, }) } .await @@ -1890,6 +1910,8 @@ impl behaviour::Conversion for PaymentIntent { enable_payment_link: Some(self.enable_payment_link.as_bool()), apply_mit_exemption: Some(self.apply_mit_exemption.as_bool()), platform_merchant_id: self.platform_merchant_id, + force_3ds_challenge: self.force_3ds_challenge, + force_3ds_challenge_trigger: self.force_3ds_challenge_trigger, }) } } @@ -1958,6 +1980,8 @@ impl behaviour::Conversion for PaymentIntent { request_extended_authorization: self.request_extended_authorization, psd2_sca_exemption_type: self.psd2_sca_exemption_type, platform_merchant_id: self.platform_merchant_id, + force_3ds_challenge: self.force_3ds_challenge, + force_3ds_challenge_trigger: self.force_3ds_challenge_trigger, }) } @@ -2048,6 +2072,8 @@ impl behaviour::Conversion for PaymentIntent { request_extended_authorization: storage_model.request_extended_authorization, psd2_sca_exemption_type: storage_model.psd2_sca_exemption_type, platform_merchant_id: storage_model.platform_merchant_id, + force_3ds_challenge: storage_model.force_3ds_challenge, + force_3ds_challenge_trigger: storage_model.force_3ds_challenge_trigger, }) } .await @@ -2114,6 +2140,8 @@ impl behaviour::Conversion for PaymentIntent { request_extended_authorization: self.request_extended_authorization, psd2_sca_exemption_type: self.psd2_sca_exemption_type, platform_merchant_id: self.platform_merchant_id, + force_3ds_challenge: self.force_3ds_challenge, + force_3ds_challenge_trigger: self.force_3ds_challenge_trigger, }) } } diff --git a/crates/hyperswitch_domain_models/src/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/revenue_recovery.rs index 3acfdc2850..e5ecc4321d 100644 --- a/crates/hyperswitch_domain_models/src/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/revenue_recovery.rs @@ -197,6 +197,7 @@ impl From<&RevenueRecoveryInvoiceData> for api_payments::PaymentsCreateIntentReq session_expiry: None, frm_metadata: None, request_external_three_ds_authentication: None, + force_3ds_challenge: None, } } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index b715c43379..47a5ad25f7 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -327,6 +327,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::PaymentConnectorCategory, api_models::enums::CardDiscovery, api_models::enums::FeatureStatus, + api_models::enums::MerchantProductType, api_models::enums::CtpServiceProvider, api_models::admin::MerchantConnectorCreate, api_models::admin::AdditionalMerchantData, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 42266d6b60..efc9dc9076 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -293,6 +293,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::FeatureStatus, api_models::enums::OrderFulfillmentTimeOrigin, api_models::enums::UIWidgetFormLayout, + api_models::enums::MerchantProductType, api_models::enums::CtpServiceProvider, api_models::admin::MerchantConnectorCreate, api_models::admin::AdditionalMerchantData, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b8fe4e8362..64d3a8c42a 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -2509,6 +2509,7 @@ fn construct_zero_auth_payments_request( request_external_three_ds_authentication: None, customer_acceptance: None, browser_info: None, + force_3ds_challenge: None, }) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index cd68709afa..d033f6f700 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -7738,7 +7738,7 @@ pub async fn payment_external_authentication( authentication_details.three_ds_requestor_url.clone(), payment_intent.psd2_sca_exemption_type, payment_intent.payment_id, - business_profile.force_3ds_challenge, + payment_intent.force_3ds_challenge_trigger.unwrap_or(false), )) .await? }; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index a701d16fe1..5d6513c765 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3841,6 +3841,8 @@ mod tests { request_extended_authorization: None, psd2_sca_exemption_type: None, platform_merchant_id: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_ok()); @@ -3913,6 +3915,8 @@ mod tests { request_extended_authorization: None, psd2_sca_exemption_type: None, platform_merchant_id: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent,).is_err()) @@ -3983,6 +3987,8 @@ mod tests { request_extended_authorization: None, psd2_sca_exemption_type: None, platform_merchant_id: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_err()) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 42f72b6c3c..fb0eb12338 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -620,7 +620,6 @@ impl GetTracker, api::PaymentsRequest> storage_scheme, ) .await?; - (Some(token_data), payment_method_info) } else { (None, payment_method_info) @@ -1850,6 +1849,7 @@ impl UpdateTracker, api::PaymentsRequest> for shipping_details, is_payment_processor_token_flow, tax_details: None, + force_3ds_challenge: payment_data.payment_intent.force_3ds_challenge, })), &m_key_store, storage_scheme, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 7638eaec9a..801c6844f9 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -315,6 +315,7 @@ impl GetTracker, api::PaymentsRequest> profile_id.clone(), session_expiry, platform_merchant_account, + &business_profile, ) .await?; @@ -1387,6 +1388,7 @@ impl PaymentCreate { profile_id: common_utils::id_type::ProfileId, session_expiry: PrimitiveDateTime, platform_merchant_account: Option<&domain::MerchantAccount>, + business_profile: &domain::Profile, ) -> RouterResult { let created_at @ modified_at @ last_synced = common_utils::date_time::now(); @@ -1515,6 +1517,9 @@ impl PaymentCreate { }), payment_method_type: None, }); + let force_3ds_challenge_trigger = request + .force_3ds_challenge + .unwrap_or(business_profile.force_3ds_challenge); Ok(storage::PaymentIntent { payment_id: payment_id.to_owned(), @@ -1576,6 +1581,8 @@ impl PaymentCreate { psd2_sca_exemption_type: request.psd2_sca_exemption_type, platform_merchant_id: platform_merchant_account .map(|platform_merchant_account| platform_merchant_account.get_id().to_owned()), + force_3ds_challenge: request.force_3ds_challenge, + force_3ds_challenge_trigger: Some(force_3ds_challenge_trigger), }) } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 9d09d56515..0baa0829fc 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -446,6 +446,10 @@ impl GetTracker, api::PaymentsRequest> payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); + payment_intent.force_3ds_challenge = request + .force_3ds_challenge + .or(payment_intent.force_3ds_challenge); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -933,6 +937,7 @@ impl UpdateTracker, api::PaymentsRequest> for shipping_details, is_payment_processor_token_flow: None, tax_details: None, + force_3ds_challenge: payment_data.payment_intent.force_3ds_challenge, })), key_store, storage_scheme, diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index 9b89ead615..98a3713605 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -365,6 +365,7 @@ impl UpdateTracker, PaymentsUpdateIn updated_by: intent.updated_by, tax_details: intent.amount_details.tax_details, active_attempt_id: Some(intent.active_attempt_id), + force_3ds_challenge: intent.force_3ds_challenge, })); let new_payment_intent = db diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index eb5b3d8817..37bde6c1e2 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2583,6 +2583,8 @@ where capture_before: payment_attempt.capture_before, extended_authorization_applied: payment_attempt.extended_authorization_applied, card_discovery: payment_attempt.card_discovery, + force_3ds_challenge: payment_intent.force_3ds_challenge, + force_3ds_challenge_trigger: payment_intent.force_3ds_challenge_trigger, issuer_error_code: payment_attempt.issuer_error_code, issuer_error_message: payment_attempt.issuer_error_message, }; @@ -2873,6 +2875,8 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay connector_mandate_id:None, shipping_cost: None, card_discovery: pa.card_discovery, + force_3ds_challenge: pi.force_3ds_challenge, + force_3ds_challenge_trigger: pi.force_3ds_challenge_trigger, issuer_error_code: pa.issuer_error_code, issuer_error_message: pa.issuer_error_message, } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 8217f9e34c..113023fbcb 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -277,6 +277,8 @@ pub async fn generate_sample_data( request_extended_authorization: None, psd2_sca_exemption_type: None, platform_merchant_id: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, }; let (connector_transaction_id, processor_transaction_data) = ConnectorTransactionId::form_id_and_data(attempt_id.clone()); diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 0e57540e0e..e5d9c62591 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -454,6 +454,8 @@ async fn payments_create_core() { connector_mandate_id: None, shipping_cost: None, card_discovery: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, issuer_error_code: None, issuer_error_message: None, }; @@ -723,6 +725,8 @@ async fn payments_create_core_adyen_no_redirect() { connector_mandate_id: None, shipping_cost: None, card_discovery: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, issuer_error_code: None, issuer_error_message: None, }, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 19281b4501..aff2753bc3 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -215,6 +215,8 @@ async fn payments_create_core() { connector_mandate_id: None, shipping_cost: None, card_discovery: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, issuer_error_code: None, issuer_error_message: None, }; @@ -493,6 +495,8 @@ async fn payments_create_core_adyen_no_redirect() { connector_mandate_id: None, shipping_cost: None, card_discovery: None, + force_3ds_challenge: None, + force_3ds_challenge_trigger: None, issuer_error_code: None, issuer_error_message: None, }, diff --git a/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/down.sql b/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/down.sql new file mode 100644 index 0000000000..bf11512b8c --- /dev/null +++ b/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent +DROP COLUMN IF EXISTS force_3ds_challenge; \ No newline at end of file diff --git a/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/up.sql b/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/up.sql new file mode 100644 index 0000000000..06e538d6db --- /dev/null +++ b/migrations/2025-03-11-171330_add-force-3ds-challenge-in-payment-intent/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_intent +ADD COLUMN IF NOT EXISTS force_3ds_challenge boolean DEFAULT false; \ No newline at end of file diff --git a/migrations/2025-03-20-085151_force-3ds-challenge-triggered/down.sql b/migrations/2025-03-20-085151_force-3ds-challenge-triggered/down.sql new file mode 100644 index 0000000000..76bf7e20de --- /dev/null +++ b/migrations/2025-03-20-085151_force-3ds-challenge-triggered/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent +DROP COLUMN IF EXISTS force_3ds_challenge_trigger; \ No newline at end of file diff --git a/migrations/2025-03-20-085151_force-3ds-challenge-triggered/up.sql b/migrations/2025-03-20-085151_force-3ds-challenge-triggered/up.sql new file mode 100644 index 0000000000..6e6349381e --- /dev/null +++ b/migrations/2025-03-20-085151_force-3ds-challenge-triggered/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_intent +ADD COLUMN IF NOT EXISTS force_3ds_challenge_trigger boolean DEFAULT false; \ No newline at end of file