diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 962cc21197..dd0d8cd010 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -23544,6 +23544,12 @@ "type": "boolean", "description": "Allow partial authorization for this payment", "nullable": true + }, + "enable_overcapture": { + "type": "boolean", + "description": "Boolean indicating whether to enable overcapture for this payment", + "example": true, + "nullable": true } } }, @@ -24150,6 +24156,16 @@ "type": "boolean", "description": "Allow partial authorization for this payment", "nullable": true + }, + "enable_overcapture": { + "type": "boolean", + "description": "Bool indicating if overcapture must be requested for this payment", + "nullable": true + }, + "is_overcapture_enabled": { + "type": "boolean", + "description": "Boolean indicating whether overcapture is effectively enabled for this payment", + "nullable": true } } }, @@ -24913,6 +24929,12 @@ "type": "boolean", "description": "Allow partial authorization for this payment", "nullable": true + }, + "enable_overcapture": { + "type": "boolean", + "description": "Boolean indicating whether to enable overcapture for this payment", + "example": true, + "nullable": true } }, "additionalProperties": false @@ -25545,6 +25567,16 @@ "type": "boolean", "description": "Allow partial authorization for this payment", "nullable": true + }, + "enable_overcapture": { + "type": "boolean", + "description": "Bool indicating if overcapture must be requested for this payment", + "nullable": true + }, + "is_overcapture_enabled": { + "type": "boolean", + "description": "Boolean indicating whether overcapture is effectively enabled for this payment", + "nullable": true } } }, @@ -26151,6 +26183,12 @@ "type": "boolean", "description": "Allow partial authorization for this payment", "nullable": true + }, + "enable_overcapture": { + "type": "boolean", + "description": "Boolean indicating whether to enable overcapture for this payment", + "example": true, + "nullable": true } } }, @@ -28531,7 +28569,7 @@ "dispute_polling_interval": { "type": "integer", "format": "int32", - "description": "Time interval (in hours) for polling the connector to check dispute statuses", + "description": "Time interval (in hours) for polling the connector to check for new disputes", "example": 2, "nullable": true }, @@ -28539,6 +28577,11 @@ "type": "boolean", "description": "Indicates if manual retry for payment is enabled or not", "nullable": true + }, + "always_enable_overcapture": { + "type": "boolean", + "description": "Bool indicating if overcapture must be requested for all payments", + "nullable": true } }, "additionalProperties": false @@ -28861,6 +28904,7 @@ "dispute_polling_interval": { "type": "integer", "format": "int32", + "description": "Time interval (in hours) for polling the connector to check dispute statuses", "example": 2, "nullable": true, "minimum": 0 @@ -28869,6 +28913,11 @@ "type": "boolean", "description": "Indicates if manual retry for payment is enabled or not", "nullable": true + }, + "always_enable_overcapture": { + "type": "boolean", + "description": "Bool indicating if overcapture must be requested for all payments", + "nullable": true } } }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 97dea34f0a..3844fa34bc 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2193,12 +2193,16 @@ pub struct ProfileCreate { #[schema(value_type = Option, example = "840")] pub merchant_country_code: Option, - /// Time interval (in hours) for polling the connector to check dispute statuses + /// Time interval (in hours) for polling the connector to check for new disputes #[schema(value_type = Option, example = 2)] pub dispute_polling_interval: Option, /// Indicates if manual retry for payment is enabled or not pub is_manual_retry_enabled: Option, + + /// Bool indicating if overcapture must be requested for all payments + #[schema(value_type = Option)] + pub always_enable_overcapture: Option, } #[nutype::nutype( @@ -2539,11 +2543,16 @@ pub struct ProfileResponse { #[schema(value_type = Option, example = "840")] pub merchant_country_code: Option, + /// Time interval (in hours) for polling the connector to check dispute statuses #[schema(value_type = Option, example = 2)] pub dispute_polling_interval: Option, /// Indicates if manual retry for payment is enabled or not pub is_manual_retry_enabled: Option, + + /// Bool indicating if overcapture must be requested for all payments + #[schema(value_type = Option)] + pub always_enable_overcapture: Option, } #[cfg(feature = "v2")] @@ -2882,11 +2891,16 @@ pub struct ProfileUpdate { #[schema(value_type = Option, example = "840")] pub merchant_country_code: Option, + /// Time interval (in hours) for polling the connector to check for new disputes #[schema(value_type = Option, example = 2)] pub dispute_polling_interval: Option, /// Indicates if manual retry for payment is enabled or not pub is_manual_retry_enabled: Option, + + /// Bool indicating if overcapture must be requested for all payments + #[schema(value_type = Option)] + pub always_enable_overcapture: Option, } #[cfg(feature = "v2")] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 7f0c797a15..1e98f73f5d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1255,6 +1255,11 @@ pub struct PaymentsRequest { /// Allow partial authorization for this payment 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, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -5561,6 +5566,14 @@ pub struct PaymentsResponse { /// Allow partial authorization for this payment pub enable_partial_authorization: Option, + + /// Bool indicating if overcapture must be requested for this payment + #[schema(value_type = Option)] + pub enable_overcapture: Option, + + /// Boolean indicating whether overcapture is effectively enabled for this payment + #[schema(value_type = Option)] + pub is_overcapture_enabled: Option, } #[cfg(feature = "v2")] diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index a6d6938075..5004ef0624 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -562,6 +562,10 @@ impl Connector { HashSet::from([PaymentMethodType::Credit, PaymentMethodType::Debit]) } + pub fn is_overcapture_supported_by_connector(self) -> bool { + matches!(self, Self::Stripe | Self::Adyen) + } + pub fn should_acknowledge_webhook_for_resource_not_found_errors(self) -> bool { matches!(self, Self::Adyenplatform) } diff --git a/crates/common_types/src/primitive_wrappers.rs b/crates/common_types/src/primitive_wrappers.rs index 1c1952db51..d7b7174afb 100644 --- a/crates/common_types/src/primitive_wrappers.rs +++ b/crates/common_types/src/primitive_wrappers.rs @@ -128,7 +128,6 @@ mod bool_wrappers { bool::from_sql(value).map(Self) } } - /// Bool that represents if Cvv should be collected during payment or not. Default is true #[derive( Clone, Copy, Debug, Eq, PartialEq, diesel::expression::AsExpression, Serialize, Deserialize, @@ -170,6 +169,152 @@ mod bool_wrappers { Self(true) } } + + /// Bool that represents if overcapture should always be requested + #[derive( + Clone, Copy, Debug, Eq, PartialEq, diesel::expression::AsExpression, Serialize, Deserialize, + )] + #[diesel(sql_type = diesel::sql_types::Bool)] + pub struct AlwaysEnableOvercaptureBool(bool); + impl AlwaysEnableOvercaptureBool { + /// returns the inner bool value + pub fn is_true(&self) -> bool { + self.0 + } + } + impl diesel::serialize::ToSql for AlwaysEnableOvercaptureBool + 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 AlwaysEnableOvercaptureBool + where + DB: diesel::backend::Backend, + bool: diesel::deserialize::FromSql, + { + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + bool::from_sql(value).map(Self) + } + } + + impl Default for AlwaysEnableOvercaptureBool { + /// Default for `AlwaysEnableOvercaptureBool` is `false` + fn default() -> Self { + Self(false) + } + } + + /// Bool that represents if overcapture is requested for this payment + #[derive( + Clone, Copy, Debug, Eq, PartialEq, diesel::expression::AsExpression, Serialize, Deserialize, + )] + #[diesel(sql_type = diesel::sql_types::Bool)] + pub struct EnableOvercaptureBool(bool); + + impl From for EnableOvercaptureBool { + fn from(value: bool) -> Self { + Self(value) + } + } + + impl From for EnableOvercaptureBool { + fn from(item: AlwaysEnableOvercaptureBool) -> Self { + Self(item.is_true()) + } + } + + impl Deref for EnableOvercaptureBool { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl diesel::serialize::ToSql for EnableOvercaptureBool + 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 EnableOvercaptureBool + where + DB: diesel::backend::Backend, + bool: diesel::deserialize::FromSql, + { + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + bool::from_sql(value).map(Self) + } + } + + impl Default for EnableOvercaptureBool { + /// Default for `EnableOvercaptureBool` is `false` + fn default() -> Self { + Self(false) + } + } + + /// Bool that represents if overcapture is applied for a payment by the connector + #[derive( + Clone, Copy, Debug, Eq, PartialEq, diesel::expression::AsExpression, Serialize, Deserialize, + )] + #[diesel(sql_type = diesel::sql_types::Bool)] + pub struct OvercaptureEnabledBool(bool); + + impl OvercaptureEnabledBool { + /// Creates a new instance of `OvercaptureEnabledBool` + pub fn new(value: bool) -> Self { + Self(value) + } + } + + impl Default for OvercaptureEnabledBool { + /// Default for `OvercaptureEnabledBool` is `false` + fn default() -> Self { + Self(false) + } + } + + impl Deref for OvercaptureEnabledBool { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl diesel::serialize::ToSql for OvercaptureEnabledBool + 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 OvercaptureEnabledBool + where + DB: diesel::backend::Backend, + bool: diesel::deserialize::FromSql, + { + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + bool::from_sql(value).map(Self) + } + } } mod u32_wrappers { @@ -235,7 +380,7 @@ mod u32_wrappers { } impl Default for DisputePollingIntervalInHours { - /// Default for `ShouldCollectCvvDuringPayment` is `true` + /// Default for `DisputePollingIntervalInHours` is `24` fn default() -> Self { Self(DEFAULT_DISPUTE_POLLING_INTERVAL_IN_HOURS) } diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 46ea87e47e..a1e13b1f40 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -79,6 +79,7 @@ pub struct Profile { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -197,6 +198,7 @@ pub struct ProfileUpdateInternal { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -253,6 +255,7 @@ impl ProfileUpdateInternal { merchant_country_code, dispute_polling_interval, is_manual_retry_enabled, + always_enable_overcapture, } = self; Profile { profile_id: source.profile_id, @@ -340,6 +343,8 @@ impl ProfileUpdateInternal { merchant_country_code: merchant_country_code.or(source.merchant_country_code), dispute_polling_interval: dispute_polling_interval.or(source.dispute_polling_interval), is_manual_retry_enabled: is_manual_retry_enabled.or(source.is_manual_retry_enabled), + always_enable_overcapture: always_enable_overcapture + .or(source.always_enable_overcapture), } } } @@ -405,6 +410,7 @@ pub struct Profile { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, pub routing_algorithm_id: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, @@ -710,6 +716,7 @@ impl ProfileUpdateInternal { dispute_polling_interval: None, split_txns_enabled: split_txns_enabled.or(source.split_txns_enabled), is_manual_retry_enabled: None, + always_enable_overcapture: None, } } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 7a1494ea92..fd1856977b 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -1,7 +1,7 @@ #[cfg(feature = "v2")] use common_types::payments as common_payments_types; use common_types::primitive_wrappers::{ - ExtendedAuthorizationAppliedBool, RequestExtendedAuthorizationBool, + ExtendedAuthorizationAppliedBool, OvercaptureEnabledBool, RequestExtendedAuthorizationBool, }; use common_utils::{ id_type, pii, @@ -97,6 +97,7 @@ pub struct PaymentAttempt { pub created_by: Option, pub connector_request_reference_id: Option, pub network_transaction_id: Option, + pub is_overcapture_enabled: Option, #[diesel(deserialize_as = RequiredFromNullable)] pub payment_method_type_v2: storage_enums::PaymentMethod, pub connector_payment_id: Option, @@ -212,6 +213,7 @@ pub struct PaymentAttempt { pub routing_approach: Option, pub connector_request_reference_id: Option, pub network_transaction_id: Option, + pub is_overcapture_enabled: Option, } #[cfg(feature = "v1")] @@ -564,6 +566,7 @@ pub enum PaymentAttemptUpdate { connector_mandate_detail: Option, charges: Option, setup_future_usage_applied: Option, + is_overcapture_enabled: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -993,6 +996,7 @@ impl PaymentAttemptUpdateInternal { network_error_message: network_error_message.or(source.network_error_message), connector_request_reference_id: connector_request_reference_id .or(source.connector_request_reference_id), + is_overcapture_enabled: source.is_overcapture_enabled, } } } @@ -1060,6 +1064,7 @@ pub struct PaymentAttemptUpdateInternal { pub routing_approach: Option, pub connector_request_reference_id: Option, pub network_transaction_id: Option, + pub is_overcapture_enabled: Option, } #[cfg(feature = "v1")] @@ -1252,6 +1257,7 @@ impl PaymentAttemptUpdate { routing_approach, connector_request_reference_id, network_transaction_id, + is_overcapture_enabled, } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); PaymentAttempt { amount: amount.unwrap_or(source.amount), @@ -1322,6 +1328,7 @@ impl PaymentAttemptUpdate { connector_request_reference_id: connector_request_reference_id .or(source.connector_request_reference_id), network_transaction_id: network_transaction_id.or(source.network_transaction_id), + is_overcapture_enabled: is_overcapture_enabled.or(source.is_overcapture_enabled), ..source } } @@ -2654,6 +2661,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::AuthenticationTypeUpdate { authentication_type, @@ -2719,6 +2727,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::ConfirmUpdate { amount, @@ -2818,6 +2827,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach, connector_request_reference_id, network_transaction_id, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::VoidUpdate { status, @@ -2884,6 +2894,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::RejectUpdate { status, @@ -2951,6 +2962,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::BlocklistUpdate { status, @@ -3018,6 +3030,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::ConnectorMandateDetailUpdate { connector_mandate_detail, @@ -3083,6 +3096,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::PaymentMethodDetailsUpdate { payment_method_id, @@ -3148,6 +3162,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::ResponseUpdate { status, @@ -3175,6 +3190,7 @@ impl From for PaymentAttemptUpdateInternal { charges, setup_future_usage_applied, network_transaction_id, + is_overcapture_enabled, } => { let (connector_transaction_id, processor_transaction_data) = connector_transaction_id @@ -3242,6 +3258,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id, + is_overcapture_enabled, } } PaymentAttemptUpdate::ErrorUpdate { @@ -3326,6 +3343,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, } } PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { @@ -3389,6 +3407,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::UpdateTrackers { payment_token, @@ -3461,6 +3480,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -3539,6 +3559,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, } } PaymentAttemptUpdate::PreprocessingUpdate { @@ -3616,6 +3637,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, } } PaymentAttemptUpdate::CaptureUpdate { @@ -3683,6 +3705,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::AmountToCaptureUpdate { status, @@ -3749,6 +3772,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::ConnectorResponse { authentication_data, @@ -3824,6 +3848,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, } } PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { @@ -3890,6 +3915,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::AuthenticationUpdate { status, @@ -3958,6 +3984,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, PaymentAttemptUpdate::ManualUpdate { status, @@ -4035,6 +4062,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, } } PaymentAttemptUpdate::PostSessionTokensUpdate { @@ -4101,6 +4129,7 @@ impl From for PaymentAttemptUpdateInternal { routing_approach: None, connector_request_reference_id: None, network_transaction_id: None, + is_overcapture_enabled: None, }, } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index a152d4085d..38123c0a3f 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -77,6 +77,7 @@ pub struct PaymentIntent { pub duty_amount: Option, pub order_date: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, pub merchant_reference_id: Option, pub billing_address: Option, pub shipping_address: Option, @@ -178,6 +179,7 @@ pub struct PaymentIntent { pub duty_amount: Option, pub order_date: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, diesel::AsExpression, PartialEq)] @@ -467,6 +469,7 @@ pub struct PaymentIntentNew { pub shipping_amount_tax: Option, pub duty_amount: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[cfg(feature = "v2")] @@ -637,6 +640,7 @@ pub struct PaymentIntentUpdateFields { pub shipping_amount_tax: Option, pub duty_amount: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } // TODO: uncomment fields as necessary @@ -803,6 +807,7 @@ impl PaymentIntentUpdateInternal { order_date: source.order_date, enable_partial_authorization: None, split_txns_enabled: source.split_txns_enabled, + enable_overcapture: None, } } } @@ -859,6 +864,7 @@ pub struct PaymentIntentUpdateInternal { pub shipping_amount_tax: Option, pub duty_amount: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -912,6 +918,7 @@ impl PaymentIntentUpdate { shipping_amount_tax, duty_amount, enable_partial_authorization, + enable_overcapture, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -972,6 +979,7 @@ impl PaymentIntentUpdate { duty_amount: duty_amount.or(source.duty_amount), enable_partial_authorization: enable_partial_authorization .or(source.enable_partial_authorization), + enable_overcapture: enable_overcapture.or(source.enable_overcapture), ..source } } @@ -1032,6 +1040,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::Update(value) => Self { amount: Some(value.amount), @@ -1082,6 +1091,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: value.shipping_amount_tax, duty_amount: value.duty_amount, enable_partial_authorization: value.enable_partial_authorization, + enable_overcapture: value.enable_overcapture, }, PaymentIntentUpdate::PaymentCreateUpdate { return_url, @@ -1139,6 +1149,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::PGStatusUpdate { status, @@ -1193,6 +1204,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::MerchantStatusUpdate { status, @@ -1247,6 +1259,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::ResponseUpdate { // amount, @@ -1309,6 +1322,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { active_attempt_id, @@ -1362,6 +1376,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::StatusAndAttemptUpdate { status, @@ -1416,6 +1431,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::ApproveUpdate { status, @@ -1469,6 +1485,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::RejectUpdate { status, @@ -1522,6 +1539,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::SurchargeApplicableUpdate { surcharge_applicable, @@ -1574,6 +1592,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { amount: Some(amount), @@ -1623,6 +1642,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::AuthorizationCountUpdate { authorization_count, @@ -1674,6 +1694,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::CompleteAuthorizeUpdate { shipping_address_id, @@ -1725,6 +1746,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::ManualUpdate { status, updated_by } => Self { status, @@ -1774,6 +1796,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, PaymentIntentUpdate::SessionResponseUpdate { tax_details, @@ -1828,6 +1851,7 @@ impl From for PaymentIntentUpdateInternal { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index d6dc2a85fc..4b5176b9d2 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -254,6 +254,7 @@ diesel::table! { merchant_country_code -> Nullable, dispute_polling_interval -> Nullable, is_manual_retry_enabled -> Nullable, + always_enable_overcapture -> Nullable, } } @@ -984,6 +985,7 @@ diesel::table! { connector_request_reference_id -> Nullable, #[max_length = 255] network_transaction_id -> Nullable, + is_overcapture_enabled -> Nullable, } } @@ -1087,6 +1089,7 @@ diesel::table! { duty_amount -> Nullable, order_date -> Nullable, enable_partial_authorization -> Nullable, + enable_overcapture -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 3be4d174ae..1a0ce0ec6e 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -249,6 +249,7 @@ diesel::table! { merchant_country_code -> Nullable, dispute_polling_interval -> Nullable, is_manual_retry_enabled -> Nullable, + always_enable_overcapture -> Nullable, #[max_length = 64] routing_algorithm_id -> Nullable, order_fulfillment_time -> Nullable, @@ -928,6 +929,7 @@ diesel::table! { connector_request_reference_id -> Nullable, #[max_length = 255] network_transaction_id -> Nullable, + is_overcapture_enabled -> Nullable, payment_method_type_v2 -> Nullable, #[max_length = 128] connector_payment_id -> Nullable, @@ -1021,6 +1023,7 @@ diesel::table! { duty_amount -> Nullable, order_date -> Nullable, enable_partial_authorization -> Nullable, + enable_overcapture -> Nullable, #[max_length = 64] merchant_reference_id -> Nullable, billing_address -> Nullable, diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index 51333bf80d..7494f45264 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -552,6 +552,7 @@ pub struct RedirectionErrorResponse { pub struct RedirectionResponse { result_code: AdyenStatus, action: AdyenRedirectAction, + amount: Option, refusal_reason: Option, refusal_reason_code: Option, psp_reference: Option, @@ -567,6 +568,7 @@ pub struct PresentToShopperResponse { psp_reference: Option, result_code: AdyenStatus, action: AdyenPtsAction, + amount: Option, refusal_reason: Option, refusal_reason_code: Option, merchant_reference: Option, @@ -579,6 +581,7 @@ pub struct PresentToShopperResponse { pub struct QrCodeResponseResponse { result_code: AdyenStatus, action: AdyenQrCodeAction, + amount: Option, refusal_reason: Option, refusal_reason_code: Option, additional_data: Option, @@ -3934,6 +3937,7 @@ pub fn get_adyen_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4003,7 +4007,10 @@ pub fn get_adyen_response( incremental_authorization_allowed: None, charges, }; - Ok((status, error, payments_response_data)) + + let txn_amount = response.amount.map(|amount| amount.value); + + Ok((status, error, payments_response_data, txn_amount)) } pub fn get_webhook_response( @@ -4016,6 +4023,7 @@ pub fn get_webhook_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4049,6 +4057,8 @@ pub fn get_webhook_response( None }; + let txn_amount = response.amount.as_ref().map(|amount| amount.value); + if is_multiple_capture_psync_flow { let capture_sync_response_list = utils::construct_captures_response_hashmap(vec![response])?; @@ -4058,6 +4068,7 @@ pub fn get_webhook_response( PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, }, + txn_amount, )) } else { let payments_response_data = PaymentsResponseData::TransactionResponse { @@ -4074,7 +4085,8 @@ pub fn get_webhook_response( incremental_authorization_allowed: None, charges: None, }; - Ok((status, error, payments_response_data)) + + Ok((status, error, payments_response_data, txn_amount)) } } @@ -4088,6 +4100,7 @@ pub fn get_redirection_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4161,7 +4174,10 @@ pub fn get_redirection_response( incremental_authorization_allowed: None, charges, }; - Ok((status, error, payments_response_data)) + + let txn_amount = response.amount.map(|amount| amount.value); + + Ok((status, error, payments_response_data, txn_amount)) } pub fn get_present_to_shopper_response( @@ -4174,6 +4190,7 @@ pub fn get_present_to_shopper_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4230,7 +4247,9 @@ pub fn get_present_to_shopper_response( incremental_authorization_allowed: None, charges, }; - Ok((status, error, payments_response_data)) + let txn_amount = response.amount.map(|amount| amount.value); + + Ok((status, error, payments_response_data, txn_amount)) } pub fn get_qr_code_response( @@ -4243,6 +4262,7 @@ pub fn get_qr_code_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4298,7 +4318,10 @@ pub fn get_qr_code_response( incremental_authorization_allowed: None, charges, }; - Ok((status, error, payments_response_data)) + + let txn_amount = response.amount.map(|amount| amount.value); + + Ok((status, error, payments_response_data, txn_amount)) } pub fn get_redirection_error_response( @@ -4311,6 +4334,7 @@ pub fn get_redirection_error_response( storage_enums::AttemptStatus, Option, PaymentsResponseData, + Option, ), errors::ConnectorError, > { @@ -4354,7 +4378,7 @@ pub fn get_redirection_error_response( charges: None, }; - Ok((status, error, payments_response_data)) + Ok((status, error, payments_response_data, None)) } pub fn get_qr_metadata( @@ -4611,7 +4635,7 @@ impl ), ) -> Result { let is_manual_capture = is_manual_capture(capture_method); - let (status, error, payment_response_data) = match item.response { + let (status, error, payment_response_data, amount) = match item.response { AdyenPaymentResponse::Response(response) => { get_adyen_response(*response, is_manual_capture, item.http_code, pmt)? } @@ -4635,9 +4659,18 @@ impl )?, }; + let minor_amount_captured = match status { + enums::AttemptStatus::Charged + | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable => amount, + _ => None, + }; + Ok(Self { status, + amount_captured: minor_amount_captured.map(|amount| amount.get_amount_as_i64()), response: error.map_or_else(|| Ok(payment_response_data), Err), + minor_amount_captured, ..item.data }) } diff --git a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs index ab9232df55..cbedbd6270 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs @@ -293,6 +293,8 @@ pub struct StripeCardData { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "payment_method_options[card][request_extended_authorization]")] request_extended_authorization: Option, + #[serde(rename = "payment_method_options[card][request_overcapture]")] + pub request_overcapture: Option, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -308,6 +310,12 @@ pub enum StripeRequestExtendedAuthorization { IfAvailable, } +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum StripeRequestOvercaptureBool { + IfAvailable, +} + #[derive(Debug, Eq, PartialEq, Serialize)] pub struct StripePayLaterData { #[serde(rename = "payment_method_data[type]")] @@ -1265,14 +1273,20 @@ fn get_bank_debit_data( } } +pub struct PaymentRequestDetails { + pub auth_type: enums::AuthenticationType, + pub payment_method_token: Option, + pub is_customer_initiated_mandate_payment: Option, + pub billing_address: StripeBillingAddress, + pub request_incremental_authorization: bool, + pub request_extended_authorization: + Option, + pub request_overcapture: Option, +} + fn create_stripe_payment_method( payment_method_data: &PaymentMethodData, - auth_type: enums::AuthenticationType, - payment_method_token: Option, - is_customer_initiated_mandate_payment: Option, - billing_address: StripeBillingAddress, - request_incremental_authorization: bool, - request_extended_authorization: Option, + payment_request_details: PaymentRequestDetails, ) -> Result< ( StripePaymentMethodData, @@ -1283,7 +1297,7 @@ fn create_stripe_payment_method( > { match payment_method_data { PaymentMethodData::Card(card_details) => { - let payment_method_auth_type = match auth_type { + let payment_method_auth_type = match payment_request_details.auth_type { enums::AuthenticationType::ThreeDs => Auth3ds::Any, enums::AuthenticationType::NoThreeDs => Auth3ds::Automatic, }; @@ -1291,11 +1305,12 @@ fn create_stripe_payment_method( StripePaymentMethodData::try_from(( card_details, payment_method_auth_type, - request_incremental_authorization, - request_extended_authorization, + payment_request_details.request_incremental_authorization, + payment_request_details.request_extended_authorization, + payment_request_details.request_overcapture, ))?, Some(StripePaymentMethodType::Card), - billing_address, + payment_request_details.billing_address, )) } PaymentMethodData::PayLater(pay_later_data) => { @@ -1306,18 +1321,19 @@ fn create_stripe_payment_method( payment_method_data_type: stripe_pm_type, }), Some(stripe_pm_type), - billing_address, + payment_request_details.billing_address, )) } PaymentMethodData::BankRedirect(bank_redirect_data) => { - let billing_address = if is_customer_initiated_mandate_payment == Some(true) { - mandatory_parameters_for_sepa_bank_debit_mandates( - &Some(billing_address.to_owned()), - is_customer_initiated_mandate_payment, - )? - } else { - billing_address - }; + let billing_address = + if payment_request_details.is_customer_initiated_mandate_payment == Some(true) { + mandatory_parameters_for_sepa_bank_debit_mandates( + &Some(payment_request_details.billing_address.to_owned()), + payment_request_details.is_customer_initiated_mandate_payment, + )? + } else { + payment_request_details.billing_address + }; let pm_type = StripePaymentMethodType::try_from(bank_redirect_data)?; let bank_redirect_data = StripePaymentMethodData::try_from(bank_redirect_data)?; @@ -1325,8 +1341,10 @@ fn create_stripe_payment_method( } PaymentMethodData::Wallet(wallet_data) => { let pm_type = get_stripe_payment_method_type_from_wallet_data(wallet_data)?; - let wallet_specific_data = - StripePaymentMethodData::try_from((wallet_data, payment_method_token))?; + let wallet_specific_data = StripePaymentMethodData::try_from(( + wallet_data, + payment_request_details.payment_method_token, + ))?; Ok(( wallet_specific_data, pm_type, @@ -1340,7 +1358,11 @@ fn create_stripe_payment_method( bank_specific_data: bank_debit_data, }); - Ok((pm_data, Some(pm_type), billing_address)) + Ok(( + pm_data, + Some(pm_type), + payment_request_details.billing_address, + )) } PaymentMethodData::BankTransfer(bank_transfer_data) => match bank_transfer_data.deref() { payment_method_data::BankTransferData::AchBankTransfer {} => Ok(( @@ -1361,7 +1383,7 @@ fn create_stripe_payment_method( MultibancoTransferData { payment_method_data_type: StripeCreditTransferTypes::Multibanco, payment_method_type: StripeCreditTransferTypes::Multibanco, - email: billing_address.email.ok_or( + email: payment_request_details.billing_address.email.ok_or( ConnectorError::MissingRequiredField { field_name: "billing_address.email", }, @@ -1379,7 +1401,7 @@ fn create_stripe_payment_method( bank_transfer_type: BankTransferType::EuBankTransfer, balance_funding_type: BankTransferType::BankTransfers, payment_method_type: StripePaymentMethodType::CustomerBalance, - country: billing_address.country.ok_or( + country: payment_request_details.billing_address.country.ok_or( ConnectorError::MissingRequiredField { field_name: "billing_address.country", }, @@ -1387,7 +1409,7 @@ fn create_stripe_payment_method( }), )), Some(StripePaymentMethodType::CustomerBalance), - billing_address, + payment_request_details.billing_address, )), payment_method_data::BankTransferData::BacsBankTransfer {} => Ok(( StripePaymentMethodData::BankTransfer(StripeBankTransferData::BacsBankTransfers( @@ -1399,7 +1421,7 @@ fn create_stripe_payment_method( }), )), Some(StripePaymentMethodType::CustomerBalance), - billing_address, + payment_request_details.billing_address, )), payment_method_data::BankTransferData::Pix { .. } => Err( ConnectorError::NotImplemented(get_unimplemented_payment_method_error_message( @@ -1517,6 +1539,7 @@ impl Auth3ds, bool, Option, + Option, )> for StripePaymentMethodData { type Error = ConnectorError; @@ -1526,11 +1549,13 @@ impl payment_method_auth_type, request_incremental_authorization, request_extended_authorization, + request_overcapture, ): ( &Card, Auth3ds, bool, Option, + Option, ), ) -> Result { Ok(Self::Card(StripeCardData { @@ -1557,6 +1582,7 @@ impl } else { None }, + request_overcapture, })) } } @@ -1908,6 +1934,7 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest .and_then(get_stripe_card_network), request_incremental_authorization: None, request_extended_authorization: None, + request_overcapture: None, }), PaymentMethodData::CardRedirect(_) | PaymentMethodData::Wallet(_) @@ -1944,21 +1971,25 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest let (payment_method_data, payment_method_type, billing_address) = create_stripe_payment_method( &item.request.payment_method_data, - item.auth_type, - item.payment_method_token.clone(), - Some( + PaymentRequestDetails { + auth_type : item.auth_type, + payment_method_token: item.payment_method_token.clone(), + is_customer_initiated_mandate_payment: Some( PaymentsAuthorizeRequestData::is_customer_initiated_mandate_payment( &item.request, ), ), - billing_address.ok_or_else(|| { + billing_address: billing_address.ok_or_else(|| { ConnectorError::MissingRequiredField { field_name: "billing_address", } })?, - item.request.request_incremental_authorization, - item.request.request_extended_authorization, - )?; + request_incremental_authorization: item.request.request_incremental_authorization, + request_extended_authorization: item.request.request_extended_authorization, + request_overcapture: item.request + .enable_overcapture + .and_then(get_stripe_overcapture_request), + })?; validate_shipping_address_against_payment_method( &shipping_address, @@ -2175,6 +2206,15 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest } } +fn get_stripe_overcapture_request( + enable_overcapture: primitive_wrappers::EnableOvercaptureBool, +) -> Option { + match enable_overcapture.deref() { + true => Some(StripeRequestOvercaptureBool::IfAvailable), + false => None, + } +} + fn get_payment_method_type_for_saved_payment_method_payment( item: &PaymentsAuthorizeRouterData, ) -> Result, error_stack::Report> { @@ -2279,12 +2319,15 @@ impl TryFrom<&TokenizationRouterData> for TokenRequest { _ => { create_stripe_payment_method( &item.request.payment_method_data, - item.auth_type, - item.payment_method_token.clone(), - None, - StripeBillingAddress::default(), - false, - None, + PaymentRequestDetails { + auth_type: item.auth_type, + payment_method_token: item.payment_method_token.clone(), + is_customer_initiated_mandate_payment: None, + billing_address: StripeBillingAddress::default(), + request_incremental_authorization: false, + request_extended_authorization: None, + request_overcapture: None, + }, )? .0 } @@ -2447,7 +2490,7 @@ pub struct PaymentIntentResponse { pub object: String, pub amount: MinorUnit, #[serde(default, deserialize_with = "deserialize_zero_minor_amount_as_none")] - // stripe gives amount_captured as 0 for payment intents instead of 0 + // stripe gives amount_captured as 0 for payment intents instead of null pub amount_received: Option, pub amount_capturable: Option, pub currency: String, @@ -2545,7 +2588,53 @@ pub struct PaymentIntentSyncResponse { #[serde(untagged)] pub enum StripeChargeEnum { ChargeId(String), - ChargeObject(StripeCharge), + ChargeObject(Box), +} + +impl StripeChargeEnum { + pub fn get_overcapture_status(&self) -> Option { + match self { + Self::ChargeObject(charge_object) => charge_object + .payment_method_details + .as_ref() + .and_then(|payment_method_details| match payment_method_details { + StripePaymentMethodDetailsResponse::Card { card } => card + .overcapture + .as_ref() + .and_then(|overcapture| match overcapture.status { + Some(StripeOvercaptureStatus::Available) => { + Some(primitive_wrappers::OvercaptureEnabledBool::new(true)) + } + Some(StripeOvercaptureStatus::Unavailable) => { + Some(primitive_wrappers::OvercaptureEnabledBool::new(false)) + } + None => None, + }), + _ => None, + }), + _ => None, + } + } + + pub fn get_maximum_capturable_amount(&self) -> Option { + match self { + Self::ChargeObject(charge_object) => { + if let Some(payment_method_details) = charge_object.payment_method_details.as_ref() + { + match payment_method_details { + StripePaymentMethodDetailsResponse::Card { card } => card + .overcapture + .as_ref() + .and_then(|overcapture| overcapture.maximum_amount_capturable), + _ => None, + } + } else { + None + } + } + _ => None, + } + } } #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] @@ -2581,7 +2670,8 @@ pub struct StripeAdditionalCardDetails { network_transaction_id: Option, extended_authorization: Option, #[serde(default, with = "common_utils::custom_serde::timestamp::option")] - pub capture_before: Option, + capture_before: Option, + overcapture: Option, } #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] @@ -2596,6 +2686,21 @@ pub enum StripeExtendedAuthorizationStatus { Enabled, } +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct StripeOvercaptureResponse { + status: Option, + #[serde(default, deserialize_with = "deserialize_zero_minor_amount_as_none")] + // stripe gives amount_captured as 0 for payment intents instead of null + maximum_amount_capturable: Option, +} + +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum StripeOvercaptureStatus { + Available, + Unavailable, +} + #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case", tag = "type")] pub enum StripePaymentMethodDetailsResponse { @@ -2771,6 +2876,7 @@ pub struct SetupIntentResponse { fn extract_payment_method_connector_response_from_latest_charge( stripe_charge_enum: &StripeChargeEnum, ) -> Option { + let is_overcapture_enabled = stripe_charge_enum.get_overcapture_status(); let additional_payment_method_details = if let StripeChargeEnum::ChargeObject(charge_object) = stripe_charge_enum { charge_object @@ -2788,9 +2894,13 @@ fn extract_payment_method_connector_response_from_latest_charge( .as_ref() .map(ExtendedAuthorizationResponseData::from); - if additional_payment_method_data.is_some() || extended_authorization_data.is_some() { + if additional_payment_method_data.is_some() + || extended_authorization_data.is_some() + || is_overcapture_enabled.is_some() + { Some(ConnectorResponseData::new( additional_payment_method_data, + is_overcapture_enabled, extended_authorization_data, )) } else { @@ -2914,6 +3024,12 @@ where .as_ref() .and_then(extract_payment_method_connector_response_from_latest_charge); + let minor_amount_capturable = item + .response + .latest_charge + .as_ref() + .and_then(StripeChargeEnum::get_maximum_capturable_amount); + Ok(Self { status, /* Commented out fields: @@ -2929,6 +3045,7 @@ where .map(|amount| amount.get_amount_as_i64()), minor_amount_captured: item.response.amount_received, connector_response: connector_response_data, + minor_amount_capturable, ..item.data }) } @@ -4260,6 +4377,7 @@ impl payment_method_auth_type, item.request.request_incremental_authorization, None, + None, ))?) } PaymentMethodData::PayLater(_) => Ok(Self::PayLater(StripePayLaterData { diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 9845275d06..22166789fb 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -6325,6 +6325,7 @@ pub(crate) fn convert_setup_mandate_router_data_to_authorize_router_data( locale: None, payment_channel: None, enable_partial_authorization: data.request.enable_partial_authorization, + enable_overcapture: None, } } diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index f2f93ea5c9..efb9fc5979 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -85,6 +85,7 @@ pub struct Profile { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -142,6 +143,7 @@ pub struct ProfileSetter { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -206,6 +208,7 @@ impl From for Profile { merchant_country_code: value.merchant_country_code, dispute_polling_interval: value.dispute_polling_interval, is_manual_retry_enabled: value.is_manual_retry_enabled, + always_enable_overcapture: value.always_enable_overcapture, } } } @@ -272,6 +275,7 @@ pub struct ProfileGeneralUpdate { pub merchant_country_code: Option, pub dispute_polling_interval: Option, pub is_manual_retry_enabled: Option, + pub always_enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -356,6 +360,7 @@ impl From for ProfileUpdateInternal { dispute_polling_interval, always_request_extended_authorization, is_manual_retry_enabled, + always_enable_overcapture, } = *update; Self { @@ -410,6 +415,7 @@ impl From for ProfileUpdateInternal { merchant_country_code, dispute_polling_interval, is_manual_retry_enabled, + always_enable_overcapture, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -467,6 +473,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -521,6 +528,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -575,6 +583,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -629,6 +638,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -683,6 +693,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -737,6 +748,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, ProfileUpdate::AcquirerConfigMapUpdate { acquirer_config_map, @@ -791,6 +803,7 @@ impl From for ProfileUpdateInternal { merchant_country_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }, } } @@ -865,6 +878,7 @@ impl super::behaviour::Conversion for Profile { merchant_country_code: self.merchant_country_code, dispute_polling_interval: self.dispute_polling_interval, is_manual_retry_enabled: self.is_manual_retry_enabled, + always_enable_overcapture: self.always_enable_overcapture, }) } @@ -965,6 +979,7 @@ impl super::behaviour::Conversion for Profile { merchant_country_code: item.merchant_country_code, dispute_polling_interval: item.dispute_polling_interval, is_manual_retry_enabled: item.is_manual_retry_enabled, + always_enable_overcapture: item.always_enable_overcapture, }) } .await @@ -2118,6 +2133,7 @@ impl super::behaviour::Conversion for Profile { dispute_polling_interval: None, split_txns_enabled: Some(self.split_txns_enabled), is_manual_retry_enabled: None, + always_enable_overcapture: None, }) } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 58a859acbc..6249053c36 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use api_models::payments::{SessionToken, VaultSessionDetails}; #[cfg(feature = "v1")] use common_types::primitive_wrappers::{ - AlwaysRequestExtendedAuthorization, RequestExtendedAuthorizationBool, + AlwaysRequestExtendedAuthorization, EnableOvercaptureBool, RequestExtendedAuthorizationBool, }; use common_utils::{ self, @@ -123,6 +123,7 @@ pub struct PaymentIntent { pub shipping_amount_tax: Option, pub duty_amount: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } impl PaymentIntent { @@ -200,6 +201,27 @@ impl PaymentIntent { .map(RequestExtendedAuthorizationBool::from) } + #[cfg(feature = "v1")] + 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, + >, + capture_method: &Option, + ) -> Option { + let is_overcapture_supported_by_connector = + connector.is_overcapture_supported_by_connector(); + if matches!(capture_method, Some(common_enums::CaptureMethod::Manual)) + && is_overcapture_supported_by_connector + { + self.enable_overcapture + .or_else(|| always_enable_overcapture.map(EnableOvercaptureBool::from)) + } else { + None + } + } + #[cfg(feature = "v2")] /// This is the url to which the customer will be redirected to, after completing the redirection flow pub fn create_finish_redirection_url( diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index c39c9c444d..3ee63c728c 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -5,7 +5,7 @@ use common_enums as storage_enums; use common_types::payments as common_payments_types; #[cfg(feature = "v1")] use common_types::primitive_wrappers::{ - ExtendedAuthorizationAppliedBool, RequestExtendedAuthorizationBool, + ExtendedAuthorizationAppliedBool, OvercaptureEnabledBool, RequestExtendedAuthorizationBool, }; #[cfg(feature = "v2")] use common_utils::{ @@ -1030,6 +1030,7 @@ pub struct PaymentAttempt { pub connector_request_reference_id: Option, pub debit_routing_savings: Option, pub network_transaction_id: Option, + pub is_overcapture_enabled: Option, } #[cfg(feature = "v1")] @@ -1451,6 +1452,7 @@ pub enum PaymentAttemptUpdate { charges: Option, setup_future_usage_applied: Option, debit_routing_savings: Option, + is_overcapture_enabled: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -1752,6 +1754,7 @@ impl PaymentAttemptUpdate { setup_future_usage_applied, network_transaction_id, debit_routing_savings: _, + is_overcapture_enabled, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1778,6 +1781,7 @@ impl PaymentAttemptUpdate { charges, setup_future_usage_applied, network_transaction_id, + is_overcapture_enabled, }, Self::UnresolvedResponseUpdate { status, @@ -2153,6 +2157,7 @@ impl behaviour::Conversion for PaymentAttempt { routing_approach: self.routing_approach, connector_request_reference_id: self.connector_request_reference_id, network_transaction_id: self.network_transaction_id, + is_overcapture_enabled: self.is_overcapture_enabled, }) } @@ -2252,6 +2257,7 @@ impl behaviour::Conversion for PaymentAttempt { connector_request_reference_id: storage_model.connector_request_reference_id, debit_routing_savings: None, network_transaction_id: storage_model.network_transaction_id, + is_overcapture_enabled: storage_model.is_overcapture_enabled, }) } .await @@ -2512,6 +2518,7 @@ impl behaviour::Conversion for PaymentAttempt { created_by: created_by.map(|cb| cb.to_string()), connector_request_reference_id, network_transaction_id, + is_overcapture_enabled: None, }) } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 8dbdd1761b..8cd3be165e 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -251,6 +251,7 @@ pub struct PaymentIntentUpdateFields { pub payment_channel: Option, pub feature_metadata: Option>, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[cfg(feature = "v1")] @@ -447,6 +448,7 @@ pub struct PaymentIntentUpdateInternal { pub shipping_amount_tax: Option, pub duty_amount: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } // This conversion is used in the `update_payment_intent` function @@ -1097,6 +1099,7 @@ impl From for DieselPaymentIntentUpdate { shipping_amount_tax: value.shipping_amount_tax, duty_amount: value.duty_amount, enable_partial_authorization: value.enable_partial_authorization, + enable_overcapture: value.enable_overcapture, })) } PaymentIntentUpdate::PaymentCreateUpdate { @@ -1265,6 +1268,7 @@ impl From for diesel_models::PaymentIntentUpdateInt shipping_amount_tax, duty_amount, enable_partial_authorization, + enable_overcapture, } = value; Self { amount, @@ -1314,6 +1318,7 @@ impl From for diesel_models::PaymentIntentUpdateInt shipping_amount_tax, duty_amount, enable_partial_authorization, + enable_overcapture, } } } @@ -1784,6 +1789,7 @@ impl behaviour::Conversion for PaymentIntent { duty_amount: None, order_date: None, enable_partial_authorization: None, + enable_overcapture: None, }) } async fn convert_back( @@ -2105,6 +2111,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_amount_tax: self.shipping_amount_tax, duty_amount: self.duty_amount, enable_partial_authorization: self.enable_partial_authorization, + enable_overcapture: self.enable_overcapture, }) } @@ -2213,6 +2220,7 @@ impl behaviour::Conversion for PaymentIntent { duty_amount: storage_model.duty_amount, order_date: storage_model.order_date, enable_partial_authorization: storage_model.enable_partial_authorization, + enable_overcapture: storage_model.enable_overcapture, }) } .await @@ -2293,6 +2301,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_amount_tax: self.shipping_amount_tax, duty_amount: self.duty_amount, enable_partial_authorization: self.enable_partial_authorization, + enable_overcapture: self.enable_overcapture, }) } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 56824d9610..e975423d9f 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -464,6 +464,7 @@ pub struct PaymentMethodBalance { pub struct ConnectorResponseData { pub additional_payment_method_data: Option, extended_authorization_response_data: Option, + is_overcapture_enabled: Option, } impl ConnectorResponseData { @@ -473,15 +474,18 @@ impl ConnectorResponseData { Self { additional_payment_method_data: Some(additional_payment_method_data), extended_authorization_response_data: None, + is_overcapture_enabled: None, } } pub fn new( additional_payment_method_data: Option, + is_overcapture_enabled: Option, extended_authorization_response_data: Option, ) -> Self { Self { additional_payment_method_data, extended_authorization_response_data, + is_overcapture_enabled, } } @@ -490,6 +494,10 @@ impl ConnectorResponseData { ) -> Option<&ExtendedAuthorizationResponseData> { self.extended_authorization_response_data.as_ref() } + + pub fn is_overcapture_enabled(&self) -> Option { + self.is_overcapture_enabled + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index a9a842dcfd..9b73e21544 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -85,6 +85,7 @@ pub struct PaymentsAuthorizeData { pub locale: Option, pub payment_channel: Option, pub enable_partial_authorization: Option, + pub enable_overcapture: Option, } #[derive(Debug, Clone)] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index e79747a480..8b91d6f7f7 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + ops::Deref, str::FromStr, sync::LazyLock, }; @@ -179,9 +180,34 @@ where payment_data, ); let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); - if Some(total_capturable_amount) == captured_amount.map(MinorUnit::new) { + let is_overcapture_enabled = *payment_data + .payment_attempt + .is_overcapture_enabled + .as_deref() + .unwrap_or(&false); + + if Some(total_capturable_amount) == captured_amount.map(MinorUnit::new) + || (is_overcapture_enabled + && captured_amount.is_some_and(|captured_amount| { + MinorUnit::new(captured_amount) > total_capturable_amount + })) + { Ok(enums::AttemptStatus::Charged) - } else if captured_amount.is_some() { + } else if captured_amount.is_some_and(|captured_amount| { + MinorUnit::new(captured_amount) > total_capturable_amount + }) { + Err(ApiErrorResponse::IntegrityCheckFailed { + reason: "captured_amount is greater than the total_capturable_amount" + .to_string(), + field_names: "captured_amount".to_string(), + connector_transaction_id: payment_data + .payment_attempt + .connector_transaction_id + .clone(), + })? + } else if captured_amount.is_some_and(|captured_amount| { + MinorUnit::new(captured_amount) < total_capturable_amount + }) { Ok(enums::AttemptStatus::PartialCharged) } else { Ok(self.status) @@ -194,22 +220,33 @@ where amount_capturable, payment_data.payment_attempt.status, ); - if Some(payment_data.payment_attempt.get_total_amount()) - == capturable_amount.map(MinorUnit::new) + let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); + let is_overcapture_enabled = *payment_data + .payment_attempt + .is_overcapture_enabled + .unwrap_or_default() + .deref(); + + if Some(total_capturable_amount) == capturable_amount.map(MinorUnit::new) + || (capturable_amount.is_some_and(|capturable_amount| { + MinorUnit::new(capturable_amount) > total_capturable_amount + }) && is_overcapture_enabled) { Ok(enums::AttemptStatus::Authorized) - } else if capturable_amount.is_some() - && payment_data - .payment_intent - .enable_partial_authorization - .is_some_and(|val| val) + } else if capturable_amount.is_some_and(|capturable_amount| { + MinorUnit::new(capturable_amount) < total_capturable_amount + }) && payment_data + .payment_intent + .enable_partial_authorization + .is_some_and(|val| val) { Ok(enums::AttemptStatus::PartiallyAuthorized) - } else if capturable_amount.is_some() - && !payment_data - .payment_intent - .enable_partial_authorization - .is_some_and(|val| val) + } else if capturable_amount.is_some_and(|capturable_amount| { + MinorUnit::new(capturable_amount) < total_capturable_amount + }) && !payment_data + .payment_intent + .enable_partial_authorization + .is_some_and(|val| val) { Err(ApiErrorResponse::IntegrityCheckFailed { reason: "capturable_amount is less than the total attempt amount" @@ -220,6 +257,19 @@ where .connector_transaction_id .clone(), })? + } else if capturable_amount.is_some_and(|capturable_amount| { + MinorUnit::new(capturable_amount) > total_capturable_amount + }) && !is_overcapture_enabled + { + Err(ApiErrorResponse::IntegrityCheckFailed { + reason: "capturable_amount is greater than the total attempt amount" + .to_string(), + field_names: "amount_capturable".to_string(), + connector_transaction_id: payment_data + .payment_attempt + .connector_transaction_id + .clone(), + })? } else { Ok(self.status) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 70e18b853c..7047164f0c 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3477,6 +3477,7 @@ impl ProfileCreateBridge for api::ProfileCreate { merchant_country_code: self.merchant_country_code, dispute_polling_interval: self.dispute_polling_interval, is_manual_retry_enabled: self.is_manual_retry_enabled, + always_enable_overcapture: self.always_enable_overcapture, })) } @@ -3974,6 +3975,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { merchant_country_code: self.merchant_country_code, dispute_polling_interval: self.dispute_polling_interval, is_manual_retry_enabled: self.is_manual_retry_enabled, + always_enable_overcapture: self.always_enable_overcapture, }, ))) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d4bf2c455d..32960f6997 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashSet, net::IpAddr, str::FromStr}; +use std::{borrow::Cow, collections::HashSet, net::IpAddr, ops::Deref, str::FromStr}; pub use ::payment_methods::helpers::{ populate_bin_details_for_payment_method_create, @@ -1125,6 +1125,26 @@ pub fn validate_recurring_details_and_token( Ok(()) } +pub fn validate_overcapture_request( + enable_overcapture: &Option, + capture_method: &Option, +) -> CustomResult<(), errors::ApiErrorResponse> { + if let Some(overcapture) = enable_overcapture { + utils::when( + *overcapture.deref() + && !matches!(*capture_method, Some(common_enums::CaptureMethod::Manual)), + || { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid overcapture request: supported only with manual capture" + .into() + })) + }, + )?; + } + + Ok(()) +} + fn validate_new_mandate_request( req: api::MandateValidationFields, is_confirm_operation: bool, @@ -3899,6 +3919,7 @@ mod tests { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_ok()); @@ -3983,6 +4004,7 @@ mod tests { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent,).is_err()) @@ -4065,6 +4087,7 @@ mod tests { shipping_amount_tax: None, duty_amount: None, enable_partial_authorization: None, + enable_overcapture: 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_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 6108ed3658..2c3349f046 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, ops::Deref}; use api_models::enums::FrmSuggestion; use async_trait::async_trait; @@ -94,12 +94,18 @@ impl GetTracker, api::Paymen helpers::validate_status_with_capture_method(payment_intent.status, capture_method)?; - helpers::validate_amount_to_capture( - payment_attempt.amount_capturable.get_amount_as_i64(), - request - .amount_to_capture - .map(|capture_amount| capture_amount.get_amount_as_i64()), - )?; + if !*payment_attempt + .is_overcapture_enabled + .unwrap_or_default() + .deref() + { + helpers::validate_amount_to_capture( + payment_attempt.amount_capturable.get_amount_as_i64(), + request + .amount_to_capture + .map(|capture_amount| capture_amount.get_amount_as_i64()), + )?; + } helpers::validate_capture_method(capture_method)?; diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index bcd71636f0..8b776bcf71 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -997,6 +997,13 @@ impl Domain> for payment_data.payment_attempt.payment_method, payment_data.payment_attempt.payment_method_type, ); + payment_data.payment_intent.enable_overcapture = payment_data + .payment_intent + .get_enable_overcapture_bool_if_connector_supports( + connector_data.connector_name, + business_profile.always_enable_overcapture, + &payment_data.payment_attempt.capture_method, + ); Ok(()) } @@ -1965,7 +1972,6 @@ impl UpdateTracker, api::PaymentsRequest> for let key_manager_state = state.into(); let is_payment_processor_token_flow = payment_data.payment_intent.is_payment_processor_token_flow; - let payment_intent_fut = tokio::spawn( async move { m_db.update_payment_intent( @@ -2018,6 +2024,7 @@ impl UpdateTracker, api::PaymentsRequest> for enable_partial_authorization: payment_data .payment_intent .enable_partial_authorization, + enable_overcapture: payment_data.payment_intent.enable_overcapture, })), &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 1f54b1b72a..171266fbb5 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1022,6 +1022,11 @@ impl ValidateRequest( payment_data.payment_attempt.connector.clone(), payment_data.payment_attempt.merchant_id.clone(), ); + let is_overcapture_enabled = router_data + .connector_response + .as_ref() + .and_then(|connector_response| { + connector_response.is_overcapture_enabled() + }).or_else(|| { + payment_data.payment_intent + .enable_overcapture + .as_ref() + .map(|enable_overcapture| common_types::primitive_wrappers::OvercaptureEnabledBool::new(*enable_overcapture.deref())) + }); + let (capture_before, extended_authorization_applied) = router_data .connector_response .as_ref() @@ -1940,6 +1952,7 @@ async fn payment_response_update_tracker( .setup_future_usage_applied, debit_routing_savings, network_transaction_id: resp_network_transaction_id, + is_overcapture_enabled, }), ), }; diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index b1460efcf4..a468f88fed 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -460,6 +460,12 @@ impl GetTracker, api::PaymentsRequest> .enable_partial_authorization .or(payment_intent.enable_partial_authorization); + helpers::validate_overcapture_request( + &request.enable_overcapture, + &payment_attempt.capture_method, + )?; + payment_intent.enable_overcapture = request.enable_overcapture; + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -968,6 +974,7 @@ impl UpdateTracker, api::PaymentsRequest> for enable_partial_authorization: payment_data .payment_intent .enable_partial_authorization, + enable_overcapture: payment_data.payment_intent.enable_overcapture, })), key_store, storage_scheme, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 1811a449af..2fe6c550f3 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -521,6 +521,7 @@ where .get_payment_attempt() .network_transaction_id .clone(), + is_overcapture_enabled: None, }; #[cfg(feature = "v1")] diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 484a3ec757..152f4089ee 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -427,6 +427,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( locale: None, payment_channel: None, enable_partial_authorization: None, + enable_overcapture: None, }; let connector_mandate_request_reference_id = payment_data .payment_attempt @@ -3403,6 +3404,8 @@ where whole_connector_response: payment_data.get_whole_connector_response(), payment_channel: payment_intent.payment_channel, enable_partial_authorization: payment_intent.enable_partial_authorization, + enable_overcapture: payment_intent.enable_overcapture, + is_overcapture_enabled: payment_attempt.is_overcapture_enabled, }; services::ApplicationResponse::JsonWithHeaders((payments_response, headers)) @@ -3700,6 +3703,8 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay payment_channel: pi.payment_channel, network_transaction_id: None, enable_partial_authorization: pi.enable_partial_authorization, + enable_overcapture: pi.enable_overcapture, + is_overcapture_enabled: pa.is_overcapture_enabled, } } } @@ -4031,6 +4036,7 @@ impl TryFrom> for types::PaymentsAuthoriz locale: None, payment_channel: None, enable_partial_authorization: None, + enable_overcapture: None, }) } } @@ -4264,6 +4270,7 @@ impl TryFrom> for types::PaymentsAuthoriz locale: Some(additional_data.state.locale.clone()), payment_channel: payment_data.payment_intent.payment_channel, enable_partial_authorization: payment_data.payment_intent.enable_partial_authorization, + enable_overcapture: payment_data.payment_intent.enable_overcapture, }) } } diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index d4bfb8b55c..c9afaa5685 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -1290,6 +1290,7 @@ mod tests { merchant_category_code: None, dispute_polling_interval: None, is_manual_retry_enabled: None, + always_enable_overcapture: None, }); let business_profile = state @@ -1405,6 +1406,8 @@ mod tests { payment_channel: None, network_transaction_id: None, enable_partial_authorization: None, + is_overcapture_enabled: None, + enable_overcapture: None, }; let content = api_webhooks::OutgoingWebhookContent::PaymentDetails(Box::new(expected_response)); diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 1bf4c825ef..525c25f736 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1241,6 +1241,7 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData { locale: None, payment_channel: None, enable_partial_authorization: data.request.enable_partial_authorization, + enable_overcapture: None, } } } diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index e86fbce57e..2576bfbd8f 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -234,6 +234,7 @@ impl ForeignTryFrom for ProfileResponse { merchant_country_code: item.merchant_country_code, dispute_polling_interval: item.dispute_polling_interval, is_manual_retry_enabled: item.is_manual_retry_enabled, + always_enable_overcapture: item.always_enable_overcapture, }) } } @@ -492,5 +493,6 @@ pub async fn create_profile_from_merchant_account( merchant_country_code: request.merchant_country_code, dispute_polling_interval: request.dispute_polling_interval, is_manual_retry_enabled: request.is_manual_retry_enabled, + always_enable_overcapture: request.always_enable_overcapture, })) } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index b73c496201..d20bcbfe64 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -68,6 +68,7 @@ impl VerifyConnectorData { locale: None, payment_channel: None, enable_partial_authorization: None, + enable_overcapture: None, } } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 5c8d79e0f0..e4266298d0 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -297,6 +297,7 @@ pub async fn generate_sample_data( tax_status: None, shipping_amount_tax: None, enable_partial_authorization: None, + enable_overcapture: None, }; let (connector_transaction_id, processor_transaction_data) = ConnectorTransactionId::form_id_and_data(attempt_id.clone()); diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 288932bb40..c35654719f 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1003,6 +1003,7 @@ impl Default for PaymentAuthorizeType { locale: None, payment_channel: None, enable_partial_authorization: None, + enable_overcapture: None, }; Self(data) } diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index c75372c8ee..e70f1fd6aa 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -468,6 +468,8 @@ async fn payments_create_core() { payment_channel: None, network_transaction_id: None, enable_partial_authorization: None, + is_overcapture_enabled: None, + enable_overcapture: None, }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); @@ -749,6 +751,8 @@ async fn payments_create_core_adyen_no_redirect() { payment_channel: None, network_transaction_id: None, enable_partial_authorization: None, + is_overcapture_enabled: None, + enable_overcapture: None, }, vec![], )); diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index f16b3e8631..c5f51ac674 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -230,6 +230,8 @@ async fn payments_create_core() { payment_channel: None, network_transaction_id: None, enable_partial_authorization: None, + is_overcapture_enabled: None, + enable_overcapture: None, }; let expected_response = @@ -519,6 +521,8 @@ async fn payments_create_core_adyen_no_redirect() { payment_channel: None, network_transaction_id: None, enable_partial_authorization: None, + is_overcapture_enabled: None, + enable_overcapture: None, }, vec![], )); diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index 022c226527..c66f1a3146 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -239,6 +239,7 @@ impl PaymentAttemptInterface for MockDb { connector_request_reference_id: payment_attempt.connector_request_reference_id, debit_routing_savings: None, network_transaction_id: payment_attempt.network_transaction_id, + is_overcapture_enabled: None, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index f5bcf1dce3..a03e1bd3fd 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -693,6 +693,7 @@ impl PaymentAttemptInterface for KVRouterStore { .clone(), debit_routing_savings: None, network_transaction_id: payment_attempt.network_transaction_id.clone(), + is_overcapture_enabled: None, }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -1902,6 +1903,7 @@ impl DataModelExt for PaymentAttempt { created_by: self.created_by.map(|created_by| created_by.to_string()), connector_request_reference_id: self.connector_request_reference_id, network_transaction_id: self.network_transaction_id, + is_overcapture_enabled: self.is_overcapture_enabled, } } @@ -1996,6 +1998,7 @@ impl DataModelExt for PaymentAttempt { connector_request_reference_id: storage_model.connector_request_reference_id, debit_routing_savings: None, network_transaction_id: storage_model.network_transaction_id, + is_overcapture_enabled: storage_model.is_overcapture_enabled, } } } diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js index a645ec5c55..3f09a83e80 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js @@ -583,7 +583,7 @@ export const connectorDetails = { status: "requires_payment_method", }, }, - }), + }), "3DSManualCapture": getCustomExchange({ Request: { payment_method: "card", @@ -638,6 +638,9 @@ export const connectorDetails = { customer_acceptance: null, }, }), + Overcapture: getCustomExchange({ + Request: {}, + }), PartialCapture: getCustomExchange({ Request: {}, }), diff --git a/cypress-tests/cypress/e2e/configs/Payment/Adyen.js b/cypress-tests/cypress/e2e/configs/Payment/Adyen.js index 44b2bd062d..a7d61a7f18 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Adyen.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Adyen.js @@ -211,7 +211,20 @@ export const connectorDetails = { }, }, }, - + Overcapture: { + Request: { + amount_to_capture: 7000, + }, + Response: { + status: 200, + body: { + status: "processing", + amount: 6000, + amount_capturable: 6000, + amount_received: 0, // Amount is updated via webhooks + }, + }, + }, PartialCapture: { Request: { amount_to_capture: 2000, diff --git a/cypress-tests/cypress/e2e/configs/Payment/Utils.js b/cypress-tests/cypress/e2e/configs/Payment/Utils.js index e5bc5710d5..07cb0aa6ab 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Utils.js @@ -413,6 +413,7 @@ export const CONNECTOR_LISTS = { DDC_RACE_CONDITION: ["worldpay"], // ucs connectors UCS_CONNECTORS: ["authorizedotnet"], + OVERCAPTURE: ["adyen"], // Add more inclusion lists }, }; diff --git a/cypress-tests/cypress/e2e/spec/Payment/00031-Overcapture.cy.js b/cypress-tests/cypress/e2e/spec/Payment/00031-Overcapture.cy.js new file mode 100644 index 0000000000..1eb9aa17e6 --- /dev/null +++ b/cypress-tests/cypress/e2e/spec/Payment/00031-Overcapture.cy.js @@ -0,0 +1,93 @@ +import * as fixtures from "../../../fixtures/imports"; +import State from "../../../utils/State"; +import getConnectorDetails, * as utils from "../../configs/Payment/Utils"; + +let connector; +let globalState; + +describe("[Payment] Overcapture", () => { + before(function () { + // Changed to regular function instead of arrow function + let skip = false; + + cy.task("getGlobalState") + .then((state) => { + globalState = new State(state); + connector = globalState.get("connectorId"); + + // Skip the test if the connector is not in the inclusion list + if ( + utils.shouldIncludeConnector( + connector, + utils.CONNECTOR_LISTS.INCLUDE.OVERCAPTURE + ) + ) { + skip = true; + } + }) + .then(() => { + if (skip) { + this.skip(); + } + }); + }); + + afterEach("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + context("[Payment] Overcapture Pre-Auth", () => { + let shouldContinue = true; + + it("create-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntent"]; + + const newData = { + ...data, + Request: { + ...data.Request, + enable_overcapture: true, + }, + }; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + newData, + "no_three_ds", + "manual", + globalState + ); + + if (shouldContinue) shouldContinue = utils.should_continue_further(data); + }); + it("confirm-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["No3DSManualCapture"]; + + cy.confirmCallTest(fixtures.confirmBody, data, true, globalState); + + if (shouldContinue) shouldContinue = utils.should_continue_further(data); + }); + + it("capture-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["Overcapture"]; + + cy.captureCallTest(fixtures.captureBody, data, globalState); + + if (shouldContinue) shouldContinue = utils.should_continue_further(data); + }); + + it("retrieve-payment-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["Overcapture"]; + + cy.retrievePaymentCallTest(globalState, data); + }); + }); +}); diff --git a/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/down.sql b/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/down.sql new file mode 100644 index 0000000000..d64f49cb02 --- /dev/null +++ b/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/down.sql @@ -0,0 +1,8 @@ +-- Remove the column `enable_overcapture` from the `payment_intent` table +ALTER TABLE payment_intent DROP COLUMN enable_overcapture; + +-- Remove the column `always_enable_overcapture` from the `business_profile` table +ALTER TABLE business_profile DROP COLUMN always_enable_overcapture; + +-- Remove the column `is_overcapture_enabled` from the `payment_attempt` table +ALTER TABLE payment_attempt DROP COLUMN is_overcapture_enabled; \ No newline at end of file diff --git a/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/up.sql b/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/up.sql new file mode 100644 index 0000000000..f1fa14a83c --- /dev/null +++ b/migrations/2025-09-08-832974_overcapture_flags_to_payment_intent_attempt_and_profile/up.sql @@ -0,0 +1,7 @@ +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS enable_overcapture BOOLEAN; + +ALTER TABLE business_profile +ADD COLUMN always_enable_overcapture BOOLEAN; + +ALTER TABLE payment_attempt +ADD COLUMN is_overcapture_enabled BOOLEAN; \ No newline at end of file