diff --git a/api-reference-v2/api-reference/payments/payments--update-intent.mdx b/api-reference-v2/api-reference/payments/payments--update-intent.mdx new file mode 100644 index 000000000000..3ef58f4db721 --- /dev/null +++ b/api-reference-v2/api-reference/payments/payments--update-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /v2/payments/{id}/update-intent +--- \ No newline at end of file diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index aed89492443d..4212b2dbd213 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -40,6 +40,7 @@ "pages": [ "api-reference/payments/payments--create-intent", "api-reference/payments/payments--get-intent", + "api-reference/payments/payments--update-intent", "api-reference/payments/payments--session-token", "api-reference/payments/payments--confirm-intent", "api-reference/payments/payments--get" diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 778e89ab30b6..64949c3812b6 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1911,6 +1911,79 @@ ] } }, + "/v2/payments/{id}/update-intent": { + "put": { + "tags": [ + "Payments" + ], + "summary": "Payments - Update Intent", + "description": "**Update a payment intent object**\n\nYou will require the 'API - Key' from the Hyperswitch dashboard to make the call.", + "operationId": "Update a Payment Intent", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Intent", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID associated to the payment intent", + "required": true, + "schema": { + "type": "string" + }, + "example": { + "X-Profile-Id": "pro_abcdefghijklmnop" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsUpdateIntentRequest" + }, + "examples": { + "Update a payment intent with minimal fields": { + "value": { + "amount_details": { + "currency": "USD", + "order_amount": 6540 + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Intent Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsIntentResponse" + } + } + } + }, + "404": { + "description": "Payment Intent Not Found" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/payments/{id}/confirm-intent": { "post": { "tags": [ @@ -3026,6 +3099,75 @@ } } }, + "AmountDetailsUpdate": { + "type": "object", + "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, + "nullable": true, + "minimum": 0 + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true + }, + "shipping_cost": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "order_tax_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "skip_external_tax_calculation": { + "allOf": [ + { + "$ref": "#/components/schemas/TaxCalculationOverride" + } + ], + "nullable": true + }, + "skip_surcharge_calculation": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeCalculationOverride" + } + ], + "nullable": true + }, + "surcharge_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + } + } + }, "AmountFilter": { "type": "object", "properties": { @@ -15898,6 +16040,176 @@ } } }, + "PaymentsUpdateIntentRequest": { + "type": "object", + "properties": { + "amount_details": { + "allOf": [ + { + "$ref": "#/components/schemas/AmountDetailsUpdate" + } + ], + "nullable": true + }, + "routing_algorithm_id": { + "type": "string", + "description": "The routing algorithm id to be used for the payment", + "nullable": true + }, + "capture_method": { + "allOf": [ + { + "$ref": "#/components/schemas/CaptureMethod" + } + ], + "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "no_three_ds", + "nullable": true + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "shipping": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "customer_present": { + "allOf": [ + { + "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" + } + ], + "nullable": true + }, + "description": { + "type": "string", + "description": "A description for the payment", + "example": "It's my first payment request", + "nullable": true + }, + "return_url": { + "type": "string", + "description": "The URL to which you want the user to be redirected after the completion of the payment operation", + "example": "https://hyperswitch.io", + "nullable": true + }, + "setup_future_usage": { + "allOf": [ + { + "$ref": "#/components/schemas/FutureUsage" + } + ], + "nullable": true + }, + "apply_mit_exemption": { + "allOf": [ + { + "$ref": "#/components/schemas/MitExemptionRequest" + } + ], + "nullable": true + }, + "statement_descriptor": { + "type": "string", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 22 + }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", + "nullable": true + }, + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true + }, + "metadata": { + "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object. This metadata will override the metadata that was passed in payments", + "nullable": true + }, + "connector_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorMetadata" + } + ], + "nullable": true + }, + "feature_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/FeatureMetadata" + } + ], + "nullable": true + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + } + ], + "nullable": true + }, + "request_incremental_authorization": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestIncrementalAuthorization" + } + ], + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "type": "object", + "description": "Additional data related to some frm(Fraud Risk Management) connectors", + "nullable": true + }, + "request_external_three_ds_authentication": { + "allOf": [ + { + "$ref": "#/components/schemas/External3dsAuthenticationRequest" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, "PayoutActionRequest": { "type": "object" }, @@ -17189,10 +17501,10 @@ }, "PresenceOfCustomerDuringPayment": { "type": "string", - "description": "Set to true to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be false when merchant's doing merchant initiated payments and customer is not present while doing the payment.", + "description": "Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment.", "enum": [ - "Present", - "Absent" + "present", + "absent" ] }, "PrimaryBusinessDetails": { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 603c0f769347..6a7f7148bd3a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -156,8 +156,8 @@ pub struct PaymentsCreateIntentRequest { #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, - /// Set to true to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be false when merchant's doing merchant initiated payments and customer is not present while doing the payment. - #[schema(example = true, value_type = Option)] + /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. + #[schema(example = "present", value_type = Option)] pub customer_present: Option, /// A description for the payment @@ -297,6 +297,99 @@ pub struct PaymentsGetIntentRequest { pub id: id_type::GlobalPaymentId, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[cfg(feature = "v2")] +pub struct PaymentsUpdateIntentRequest { + pub amount_details: Option, + + /// The routing algorithm id to be used for the payment + #[schema(value_type = Option)] + pub routing_algorithm_id: Option, + + #[schema(value_type = Option, example = "automatic")] + pub capture_method: Option, + + #[schema(value_type = Option, example = "no_three_ds", default = "no_three_ds")] + pub authentication_type: Option, + + /// The billing details of the payment. This address will be used for invoicing. + pub billing: Option
, + + /// The shipping address for the payment + pub shipping: Option
, + + /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. + #[schema(example = "present", value_type = Option)] + pub customer_present: Option, + + /// A description for the payment + #[schema(example = "It's my first payment request", value_type = Option)] + pub description: Option, + + /// The URL to which you want the user to be redirected after the completion of the payment operation + #[schema(value_type = Option, example = "https://hyperswitch.io")] + pub return_url: Option, + + #[schema(value_type = Option, example = "off_session")] + pub setup_future_usage: Option, + + /// Apply MIT exemption for a payment + #[schema(value_type = Option)] + pub apply_mit_exemption: Option, + + /// For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters. + #[schema(max_length = 22, example = "Hyperswitch Router", value_type = Option)] + pub statement_descriptor: Option, + + /// Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount + #[schema(value_type = Option>, example = r#"[{ + "product_name": "Apple iPhone 16", + "quantity": 1, + "amount" : 69000 + "product_img_link" : "https://dummy-img-link.com" + }]"#)] + pub order_details: Option>, + + /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent + #[schema(value_type = Option>)] + pub allowed_payment_method_types: Option>, + + /// Metadata is useful for storing additional, unstructured information on an object. This metadata will override the metadata that was passed in payments + #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] + pub metadata: Option, + + /// Some connectors like Apple pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. + #[schema(value_type = Option)] + pub connector_metadata: Option, + + /// Additional data that might be required by hyperswitch based on the requested features by the merchants. + #[schema(value_type = Option)] + pub feature_metadata: Option, + + /// Configure a custom payment link for the particular payment + #[schema(value_type = Option)] + pub payment_link_config: Option, + + /// Request an incremental authorization, i.e., increase the authorized amount on a confirmed payment before you capture it. + #[schema(value_type = Option)] + pub request_incremental_authorization: Option, + + /// Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config + ///(900) for 15 mins + #[schema(value_type = Option, example = 900)] + pub session_expiry: Option, + + /// Additional data related to some frm(Fraud Risk Management) connectors + #[schema(value_type = Option, example = r#"{ "coverage_request" : "fraud", "fulfillment_method" : "delivery" }"#)] + pub frm_metadata: Option, + + /// Whether to perform external authentication (if applicable) + #[schema(value_type = Option)] + pub request_external_three_ds_authentication: + Option, +} + #[derive(Debug, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] #[cfg(feature = "v2")] @@ -352,8 +445,8 @@ pub struct PaymentsIntentResponse { #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, - /// Set to true to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be false when merchant's doing merchant initiated payments and customer is not present while doing the payment. - #[schema(example = true, value_type = PresenceOfCustomerDuringPayment)] + /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. + #[schema(example = "present", value_type = PresenceOfCustomerDuringPayment)] pub customer_present: common_enums::PresenceOfCustomerDuringPayment, /// A description for the payment @@ -453,6 +546,32 @@ pub struct AmountDetails { tax_on_surcharge: Option, } +#[cfg(feature = "v2")] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct AmountDetailsUpdate { + /// 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 = Option, example = 6540)] + #[serde(default, deserialize_with = "amount::deserialize_option")] + order_amount: Option, + /// The currency of the order + #[schema(example = "USD", value_type = Option)] + currency: Option, + /// The shipping cost of the order. This has to be collected from the merchant + shipping_cost: Option, + /// Tax amount related to the order. This will be calculated by the external tax provider + order_tax_amount: Option, + /// The action to whether calculate tax by calling external tax provider or not + #[schema(value_type = Option)] + skip_external_tax_calculation: Option, + /// The action to whether calculate surcharge or not + #[schema(value_type = Option)] + skip_surcharge_calculation: Option, + /// The surcharge amount to be added to the order, collected from the merchant + surcharge_amount: Option, + /// tax on surcharge amount + tax_on_surcharge: Option, +} + #[cfg(feature = "v2")] pub struct AmountDetailsSetter { pub order_amount: Amount, @@ -552,10 +671,10 @@ impl AmountDetails { self.order_tax_amount } pub fn skip_external_tax_calculation(&self) -> common_enums::TaxCalculationOverride { - self.skip_external_tax_calculation.clone() + self.skip_external_tax_calculation } pub fn skip_surcharge_calculation(&self) -> common_enums::SurchargeCalculationOverride { - self.skip_surcharge_calculation.clone() + self.skip_surcharge_calculation } pub fn surcharge_amount(&self) -> Option { self.surcharge_amount @@ -565,6 +684,33 @@ impl AmountDetails { } } +#[cfg(feature = "v2")] +impl AmountDetailsUpdate { + pub fn order_amount(&self) -> Option { + self.order_amount + } + pub fn currency(&self) -> Option { + self.currency + } + pub fn shipping_cost(&self) -> Option { + self.shipping_cost + } + pub fn order_tax_amount(&self) -> Option { + self.order_tax_amount + } + pub fn skip_external_tax_calculation(&self) -> Option { + self.skip_external_tax_calculation + } + pub fn skip_surcharge_calculation(&self) -> Option { + self.skip_surcharge_calculation + } + pub fn surcharge_amount(&self) -> Option { + self.surcharge_amount + } + pub fn tax_on_surcharge(&self) -> Option { + self.tax_on_surcharge + } +} #[cfg(feature = "v1")] #[derive( Default, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index c3c876b22aa6..6c38172e5406 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3332,8 +3332,9 @@ pub enum MitExemptionRequest { Skip, } -/// Set to true to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be false when merchant's doing merchant initiated payments and customer is not present while doing the payment. +/// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema)] +#[serde(rename_all = "snake_case")] pub enum PresenceOfCustomerDuringPayment { /// Customer is present during the payment. This is the default value #[default] @@ -3352,7 +3353,9 @@ impl From for TransactionType { } } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema)] +#[derive( + Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum TaxCalculationOverride { /// Skip calling the external tax provider @@ -3362,7 +3365,27 @@ pub enum TaxCalculationOverride { Calculate, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema)] +impl From> for TaxCalculationOverride { + fn from(value: Option) -> Self { + match value { + Some(true) => Self::Calculate, + _ => Self::Skip, + } + } +} + +impl TaxCalculationOverride { + pub fn as_bool(self) -> bool { + match self { + Self::Skip => false, + Self::Calculate => true, + } + } +} + +#[derive( + Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum SurchargeCalculationOverride { /// Skip calculating surcharge @@ -3372,6 +3395,24 @@ pub enum SurchargeCalculationOverride { Calculate, } +impl From> for SurchargeCalculationOverride { + fn from(value: Option) -> Self { + match value { + Some(true) => Self::Calculate, + _ => Self::Skip, + } + } +} + +impl SurchargeCalculationOverride { + pub fn as_bool(self) -> bool { + match self { + Self::Skip => false, + Self::Calculate => true, + } + } +} + /// Connector Mandate Status #[derive( Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, strum::Display, diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index 7c5c3a8de178..801592712928 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "v2")] use crate::payment_attempt::PaymentAttemptUpdateInternal; +#[cfg(feature = "v1")] +use crate::payment_intent::PaymentIntentUpdate; #[cfg(feature = "v2")] use crate::payment_intent::PaymentIntentUpdateInternal; use crate::{ @@ -10,7 +12,7 @@ use crate::{ customers::{Customer, CustomerNew, CustomerUpdateInternal}, errors, payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, - payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, + payment_intent::PaymentIntentNew, payout_attempt::{PayoutAttempt, PayoutAttemptNew, PayoutAttemptUpdate}, payouts::{Payouts, PayoutsNew, PayoutsUpdate}, refund::{Refund, RefundNew, RefundUpdate}, @@ -115,11 +117,9 @@ impl DBOperation { DBResult::PaymentIntent(Box::new(a.orig.update(conn, a.update_data).await?)) } #[cfg(feature = "v2")] - Updateable::PaymentIntentUpdate(a) => DBResult::PaymentIntent(Box::new( - a.orig - .update(conn, PaymentIntentUpdateInternal::from(a.update_data)) - .await?, - )), + Updateable::PaymentIntentUpdate(a) => { + DBResult::PaymentIntent(Box::new(a.orig.update(conn, a.update_data).await?)) + } #[cfg(feature = "v1")] Updateable::PaymentAttemptUpdate(a) => DBResult::PaymentAttempt(Box::new( a.orig.update_with_attempt_id(conn, a.update_data).await?, @@ -247,13 +247,20 @@ pub struct AddressUpdateMems { pub orig: Address, pub update_data: AddressUpdateInternal, } - +#[cfg(feature = "v1")] #[derive(Debug, Serialize, Deserialize)] pub struct PaymentIntentUpdateMems { pub orig: PaymentIntent, pub update_data: PaymentIntentUpdate, } +#[cfg(feature = "v2")] +#[derive(Debug, Serialize, Deserialize)] +pub struct PaymentIntentUpdateMems { + pub orig: PaymentIntent, + pub update_data: PaymentIntentUpdateInternal, +} + #[derive(Debug, Serialize, Deserialize)] pub struct PaymentAttemptUpdateMems { pub orig: PaymentAttempt, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 9f0bf17230da..b29da50d0a7d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -140,7 +140,8 @@ pub struct PaymentIntent { pub psd2_sca_exemption_type: Option, } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, diesel::AsExpression, PartialEq)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] pub struct PaymentLinkConfigRequestForPayments { /// custom theme for the payment link pub theme: Option, @@ -359,22 +360,6 @@ pub struct PaymentIntentNew { pub psd2_sca_exemption_type: 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, - updated_by: String, - }, -} - #[cfg(feature = "v1")] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaymentIntentUpdate { @@ -516,34 +501,42 @@ pub struct PaymentIntentUpdateFields { // TODO: uncomment fields as necessary #[cfg(feature = "v2")] -#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, Serialize, Deserialize)] #[diesel(table_name = payment_intent)] pub struct PaymentIntentUpdateInternal { - // pub amount: Option, - // pub currency: Option, pub status: Option, - // pub amount_captured: Option, - // pub customer_id: Option, - // pub return_url: Option<>, - // pub setup_future_usage: Option, - // pub metadata: Option, - pub modified_at: PrimitiveDateTime, pub active_attempt_id: Option, - // pub description: Option, - // pub statement_descriptor: Option, - // #[diesel(deserialize_as = super::OptionalDieselArray)] - // pub order_details: Option>, - // pub attempt_count: Option, + pub modified_at: PrimitiveDateTime, + pub amount: Option, + pub currency: Option, + pub shipping_cost: Option, + pub tax_details: Option, + pub skip_external_tax_calculation: Option, + pub surcharge_applicable: Option, + pub surcharge_amount: Option, + pub tax_on_surcharge: Option, + pub routing_algorithm_id: Option, + pub capture_method: Option, + pub authentication_type: Option, + pub billing_address: Option, + pub shipping_address: Option, + pub customer_present: Option, + pub description: Option, + pub return_url: Option, + pub setup_future_usage: Option, + pub apply_mit_exemption: Option, + pub statement_descriptor: Option, + pub order_details: Option>>, + pub allowed_payment_method_types: Option, + pub metadata: Option, + pub connector_metadata: Option, + pub feature_metadata: Option, + pub payment_link_config: Option, + pub request_incremental_authorization: Option, + pub session_expiry: Option, + pub frm_metadata: Option, + pub request_external_three_ds_authentication: Option, pub updated_by: String, - // pub surcharge_applicable: Option, - // pub authorization_count: Option, - // pub session_expiry: Option, - // pub request_external_three_ds_authentication: Option, - // pub frm_metadata: Option, - // pub customer_details: Option, - // pub billing_address: Option, - // pub shipping_address: Option, - // pub frm_merchant_decision: Option, } #[cfg(feature = "v1")] @@ -589,66 +582,6 @@ pub struct PaymentIntentUpdateInternal { pub tax_details: Option, } -#[cfg(feature = "v2")] -impl PaymentIntentUpdate { - pub fn apply_changeset(self, source: PaymentIntent) -> PaymentIntent { - let PaymentIntentUpdateInternal { - // amount, - // currency, - status, - // amount_captured, - // customer_id, - // return_url, - // setup_future_usage, - // metadata, - modified_at: _, - active_attempt_id, - // description, - // statement_descriptor, - // order_details, - // attempt_count, - // frm_merchant_decision, - updated_by, - // surcharge_applicable, - // authorization_count, - // session_expiry, - // request_external_three_ds_authentication, - // frm_metadata, - // customer_details, - // billing_address, - // shipping_address, - } = self.into(); - PaymentIntent { - // amount: amount.unwrap_or(source.amount), - // currency: currency.unwrap_or(source.currency), - status: status.unwrap_or(source.status), - // amount_captured: amount_captured.or(source.amount_captured), - // customer_id: customer_id.or(source.customer_id), - // return_url: return_url.or(source.return_url), - // setup_future_usage: setup_future_usage.or(source.setup_future_usage), - // metadata: metadata.or(source.metadata), - modified_at: common_utils::date_time::now(), - active_attempt_id: active_attempt_id.or(source.active_attempt_id), - // description: description.or(source.description), - // statement_descriptor: statement_descriptor.or(source.statement_descriptor), - // order_details: order_details.or(source.order_details), - // attempt_count: attempt_count.unwrap_or(source.attempt_count), - // frm_merchant_decision: frm_merchant_decision.or(source.frm_merchant_decision), - updated_by, - // surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), - // authorization_count: authorization_count.or(source.authorization_count), - // session_expiry: session_expiry.or(source.session_expiry), - // request_external_three_ds_authentication: request_external_three_ds_authentication - // .or(source.request_external_three_ds_authentication), - // frm_metadata: frm_metadata.or(source.frm_metadata), - // customer_details: customer_details.or(source.customer_details), - // billing_address: billing_address.or(source.billing_address), - // shipping_address: shipping_address.or(source.shipping_address), - ..source - } - } -} - #[cfg(feature = "v1")] impl PaymentIntentUpdate { pub fn apply_changeset(self, source: PaymentIntent) -> PaymentIntent { @@ -739,30 +672,6 @@ impl PaymentIntentUpdate { } } -#[cfg(feature = "v2")] -impl From for PaymentIntentUpdateInternal { - fn from(payment_intent_update: PaymentIntentUpdate) -> Self { - match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { - status, - active_attempt_id, - updated_by, - } => Self { - status: Some(status), - active_attempt_id: Some(active_attempt_id), - modified_at: common_utils::date_time::now(), - updated_by, - }, - PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { - status: Some(status), - active_attempt_id: None, - modified_at: common_utils::date_time::now(), - updated_by, - }, - } - } -} - #[cfg(feature = "v1")] impl From for PaymentIntentUpdateInternal { fn from(payment_intent_update: PaymentIntentUpdate) -> Self { diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index d94e921c120b..31732e132868 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -302,12 +302,8 @@ impl From for payments::AmountDetails { payment_method_type: None, } }), - skip_external_tax_calculation: payments::TaxCalculationOverride::from( - amount_details.skip_external_tax_calculation(), - ), - skip_surcharge_calculation: payments::SurchargeCalculationOverride::from( - amount_details.skip_surcharge_calculation(), - ), + skip_external_tax_calculation: amount_details.skip_external_tax_calculation(), + skip_surcharge_calculation: amount_details.skip_surcharge_calculation(), surcharge_amount: amount_details.surcharge_amount(), tax_on_surcharge: amount_details.tax_on_surcharge(), // We will not receive this in the request. This will be populated after calling the connector / processor @@ -315,23 +311,3 @@ impl From for payments::AmountDetails { } } } - -#[cfg(feature = "v2")] -impl From for payments::SurchargeCalculationOverride { - fn from(surcharge_calculation_override: common_enums::SurchargeCalculationOverride) -> Self { - match surcharge_calculation_override { - common_enums::SurchargeCalculationOverride::Calculate => Self::Calculate, - common_enums::SurchargeCalculationOverride::Skip => Self::Skip, - } - } -} - -#[cfg(feature = "v2")] -impl From for payments::TaxCalculationOverride { - fn from(tax_calculation_override: common_enums::TaxCalculationOverride) -> Self { - match tax_calculation_override { - common_enums::TaxCalculationOverride::Calculate => Self::Calculate, - common_enums::TaxCalculationOverride::Skip => Self::Skip, - } - } -} diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 207668817e36..9e6477fa26ba 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -17,6 +17,8 @@ use diesel_models::payment_intent::TaxDetails; #[cfg(feature = "v2")] use error_stack::ResultExt; use masking::Secret; +#[cfg(feature = "v2")] +use payment_intent::PaymentIntentUpdate; use router_derive::ToEncryption; use rustc_hash::FxHashMap; use serde_json::Value; @@ -155,44 +157,6 @@ impl PaymentIntent { } } -#[cfg(feature = "v2")] -#[derive(Clone, Debug, PartialEq, Copy, serde::Serialize)] -pub enum TaxCalculationOverride { - /// Skip calling the external tax provider - Skip, - /// Calculate tax by calling the external tax provider - Calculate, -} - -#[cfg(feature = "v2")] -#[derive(Clone, Debug, PartialEq, Copy, serde::Serialize)] -pub enum SurchargeCalculationOverride { - /// Skip calculating surcharge - Skip, - /// Calculate surcharge - Calculate, -} - -#[cfg(feature = "v2")] -impl From> for TaxCalculationOverride { - fn from(value: Option) -> Self { - match value { - Some(true) => Self::Calculate, - _ => Self::Skip, - } - } -} - -#[cfg(feature = "v2")] -impl From> for SurchargeCalculationOverride { - fn from(value: Option) -> Self { - match value { - Some(true) => Self::Calculate, - _ => Self::Skip, - } - } -} - #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, serde::Serialize)] pub struct AmountDetails { @@ -205,9 +169,9 @@ pub struct AmountDetails { /// Tax details related to the order. This will be calculated by the external tax provider pub tax_details: Option, /// The action to whether calculate tax by calling external tax provider or not - pub skip_external_tax_calculation: TaxCalculationOverride, + pub skip_external_tax_calculation: common_enums::TaxCalculationOverride, /// The action to whether calculate surcharge or not - pub skip_surcharge_calculation: SurchargeCalculationOverride, + pub skip_surcharge_calculation: common_enums::SurchargeCalculationOverride, /// The surcharge amount to be added to the order, collected from the merchant pub surcharge_amount: Option, /// tax on surcharge amount @@ -221,18 +185,12 @@ pub struct AmountDetails { impl AmountDetails { /// Get the action to whether calculate surcharge or not as a boolean value fn get_surcharge_action_as_bool(&self) -> bool { - match self.skip_surcharge_calculation { - SurchargeCalculationOverride::Skip => false, - SurchargeCalculationOverride::Calculate => true, - } + self.skip_surcharge_calculation.as_bool() } /// Get the action to whether calculate external tax or not as a boolean value fn get_external_tax_action_as_bool(&self) -> bool { - match self.skip_external_tax_calculation { - TaxCalculationOverride::Skip => false, - TaxCalculationOverride::Calculate => true, - } + self.skip_external_tax_calculation.as_bool() } /// Calculate the net amount for the order @@ -250,20 +208,22 @@ impl AmountDetails { let net_amount = self.calculate_net_amount(); let surcharge_amount = match self.skip_surcharge_calculation { - SurchargeCalculationOverride::Skip => self.surcharge_amount, - SurchargeCalculationOverride::Calculate => None, + common_enums::SurchargeCalculationOverride::Skip => self.surcharge_amount, + common_enums::SurchargeCalculationOverride::Calculate => None, }; let tax_on_surcharge = match self.skip_surcharge_calculation { - SurchargeCalculationOverride::Skip => self.tax_on_surcharge, - SurchargeCalculationOverride::Calculate => None, + common_enums::SurchargeCalculationOverride::Skip => self.tax_on_surcharge, + common_enums::SurchargeCalculationOverride::Calculate => None, }; let order_tax_amount = match self.skip_external_tax_calculation { - TaxCalculationOverride::Skip => self.tax_details.as_ref().and_then(|tax_details| { - tax_details.get_tax_amount(confirm_intent_request.payment_method_subtype) - }), - TaxCalculationOverride::Calculate => None, + common_enums::TaxCalculationOverride::Skip => { + self.tax_details.as_ref().and_then(|tax_details| { + tax_details.get_tax_amount(confirm_intent_request.payment_method_subtype) + }) + } + common_enums::TaxCalculationOverride::Calculate => None, }; payment_attempt::AttemptAmountDetails { @@ -277,6 +237,33 @@ impl AmountDetails { order_tax_amount, } } + + pub fn update_from_request(self, req: &api_models::payments::AmountDetailsUpdate) -> Self { + Self { + order_amount: req + .order_amount() + .unwrap_or(self.order_amount.into()) + .into(), + currency: req.currency().unwrap_or(self.currency), + shipping_cost: req.shipping_cost().or(self.shipping_cost), + tax_details: req + .order_tax_amount() + .map(|order_tax_amount| TaxDetails { + default: Some(diesel_models::DefaultTax { order_tax_amount }), + payment_method_type: None, + }) + .or(self.tax_details), + skip_external_tax_calculation: req + .skip_external_tax_calculation() + .unwrap_or(self.skip_external_tax_calculation), + skip_surcharge_calculation: req + .skip_surcharge_calculation() + .unwrap_or(self.skip_surcharge_calculation), + surcharge_amount: req.surcharge_amount().or(self.surcharge_amount), + tax_on_surcharge: req.tax_on_surcharge().or(self.tax_on_surcharge), + amount_captured: self.amount_captured, + } + } } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index b0ffe519d630..9b6b6fe55720 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1,4 +1,3 @@ -use common_enums as storage_enums; #[cfg(feature = "v2")] use common_utils::ext_traits::{Encode, ValueExt}; use common_utils::{ @@ -14,8 +13,6 @@ use common_utils::{ MinorUnit, }, }; -#[cfg(feature = "v2")] -use diesel_models::types::OrderDetailsWithAmount; use diesel_models::{ PaymentIntent as DieselPaymentIntent, PaymentIntentNew as DieselPaymentIntentNew, }; @@ -29,6 +26,8 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v1", feature = "olap"))] use super::payment_attempt::PaymentAttempt; use super::PaymentIntent; +#[cfg(feature = "v2")] +use crate::address::Address; use crate::{ behaviour, errors, merchant_key_store::MerchantKeyStore, @@ -44,7 +43,7 @@ pub trait PaymentIntentInterface { this: PaymentIntent, payment_intent: PaymentIntentUpdate, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result; async fn insert_payment_intent( @@ -52,7 +51,7 @@ pub trait PaymentIntentInterface { state: &KeyManagerState, new: PaymentIntent, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result; #[cfg(feature = "v1")] @@ -62,7 +61,7 @@ pub trait PaymentIntentInterface { payment_id: &id_type::PaymentId, merchant_id: &id_type::MerchantId, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result; #[cfg(feature = "v2")] @@ -71,7 +70,7 @@ pub trait PaymentIntentInterface { state: &KeyManagerState, id: &id_type::GlobalPaymentId, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result; #[cfg(all(feature = "v1", feature = "olap"))] @@ -81,7 +80,7 @@ pub trait PaymentIntentInterface { merchant_id: &id_type::MerchantId, filters: &PaymentIntentFetchConstraints, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result, errors::StorageError>; #[cfg(all(feature = "v1", feature = "olap"))] @@ -91,7 +90,7 @@ pub trait PaymentIntentInterface { merchant_id: &id_type::MerchantId, time_range: &common_utils::types::TimeRange, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result, errors::StorageError>; #[cfg(all(feature = "v1", feature = "olap"))] @@ -109,7 +108,7 @@ pub trait PaymentIntentInterface { merchant_id: &id_type::MerchantId, constraints: &PaymentIntentFetchConstraints, merchant_key_store: &MerchantKeyStore, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result, errors::StorageError>; #[cfg(all(feature = "v1", feature = "olap"))] @@ -117,7 +116,7 @@ pub trait PaymentIntentInterface { &self, merchant_id: &id_type::MerchantId, constraints: &PaymentIntentFetchConstraints, - storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result, errors::StorageError>; } @@ -133,39 +132,52 @@ pub struct CustomerData { #[derive(Debug, Clone, Serialize)] pub struct PaymentIntentUpdateFields { pub amount: Option, - pub currency: Option, - pub setup_future_usage: Option, - pub status: storage_enums::IntentStatus, - pub customer_id: Option, - pub shipping_address: Option>>, - pub billing_address: Option>>, - pub return_url: Option, - pub description: Option, - pub statement_descriptor: Option, - pub order_details: Option>, + pub currency: Option, + pub shipping_cost: Option, + pub tax_details: Option, + pub skip_external_tax_calculation: Option, + pub skip_surcharge_calculation: Option, + pub surcharge_amount: Option, + pub tax_on_surcharge: Option, + pub routing_algorithm_id: Option, + pub capture_method: Option, + pub authentication_type: Option, + pub billing_address: Option>, + pub shipping_address: Option>, + pub customer_present: Option, + pub description: Option, + pub return_url: Option, + pub setup_future_usage: Option, + pub apply_mit_exemption: Option, + pub statement_descriptor: Option, + pub order_details: Option>>, + pub allowed_payment_method_types: Option>, pub metadata: Option, - pub payment_confirm_source: Option, - pub updated_by: String, + pub connector_metadata: Option, + pub feature_metadata: Option, + pub payment_link_config: Option, + pub request_incremental_authorization: Option, pub session_expiry: Option, - pub request_external_three_ds_authentication: Option, pub frm_metadata: Option, - pub customer_details: Option>>, - pub merchant_order_reference_id: Option, - pub is_payment_processor_token_flow: Option, + pub request_external_three_ds_authentication: + Option, + + // updated_by is set internally, field not present in request + pub updated_by: String, } #[cfg(feature = "v1")] #[derive(Debug, Clone, Serialize)] pub struct PaymentIntentUpdateFields { pub amount: MinorUnit, - pub currency: storage_enums::Currency, - pub setup_future_usage: Option, - pub status: storage_enums::IntentStatus, + pub currency: common_enums::Currency, + pub setup_future_usage: Option, + pub status: common_enums::IntentStatus, pub customer_id: Option, pub shipping_address_id: Option, pub billing_address_id: Option, pub return_url: Option, - pub business_country: Option, + pub business_country: Option, pub business_label: Option, pub description: Option, pub statement_descriptor_name: Option, @@ -173,7 +185,7 @@ pub struct PaymentIntentUpdateFields { pub order_details: Option>, pub metadata: Option, pub frm_metadata: Option, - pub payment_confirm_source: Option, + pub payment_confirm_source: Option, pub updated_by: String, pub fingerprint_id: Option, pub session_expiry: Option, @@ -190,7 +202,7 @@ pub struct PaymentIntentUpdateFields { #[derive(Debug, Clone, Serialize)] pub enum PaymentIntentUpdate { ResponseUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, amount_captured: Option, return_url: Option, updated_by: String, @@ -204,7 +216,7 @@ pub enum PaymentIntentUpdate { Update(Box), PaymentCreateUpdate { return_url: Option, - status: Option, + status: Option, customer_id: Option, shipping_address_id: Option, billing_address_id: Option, @@ -212,13 +224,13 @@ pub enum PaymentIntentUpdate { updated_by: String, }, MerchantStatusUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, shipping_address_id: Option, billing_address_id: Option, updated_by: String, }, PGStatusUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, incremental_authorization_allowed: Option, updated_by: String, }, @@ -228,18 +240,18 @@ pub enum PaymentIntentUpdate { updated_by: String, }, StatusAndAttemptUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, active_attempt_id: String, attempt_count: i16, updated_by: String, }, ApproveUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, merchant_decision: Option, updated_by: String, }, RejectUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, merchant_decision: Option, updated_by: String, }, @@ -257,7 +269,7 @@ pub enum PaymentIntentUpdate { shipping_address_id: Option, }, ManualUpdate { - status: Option, + status: Option, updated_by: String, }, SessionResponseUpdate { @@ -273,72 +285,41 @@ pub enum PaymentIntentUpdate { pub enum PaymentIntentUpdate { /// PreUpdate tracker of ConfirmIntent ConfirmIntent { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, active_attempt_id: id_type::GlobalAttemptId, updated_by: String, }, /// PostUpdate tracker of ConfirmIntent ConfirmIntentPostUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, updated_by: String, }, /// SyncUpdate of ConfirmIntent in PostUpdateTrackers SyncUpdate { - status: storage_enums::IntentStatus, + status: common_enums::IntentStatus, updated_by: String, }, -} - -#[cfg(feature = "v2")] -#[derive(Clone, Debug, Default)] -pub struct PaymentIntentUpdateInternal { - pub amount: Option, - pub currency: Option, - pub status: Option, - pub amount_captured: Option, - pub customer_id: Option, - pub return_url: Option, - pub setup_future_usage: Option, - pub off_session: Option, - pub metadata: Option, - pub modified_at: Option, - pub active_attempt_id: Option, - pub description: Option, - pub statement_descriptor: Option, - pub order_details: Option>, - pub attempt_count: Option, - pub frm_merchant_decision: Option, - pub payment_confirm_source: Option, - pub updated_by: String, - pub surcharge_applicable: Option, - pub authorization_count: Option, - pub session_expiry: Option, - pub request_external_three_ds_authentication: Option, - pub frm_metadata: Option, - pub customer_details: Option>>, - pub billing_address: Option>>, - pub shipping_address: Option>>, - pub merchant_order_reference_id: Option, - pub is_payment_processor_token_flow: Option, + /// UpdateIntent + UpdateIntent(Box), } #[cfg(feature = "v1")] #[derive(Clone, Debug, Default)] pub struct PaymentIntentUpdateInternal { pub amount: Option, - pub currency: Option, - pub status: Option, + pub currency: Option, + pub status: Option, pub amount_captured: Option, pub customer_id: Option, pub return_url: Option, - pub setup_future_usage: Option, + pub setup_future_usage: Option, pub off_session: Option, pub metadata: Option, pub billing_address_id: Option, pub shipping_address_id: Option, pub modified_at: Option, pub active_attempt_id: Option, - pub business_country: Option, + pub business_country: Option, pub business_label: Option, pub description: Option, pub statement_descriptor_name: Option, @@ -348,7 +329,7 @@ pub struct PaymentIntentUpdateInternal { // Denotes the action(approve or reject) taken by merchant in case of manual review. // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment pub merchant_decision: Option, - pub payment_confirm_source: Option, + pub payment_confirm_source: Option, pub updated_by: String, pub surcharge_applicable: Option, @@ -379,49 +360,185 @@ impl From for diesel_models::PaymentIntentUpdateInternal { status: Some(status), active_attempt_id: Some(active_attempt_id), modified_at: common_utils::date_time::now(), + 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::ConfirmIntentPostUpdate { status, updated_by } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), + 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, updated_by } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), + 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::UpdateIntent(boxed_intent) => { + let PaymentIntentUpdateFields { + amount, + currency, + shipping_cost, + tax_details, + skip_external_tax_calculation, + skip_surcharge_calculation, + surcharge_amount, + tax_on_surcharge, + routing_algorithm_id, + capture_method, + authentication_type, + billing_address, + shipping_address, + customer_present, + description, + return_url, + setup_future_usage, + apply_mit_exemption, + statement_descriptor, + order_details, + allowed_payment_method_types, + metadata, + connector_metadata, + feature_metadata, + payment_link_config, + request_incremental_authorization, + session_expiry, + frm_metadata, + request_external_three_ds_authentication, + updated_by, + } = *boxed_intent; + Self { + status: None, + active_attempt_id: None, + modified_at: common_utils::date_time::now(), -// This conversion is required for the `apply_changeset` function used for mockdb -#[cfg(feature = "v2")] -impl From for PaymentIntentUpdateInternal { - fn from(payment_intent_update: PaymentIntentUpdate) -> Self { - match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { - status, - active_attempt_id, - updated_by, - } => Self { - status: Some(status), - active_attempt_id: Some(active_attempt_id), - updated_by, - ..Default::default() - }, - PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { - status: Some(status), - updated_by, - ..Default::default() - }, - PaymentIntentUpdate::SyncUpdate { status, updated_by } => Self { - status: Some(status), - updated_by, - ..Default::default() - }, + amount, + currency, + shipping_cost, + tax_details, + skip_external_tax_calculation: skip_external_tax_calculation + .map(|val| val.as_bool()), + surcharge_applicable: skip_surcharge_calculation.map(|val| val.as_bool()), + surcharge_amount, + tax_on_surcharge, + routing_algorithm_id, + capture_method, + authentication_type, + billing_address: billing_address.map(Encryption::from), + shipping_address: shipping_address.map(Encryption::from), + customer_present: customer_present.map(|val| val.as_bool()), + description, + return_url, + setup_future_usage, + apply_mit_exemption: apply_mit_exemption.map(|val| val.as_bool()), + statement_descriptor, + order_details, + allowed_payment_method_types: allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types.encode_to_value() + }) + .and_then(|r| r.ok().map(Secret::new)), + metadata, + connector_metadata, + feature_metadata, + payment_link_config, + request_incremental_authorization, + session_expiry, + frm_metadata, + request_external_three_ds_authentication: + request_external_three_ds_authentication.map(|val| val.as_bool()), + + updated_by, + } + } } } } @@ -623,6 +740,7 @@ impl From for PaymentIntentUpdateInternal { } } +#[cfg(feature = "v1")] use diesel_models::{ PaymentIntentUpdate as DieselPaymentIntentUpdate, PaymentIntentUpdateFields as DieselPaymentIntentUpdateFields, @@ -814,75 +932,6 @@ impl From for DieselPaymentIntentUpdate { } } -// TODO: evaluate if we will be using the same update struct for v2 as well, uncomment this and make necessary changes if necessary -#[cfg(feature = "v2")] -impl From for diesel_models::PaymentIntentUpdateInternal { - fn from(value: PaymentIntentUpdateInternal) -> Self { - todo!() - // let modified_at = common_utils::date_time::now(); - // let PaymentIntentUpdateInternal { - // amount, - // currency, - // status, - // amount_captured, - // customer_id, - // return_url, - // setup_future_usage, - // off_session, - // metadata, - // modified_at: _, - // active_attempt_id, - // description, - // statement_descriptor, - // order_details, - // attempt_count, - // frm_merchant_decision, - // payment_confirm_source, - // updated_by, - // surcharge_applicable, - // authorization_count, - // session_expiry, - // request_external_three_ds_authentication, - // frm_metadata, - // customer_details, - // billing_address, - // merchant_order_reference_id, - // shipping_address, - // is_payment_processor_token_flow, - // } = value; - // Self { - // amount, - // currency, - // status, - // amount_captured, - // customer_id, - // return_url, - // setup_future_usage, - // off_session, - // metadata, - // modified_at, - // active_attempt_id, - // description, - // statement_descriptor, - // order_details, - // attempt_count, - // frm_merchant_decision, - // payment_confirm_source, - // updated_by, - // surcharge_applicable, - // authorization_count, - // session_expiry, - // request_external_three_ds_authentication, - // frm_metadata, - // customer_details: customer_details.map(Encryption::from), - // billing_address: billing_address.map(Encryption::from), - // merchant_order_reference_id, - // shipping_address: shipping_address.map(Encryption::from), - // is_payment_processor_token_flow, - // } - } -} - #[cfg(feature = "v1")] impl From for diesel_models::PaymentIntentUpdateInternal { fn from(value: PaymentIntentUpdateInternal) -> Self { @@ -989,11 +1038,11 @@ pub struct PaymentIntentListParams { pub ending_at: Option, pub amount_filter: Option, pub connector: Option>, - pub currency: Option>, - pub status: Option>, - pub payment_method: Option>, - pub payment_method_type: Option>, - pub authentication_type: Option>, + pub currency: Option>, + pub status: Option>, + pub payment_method: Option>, + pub payment_method_type: Option>, + pub authentication_type: Option>, pub merchant_connector_id: Option>, pub profile_id: Option>, pub customer_id: Option, @@ -1001,7 +1050,7 @@ pub struct PaymentIntentListParams { pub ending_before_id: Option, pub limit: Option, pub order: api_models::payments::Order, - pub card_network: Option>, + pub card_network: Option>, pub merchant_order_reference_id: Option, } @@ -1328,10 +1377,10 @@ impl behaviour::Conversion for PaymentIntent { tax_on_surcharge: storage_model.tax_on_surcharge, shipping_cost: storage_model.shipping_cost, tax_details: storage_model.tax_details, - skip_external_tax_calculation: super::TaxCalculationOverride::from( + skip_external_tax_calculation: common_enums::TaxCalculationOverride::from( storage_model.skip_external_tax_calculation, ), - skip_surcharge_calculation: super::SurchargeCalculationOverride::from( + skip_surcharge_calculation: common_enums::SurchargeCalculationOverride::from( storage_model.surcharge_applicable, ), amount_captured: storage_model.amount_captured, @@ -1396,10 +1445,9 @@ impl behaviour::Conversion for PaymentIntent { .unwrap_or_default(), authorization_count: storage_model.authorization_count, session_expiry: storage_model.session_expiry, - request_external_three_ds_authentication: - common_enums::External3dsAuthenticationRequest::from( - storage_model.request_external_three_ds_authentication, - ), + request_external_three_ds_authentication: storage_model + .request_external_three_ds_authentication + .into(), frm_metadata: storage_model.frm_metadata, customer_details: data.customer_details, billing_address, @@ -1410,15 +1458,9 @@ impl behaviour::Conversion for PaymentIntent { organization_id: storage_model.organization_id, authentication_type: storage_model.authentication_type.unwrap_or_default(), prerouting_algorithm: storage_model.prerouting_algorithm, - enable_payment_link: common_enums::EnablePaymentLinkRequest::from( - storage_model.enable_payment_link, - ), - apply_mit_exemption: common_enums::MitExemptionRequest::from( - storage_model.apply_mit_exemption, - ), - customer_present: common_enums::PresenceOfCustomerDuringPayment::from( - storage_model.customer_present, - ), + enable_payment_link: storage_model.enable_payment_link.into(), + apply_mit_exemption: storage_model.apply_mit_exemption.into(), + customer_present: storage_model.customer_present.into(), payment_link_config: storage_model.payment_link_config, routing_algorithm_id: storage_model.routing_algorithm_id, }) diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs index c43d72ce8def..bbe1a65de860 100644 --- a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs +++ b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs @@ -62,5 +62,8 @@ pub struct PaymentCreateIntent; #[derive(Debug, Clone)] pub struct PaymentGetIntent; +#[derive(Debug, Clone)] +pub struct PaymentUpdateIntent; + #[derive(Debug, Clone)] pub struct PostSessionTokens; diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 2221055be5ab..ba2975349c0f 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -124,6 +124,7 @@ Never share your secret api keys. Keep them guarded and secure. //Routes for payments routes::payments::payments_create_intent, routes::payments::payments_get_intent, + routes::payments::payments_update_intent, routes::payments::payments_confirm_intent, routes::payments::payment_status, @@ -353,9 +354,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentsSessionRequest, api_models::payments::PaymentsSessionResponse, api_models::payments::PaymentsCreateIntentRequest, + api_models::payments::PaymentsUpdateIntentRequest, api_models::payments::PaymentsIntentResponse, api_models::payments::PazeWalletData, api_models::payments::AmountDetails, + api_models::payments::AmountDetailsUpdate, api_models::payments::SessionToken, api_models::payments::ApplePaySessionResponse, api_models::payments::ThirdPartySdkSessionResponse, diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 4c9d069a4586..927f4e69e3a8 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -646,6 +646,43 @@ pub fn payments_create_intent() {} )] #[cfg(feature = "v2")] pub fn payments_get_intent() {} + +/// Payments - Update Intent +/// +/// **Update a payment intent object** +/// +/// You will require the 'API - Key' from the Hyperswitch dashboard to make the call. +#[utoipa::path( + put, + path = "/v2/payments/{id}/update-intent", + params (("id" = String, Path, description = "The unique identifier for the Payment Intent"), + ( + "X-Profile-Id" = String, Header, + description = "Profile ID associated to the payment intent", + example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + ), + ), + request_body( + content = PaymentsUpdateIntentRequest, + examples( + ( + "Update a payment intent with minimal fields" = ( + value = json!({"amount_details": {"order_amount": 6540, "currency": "USD"}}) + ) + ), + ), + ), + responses( + (status = 200, description = "Payment Intent Updated", body = PaymentsIntentResponse), + (status = 404, description = "Payment Intent Not Found") + ), + tag = "Payments", + operation_id = "Update a Payment Intent", + security(("api_key" = [])), +)] +#[cfg(feature = "v2")] +pub fn payments_update_intent() {} + /// Payments - Confirm Intent /// /// **Confirms a payment intent object with the payment method data** diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 99b0a6354006..dea0ca351231 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1001,12 +1001,13 @@ where #[instrument(skip_all, fields(payment_id, merchant_id))] pub async fn payments_intent_operation_core( state: &SessionState, - _req_state: ReqState, + req_state: ReqState, merchant_account: domain::MerchantAccount, profile: domain::Profile, key_store: domain::MerchantKeyStore, operation: Op, req: Req, + payment_id: id_type::GlobalPaymentId, header_payload: HeaderPayload, ) -> RouterResult<(D, Req, Option)> where @@ -1023,8 +1024,6 @@ where .to_validate_request()? .validate_request(&req, &merchant_account)?; - let payment_id = id_type::GlobalPaymentId::generate(&state.conf.cell_information.id.clone()); - tracing::Span::current().record("global_payment_id", payment_id.get_string_repr()); let operations::GetTrackerResponse { mut payment_data } = operation @@ -1051,6 +1050,22 @@ where .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + + let (_operation, payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + req_state, + payment_data, + customer.clone(), + merchant_account.storage_scheme, + None, + &key_store, + None, + header_payload, + ) + .await?; + Ok((payment_data, req, customer)) } @@ -1436,6 +1451,7 @@ pub async fn payments_intent_core( key_store: domain::MerchantKeyStore, operation: Op, req: Req, + payment_id: id_type::GlobalPaymentId, header_payload: HeaderPayload, ) -> RouterResponse where @@ -1453,6 +1469,7 @@ where key_store, operation.clone(), req, + payment_id, header_payload.clone(), ) .await?; diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 5cdfff60b451..f957b939acfe 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -36,6 +36,8 @@ pub mod payment_confirm_intent; pub mod payment_create_intent; #[cfg(feature = "v2")] pub mod payment_get_intent; +#[cfg(feature = "v2")] +pub mod payment_update_intent; #[cfg(feature = "v2")] pub mod payment_get; @@ -52,6 +54,8 @@ pub use self::payment_get::PaymentGet; #[cfg(feature = "v2")] pub use self::payment_get_intent::PaymentGetIntent; pub use self::payment_response::PaymentResponse; +#[cfg(feature = "v2")] +pub use self::payment_update_intent::PaymentUpdateIntent; #[cfg(feature = "v1")] pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs new file mode 100644 index 000000000000..f8ce03d558e9 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -0,0 +1,447 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsUpdateIntentRequest}; +use async_trait::async_trait; +use common_utils::{ + errors::CustomResult, + ext_traits::{Encode, ValueExt}, + types::keymanager::ToEncryptable, +}; +use diesel_models::types::FeatureMetadata; +use error_stack::ResultExt; +use hyperswitch_domain_models::{ + payments::payment_intent::{PaymentIntentUpdate, PaymentIntentUpdateFields}, + ApiModelToDieselModelConvertor, +}; +use masking::PeekInterface; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payments::{ + self, + operations::{self, ValidateStatusForOperation}, + }, + }, + db::errors::StorageErrorExt, + routes::{app::ReqState, SessionState}, + types::{ + api, + domain::{self, types as domain_types}, + storage::{self, enums}, + }, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentUpdateIntent; + +impl ValidateStatusForOperation for PaymentUpdateIntent { + /// 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::RequiresPaymentMethod => 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::RequiresCapture + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: ["requires_payment_method".to_string()].join(", "), + }) + } + } + } +} + +impl Operation for &PaymentUpdateIntent { + type Data = payments::PaymentIntentData; + 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 Operation for PaymentUpdateIntent { + type Data = payments::PaymentIntentData; + 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) + } +} + +type PaymentsUpdateIntentOperation<'b, F> = + BoxedOperation<'b, F, PaymentsUpdateIntentRequest, payments::PaymentIntentData>; + +#[async_trait] +impl GetTracker, PaymentsUpdateIntentRequest> + for PaymentUpdateIntent +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + request: &PaymentsUpdateIntentRequest, + 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 PaymentsUpdateIntentRequest { + amount_details, + routing_algorithm_id, + capture_method, + authentication_type, + billing, + shipping, + customer_present, + description, + return_url, + setup_future_usage, + apply_mit_exemption, + statement_descriptor, + order_details, + allowed_payment_method_types, + metadata, + connector_metadata, + feature_metadata, + payment_link_config, + request_incremental_authorization, + session_expiry, + frm_metadata, + request_external_three_ds_authentication, + } = request.clone(); + + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::PaymentIntent), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::to_encryptable( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent { + shipping_address: shipping.map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode shipping address")?.map(masking::Secret::new), + billing_address: billing.map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode billing address")?.map(masking::Secret::new), + customer_details: None, + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let decrypted_payment_intent = + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + + let order_details = order_details.clone().map(|order_details| { + order_details + .into_iter() + .map(|order_detail| { + masking::Secret::new( + diesel_models::types::OrderDetailsWithAmount::convert_from(order_detail), + ) + }) + .collect() + }); + + let session_expiry = session_expiry.map(|expiry| { + payment_intent + .created_at + .saturating_add(time::Duration::seconds(i64::from(expiry))) + }); + + let updated_amount_details = match amount_details { + Some(details) => payment_intent.amount_details.update_from_request(&details), + None => payment_intent.amount_details, + }; + + let payment_intent = hyperswitch_domain_models::payments::PaymentIntent { + amount_details: updated_amount_details, + description: description.or(payment_intent.description), + return_url: return_url.or(payment_intent.return_url), + metadata: metadata.or(payment_intent.metadata), + statement_descriptor: statement_descriptor.or(payment_intent.statement_descriptor), + modified_at: common_utils::date_time::now(), + order_details, + connector_metadata: connector_metadata.or(payment_intent.connector_metadata), + feature_metadata: (feature_metadata + .map(FeatureMetadata::convert_from) + .or(payment_intent.feature_metadata)), + updated_by: storage_scheme.to_string(), + request_incremental_authorization: request_incremental_authorization + .unwrap_or(payment_intent.request_incremental_authorization), + session_expiry: session_expiry.unwrap_or(payment_intent.session_expiry), + request_external_three_ds_authentication: request_external_three_ds_authentication + .unwrap_or(payment_intent.request_external_three_ds_authentication), + frm_metadata: frm_metadata.or(payment_intent.frm_metadata), + billing_address: decrypted_payment_intent + .billing_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode billing address")?, + shipping_address: decrypted_payment_intent + .shipping_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode shipping address")?, + capture_method: capture_method.unwrap_or(payment_intent.capture_method), + authentication_type: authentication_type.unwrap_or(payment_intent.authentication_type), + payment_link_config: payment_link_config + .map(ApiModelToDieselModelConvertor::convert_from) + .or(payment_intent.payment_link_config), + apply_mit_exemption: apply_mit_exemption.unwrap_or(payment_intent.apply_mit_exemption), + customer_present: customer_present.unwrap_or(payment_intent.customer_present), + routing_algorithm_id: routing_algorithm_id.or(payment_intent.routing_algorithm_id), + allowed_payment_method_types: allowed_payment_method_types + .or(payment_intent.allowed_payment_method_types), + ..payment_intent + }; + + let payment_data = payments::PaymentIntentData { + flow: PhantomData, + payment_intent, + sessions_token: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsUpdateIntentRequest> + for PaymentUpdateIntent +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: payments::PaymentIntentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<( + PaymentsUpdateIntentOperation<'b, F>, + payments::PaymentIntentData, + )> + where + F: 'b + Send, + { + let db = &*state.store; + let key_manager_state = &state.into(); + + let intent = payment_data.payment_intent.clone(); + + let payment_intent_update = + PaymentIntentUpdate::UpdateIntent(Box::new(PaymentIntentUpdateFields { + amount: Some(intent.amount_details.order_amount), + currency: Some(intent.amount_details.currency), + shipping_cost: intent.amount_details.shipping_cost, + skip_external_tax_calculation: Some( + intent.amount_details.skip_external_tax_calculation, + ), + skip_surcharge_calculation: Some(intent.amount_details.skip_surcharge_calculation), + surcharge_amount: intent.amount_details.surcharge_amount, + tax_on_surcharge: intent.amount_details.tax_on_surcharge, + routing_algorithm_id: intent.routing_algorithm_id, + capture_method: Some(intent.capture_method), + authentication_type: Some(intent.authentication_type), + billing_address: intent.billing_address, + shipping_address: intent.shipping_address, + customer_present: Some(intent.customer_present), + description: intent.description, + return_url: intent.return_url, + setup_future_usage: Some(intent.setup_future_usage), + apply_mit_exemption: Some(intent.apply_mit_exemption), + statement_descriptor: intent.statement_descriptor, + order_details: intent.order_details, + allowed_payment_method_types: intent.allowed_payment_method_types, + metadata: intent.metadata, + connector_metadata: intent.connector_metadata, + feature_metadata: intent.feature_metadata, + payment_link_config: intent.payment_link_config, + request_incremental_authorization: Some(intent.request_incremental_authorization), + session_expiry: Some(intent.session_expiry), + frm_metadata: intent.frm_metadata, + request_external_three_ds_authentication: Some( + intent.request_external_three_ds_authentication, + ), + updated_by: intent.updated_by, + tax_details: intent.amount_details.tax_details, + })); + + let new_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("Could not update Intent")?; + + payment_data.payment_intent = new_payment_intent; + + Ok((Box::new(self), payment_data)) + } +} + +impl + ValidateRequest> + for PaymentUpdateIntent +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsUpdateIntentRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + Ok(operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }) + } +} + +#[async_trait] +impl Domain> + for PaymentUpdateIntent +{ + #[instrument(skip_all)] + async fn get_customer_details<'a>( + &'a self, + _state: &SessionState, + _payment_data: &mut payments::PaymentIntentData, + _merchant_key_store: &domain::MerchantKeyStore, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsUpdateIntentRequest, payments::PaymentIntentData>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut payments::PaymentIntentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + PaymentsUpdateIntentOperation<'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, + _payment_data: &mut payments::PaymentIntentData, + _mechant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + Ok(api::ConnectorCallType::Skip) + } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentIntentData, + ) -> CustomResult { + Ok(false) + } +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index fb8962117c10..6e277ed460f0 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -3526,12 +3526,8 @@ impl currency: intent_amount_details.currency, shipping_cost: attempt_amount_details.shipping_cost, order_tax_amount: attempt_amount_details.order_tax_amount, - external_tax_calculation: common_enums::TaxCalculationOverride::foreign_from( - intent_amount_details.skip_external_tax_calculation, - ), - surcharge_calculation: common_enums::SurchargeCalculationOverride::foreign_from( - intent_amount_details.skip_surcharge_calculation, - ), + 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, @@ -3569,12 +3565,8 @@ impl .tax_details .as_ref() .and_then(|tax_details| tax_details.get_default_tax_amount())), - external_tax_calculation: common_enums::TaxCalculationOverride::foreign_from( - intent_amount_details.skip_external_tax_calculation, - ), - surcharge_calculation: common_enums::SurchargeCalculationOverride::foreign_from( - intent_amount_details.skip_surcharge_calculation, - ), + 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) .or(intent_amount_details.surcharge_amount), @@ -3629,76 +3621,14 @@ impl ForeignFrom order_tax_amount: amount_details.tax_details.and_then(|tax_details| { tax_details.default.map(|default| default.order_tax_amount) }), - external_tax_calculation: common_enums::TaxCalculationOverride::foreign_from( - amount_details.skip_external_tax_calculation, - ), - surcharge_calculation: common_enums::SurchargeCalculationOverride::foreign_from( - amount_details.skip_surcharge_calculation, - ), + external_tax_calculation: amount_details.skip_external_tax_calculation, + surcharge_calculation: amount_details.skip_surcharge_calculation, surcharge_amount: amount_details.surcharge_amount, tax_on_surcharge: amount_details.tax_on_surcharge, } } } -#[cfg(feature = "v2")] -impl ForeignFrom - for hyperswitch_domain_models::payments::TaxCalculationOverride -{ - fn foreign_from(tax_calculation_override: common_enums::TaxCalculationOverride) -> Self { - match tax_calculation_override { - common_enums::TaxCalculationOverride::Calculate => Self::Calculate, - common_enums::TaxCalculationOverride::Skip => Self::Skip, - } - } -} - -#[cfg(feature = "v2")] -impl ForeignFrom - for common_enums::TaxCalculationOverride -{ - fn foreign_from( - tax_calculation_override: hyperswitch_domain_models::payments::TaxCalculationOverride, - ) -> Self { - match tax_calculation_override { - hyperswitch_domain_models::payments::TaxCalculationOverride::Calculate => { - Self::Calculate - } - hyperswitch_domain_models::payments::TaxCalculationOverride::Skip => Self::Skip, - } - } -} - -#[cfg(feature = "v2")] -impl ForeignFrom - for hyperswitch_domain_models::payments::SurchargeCalculationOverride -{ - fn foreign_from( - surcharge_calculation_override: common_enums::SurchargeCalculationOverride, - ) -> Self { - match surcharge_calculation_override { - common_enums::SurchargeCalculationOverride::Calculate => Self::Calculate, - common_enums::SurchargeCalculationOverride::Skip => Self::Skip, - } - } -} - -#[cfg(feature = "v2")] -impl ForeignFrom - for common_enums::SurchargeCalculationOverride -{ - fn foreign_from( - surcharge_calculation_override: hyperswitch_domain_models::payments::SurchargeCalculationOverride, - ) -> Self { - match surcharge_calculation_override { - hyperswitch_domain_models::payments::SurchargeCalculationOverride::Calculate => { - Self::Calculate - } - hyperswitch_domain_models::payments::SurchargeCalculationOverride::Skip => Self::Skip, - } - } -} - #[cfg(feature = "v2")] impl ForeignFrom for diesel_models::PaymentLinkConfigRequestForPayments diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index be24084bcc05..75b06e7f813b 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -555,6 +555,10 @@ impl Payments { web::resource("/get-intent") .route(web::get().to(payments::payments_get_intent)), ) + .service( + web::resource("/update-intent") + .route(web::put().to(payments::payments_update_intent)), + ) .service( web::resource("/create-external-sdk-tokens") .route(web::post().to(payments::payments_connector_session)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 762a52272079..614676e203f8 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -142,6 +142,7 @@ impl From for ApiIdentifier { | Flow::PaymentsCreateIntent | Flow::PaymentsGetIntent | Flow::PaymentsPostSessionTokens + | Flow::PaymentsUpdateIntent | Flow::PaymentStartRedirection => Self::Payments, Flow::PayoutsCreate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 27a5462c05ea..7a8e65e0604f 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -118,6 +118,9 @@ pub async fn payments_create_intent( return api::log_and_return_error_response(err); } }; + let global_payment_id = + common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id.clone()); + Box::pin(api::server_wrap( flow, state, @@ -138,6 +141,7 @@ pub async fn payments_create_intent( auth.key_store, payments::operations::PaymentIntentCreate, req, + global_payment_id.clone(), header_payload.clone(), ) }, @@ -178,6 +182,8 @@ pub async fn payments_get_intent( id: path.into_inner(), }; + let global_payment_id = payload.id.clone(); + Box::pin(api::server_wrap( flow, state, @@ -198,6 +204,62 @@ pub async fn payments_get_intent( auth.key_store, payments::operations::PaymentGetIntent, req, + global_payment_id.clone(), + header_payload.clone(), + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdateIntent, payment_id))] +pub async fn payments_update_intent( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + use hyperswitch_domain_models::payments::PaymentIntentData; + + let flow = Flow::PaymentsUpdateIntent; + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id: path.into_inner(), + payload: json_payload.into_inner(), + }; + + let global_payment_id = internal_payload.global_payment_id.clone(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| { + payments::payments_intent_core::< + api_types::PaymentUpdateIntent, + payment_types::PaymentsIntentResponse, + _, + _, + PaymentIntentData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + payments::operations::PaymentUpdateIntent, + req.payload, + global_payment_id.clone(), header_payload.clone(), ) }, diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 524712f1ca0a..8be423c037be 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -19,13 +19,15 @@ pub use api_models::payments::{ VerifyResponse, WalletData, }; #[cfg(feature = "v2")] -pub use api_models::payments::{PaymentsCreateIntentRequest, PaymentsIntentResponse}; +pub use api_models::payments::{ + PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest, +}; use error_stack::ResultExt; pub use hyperswitch_domain_models::router_flow_types::payments::{ Approve, Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, PaymentCreateIntent, - PaymentGetIntent, PaymentMethodToken, PostProcessing, PostSessionTokens, PreProcessing, Reject, - SdkSessionUpdate, Session, SetupMandate, Void, + PaymentGetIntent, PaymentMethodToken, PaymentUpdateIntent, PostProcessing, PostSessionTokens, + PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, }; pub use hyperswitch_interfaces::api::payments::{ ConnectorCustomer, MandateSetup, Payment, PaymentApprove, PaymentAuthorize, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 27ddc10766d2..3ae2de9ffdf2 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -168,6 +168,8 @@ pub enum Flow { PaymentsCreateIntent, /// Payments Get Intent flow PaymentsGetIntent, + /// Payments Update Intent flow + PaymentsUpdateIntent, #[cfg(feature = "payouts")] /// Payouts create flow PayoutsCreate, diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index ebf75a58b2c6..fe64136b7431 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -10,6 +10,8 @@ use common_utils::{ }; #[cfg(feature = "olap")] use diesel::{associations::HasTable, ExpressionMethods, JoinOnDsl, QueryDsl}; +#[cfg(feature = "v1")] +use diesel_models::payment_intent::PaymentIntentUpdate as DieselPaymentIntentUpdate; #[cfg(feature = "olap")] use diesel_models::query::generics::db_metrics; #[cfg(all(feature = "v1", feature = "olap"))] @@ -23,11 +25,7 @@ use diesel_models::schema_v2::{ payment_intent::dsl as pi_dsl, }; use diesel_models::{ - enums::MerchantStorageScheme, - kv, - payment_intent::{ - PaymentIntent as DieselPaymentIntent, PaymentIntentUpdate as DieselPaymentIntentUpdate, - }, + enums::MerchantStorageScheme, kv, payment_intent::PaymentIntent as DieselPaymentIntent, }; use error_stack::ResultExt; #[cfg(feature = "olap")]