From c90744a6aa06490db38251d0251baf0e255fba32 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:07:57 +0530 Subject: [PATCH] feat(core): Add support for partial auth in proxy payments [V2] (#9503) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference/v1/openapi_spec_v1.json | 6 +++ api-reference/v2/openapi_spec_v2.json | 24 +++++++++ crates/api_models/src/payments.rs | 36 ++++++++++--- crates/common_types/src/primitive_wrappers.rs | 47 ++++++++++++++++ crates/diesel_models/src/payment_intent.rs | 20 ++++--- .../src/connectors/chargebee/transformers.rs | 2 + .../src/connectors/nuvei/transformers.rs | 13 +++-- .../connectors/stripebilling/transformers.rs | 2 + .../connectors/worldpayvantiv/transformers.rs | 26 ++++++++- .../hyperswitch_domain_models/src/payments.rs | 10 ++-- .../src/payments/payment_intent.rs | 25 ++++++--- .../src/revenue_recovery.rs | 5 ++ .../src/router_data.rs | 54 +++++++++++++------ .../src/router_request_types.rs | 6 ++- crates/router/src/connector/utils.rs | 4 +- crates/router/src/core/payment_methods.rs | 1 + .../operations/payment_update_intent.rs | 4 ++ .../router/src/core/payments/transformers.rs | 10 ++-- .../src/core/revenue_recovery/transformers.rs | 7 +-- .../src/services/kafka/payment_intent.rs | 8 ++- .../services/kafka/payment_intent_event.rs | 8 ++- 21 files changed, 259 insertions(+), 59 deletions(-) diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 6ac0ad192b..49ce347b1b 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -23114,6 +23114,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true } } @@ -23587,6 +23588,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true }, "enable_overcapture": { @@ -24205,6 +24207,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true }, "enable_overcapture": { @@ -24991,6 +24994,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true }, "enable_overcapture": { @@ -25635,6 +25639,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true }, "enable_overcapture": { @@ -26259,6 +26264,7 @@ "enable_partial_authorization": { "type": "boolean", "description": "Allow partial authorization for this payment", + "default": false, "nullable": true }, "enable_overcapture": { diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 523732de9e..e89f7abdf1 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -19301,6 +19301,12 @@ } ], "nullable": true + }, + "enable_partial_authorization": { + "type": "boolean", + "description": "Allow partial authorization for this payment", + "default": false, + "nullable": true } }, "additionalProperties": false @@ -19672,6 +19678,12 @@ }, "payment_type": { "$ref": "#/components/schemas/PaymentType" + }, + "enable_partial_authorization": { + "type": "boolean", + "description": "Allow partial authorization for this payment", + "default": false, + "nullable": true } }, "additionalProperties": false @@ -20131,6 +20143,12 @@ "type": "boolean", "description": "Stringified connector raw response body. Only returned if `return_raw_connector_response` is true", "nullable": true + }, + "enable_partial_authorization": { + "type": "boolean", + "description": "Allow partial authorization for this payment", + "default": false, + "nullable": true } }, "additionalProperties": false @@ -20604,6 +20622,12 @@ } ], "nullable": true + }, + "enable_partial_authorization": { + "type": "boolean", + "description": "Allow partial authorization for this payment", + "default": false, + "nullable": true } }, "additionalProperties": false diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f4e2451ab6..840ab7537f 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -10,11 +10,11 @@ use cards::CardNumber; #[cfg(feature = "v2")] use common_enums::enums::PaymentConnectorTransmission; use common_enums::ProductType; -use common_types::payments as common_payments_types; #[cfg(feature = "v1")] use common_types::primitive_wrappers::{ ExtendedAuthorizationAppliedBool, RequestExtendedAuthorizationBool, }; +use common_types::{payments as common_payments_types, primitive_wrappers}; use common_utils::{ consts::default_payments_list_limit, crypto, @@ -310,6 +310,10 @@ pub struct PaymentsCreateIntentRequest { /// Merchant connector details used to make payments. #[schema(value_type = Option)] pub merchant_connector_details: Option, + + /// Allow partial authorization for this payment + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] @@ -486,6 +490,10 @@ pub struct PaymentsUpdateIntentRequest { #[schema(value_type = Option)] /// Whether to set / unset the active attempt id pub set_active_attempt_id: Option, + + /// Allow partial authorization for this payment + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] @@ -518,6 +526,7 @@ impl PaymentsUpdateIntentRequest { session_expiry: None, frm_metadata: None, request_external_three_ds_authentication: None, + enable_partial_authorization: None, } } } @@ -662,6 +671,10 @@ pub struct PaymentsIntentResponse { /// The type of the payment that differentiates between normal and various types of mandate payments #[schema(value_type = PaymentType)] pub payment_type: api_enums::PaymentType, + + /// Allow partial authorization for this payment + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] @@ -1271,12 +1284,13 @@ pub struct PaymentsRequest { pub order_date: Option, /// Allow partial authorization for this payment - pub enable_partial_authorization: Option, + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, /// Boolean indicating whether to enable overcapture for this payment #[remove_in(PaymentsConfirmRequest)] #[schema(value_type = Option, example = true)] - pub enable_overcapture: Option, + pub enable_overcapture: Option, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -5664,15 +5678,16 @@ pub struct PaymentsResponse { pub whole_connector_response: Option>, /// Allow partial authorization for this payment - pub enable_partial_authorization: Option, + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, /// Bool indicating if overcapture must be requested for this payment #[schema(value_type = Option)] - pub enable_overcapture: Option, + pub enable_overcapture: Option, /// Boolean indicating whether overcapture is effectively enabled for this payment #[schema(value_type = Option)] - pub is_overcapture_enabled: Option, + pub is_overcapture_enabled: Option, /// Contains card network response details (e.g., Visa/Mastercard advice codes). #[schema(value_type = Option)] @@ -6099,6 +6114,10 @@ pub struct PaymentsRequest { /// Stringified connector raw response body. Only returned if `return_raw_connector_response` is true pub return_raw_connector_response: Option, + + /// Allow partial authorization for this payment + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] @@ -6133,6 +6152,7 @@ impl From<&PaymentsRequest> for PaymentsCreateIntentRequest { .request_external_three_ds_authentication, force_3ds_challenge: request.force_3ds_challenge, merchant_connector_details: request.merchant_connector_details.clone(), + enable_partial_authorization: request.enable_partial_authorization, } } } @@ -9784,6 +9804,10 @@ pub struct RecoveryPaymentsCreate { /// Type of action that needs to be taken after consuming the recovery payload. For example: scheduling a failed payment or stopping the invoice. pub action: common_payments_types::RecoveryAction, + /// Allow partial authorization for this payment + #[schema(value_type = Option, default = false)] + pub enable_partial_authorization: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] pub metadata: Option, diff --git a/crates/common_types/src/primitive_wrappers.rs b/crates/common_types/src/primitive_wrappers.rs index d7b7174afb..925dcf0af5 100644 --- a/crates/common_types/src/primitive_wrappers.rs +++ b/crates/common_types/src/primitive_wrappers.rs @@ -92,6 +92,53 @@ mod bool_wrappers { } } + /// Bool that represents if Enable Partial Authorization is Requested or not + #[derive( + Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, diesel::expression::AsExpression, + )] + #[diesel(sql_type = diesel::sql_types::Bool)] + pub struct EnablePartialAuthorizationBool(bool); + impl Deref for EnablePartialAuthorizationBool { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl From for EnablePartialAuthorizationBool { + fn from(value: bool) -> Self { + Self(value) + } + } + impl EnablePartialAuthorizationBool { + /// returns the inner bool value + pub fn is_true(&self) -> bool { + self.0 + } + } + impl diesel::serialize::ToSql for EnablePartialAuthorizationBool + where + DB: diesel::backend::Backend, + bool: diesel::serialize::ToSql, + { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, DB>, + ) -> diesel::serialize::Result { + self.0.to_sql(out) + } + } + impl diesel::deserialize::FromSql + for EnablePartialAuthorizationBool + where + DB: diesel::backend::Backend, + bool: diesel::deserialize::FromSql, + { + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + bool::from_sql(value).map(Self) + } + } + /// Bool that represents if Extended Authorization is always Requested or not #[derive( Clone, Copy, Debug, Eq, PartialEq, diesel::expression::AsExpression, Serialize, Deserialize, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 0e578012a3..e1640435b1 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -1,5 +1,7 @@ use common_enums::{PaymentMethodType, RequestIncrementalAuthorization}; -use common_types::primitive_wrappers::RequestExtendedAuthorizationBool; +use common_types::primitive_wrappers::{ + EnablePartialAuthorizationBool, RequestExtendedAuthorizationBool, +}; use common_utils::{encryption::Encryption, pii, types::MinorUnit}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use masking::ExposeInterface; @@ -76,7 +78,7 @@ pub struct PaymentIntent { pub shipping_amount_tax: Option, pub duty_amount: Option, pub order_date: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, pub merchant_reference_id: Option, pub billing_address: Option, @@ -180,7 +182,7 @@ pub struct PaymentIntent { pub shipping_amount_tax: Option, pub duty_amount: Option, pub order_date: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, } @@ -359,7 +361,7 @@ pub struct PaymentIntentNew { pub organization_id: common_utils::id_type::OrganizationId, pub tax_details: Option, pub skip_external_tax_calculation: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub split_txns_enabled: Option, pub merchant_reference_id: Option, pub billing_address: Option, @@ -470,7 +472,7 @@ pub struct PaymentIntentNew { pub order_date: Option, pub shipping_amount_tax: Option, pub duty_amount: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, } @@ -641,7 +643,7 @@ pub struct PaymentIntentUpdateFields { pub order_date: Option, pub shipping_amount_tax: Option, pub duty_amount: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, } @@ -687,6 +689,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub force_3ds_challenge: Option, pub is_iframe_redirection_enabled: Option, + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] @@ -730,6 +733,7 @@ impl PaymentIntentUpdateInternal { updated_by, force_3ds_challenge, is_iframe_redirection_enabled, + enable_partial_authorization, } = self; PaymentIntent { @@ -807,7 +811,7 @@ impl PaymentIntentUpdateInternal { shipping_amount_tax: source.shipping_amount_tax, duty_amount: source.duty_amount, order_date: source.order_date, - enable_partial_authorization: None, + enable_partial_authorization: source.enable_partial_authorization, split_txns_enabled: source.split_txns_enabled, enable_overcapture: None, active_attempt_id_type: source.active_attempt_id_type, @@ -867,7 +871,7 @@ pub struct PaymentIntentUpdateInternal { pub order_date: Option, pub shipping_amount_tax: Option, pub duty_amount: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, } diff --git a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs index 953dfdca78..3bf78b0e68 100644 --- a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs @@ -782,6 +782,8 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvoiceD next_billing_at: invoice_next_billing_time, billing_started_at, metadata: None, + // TODO! This field should be handled for billing connnector integrations + enable_partial_authorization: None, }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs index 3977691605..e9d766f912 100644 --- a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs @@ -1,6 +1,9 @@ use common_enums::{enums, CaptureMethod, FutureUsage, PaymentChannel}; -use common_types::payments::{ - ApplePayPaymentData, ApplePayPredecryptData, GPayPredecryptData, GpayTokenizationData, +use common_types::{ + payments::{ + ApplePayPaymentData, ApplePayPredecryptData, GPayPredecryptData, GpayTokenizationData, + }, + primitive_wrappers, }; use common_utils::{ crypto::{self, GenerateDigest}, @@ -465,9 +468,9 @@ pub enum PartialApprovalFlag { Disabled, } -impl From for PartialApprovalFlag { - fn from(value: bool) -> Self { - if value { +impl From for PartialApprovalFlag { + fn from(value: primitive_wrappers::EnablePartialAuthorizationBool) -> Self { + if value.is_true() { Self::Enabled } else { Self::Disabled diff --git a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs index 17b7fbd6ed..cd5ac5ea49 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs @@ -423,6 +423,8 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvo next_billing_at, billing_started_at, metadata: None, + // TODO! This field should be handled for billing connnector integrations + enable_partial_authorization: None, }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/worldpayvantiv/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpayvantiv/transformers.rs index 98761cb8cc..2b4d28bd4b 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpayvantiv/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpayvantiv/transformers.rs @@ -1,6 +1,6 @@ use common_utils::{ ext_traits::Encode, - types::{MinorUnit, StringMinorUnitForConnector}, + types::{MinorUnit, StringMajorUnit, StringMinorUnitForConnector}, }; use error_stack::ResultExt; use hyperswitch_domain_models::{ @@ -562,6 +562,21 @@ impl TryFrom TryFrom, pub reject_type: Option, pub dupe_txn_id: Option, - pub amount: Option, + pub amount: Option, pub purchase_currency: Option, pub post_day: Option, pub reported_timestamp: Option, @@ -1921,6 +1937,7 @@ impl }), connector_response, amount_captured: sale_response.approved_amount.map(MinorUnit::get_amount_as_i64), + minor_amount_captured: sale_response.approved_amount, ..item.data }) } @@ -1998,6 +2015,11 @@ impl } else { None }, + minor_amount_captured: if payment_flow_type == WorldpayvantivPaymentFlow::Sale { + auth_response.approved_amount + } else { + None + }, ..item.data }) } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 2cc1bcf3b0..2fc41ee558 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] use api_models::payments::{SessionToken, VaultSessionDetails}; +use common_types::primitive_wrappers; #[cfg(feature = "v1")] use common_types::primitive_wrappers::{ AlwaysRequestExtendedAuthorization, EnableOvercaptureBool, RequestExtendedAuthorizationBool, @@ -122,7 +123,7 @@ pub struct PaymentIntent { pub order_date: Option, pub shipping_amount_tax: Option, pub duty_amount: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: Option, pub enable_overcapture: Option, } @@ -213,9 +214,7 @@ impl PaymentIntent { pub fn get_enable_overcapture_bool_if_connector_supports( &self, connector: common_enums::connector_enums::Connector, - always_enable_overcapture: Option< - common_types::primitive_wrappers::AlwaysEnableOvercaptureBool, - >, + always_enable_overcapture: Option, capture_method: &Option, ) -> Option { let is_overcapture_supported_by_connector = @@ -550,6 +549,8 @@ pub struct PaymentIntent { /// Indicates whether the payment_id was provided by the merchant (true), /// or generated internally by Hyperswitch (false) pub is_payment_id_from_merchant: Option, + /// Denotes whether merchant requested for partial authorization to be enabled for this payment. + pub enable_partial_authorization: Option, } #[cfg(feature = "v2")] @@ -740,6 +741,7 @@ impl PaymentIntent { created_by: None, is_iframe_redirection_enabled: None, is_payment_id_from_merchant: None, + enable_partial_authorization: request.enable_partial_authorization, }) } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 51cd111d15..8da2bc446f 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1,3 +1,4 @@ +use common_types::primitive_wrappers; #[cfg(feature = "v1")] use common_utils::consts::PAYMENTS_LIST_MAX_LIMIT_V2; #[cfg(feature = "v2")] @@ -208,6 +209,7 @@ pub struct PaymentIntentUpdateFields { pub updated_by: String, pub force_3ds_challenge: Option, pub is_iframe_redirection_enabled: Option, + pub enable_partial_authorization: Option, } #[cfg(feature = "v1")] @@ -250,8 +252,8 @@ pub struct PaymentIntentUpdateFields { pub is_confirm_operation: bool, pub payment_channel: Option, pub feature_metadata: Option>, - pub enable_partial_authorization: Option, - pub enable_overcapture: Option, + pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -452,8 +454,8 @@ pub struct PaymentIntentUpdateInternal { pub order_date: Option, pub shipping_amount_tax: Option, pub duty_amount: Option, - pub enable_partial_authorization: Option, - pub enable_overcapture: Option, + pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } // This conversion is used in the `update_payment_intent` function @@ -504,6 +506,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::ConfirmIntentPostUpdate { @@ -549,6 +552,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::SyncUpdate { status, @@ -592,6 +596,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::CaptureUpdate { status, @@ -635,6 +640,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::SessionIntentUpdate { prerouting_algorithm, @@ -681,6 +687,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::UpdateIntent(boxed_intent) => { let PaymentIntentUpdateFields { @@ -717,6 +724,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge, is_iframe_redirection_enabled, + enable_partial_authorization, } = *boxed_intent; Ok(Self { status: None, @@ -762,6 +770,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge, is_iframe_redirection_enabled, + enable_partial_authorization, }) } PaymentIntentUpdate::RecordUpdate { @@ -807,6 +816,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), PaymentIntentUpdate::VoidUpdate { status, updated_by } => Ok(Self { status: Some(status), @@ -846,6 +856,7 @@ impl TryFrom for diesel_models::PaymentIntentUpdateInternal updated_by, force_3ds_challenge: None, is_iframe_redirection_enabled: None, + enable_partial_authorization: None, }), } } @@ -1741,6 +1752,7 @@ impl behaviour::Conversion for PaymentIntent { created_by, is_iframe_redirection_enabled, is_payment_id_from_merchant, + enable_partial_authorization, } = self; Ok(DieselPaymentIntent { skip_external_tax_calculation: Some(amount_details.get_external_tax_action_as_bool()), @@ -1836,7 +1848,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_amount_tax: None, duty_amount: None, order_date: None, - enable_partial_authorization: None, + enable_partial_authorization, enable_overcapture: None, }) } @@ -1983,6 +1995,7 @@ impl behaviour::Conversion for PaymentIntent { .and_then(|created_by| created_by.parse::().ok()), is_iframe_redirection_enabled: storage_model.is_iframe_redirection_enabled, is_payment_id_from_merchant: storage_model.is_payment_id_from_merchant, + enable_partial_authorization: storage_model.enable_partial_authorization, }) } .await @@ -2078,7 +2091,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_amount_tax: None, duty_amount: None, order_date: None, - enable_partial_authorization: None, + enable_partial_authorization: self.enable_partial_authorization, }) } } diff --git a/crates/hyperswitch_domain_models/src/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/revenue_recovery.rs index 8377fb06cb..feb50d2cf3 100644 --- a/crates/hyperswitch_domain_models/src/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/revenue_recovery.rs @@ -1,5 +1,6 @@ use api_models::{payments as api_payments, webhooks}; use common_enums::enums as common_enums; +use common_types::primitive_wrappers; use common_utils::{id_type, pii, types as util_types}; use time::PrimitiveDateTime; @@ -79,6 +80,8 @@ pub struct RevenueRecoveryInvoiceData { pub billing_started_at: Option, /// metadata of the merchant pub metadata: Option, + /// Allow partial authorization for this payment + pub enable_partial_authorization: Option, } #[derive(Clone, Debug)] @@ -166,6 +169,7 @@ impl From<&RevenueRecoveryInvoiceData> for api_payments::PaymentsCreateIntentReq request_external_three_ds_authentication: None, force_3ds_challenge: None, merchant_connector_details: None, + enable_partial_authorization: data.enable_partial_authorization, } } } @@ -181,6 +185,7 @@ impl From<&BillingConnectorInvoiceSyncResponse> for RevenueRecoveryInvoiceData { next_billing_at: data.ends_at, billing_started_at: data.created_at, metadata: None, + enable_partial_authorization: None, } } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index ad778e9aa7..0c22d02550 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -799,11 +799,23 @@ impl fn get_attempt_status_for_db_update( &self, - _payment_data: &payments::PaymentConfirmData, + payment_data: &payments::PaymentConfirmData, ) -> common_enums::AttemptStatus { - // For this step, consider whatever status was given by the connector module - // We do not need to check for amount captured or amount capturable here because we are authorizing the whole amount - self.status + match self.status { + common_enums::AttemptStatus::Charged => { + let amount_captured = self + .get_captured_amount(payment_data) + .unwrap_or(MinorUnit::zero()); + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } } fn get_amount_capturable( @@ -812,7 +824,7 @@ impl ) -> Option { // Based on the status of the response, we can determine the amount capturable let intent_status = common_enums::IntentStatus::from(self.status); - match intent_status { + let amount_capturable_from_intent_status = match intent_status { // If the status is already succeeded / failed we cannot capture any more amount // So set the amount capturable to zero common_enums::IntentStatus::Succeeded @@ -839,7 +851,10 @@ impl // Invalid statues for this flow, after doing authorization this state is invalid common_enums::IntentStatus::PartiallyCaptured | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, - } + }; + self.minor_amount_capturable + .or(amount_capturable_from_intent_status) + .or(Some(payment_data.payment_attempt.get_total_amount())) } fn get_captured_amount( @@ -848,7 +863,7 @@ impl ) -> Option { // Based on the status of the response, we can determine the amount that was captured let intent_status = common_enums::IntentStatus::from(self.status); - match intent_status { + let amount_captured_from_intent_status = match intent_status { // If the status is succeeded then we have captured the whole amount // we need not check for `amount_to_capture` here because passing `amount_to_capture` when authorizing is not supported common_enums::IntentStatus::Succeeded | common_enums::IntentStatus::Conflicted => { @@ -875,10 +890,12 @@ impl // Invalid statues for this flow common_enums::IntentStatus::PartiallyCaptured | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, - } + }; + self.minor_amount_captured + .or(amount_captured_from_intent_status) + .or(Some(payment_data.payment_attempt.get_total_amount())) } } - #[cfg(feature = "v2")] impl TrackerPostUpdateObjects< @@ -1274,7 +1291,7 @@ impl // Based on the status of the response, we can determine the amount capturable let intent_status = common_enums::IntentStatus::from(self.status); - match intent_status { + let amount_capturable_from_intent_status = match intent_status { // If the status is already succeeded / failed we cannot capture any more amount common_enums::IntentStatus::Succeeded | common_enums::IntentStatus::Failed @@ -1290,14 +1307,16 @@ impl common_enums::IntentStatus::RequiresPaymentMethod | common_enums::IntentStatus::RequiresConfirmation => None, common_enums::IntentStatus::RequiresCapture - | common_enums::IntentStatus::PartiallyAuthorizedAndRequiresCapture => { + | common_enums::IntentStatus::PartiallyAuthorizedAndRequiresCapture + | common_enums::IntentStatus::PartiallyCaptured => { let total_amount = payment_attempt.amount_details.get_net_amount(); Some(total_amount) } // Invalid statues for this flow - common_enums::IntentStatus::PartiallyCaptured - | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, - } + common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + }; + self.minor_amount_capturable + .or(amount_capturable_from_intent_status) } fn get_captured_amount( @@ -1308,7 +1327,7 @@ impl // Based on the status of the response, we can determine the amount capturable let intent_status = common_enums::IntentStatus::from(self.status); - match intent_status { + let amount_captured_from_intent_status = match intent_status { // If the status is succeeded then we have captured the whole amount or amount_to_capture common_enums::IntentStatus::Succeeded | common_enums::IntentStatus::Conflicted => { let amount_to_capture = payment_attempt.amount_details.get_amount_to_capture(); @@ -1338,7 +1357,10 @@ impl // Invalid statues for this flow common_enums::IntentStatus::PartiallyCaptured | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, - } + }; + self.minor_amount_captured + .or(amount_captured_from_intent_status) + .or(Some(payment_data.payment_attempt.get_total_amount())) } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 3f9d4f333c..e21f875f25 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -85,7 +85,8 @@ pub struct PaymentsAuthorizeData { pub order_id: Option, pub locale: Option, pub payment_channel: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: + Option, pub enable_overcapture: Option, } @@ -1402,7 +1403,8 @@ pub struct SetupMandateRequestData { pub shipping_cost: Option, pub connector_testing_data: Option, pub customer_id: Option, - pub enable_partial_authorization: Option, + pub enable_partial_authorization: + Option, pub payment_channel: Option, } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8b91d6f7f7..8c33ce1044 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -238,7 +238,7 @@ where }) && payment_data .payment_intent .enable_partial_authorization - .is_some_and(|val| val) + .is_some_and(|val| val.is_true()) { Ok(enums::AttemptStatus::PartiallyAuthorized) } else if capturable_amount.is_some_and(|capturable_amount| { @@ -246,7 +246,7 @@ where }) && !payment_data .payment_intent .enable_partial_authorization - .is_some_and(|val| val) + .is_some_and(|val| val.is_true()) { Err(ApiErrorResponse::IntegrityCheckFailed { reason: "capturable_amount is less than the total attempt amount" diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index ed78b942e9..9d260cecd0 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3437,6 +3437,7 @@ fn construct_zero_auth_payments_request( is_iframe_redirection_enabled: None, merchant_connector_details: None, return_raw_connector_response: None, + enable_partial_authorization: None, }) } 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 2100ec3762..2af8ee3a7e 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -190,6 +190,7 @@ impl GetTracker, PaymentsUpda frm_metadata, request_external_three_ds_authentication, set_active_attempt_id, + enable_partial_authorization, } = request.clone(); let batch_encrypted_data = domain_types::crypto_operation( @@ -296,6 +297,8 @@ impl GetTracker, PaymentsUpda allowed_payment_method_types: allowed_payment_method_types .or(payment_intent.allowed_payment_method_types), active_attempt_id, + enable_partial_authorization: enable_partial_authorization + .or(payment_intent.enable_partial_authorization), ..payment_intent }; @@ -381,6 +384,7 @@ impl UpdateTracker, PaymentsUpdateIn active_attempt_id: Some(intent.active_attempt_id), force_3ds_challenge: intent.force_3ds_challenge, is_iframe_redirection_enabled: intent.is_iframe_redirection_enabled, + enable_partial_authorization: intent.enable_partial_authorization, })); let new_payment_intent = db diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index cbb022a9f7..711b05154b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -434,7 +434,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( order_id: None, locale: None, payment_channel: None, - enable_partial_authorization: None, + enable_partial_authorization: payment_data.payment_intent.enable_partial_authorization, enable_overcapture: None, }; let connector_mandate_request_reference_id = payment_data @@ -479,8 +479,11 @@ pub async fn construct_payment_router_data_for_authorize<'a>( connector_wallets_details: None, request, response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), - amount_captured: None, - minor_amount_captured: None, + amount_captured: payment_data + .payment_intent + .amount_captured + .map(|amt| amt.get_amount_as_i64()), + minor_amount_captured: payment_data.payment_intent.amount_captured, access_token: None, session_token: None, reference_id: None, @@ -2467,6 +2470,7 @@ where request_external_three_ds_authentication: payment_intent .request_external_three_ds_authentication, payment_type, + enable_partial_authorization: payment_intent.enable_partial_authorization, }, vec![], ))) diff --git a/crates/router/src/core/revenue_recovery/transformers.rs b/crates/router/src/core/revenue_recovery/transformers.rs index df6e430a11..2016d7b5cc 100644 --- a/crates/router/src/core/revenue_recovery/transformers.rs +++ b/crates/router/src/core/revenue_recovery/transformers.rs @@ -12,7 +12,9 @@ impl ForeignFrom for RevenueRecoveryPaymentsAttemptStatus { AttemptStatus::Authorized | AttemptStatus::Charged | AttemptStatus::AutoRefunded - | AttemptStatus::PartiallyAuthorized => Self::Succeeded, + | AttemptStatus::PartiallyAuthorized + | AttemptStatus::PartialCharged + | AttemptStatus::PartialChargedAndChargeable => Self::Succeeded, AttemptStatus::Started | AttemptStatus::AuthenticationSuccessful @@ -32,8 +34,6 @@ impl ForeignFrom for RevenueRecoveryPaymentsAttemptStatus { AttemptStatus::Voided | AttemptStatus::VoidedPostCharge | AttemptStatus::ConfirmationAwaited - | AttemptStatus::PartialCharged - | AttemptStatus::PartialChargedAndChargeable | AttemptStatus::PaymentMethodAwaited | AttemptStatus::AuthenticationPending | AttemptStatus::DeviceDataCollectionPending @@ -57,6 +57,7 @@ impl ForeignFrom next_billing_at: None, billing_started_at: data.billing_started_at, metadata: data.metadata, + enable_partial_authorization: data.enable_partial_authorization, } } } diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs index cd31a22104..366c3a9f96 100644 --- a/crates/router/src/services/kafka/payment_intent.rs +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -1,5 +1,8 @@ #[cfg(feature = "v2")] -use ::common_types::{payments, primitive_wrappers::RequestExtendedAuthorizationBool}; +use ::common_types::{ + payments, + primitive_wrappers::{EnablePartialAuthorizationBool, RequestExtendedAuthorizationBool}, +}; #[cfg(feature = "v2")] use common_enums; #[cfg(feature = "v2")] @@ -178,6 +181,7 @@ pub struct KafkaPaymentIntent<'a> { pub customer_present: common_enums::PresenceOfCustomerDuringPayment, pub routing_algorithm_id: Option<&'a id_type::RoutingId>, pub payment_link_config: Option<&'a PaymentLinkConfigRequestForPayments>, + pub enable_partial_authorization: Option, #[serde(flatten)] infra_values: Option, @@ -239,6 +243,7 @@ impl<'a> KafkaPaymentIntent<'a> { created_by, is_iframe_redirection_enabled, is_payment_id_from_merchant, + enable_partial_authorization, } = intent; Self { @@ -314,6 +319,7 @@ impl<'a> KafkaPaymentIntent<'a> { routing_algorithm_id: routing_algorithm_id.as_ref(), payment_link_config: payment_link_config.as_ref(), infra_values, + enable_partial_authorization: *enable_partial_authorization, } } } diff --git a/crates/router/src/services/kafka/payment_intent_event.rs b/crates/router/src/services/kafka/payment_intent_event.rs index 37e2eb6146..9492ae23cf 100644 --- a/crates/router/src/services/kafka/payment_intent_event.rs +++ b/crates/router/src/services/kafka/payment_intent_event.rs @@ -1,5 +1,8 @@ #[cfg(feature = "v2")] -use ::common_types::{payments, primitive_wrappers::RequestExtendedAuthorizationBool}; +use ::common_types::{ + payments, + primitive_wrappers::{EnablePartialAuthorizationBool, RequestExtendedAuthorizationBool}, +}; #[cfg(feature = "v2")] use common_enums::{self, RequestIncrementalAuthorization}; use common_utils::{ @@ -131,6 +134,7 @@ pub struct KafkaPaymentIntentEvent<'a> { pub customer_present: common_enums::PresenceOfCustomerDuringPayment, pub routing_algorithm_id: Option<&'a id_type::RoutingId>, pub payment_link_config: Option<&'a PaymentLinkConfigRequestForPayments>, + pub enable_partial_authorization: Option, #[serde(flatten)] infra_values: Option, @@ -251,6 +255,7 @@ impl<'a> KafkaPaymentIntentEvent<'a> { created_by, is_iframe_redirection_enabled, is_payment_id_from_merchant, + enable_partial_authorization, } = intent; Self { @@ -326,6 +331,7 @@ impl<'a> KafkaPaymentIntentEvent<'a> { routing_algorithm_id: routing_algorithm_id.as_ref(), payment_link_config: payment_link_config.as_ref(), infra_values, + enable_partial_authorization: *enable_partial_authorization, } } }