diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 63777d5283a7..3f1a0ae01de8 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6495,88 +6495,6 @@ "greater_than_equal" ] }, - "ConfirmIntentAmountDetailsResponse": { - "type": "object", - "required": [ - "currency", - "external_tax_calculation", - "surcharge_calculation", - "net_amount", - "amount_capturable" - ], - "properties": { - "order_amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", - "example": 6540, - "minimum": 0 - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "shipping_cost": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "external_tax_calculation": { - "$ref": "#/components/schemas/TaxCalculationOverride" - }, - "surcharge_calculation": { - "$ref": "#/components/schemas/SurchargeCalculationOverride" - }, - "surcharge_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "tax_on_surcharge": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "net_amount": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_to_capture": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "amount_capturable": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_captured": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - } - } - }, "Connector": { "type": "string", "description": "A connector is an integration to fulfill payments", @@ -12605,6 +12523,88 @@ } } }, + "PaymentAmountDetailsResponse": { + "type": "object", + "required": [ + "currency", + "external_tax_calculation", + "surcharge_calculation", + "net_amount", + "amount_capturable" + ], + "properties": { + "order_amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", + "example": 6540, + "minimum": 0 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "shipping_cost": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "order_tax_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "external_tax_calculation": { + "$ref": "#/components/schemas/TaxCalculationOverride" + }, + "surcharge_calculation": { + "$ref": "#/components/schemas/SurchargeCalculationOverride" + }, + "surcharge_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "net_amount": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_to_capture": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "amount_capturable": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_captured": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + } + } + }, "PaymentAttemptResponse": { "type": "object", "required": [ @@ -14467,42 +14467,12 @@ }, "PaymentsCaptureRequest": { "type": "object", - "required": [ - "amount_to_capture" - ], "properties": { - "merchant_id": { - "type": "string", - "description": "The unique identifier for the merchant", - "nullable": true - }, "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", - "example": 6540 - }, - "refund_uncaptured_amount": { - "type": "boolean", - "description": "Decider to refund the uncaptured amount", - "nullable": true - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements.", - "nullable": true - }, - "statement_descriptor_prefix": { - "type": "string", - "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", - "nullable": true - }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", + "example": 6540, "nullable": true } } @@ -14605,7 +14575,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", @@ -16354,7 +16324,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 746883028adb..0d750ea7bf53 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -17372,7 +17372,7 @@ "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", "example": 6540 }, "refund_uncaptured_amount": { diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 6015682f7c38..a78aaf483aa0 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -11,7 +11,7 @@ use super::{ ))] use crate::payment_methods::CustomerPaymentMethodsListResponse; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::payment_methods::CustomerPaymentMethodsListResponse; +use crate::{events, payment_methods::CustomerPaymentMethodsListResponse}; use crate::{ payment_methods::{ CustomerDefaultPaymentMethodResponse, DefaultPaymentMethod, ListCountriesCurrenciesRequest, @@ -418,3 +418,12 @@ impl ApiEventMetric for PaymentStartRedirectionRequest { }) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for events::PaymentsCaptureResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0ae906f3dc0f..a7211e6f82ad 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -621,7 +621,7 @@ pub struct AmountDetailsResponse { #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] -pub struct ConfirmIntentAmountDetailsResponse { +pub struct PaymentAmountDetailsResponse { /// The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies) #[schema(value_type = u64, example = 6540)] #[serde(default, deserialize_with = "amount::deserialize")] @@ -4077,6 +4077,7 @@ pub struct PhoneDetails { pub country_code: Option, } +#[cfg(feature = "v1")] #[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] pub struct PaymentsCaptureRequest { /// The unique identifier for the payment @@ -4085,7 +4086,7 @@ pub struct PaymentsCaptureRequest { /// The unique identifier for the merchant #[schema(value_type = Option)] pub merchant_id: Option, - /// The Amount to be captured/ debited from the user's payment method. + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. #[schema(value_type = i64, example = 6540)] pub amount_to_capture: Option, /// Decider to refund the uncaptured amount @@ -4099,6 +4100,28 @@ pub struct PaymentsCaptureRequest { pub merchant_connector_details: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureRequest { + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. + #[schema(value_type = Option, example = 6540)] + pub amount_to_capture: Option, +} + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureResponse { + /// The unique identifier for the payment + pub id: id_type::GlobalPaymentId, + + /// Status of the payment + #[schema(value_type = IntentStatus, example = "succeeded")] + pub status: common_enums::IntentStatus, + + /// Amount details related to the payment + pub amount: PaymentAmountDetailsResponse, +} + #[derive(Default, Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct UrlDetails { pub url: String, @@ -4801,7 +4824,7 @@ pub struct PaymentsConfirmIntentResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, /// The identifier for the customer #[schema( @@ -4879,7 +4902,7 @@ pub struct PaymentsRetrieveResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, /// The identifier for the customer #[schema( diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 6ddc26d49bdb..03facc2ebab4 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -754,7 +754,6 @@ pub enum PaymentAttemptUpdate { #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_attempt)] pub struct PaymentAttemptUpdateInternal { - // net_amount: Option, pub status: Option, // authentication_type: Option, pub error_message: Option, @@ -774,7 +773,8 @@ pub struct PaymentAttemptUpdateInternal { // multiple_capture_count: Option, // pub surcharge_amount: Option, // tax_on_surcharge: Option, - // amount_capturable: Option, + pub amount_capturable: Option, + pub amount_to_capture: Option, pub updated_by: String, pub merchant_connector_id: Option, pub connector: Option, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 57549ed201f2..8b834ee5d827 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -369,6 +369,23 @@ pub struct PaymentIntentNew { pub split_payments: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentIntentUpdate { + /// Update the payment intent details on payment intent confirmation, before calling the connector + ConfirmIntent { + status: storage_enums::IntentStatus, + active_attempt_id: common_utils::id_type::GlobalAttemptId, + updated_by: String, + }, + /// Update the payment intent details on payment intent confirmation, after calling the connector + ConfirmIntentPostUpdate { + status: storage_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, +} + #[cfg(feature = "v1")] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaymentIntentUpdate { @@ -514,8 +531,9 @@ pub struct PaymentIntentUpdateFields { #[diesel(table_name = payment_intent)] pub struct PaymentIntentUpdateInternal { pub status: Option, - pub active_attempt_id: Option, + pub amount_captured: Option, pub modified_at: PrimitiveDateTime, + pub active_attempt_id: Option, pub amount: Option, pub currency: Option, pub shipping_cost: Option, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 0243c2549204..40ab88663c9e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -226,7 +226,7 @@ impl AmountDetails { common_enums::TaxCalculationOverride::Calculate => None, }; - payment_attempt::AttemptAmountDetails { + payment_attempt::AttemptAmountDetails::from(payment_attempt::AttemptAmountDetailsSetter { net_amount, amount_to_capture: None, surcharge_amount, @@ -235,7 +235,7 @@ impl AmountDetails { amount_capturable: MinorUnit::zero(), shipping_cost: self.shipping_cost, order_tax_amount, - } + }) } pub fn update_from_request(self, req: &api_models::payments::AmountDetailsUpdate) -> Self { @@ -599,6 +599,17 @@ where pub should_sync_with_connector: bool, } +#[cfg(feature = "v2")] +#[derive(Clone)] +pub struct PaymentCaptureData +where + F: Clone, +{ + pub flow: PhantomData, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, +} + #[cfg(feature = "v2")] impl PaymentStatusData where diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 591ccc4ff981..0a7ada39ecdb 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -189,6 +189,27 @@ pub trait PaymentAttemptInterface { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct AttemptAmountDetails { + /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. + net_amount: MinorUnit, + /// The amount that has to be captured, + amount_to_capture: Option, + /// Surcharge amount for the payment attempt. + /// This is either derived by surcharge rules, or sent by the merchant + surcharge_amount: Option, + /// Tax amount for the payment attempt + /// This is either derived by surcharge rules, or sent by the merchant + tax_on_surcharge: Option, + /// The total amount that can be captured for this payment attempt. + amount_capturable: MinorUnit, + /// Shipping cost for the payment attempt. + shipping_cost: Option, + /// Tax amount for the order. + /// This is either derived by calling an external tax processor, or sent by the merchant + order_tax_amount: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct AttemptAmountDetailsSetter { /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. pub net_amount: MinorUnit, /// The amount that has to be captured, @@ -208,6 +229,67 @@ pub struct AttemptAmountDetails { pub order_tax_amount: Option, } +/// Set the fields of amount details, since the fields are not public +impl From for AttemptAmountDetails { + fn from(setter: AttemptAmountDetailsSetter) -> Self { + Self { + net_amount: setter.net_amount, + amount_to_capture: setter.amount_to_capture, + surcharge_amount: setter.surcharge_amount, + tax_on_surcharge: setter.tax_on_surcharge, + amount_capturable: setter.amount_capturable, + shipping_cost: setter.shipping_cost, + order_tax_amount: setter.order_tax_amount, + } + } +} + +impl AttemptAmountDetails { + pub fn get_net_amount(&self) -> MinorUnit { + self.net_amount + } + + pub fn get_amount_to_capture(&self) -> Option { + self.amount_to_capture + } + + pub fn get_surcharge_amount(&self) -> Option { + self.surcharge_amount + } + + pub fn get_tax_on_surcharge(&self) -> Option { + self.tax_on_surcharge + } + + pub fn get_amount_capturable(&self) -> MinorUnit { + self.amount_capturable + } + + pub fn get_shipping_cost(&self) -> Option { + self.shipping_cost + } + + pub fn get_order_tax_amount(&self) -> Option { + self.order_tax_amount + } + + pub fn set_amount_to_capture(&mut self, amount_to_capture: MinorUnit) { + self.amount_to_capture = Some(amount_to_capture); + } + + /// Validate the amount to capture that is sent in the request + pub fn validate_amount_to_capture( + &self, + request_amount_to_capture: MinorUnit, + ) -> Result<(), ValidationError> { + common_utils::fp_utils::when(request_amount_to_capture > self.get_net_amount(), || { + Err(ValidationError::IncorrectValueProvided { + field_name: "amount_to_capture", + }) + }) + } +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct ErrorDetails { /// The error code that was returned by the connector. @@ -657,6 +739,7 @@ impl NetAmount { #[cfg(feature = "v2")] impl PaymentAttempt { + #[track_caller] pub fn get_total_amount(&self) -> MinorUnit { todo!(); } @@ -1339,15 +1422,28 @@ pub enum PaymentAttemptUpdate { updated_by: String, redirection_data: Option, connector_metadata: Option, + amount_capturable: Option, }, /// Update the payment attempt after force syncing with the connector SyncUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, + updated_by: String, + }, + PreCaptureUpdate { + amount_to_capture: Option, + updated_by: String, + }, + /// Update the payment after attempting capture with the connector + CaptureUpdate { + status: storage_enums::AttemptStatus, + amount_capturable: Option, updated_by: String, }, /// Update the payment attempt on confirming the intent, after calling the connector on error response ErrorUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, error: ErrorDetails, updated_by: String, connector_payment_id: Option, @@ -1985,11 +2081,14 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: Some(connector), redirection_data: None, connector_metadata: None, + amount_capturable: None, + amount_to_capture: None, }, PaymentAttemptUpdate::ErrorUpdate { status, error, connector_payment_id, + amount_capturable, updated_by, } => Self { status: Some(status), @@ -2006,6 +2105,8 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_capturable, + amount_to_capture: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, @@ -2013,8 +2114,10 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, redirection_data, connector_metadata, + amount_capturable, } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2029,9 +2132,15 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: redirection_data .map(diesel_models::payment_attempt::RedirectForm::from), connector_metadata, + amount_to_capture: None, }, - PaymentAttemptUpdate::SyncUpdate { status, updated_by } => Self { + PaymentAttemptUpdate::SyncUpdate { + status, + amount_capturable, + updated_by, + } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2045,6 +2154,50 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_to_capture: None, + }, + PaymentAttemptUpdate::CaptureUpdate { + status, + amount_capturable, + updated_by, + } => Self { + status: Some(status), + amount_capturable, + amount_to_capture: None, + error_message: None, + error_code: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + connector_metadata: None, + }, + PaymentAttemptUpdate::PreCaptureUpdate { + amount_to_capture, + updated_by, + } => Self { + amount_to_capture, + error_message: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_code: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + status: None, + connector_metadata: None, + amount_capturable: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 4fd861acb65c..cbd3a8137ca9 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -292,11 +292,18 @@ pub enum PaymentIntentUpdate { /// PostUpdate tracker of ConfirmIntent ConfirmIntentPostUpdate { status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// SyncUpdate of ConfirmIntent in PostUpdateTrackers SyncUpdate { status: common_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, + CaptureUpdate { + status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// UpdateIntent @@ -361,6 +368,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { active_attempt_id: Some(active_attempt_id), modified_at: common_utils::date_time::now(), amount: None, + amount_captured: None, currency: None, shipping_cost: None, tax_details: None, @@ -392,12 +400,57 @@ impl From for diesel_models::PaymentIntentUpdateInternal { updated_by, }, - PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntentPostUpdate { + status, + updated_by, + amount_captured, + } => Self { + status: Some(status), + active_attempt_id: None, + modified_at: common_utils::date_time::now(), + amount_captured, + amount: None, + currency: None, + shipping_cost: None, + tax_details: None, + skip_external_tax_calculation: None, + surcharge_applicable: None, + surcharge_amount: None, + tax_on_surcharge: None, + routing_algorithm_id: None, + capture_method: None, + authentication_type: None, + billing_address: None, + shipping_address: None, + customer_present: None, + description: None, + return_url: None, + setup_future_usage: None, + apply_mit_exemption: None, + statement_descriptor: None, + order_details: None, + allowed_payment_method_types: None, + metadata: None, + connector_metadata: None, + feature_metadata: None, + payment_link_config: None, + request_incremental_authorization: None, + session_expiry: None, + frm_metadata: None, + request_external_three_ds_authentication: None, + updated_by, + }, + PaymentIntentUpdate::SyncUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, currency: None, + amount_captured, shipping_cost: None, tax_details: None, skip_external_tax_calculation: None, @@ -427,8 +480,13 @@ impl From for diesel_models::PaymentIntentUpdateInternal { request_external_three_ds_authentication: None, updated_by, }, - PaymentIntentUpdate::SyncUpdate { status, updated_by } => Self { + PaymentIntentUpdate::CaptureUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), + amount_captured, active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, @@ -499,7 +557,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { status: None, active_attempt_id: None, modified_at: common_utils::date_time::now(), - + amount_captured: None, amount, currency, shipping_cost, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index b070c6bc848a..f69c1db0e10b 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -405,15 +405,30 @@ use crate::{ /// Get updatable trakcer objects of payment intent and payment attempt #[cfg(feature = "v2")] -pub trait TrackerPostUpdateObjects { +pub trait TrackerPostUpdateObjects { fn get_payment_intent_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate; + fn get_payment_attempt_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate; + + /// Get the amount that can be captured for the payment + fn get_amount_capturable(&self, payment_data: &D) -> Option; + + /// Get the amount that has been captured for the payment + fn get_captured_amount(&self, payment_data: &D) -> Option; + + /// Get the attempt status based on parameters like captured amount and amount capturable + fn get_attempt_status_for_db_update( + &self, + payment_data: &D, + ) -> common_enums::enums::AttemptStatus; } #[cfg(feature = "v2")] @@ -421,6 +436,7 @@ impl TrackerPostUpdateObjects< router_flow_types::Authorize, router_request_types::PaymentsAuthorizeData, + payments::PaymentConfirmData, > for RouterData< router_flow_types::Authorize, @@ -430,11 +446,16 @@ impl { fn get_payment_intent_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { @@ -442,6 +463,7 @@ impl .attempt_status .map(common_enums::IntentStatus::from) .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, updated_by: storage_scheme.to_string(), }, } @@ -449,8 +471,11 @@ impl fn get_payment_attempt_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -463,7 +488,8 @@ impl incremental_authorization_allowed, charge_id, } => { - let attempt_status = self.status; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); + let connector_payment_id = match resource_id { router_request_types::ResponseId::NoResponseId => None, router_request_types::ResponseId::ConnectorTransactionId(id) @@ -475,6 +501,7 @@ impl connector_payment_id, updated_by: storage_scheme.to_string(), redirection_data: *redirection_data.clone(), + amount_capturable, connector_metadata: connector_metadata.clone().map(Secret::new), } } @@ -528,16 +555,310 @@ impl PaymentAttemptUpdate::ErrorUpdate { status: attempt_status, error: error_details, + amount_capturable, connector_payment_id: connector_transaction_id, updated_by: storage_scheme.to_string(), } } } } + + fn get_attempt_status_for_db_update( + &self, + _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 + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> 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 { + // If the status is already succeeded / failed we cannot capture any more amount + // So set the amount capturable to zero + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // If status is requires capture, get the total amount that can be captured + // This is in cases where the capture method will be `manual` or `manual_multiple` + // We do not need to handle the case where amount_to_capture is provided here as it cannot be passed in authroize flow + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow, after doing authorization this state is invalid + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> 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 { + // 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 => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // No amount has been captured yet + common_enums::IntentStatus::RequiresCapture => Some(MinorUnit::zero()), + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } #[cfg(feature = "v2")] -impl TrackerPostUpdateObjects +impl + TrackerPostUpdateObjects< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + payments::PaymentCaptureData, + > + for RouterData< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + router_response_types::PaymentsResponseData, + > +{ + fn get_payment_intent_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); + match self.response { + Ok(ref _response) => PaymentIntentUpdate::CaptureUpdate { + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + Err(ref error) => PaymentIntentUpdate::CaptureUpdate { + status: error + .attempt_status + .map(common_enums::IntentStatus::from) + .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + } + } + + fn get_payment_attempt_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + + match self.response { + Ok(ref response_router_data) => match response_router_data { + router_response_types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + mandate_reference, + connector_metadata, + network_txn_id, + connector_response_reference_id, + incremental_authorization_allowed, + charge_id, + } => { + let attempt_status = self.status; + + PaymentAttemptUpdate::CaptureUpdate { + status: attempt_status, + amount_capturable, + updated_by: storage_scheme.to_string(), + } + } + router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::SessionTokenResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::TransactionUnresolvedResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::TokenizationResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::ConnectorCustomerResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::ThreeDSEnrollmentResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PreProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::IncrementalAuthorizationResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PostProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionUpdateResponse { .. } => { + todo!() + } + }, + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status, + connector_transaction_id, + } = error_response.clone(); + let attempt_status = attempt_status.unwrap_or(self.status); + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status: attempt_status, + error: error_details, + amount_capturable, + connector_payment_id: connector_transaction_id, + updated_by: storage_scheme.to_string(), + } + } + } + } + + fn get_attempt_status_for_db_update( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> common_enums::AttemptStatus { + 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( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> 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 { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> 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 { + // If the status is succeeded then we have captured the whole amount + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture(); + + let amount_captured = amount_to_capture + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + todo!() + } + } + } +} + +#[cfg(feature = "v2")] +impl + TrackerPostUpdateObjects< + router_flow_types::PSync, + router_request_types::PaymentsSyncData, + payments::PaymentStatusData, + > for RouterData< router_flow_types::PSync, router_request_types::PaymentsSyncData, @@ -546,11 +867,16 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::SyncUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::SyncUpdate { @@ -558,6 +884,7 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -579,15 +909,11 @@ impl TrackerPostUpdateObjects { - let attempt_status = self.status; - let connector_payment_id = match resource_id { - router_request_types::ResponseId::NoResponseId => None, - router_request_types::ResponseId::ConnectorTransactionId(id) - | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), - }; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); PaymentAttemptUpdate::SyncUpdate { status: attempt_status, + amount_capturable, updated_by: storage_scheme.to_string(), } } @@ -641,10 +967,106 @@ impl TrackerPostUpdateObjects, + ) -> common_enums::AttemptStatus { + 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 + .as_ref() + .map(|attempt| attempt.amount_details.get_net_amount()) + .unwrap_or(MinorUnit::zero()); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // 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 { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + 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, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // 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 { + // If the status is succeeded then we have captured the whole amount or amount_to_capture + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_attempt.amount_details.get_amount_to_capture(); + + let amount_captured = + amount_to_capture.unwrap_or(payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + 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, + } + } } diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index c7425aed2a51..7553a780c272 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -639,7 +639,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ErrorDetails, api_models::payments::CtpServiceDetails, common_utils::types::BrowserInformation, - api_models::payments::ConfirmIntentAmountDetailsResponse, + api_models::payments::PaymentAmountDetailsResponse, routes::payments::ForceSync, )), modifiers(&SecurityAddon) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f758dc0b0e55..e68bd09ba462 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -40,7 +40,7 @@ use helpers::{decrypt_paze_token, ApplePayData}; use hyperswitch_domain_models::payments::payment_intent::CustomerData; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{ - PaymentConfirmData, PaymentIntentData, PaymentStatusData, + PaymentCaptureData, PaymentConfirmData, PaymentIntentData, PaymentStatusData, }; #[cfg(feature = "v2")] use hyperswitch_domain_models::router_response_types::RedirectForm; @@ -149,7 +149,7 @@ where services::api::ConnectorIntegration, RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, // To perform router related operation for PaymentResponse PaymentResponse: Operation, @@ -1519,8 +1519,12 @@ where FData: Send + Sync + Clone, Op: Operation + ValidateStatusForOperation + Send + Sync + Clone, Req: Debug, - D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, - Res: transformers::ToResponse, + D: OperationSessionGetters + + OperationSessionSetters + + transformers::GenerateResponse + + Send + + Sync + + Clone, // To create connector flow specific interface data D: ConstructFlowSpecificData, RouterData: Feature, @@ -1534,13 +1538,14 @@ where // To create updatable objects in post update tracker RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, { // Validate the request fields - let validate_result = operation + operation .to_validate_request()? .validate_request(&req, &merchant_account)?; + // Get the tracker related information. This includes payment intent and payment attempt let get_tracker_response = operation .to_get_tracker()? .get_trackers( @@ -1569,12 +1574,8 @@ where ) .await?; - Res::generate_response( - payment_data, - customer, - &state.base_url, - operation, - &state.conf.connector_request_reference_id_config, + payment_data.generate_response( + &state, connector_http_status_code, external_latency, header_payload.x_hs_latency, @@ -6640,7 +6641,7 @@ pub async fn payment_start_redirection( services::RedirectionFormData { redirect_form: redirection_data, payment_method_data: None, - amount: payment_attempt.amount_details.net_amount.to_string(), + amount: payment_attempt.amount_details.get_net_amount().to_string(), currency: payment_intent.amount_details.currency.to_string(), }, ))) @@ -7800,3 +7801,218 @@ impl OperationSessionSetters for PaymentStatusData { todo!() } } + +#[cfg(feature = "v2")] +impl OperationSessionGetters for PaymentCaptureData { + #[track_caller] + fn get_payment_attempt(&self) -> &storage::PaymentAttempt { + &self.payment_attempt + } + + fn get_payment_intent(&self) -> &storage::PaymentIntent { + &self.payment_intent + } + + fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> { + todo!() + } + + fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> { + todo!() + } + + // what is this address find out and not required remove this + fn get_address(&self) -> &PaymentAddress { + todo!() + } + + fn get_creds_identifier(&self) -> Option<&str> { + None + } + + fn get_token(&self) -> Option<&str> { + todo!() + } + + fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> { + todo!() + } + + fn get_payment_link_data(&self) -> Option { + todo!() + } + + fn get_ephemeral_key(&self) -> Option { + todo!() + } + + fn get_setup_mandate(&self) -> Option<&MandateData> { + todo!() + } + + fn get_poll_config(&self) -> Option { + todo!() + } + + fn get_authentication(&self) -> Option<&storage::Authentication> { + todo!() + } + + fn get_frm_message(&self) -> Option { + todo!() + } + + fn get_refunds(&self) -> Vec { + todo!() + } + + fn get_disputes(&self) -> Vec { + todo!() + } + + fn get_authorizations(&self) -> Vec { + todo!() + } + + fn get_attempts(&self) -> Option> { + todo!() + } + + fn get_recurring_details(&self) -> Option<&RecurringDetails> { + todo!() + } + + fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> { + Some(&self.payment_intent.profile_id) + } + + fn get_currency(&self) -> storage_enums::Currency { + self.payment_intent.amount_details.currency + } + + fn get_amount(&self) -> api::Amount { + todo!() + } + + fn get_payment_attempt_connector(&self) -> Option<&str> { + todo!() + } + + fn get_billing_address(&self) -> Option { + todo!() + } + + fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> { + todo!() + } + + fn get_sessions_token(&self) -> Vec { + todo!() + } + + fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { + todo!() + } + + fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> { + todo!() + } + + fn get_force_sync(&self) -> Option { + todo!() + } + + fn get_capture_method(&self) -> Option { + todo!() + } + + #[cfg(feature = "v2")] + fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> { + Some(&self.payment_attempt) + } +} + +#[cfg(feature = "v2")] +impl OperationSessionSetters for PaymentCaptureData { + fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) { + self.payment_intent = payment_intent; + } + + fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) { + self.payment_attempt = payment_attempt; + } + + fn set_payment_method_data(&mut self, _payment_method_data: Option) { + todo!() + } + + fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option) { + todo!() + } + + fn set_email_if_not_present(&mut self, _email: pii::Email) { + todo!() + } + + fn set_pm_token(&mut self, _token: String) { + todo!() + } + + fn set_connector_customer_id(&mut self, _customer_id: Option) { + // TODO: handle this case. Should we add connector_customer_id in paymentConfirmData? + } + + fn push_sessions_token(&mut self, _token: api::SessionToken) { + todo!() + } + + fn set_surcharge_details(&mut self, _surcharge_details: Option) { + todo!() + } + + #[track_caller] + fn set_merchant_connector_id_in_attempt( + &mut self, + merchant_connector_id: Option, + ) { + todo!() + } + + fn set_frm_message(&mut self, _frm_message: FraudCheck) { + todo!() + } + + fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) { + self.payment_intent.status = status; + } + + fn set_authentication_type_in_attempt( + &mut self, + _authentication_type: Option, + ) { + todo!() + } + + fn set_recurring_mandate_payment_data( + &mut self, + _recurring_mandate_payment_data: + hyperswitch_domain_models::router_data::RecurringMandatePaymentData, + ) { + todo!() + } + + fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) { + todo!() + } + + fn set_setup_future_usage_in_payment_intent( + &mut self, + setup_future_usage: storage_enums::FutureUsage, + ) { + self.payment_intent.setup_future_usage = setup_future_usage; + } + + fn set_connector_in_payment_attempt(&mut self, connector: Option) { + todo!() + } +} diff --git a/crates/router/src/core/payments/flows/capture_flow.rs b/crates/router/src/core/payments/flows/capture_flow.rs index 8b6fa24fbfd7..1863d673ee9c 100644 --- a/crates/router/src/core/payments/flows/capture_flow.rs +++ b/crates/router/src/core/payments/flows/capture_flow.rs @@ -11,12 +11,12 @@ use crate::{ types::{self, api, domain}, }; +#[cfg(feature = "v1")] #[async_trait] impl ConstructFlowSpecificData for PaymentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &SessionState, @@ -45,7 +45,24 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData + for hyperswitch_domain_models::payments::PaymentCaptureData +{ async fn construct_router_data<'a>( &self, state: &SessionState, @@ -56,8 +73,21 @@ impl merchant_connector_account: &domain::MerchantConnectorAccount, merchant_recipient_data: Option, header_payload: Option, - ) -> RouterResult { - todo!() + ) -> RouterResult< + types::RouterData, + > { + Box::pin(transformers::construct_payment_router_data_for_capture( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index a7e60274d135..38911bb2d6df 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -42,6 +42,9 @@ pub mod payment_update_intent; #[cfg(feature = "v2")] pub mod payment_get; +#[cfg(feature = "v2")] +pub mod payment_capture_v2; + use api_models::enums::FrmSuggestion; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use api_models::routing::RoutableConnectorChoice; @@ -439,7 +442,7 @@ pub trait PostUpdateTracker: Send { where F: 'b + Send + Sync, types::RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; async fn save_pm_and_mandate<'b>( &self, diff --git a/crates/router/src/core/payments/operations/payment_capture_v2.rs b/crates/router/src/core/payments/operations/payment_capture_v2.rs new file mode 100644 index 000000000000..0ee62ea73c1f --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_capture_v2.rs @@ -0,0 +1,324 @@ +use api_models::{enums::FrmSuggestion, payments::PaymentsCaptureRequest}; +use async_trait::async_trait; +use error_stack::ResultExt; +use hyperswitch_domain_models::payments::PaymentCaptureData; +use router_env::{instrument, tracing}; + +use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payments::operations::{self, ValidateStatusForOperation}, + }, + routes::{app::ReqState, SessionState}, + types::{ + api::{self, ConnectorCallType}, + domain::{self}, + storage::{self, enums as storage_enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentsCapture; + +impl ValidateStatusForOperation for PaymentsCapture { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => Ok(()), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: [ + common_enums::IntentStatus::RequiresCapture, + common_enums::IntentStatus::PartiallyCapturedAndCapturable, + ] + .map(|enum_value| enum_value.to_string()) + .join(", "), + }) + } + } + } +} + +type BoxedConfirmOperation<'b, F> = + super::BoxedOperation<'b, F, PaymentsCaptureRequest, PaymentCaptureData>; + +// TODO: change the macro to include changes for v2 +// TODO: PaymentData in the macro should be an input +impl Operation for &PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(*self) + } +} +#[automatically_derived] +impl Operation for PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(self) + } +} + +impl ValidateRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsCaptureRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + let validate_result = operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }; + + Ok(validate_result) + } +} + +#[async_trait] +impl GetTracker, PaymentsCaptureRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + request: &PaymentsCaptureRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let active_attempt_id = payment_intent + .active_attempt_id + .as_ref() + .get_required_value("active_attempt_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Active attempt id is none when capturing the payment")?; + + let mut payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + key_store, + active_attempt_id, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not find payment attempt given the attempt id")?; + + if let Some(amount_to_capture) = request.amount_to_capture { + payment_attempt + .amount_details + .validate_amount_to_capture(amount_to_capture) + .change_context(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "`amount_to_capture` is greater than the net amount {}", + payment_attempt.amount_details.get_net_amount() + ), + })?; + + payment_attempt + .amount_details + .set_amount_to_capture(amount_to_capture); + } + + let payment_data = PaymentCaptureData { + flow: std::marker::PhantomData, + payment_intent, + payment_attempt, + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl Domain> for PaymentsCapture { + async fn get_customer_details<'a>( + &'a self, + state: &SessionState, + payment_data: &mut PaymentCaptureData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult<(BoxedConfirmOperation<'a, F>, Option), errors::StorageError> + { + match payment_data.payment_intent.customer_id.clone() { + Some(id) => { + let customer = state + .store + .find_customer_by_global_id( + &state.into(), + &id, + &payment_data.payment_intent.merchant_id, + merchant_key_store, + storage_scheme, + ) + .await?; + + Ok((Box::new(self), Some(customer))) + } + None => Ok((Box::new(self), None)), + } + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut PaymentCaptureData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + BoxedConfirmOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + #[instrument(skip_all)] + async fn perform_routing<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + // TODO: do not take the whole payment data here + payment_data: &mut PaymentCaptureData, + _mechant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + let payment_attempt = &payment_data.payment_attempt; + let connector = payment_attempt + .connector + .as_ref() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Connector is none when constructing response")?; + + let merchant_connector_id = payment_attempt + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant connector id is none when constructing response")?; + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + Some(merchant_connector_id.to_owned()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + Ok(ConnectorCallType::PreDetermined(connector_data)) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsCaptureRequest> for PaymentsCapture { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: PaymentCaptureData, + _customer: Option, + storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentCaptureData)> + where + F: 'b + Send, + { + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture: payment_data.payment_attempt.amount_details.get_amount_to_capture(), updated_by: storage_scheme.to_string() }; + + let payment_attempt = state + .store + .update_payment_attempt( + &state.into(), + key_store, + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not update payment attempt")?; + + payment_data.payment_attempt = payment_attempt; + Ok((Box::new(self), payment_data)) + } +} diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index 92c8240d187c..403ee5443148 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -1,38 +1,23 @@ -use api_models::{ - admin::ExtendedCardInfoConfig, - enums::FrmSuggestion, - payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsRetrieveRequest}, -}; +use api_models::{enums::FrmSuggestion, payments::PaymentsRetrieveRequest}; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; -use hyperswitch_domain_models::payments::{ - payment_attempt::PaymentAttempt, PaymentIntent, PaymentStatusData, -}; +use hyperswitch_domain_models::payments::PaymentStatusData; use router_env::{instrument, tracing}; -use tracing_futures::Instrument; use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - authentication, errors::{self, CustomResult, RouterResult, StorageErrorExt}, - payments::{ - self, helpers, - operations::{self, ValidateStatusForOperation}, - populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, - }, - utils as core_utils, + payments::operations::{self, ValidateStatusForOperation}, }, routes::{app::ReqState, SessionState}, - services, types::{ - self, - api::{self, ConnectorCallType, PaymentIdTypeExt}, + api::{self, ConnectorCallType}, domain::{self}, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy)] @@ -107,7 +92,7 @@ impl ValidateRequest( &'b self, - request: &PaymentsRetrieveRequest, + _request: &PaymentsRetrieveRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult { let validate_result = operations::ValidateResult { @@ -133,7 +118,7 @@ impl GetTracker, PaymentsRetriev merchant_account: &domain::MerchantAccount, _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, - header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -231,12 +216,12 @@ impl Domain( &'a self, - state: &'a SessionState, - payment_data: &mut PaymentStatusData, - storage_scheme: storage_enums::MerchantStorageScheme, - key_store: &domain::MerchantKeyStore, - customer: &Option, - business_profile: &domain::Profile, + _state: &'a SessionState, + _payment_data: &mut PaymentStatusData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, @@ -248,12 +233,12 @@ impl Domain( &'a self, - merchant_account: &domain::MerchantAccount, - business_profile: &domain::Profile, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, state: &SessionState, // TODO: do not take the whole payment data here payment_data: &mut PaymentStatusData, - mechant_key_store: &domain::MerchantKeyStore, + _mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult { match &payment_data.payment_attempt { Some(payment_attempt) if payment_data.should_sync_with_connector => { @@ -294,15 +279,15 @@ impl UpdateTracker, PaymentsRetrieveReq #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - state: &'b SessionState, - req_state: ReqState, - mut payment_data: PaymentStatusData, - customer: Option, - storage_scheme: storage_enums::MerchantStorageScheme, - updated_customer: Option, - key_store: &domain::MerchantKeyStore, - frm_suggestion: Option, - header_payload: hyperswitch_domain_models::payments::HeaderPayload, + _state: &'b SessionState, + _req_state: ReqState, + payment_data: PaymentStatusData, + _customer: Option, + _storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + _key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentStatusData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 01a4e0860918..988429a0eecb 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2200,6 +2200,88 @@ impl Operation for PaymentResp } } +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = hyperswitch_domain_models::payments::PaymentCaptureData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + PostUpdateTracker< + F, + hyperswitch_domain_models::payments::PaymentCaptureData, + types::PaymentsCaptureData, + > for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::PaymentsCaptureData, + hyperswitch_domain_models::payments::PaymentCaptureData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} + #[cfg(feature = "v2")] #[async_trait] impl PostUpdateTracker, types::PaymentsAuthorizeData> @@ -2219,6 +2301,7 @@ impl PostUpdateTracker, types::PaymentsAuthor hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsAuthorizeData, + PaymentConfirmData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2228,9 +2311,10 @@ impl PostUpdateTracker, types::PaymentsAuthor let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let updated_payment_intent = db .update_payment_intent( @@ -2293,6 +2377,7 @@ impl PostUpdateTracker, types::PaymentsSyncDat hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsSyncData, + PaymentStatusData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2302,9 +2387,10 @@ impl PostUpdateTracker, types::PaymentsSyncDat let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let payment_attempt = payment_data .payment_attempt diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8652844623f1..d4c3877e5d97 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -19,8 +19,6 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; -#[cfg(feature = "v2")] use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; #[cfg(feature = "v2")] @@ -183,7 +181,7 @@ where #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_authorize<'a>( state: &'a SessionState, - payment_data: PaymentConfirmData, + payment_data: hyperswitch_domain_models::payments::PaymentConfirmData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -259,9 +257,9 @@ pub async fn construct_payment_router_data_for_authorize<'a>( amount: payment_data .payment_attempt .amount_details - .net_amount + .get_net_amount() .get_amount_as_i64(), - minor_amount: payment_data.payment_attempt.amount_details.net_amount, + minor_amount: payment_data.payment_attempt.amount_details.get_net_amount(), order_tax_amount: None, currency: payment_data.payment_intent.amount_details.currency, browser_info: None, @@ -382,6 +380,174 @@ pub async fn construct_payment_router_data_for_authorize<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_capture<'a>( + state: &'a SessionState, + payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + use masking::ExposeOptionInterface; + + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let customer_id = customer + .to_owned() + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + + let payment_method = payment_data.payment_attempt.payment_method_type; + + let connector_request_reference_id = payment_data + .payment_intent + .merchant_reference_id + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or(payment_data.payment_attempt.id.get_string_repr().to_owned()); + + let connector_mandate_request_reference_id = payment_data + .payment_attempt + .connector_mandate_detail + .as_ref() + .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + + let connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_id, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + let amount = payment_data.payment_attempt.amount_details.get_net_amount(); + let request = types::PaymentsCaptureData { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.payment_intent.amount_details.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .clone() + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }; + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data + .payment_attempt + .payment_id + .get_string_repr() + .to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: payment_data + .payment_attempt + .get_id() + .get_string_repr() + .to_owned(), + status: payment_data.payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: evaluate why we need to send merchant's return url here + // This should be the return url of application, since application takes care of the redirection + return_url: payment_data + .payment_intent + .return_url + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: payment_data.payment_attempt.authentication_type, + connector_meta_data: None, + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id, + psd2_sca_exemption_type: None, + authentication_id: None, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -424,7 +590,7 @@ pub async fn construct_router_data_for_psync<'a>( .unwrap_or(attempt.id.get_string_repr().to_owned()); let request = types::PaymentsSyncData { - amount: attempt.amount_details.net_amount, + amount: attempt.amount_details.get_net_amount(), integrity_object: None, mandate_id: None, connector_transaction_id: match attempt.get_connector_payment_id() { @@ -521,7 +687,7 @@ pub async fn construct_router_data_for_psync<'a>( #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_sdk_session<'a>( _state: &'a SessionState, - payment_data: PaymentIntentData, + payment_data: hyperswitch_domain_models::payments::PaymentIntentData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -915,6 +1081,57 @@ where ) -> RouterResponse; } +/// Generate a response from the given Data. This should be implemented on a payment data object +pub trait GenerateResponse +where + Self: Sized, +{ + #[cfg(feature = "v2")] + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse; +} + +#[cfg(feature = "v2")] +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentCaptureData +where + F: Clone, +{ + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = &self.payment_intent; + let payment_attempt = &self.payment_attempt; + + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( + &payment_intent.amount_details, + &payment_attempt.amount_details, + )); + + let response = api_models::payments::PaymentsCaptureResponse { + id: payment_intent.id.clone(), + amount, + status: payment_intent.status, + }; + + Ok(services::ApplicationResponse::JsonWithHeaders(( + response, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsResponse where @@ -1170,28 +1387,23 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsConfirmIntentResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentConfirmData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_payment_attempt(); + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let payment_attempt = self.payment_attempt; - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, &payment_attempt.amount_details, )); @@ -1215,7 +1427,7 @@ where .clone() .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); + let payment_address = self.payment_address; let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { @@ -1227,14 +1439,17 @@ where }); // TODO: Add support for other next actions, currently only supporting redirect to url - let redirect_to_url = payment_intent - .create_start_redirection_url(base_url, merchant_account.publishable_key.clone())?; + let redirect_to_url = payment_intent.create_start_redirection_url( + &state.base_url, + merchant_account.publishable_key.clone(), + )?; + let next_action = payment_attempt .redirection_data .as_ref() .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); - let response = Self { + let response = api_models::payments::PaymentsConfirmIntentResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, @@ -1261,70 +1476,73 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsRetrieveResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentStatusData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - _base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, - _merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_optional_payment_attempt(); + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let optional_payment_attempt = self.payment_attempt.as_ref(); - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, - payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), + optional_payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), )); let connector = - payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); + optional_payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); - let merchant_connector_id = payment_attempt + let merchant_connector_id = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.merchant_connector_id.clone()); - let error = payment_attempt + let error = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.error.clone()) .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); - let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { payment_method_data: None, - billing: payment_address + billing: self + .payment_address .get_request_payment_method_billing() .cloned() .map(From::from), }); - let response = Self { + let response = api_models::payments::PaymentsRetrieveResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, customer_id: payment_intent.customer_id.clone(), connector, - billing: payment_address + billing: self + .payment_address .get_payment_billing() .cloned() .map(From::from), - shipping: payment_address.get_shipping().cloned().map(From::from), + shipping: self.payment_address.get_shipping().cloned().map(From::from), client_secret: payment_intent.client_secret.clone(), created: payment_intent.created_at, payment_method_data, - payment_method_type: payment_attempt.map(|attempt| attempt.payment_method_type), - payment_method_subtype: payment_attempt.map(|attempt| attempt.payment_method_subtype), - connector_transaction_id: payment_attempt + payment_method_type: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_type), + payment_method_subtype: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_subtype), + connector_transaction_id: self + .payment_attempt + .as_ref() .and_then(|attempt| attempt.connector_payment_id.clone()), connector_reference_id: None, merchant_connector_id, @@ -2838,7 +3056,44 @@ impl TryFrom> for types::PaymentsCaptureD type Error = error_stack::Report; fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { - todo!() + use masking::ExposeOptionInterface; + + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.get_total_amount()); + + let amount = payment_data.payment_attempt.get_total_amount(); + Ok(Self { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }) } } @@ -3631,7 +3886,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, &hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3642,15 +3897,15 @@ impl Self { order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, - shipping_cost: attempt_amount_details.shipping_cost, - order_tax_amount: attempt_amount_details.order_tax_amount, + shipping_cost: attempt_amount_details.get_shipping_cost(), + order_tax_amount: attempt_amount_details.get_order_tax_amount(), external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, - surcharge_amount: attempt_amount_details.surcharge_amount, - tax_on_surcharge: attempt_amount_details.tax_on_surcharge, - net_amount: attempt_amount_details.net_amount, - amount_to_capture: attempt_amount_details.amount_to_capture, - amount_capturable: attempt_amount_details.amount_capturable, + surcharge_amount: attempt_amount_details.get_surcharge_amount(), + tax_on_surcharge: attempt_amount_details.get_tax_on_surcharge(), + net_amount: attempt_amount_details.get_net_amount(), + amount_to_capture: attempt_amount_details.get_amount_to_capture(), + amount_capturable: attempt_amount_details.get_amount_capturable(), amount_captured: intent_amount_details.amount_captured, } } @@ -3663,7 +3918,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, Option<&hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails>, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3675,10 +3930,10 @@ impl order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, shipping_cost: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.shipping_cost) + .and_then(|attempt_amount| attempt_amount.get_shipping_cost()) .or(intent_amount_details.shipping_cost), order_tax_amount: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.order_tax_amount) + .and_then(|attempt_amount| attempt_amount.get_order_tax_amount()) .or(intent_amount_details .tax_details .as_ref() @@ -3686,17 +3941,18 @@ impl external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, surcharge_amount: attempt_amount_details - .and_then(|attempt| attempt.surcharge_amount) + .and_then(|attempt| attempt.get_surcharge_amount()) .or(intent_amount_details.surcharge_amount), tax_on_surcharge: attempt_amount_details - .and_then(|attempt| attempt.tax_on_surcharge) + .and_then(|attempt| attempt.get_tax_on_surcharge()) .or(intent_amount_details.tax_on_surcharge), net_amount: attempt_amount_details - .map(|attempt| attempt.net_amount) + .map(|attempt| attempt.get_net_amount()) .unwrap_or(intent_amount_details.calculate_net_amount()), - amount_to_capture: attempt_amount_details.and_then(|attempt| attempt.amount_to_capture), + amount_to_capture: attempt_amount_details + .and_then(|attempt| attempt.get_amount_to_capture()), amount_capturable: attempt_amount_details - .map(|attempt| attempt.amount_capturable) + .map(|attempt| attempt.get_amount_capturable()) .unwrap_or(MinorUnit::zero()), amount_captured: intent_amount_details.amount_captured, } diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 139ae18c1717..569cd330a079 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -20,7 +20,10 @@ use crate::{ api_locking, errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt}, metrics, - payments::{self, transformers::ToResponse}, + payments::{ + self, + transformers::{GenerateResponse, ToResponse}, + }, webhooks::utils::construct_webhook_router_data, }, db::StorageInterface, @@ -433,12 +436,8 @@ async fn payments_incoming_webhook_flow( )) .await?; - let response = api_models::payments::PaymentsRetrieveResponse::generate_response( - payment_data, - customer, - &state.base_url, - payments::operations::PaymentGet, - &state.conf.connector_request_reference_id_config, + let response = payment_data.generate_response( + &state, connector_http_status_code, external_latency, None, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 766679491c54..852fd0fbd7eb 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -575,6 +575,9 @@ impl Payments { .service( web::resource("/finish-redirection/{publishable_key}/{profile_id}") .route(web::get().to(payments::payments_finish_redirection)), + ) + .service( + web::resource("/capture").route(web::post().to(payments::payments_capture)), ), ); diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 2893cb8fa044..a9005644bebb 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2447,3 +2447,75 @@ pub async fn payments_finish_redirection( ) .await } + +#[cfg(feature = "v2")] +#[instrument(skip(state, req), fields(flow, payment_id))] +pub async fn payments_capture( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, + path: web::Path, +) -> impl Responder { + use hyperswitch_domain_models::payments::PaymentCaptureData; + let flow = Flow::PaymentsCapture; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: payload.into_inner(), + }; + + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| async { + let payment_id = req.global_payment_id; + let request = req.payload; + + let operation = payments::operations::payment_capture_v2::PaymentsCapture; + + Box::pin(payments::payments_core::< + api_types::Capture, + api_models::payments::PaymentsCaptureResponse, + _, + _, + _, + PaymentCaptureData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + )) + .await + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfileAccountWrite, + }, + req.headers(), + ), + locking_action, + )) + .await +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index de4c063c41aa..5ee7094700c8 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -470,7 +470,7 @@ impl Capturable for PaymentsSyncData { payment_data .payment_attempt .amount_details - .amount_to_capture + .get_amount_to_capture() .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) .map(|amt| amt.get_amount_as_i64()) }