From fdebe64255843164cb299767b23688440023980e Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Wed, 6 Nov 2024 14:52:26 +0530 Subject: [PATCH] feat(core): Add payments update-intent API for v2 --- .../payments/payments--update-intent.mdx | 3 + api-reference-v2/mint.json | 1 + api-reference-v2/openapi_spec.json | 233 ++++++++++++ crates/api_models/src/payments.rs | 116 ++++++ crates/diesel_models/src/kv.rs | 21 +- crates/diesel_models/src/payment_intent.rs | 133 +------ .../hyperswitch_domain_models/src/payments.rs | 12 + .../src/payments/payment_intent.rs | 253 +++++++------ .../src/router_flow_types/payments.rs | 3 + crates/openapi/src/openapi_v2.rs | 2 + crates/openapi/src/routes/payments.rs | 31 ++ crates/router/src/core/payments.rs | 243 ++++++++++++- crates/router/src/core/payments/operations.rs | 4 + .../operations/payment_update_intent.rs | 341 ++++++++++++++++++ crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/payments.rs | 59 +++ crates/router/src/types/api/payments.rs | 8 +- crates/router_env/src/logger/types.rs | 2 + .../src/payments/payment_intent.rs | 8 +- 20 files changed, 1216 insertions(+), 262 deletions(-) create mode 100644 api-reference-v2/api-reference/payments/payments--update-intent.mdx create mode 100644 crates/router/src/core/payments/operations/payment_update_intent.rs 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..4c295b3f8347 --- /dev/null +++ b/api-reference-v2/api-reference/payments/payments--update-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /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 17dd6dfb7ffa..94f615d3331d 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -39,6 +39,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" ] diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index aed80400f6d6..d15989607bca 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1900,6 +1900,67 @@ ] } }, + "/v2/payments/{id}/update-intent": { + "post": { + "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" + } + } + ], + "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": [ @@ -15081,6 +15142,178 @@ } } }, + "PaymentsUpdateIntentRequest": { + "type": "object", + "required": [ + "capture_method", + "authentication_type", + "customer_present", + "setup_future_usage", + "apply_mit_exemption", + "payment_link_enabled", + "request_incremental_authorization", + "request_external_three_ds_authentication" + ], + "properties": { + "amount_details": { + "allOf": [ + { + "$ref": "#/components/schemas/AmountDetails" + } + ], + "nullable": true + }, + "merchant_reference_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "routing_algorithm_id": { + "type": "string", + "description": "The routing algorithm id to be used for the payment", + "nullable": true + }, + "capture_method": { + "$ref": "#/components/schemas/CaptureMethod" + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "no_three_ds" + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "shipping": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 64, + "minLength": 1 + }, + "customer_present": { + "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" + }, + "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": { + "$ref": "#/components/schemas/FutureUsage" + }, + "apply_mit_exemption": { + "$ref": "#/components/schemas/MitExemptionRequest" + }, + "payment_method_token": { + "type": "string", + "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.", + "nullable": true + }, + "connector_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorMetadata" + } + ], + "nullable": true + }, + "feature_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/FeatureMetadata" + } + ], + "nullable": true + }, + "payment_link_enabled": { + "$ref": "#/components/schemas/EnablePaymentLinkRequest" + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + } + ], + "nullable": true + }, + "request_incremental_authorization": { + "$ref": "#/components/schemas/RequestIncrementalAuthorization" + }, + "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": { + "$ref": "#/components/schemas/External3dsAuthenticationRequest" + } + }, + "additionalProperties": false + }, "PayoutActionRequest": { "type": "object" }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 36c0036cdade..9c45cf559321 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -293,6 +293,122 @@ 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, + + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + value_type = Option, + min_length = 30, + max_length = 30, + example = "pay_mbabizu24mvu3mela5njyhpit4" + )] + pub merchant_reference_id: Option, + + /// The routing algorithm id to be used for the payment + #[schema(value_type = Option)] + pub routing_algorithm_id: Option, + + #[schema(value_type = CaptureMethod, example = "automatic")] + pub capture_method: Option, + + #[schema(value_type = AuthenticationType, 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. + #[schema(value_type = Option
)] + pub billing: Option
, + + /// The shipping address for the payment + #[schema(value_type = Option
)] + pub shipping: Option
, + + /// The identifier for the customer + #[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)] + 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 = FutureUsage, example = "off_session")] + pub setup_future_usage: Option, + + /// Apply MIT exemption for a payment + #[schema(value_type = MitExemptionRequest)] + pub apply_mit_exemption: Option, + + // TODO: Verify this is required + pub payment_method_token: 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. + #[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, + + /// Whether to generate the payment link for this payment or not (if applicable) + #[schema(value_type = EnablePaymentLinkRequest)] + pub payment_link_enabled: 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 = RequestIncrementalAuthorization)] + 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(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 = External3dsAuthenticationRequest)] + pub request_external_three_ds_authentication: + Option, +} + #[derive(Debug, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] #[cfg(feature = "v2")] 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 eaf4497959e4..5063882dc356 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -350,22 +350,6 @@ pub struct PaymentIntentNew { pub skip_external_tax_calculation: 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 { @@ -507,33 +491,34 @@ 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 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 customer_id: Option, + pub return_url: Option, // TODO: Check this + 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 description: Option, + pub statement_descriptor: Option, + #[diesel(deserialize_as = super::OptionalDieselArray)] + pub order_details: Option>, // pub attempt_count: 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 session_expiry: Option, + // Setting it to common_enums::External3dsAuthenticationRequest gives error on `AsChangeset` derivation + pub request_external_three_ds_authentication: Option, + pub frm_metadata: Option, // pub customer_details: Option, - // pub billing_address: Option, - // pub shipping_address: Option, + pub billing_address: Option, + pub shipping_address: Option, // pub frm_merchant_decision: Option, } @@ -580,66 +565,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 { @@ -730,30 +655,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/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index e0f0aa334ac4..3ec0c8c06d8e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -20,6 +20,7 @@ use masking::Secret; use router_derive::ToEncryption; use rustc_hash::FxHashMap; use serde_json::Value; +use payment_intent::PaymentIntentUpdate; use time::PrimitiveDateTime; pub mod payment_attempt; @@ -540,3 +541,14 @@ where /// This will depend on the payment status and the force sync flag in the request pub should_sync_with_connector: bool, } + +#[cfg(feature = "v2")] +#[derive(Clone)] +pub struct PaymentUpdateData +where + F: Clone, +{ + pub flow: PhantomData, + pub payment_intent: PaymentIntent, + pub payment_intent_update: PaymentIntentUpdate, +} diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 71dc481d9651..4ec2adb8254f 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1,4 +1,5 @@ -use common_enums as storage_enums; +use api_models::payments::{Address, OrderDetailsWithAmount}; +use common_enums::{self as storage_enums, External3dsAuthenticationRequest}; #[cfg(feature = "v2")] use common_utils::ext_traits::{Encode, ValueExt}; use common_utils::{ @@ -11,7 +12,7 @@ use common_utils::{ type_name, types::{ keymanager::{self, KeyManagerState, ToEncryptable}, - MinorUnit, + MinorUnit, StatementDescriptor, }, }; use diesel_models::{ @@ -284,39 +285,40 @@ pub enum PaymentIntentUpdate { status: storage_enums::IntentStatus, updated_by: String, }, -} + UpdateIntent { + // TODO: Add more amount_details fields + amount: Option, + currency: Option, -#[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, + // // merchant_reference_id: Option, + // // routing_algorithm_id: Option, + // // capture_method: Option + // // authentication_type: Option + billing_address: Option>>, + shipping_address: Option>>, + customer_id: Option, + // // customer_present: Option, + description: Option, + return_url: Option, + setup_future_usage: Option, + // // apply_mit_exemption: Option, + // // payment_method_token: Option, + statement_descriptor: Option, + order_details: Option>>, + // // allowed_payment_method_types: Option, + metadata: Option, + // // connector_metadata: Option, + // // feature_metadata: Option, + // // payment_link_enabled: Option, + // // payment_link_config: Option, + // // request_incremental_authorization: Option, + session_expiry: Option, + frm_metadata: Option, + request_external_three_ds_authentication: Option, + + // updated_by is set internally, field not present in request + updated_by: String, + }, } #[cfg(feature = "v1")] @@ -377,47 +379,110 @@ impl From for diesel_models::PaymentIntentUpdateInternal { active_attempt_id: Some(active_attempt_id), modified_at: common_utils::date_time::now(), updated_by, + amount: None, + currency: None, + customer_id: None, + return_url: None, + setup_future_usage: None, + metadata: None, + description: None, + statement_descriptor: None, + order_details: None, + session_expiry: None, + request_external_three_ds_authentication: None, + frm_metadata: None, + billing_address: None, + shipping_address: None, }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), updated_by, + amount: None, + currency: None, + customer_id: None, + return_url: None, + setup_future_usage: None, + metadata: None, + description: None, + statement_descriptor: None, + order_details: None, + session_expiry: None, + request_external_three_ds_authentication: None, + frm_metadata: None, + billing_address: None, + shipping_address: None, }, PaymentIntentUpdate::SyncUpdate { status, updated_by } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), updated_by, + amount: None, + currency: None, + customer_id: None, + return_url: None, + setup_future_usage: None, + metadata: None, + description: None, + statement_descriptor: None, + order_details: None, + session_expiry: None, + request_external_three_ds_authentication: None, + frm_metadata: None, + billing_address: None, + shipping_address: None, }, - } - } -} - -// 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, + PaymentIntentUpdate::UpdateIntent { + amount, + currency, + customer_id, + return_url, + setup_future_usage, + metadata, + description, + statement_descriptor, + order_details, updated_by, + session_expiry, + request_external_three_ds_authentication, + frm_metadata, + billing_address, + shipping_address, } => 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), + amount, + currency, + status: None, + customer_id, + return_url, + setup_future_usage, + metadata, + modified_at: common_utils::date_time::now(), + active_attempt_id: None, + description, + statement_descriptor, + order_details: order_details + .map(|order_details| { + order_details + .into_iter() + .map(|order_detail| order_detail.encode_to_value().map(Secret::new)) + .collect::, _>>() + }) + .and_then(|r| r.ok()), updated_by, - ..Default::default() + session_expiry, + request_external_three_ds_authentication: + match request_external_three_ds_authentication { + Some(internal) => match internal { + External3dsAuthenticationRequest::Enable => Some(true), + External3dsAuthenticationRequest::Skip => Some(false), + }, + None => None, + }, + frm_metadata, + billing_address: billing_address.map(Encryption::from), + shipping_address: shipping_address.map(Encryption::from), }, } } @@ -620,6 +685,7 @@ impl From for PaymentIntentUpdateInternal { } } +#[cfg(feature = "v1")] use diesel_models::{ PaymentIntentUpdate as DieselPaymentIntentUpdate, PaymentIntentUpdateFields as DieselPaymentIntentUpdateFields, @@ -811,75 +877,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 { 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 40f694b80a23..82330472851c 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 for refunds @@ -326,6 +327,7 @@ 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, diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index e245c5af68a4..2147cec4c917 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -649,6 +649,37 @@ 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( + post, + path = "/v2/payments/{id}/update-intent", + params (("id" = String, Path, description = "The unique identifier for the Payment Intent")), + 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 19455abeac8b..b828b6561d6c 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -37,7 +37,7 @@ use futures::future::join_all; use helpers::{decrypt_paze_token, ApplePayData}; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{ - PaymentConfirmData, PaymentIntentData, PaymentStatusData, + PaymentConfirmData, PaymentIntentData, PaymentStatusData, PaymentUpdateData, }; pub use hyperswitch_domain_models::{ mandates::{CustomerAcceptance, MandateData}, @@ -1007,12 +1007,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: Option, header_payload: HeaderPayload, ) -> RouterResult<(D, Req, Option)> where @@ -1029,7 +1030,11 @@ where .to_validate_request()? .validate_request(&req, &merchant_account)?; - let payment_id = id_type::GlobalPaymentId::generate(state.conf.cell_information.id.clone()); + // If payment_id is not present in request, generate new payment_id + let payment_id = match payment_id { + Some(id) => id, + None => id_type::GlobalPaymentId::generate(state.conf.cell_information.id.clone()), + }; tracing::Span::current().record("global_payment_id", payment_id.get_string_repr()); @@ -1060,6 +1065,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, //TODO: fix this + &key_store, + None, + header_payload, + ) + .await?; + Ok((payment_data, req, customer)) } @@ -1445,6 +1466,7 @@ pub async fn payments_intent_core( key_store: domain::MerchantKeyStore, operation: Op, req: Req, + payment_id: Option, header_payload: HeaderPayload, ) -> RouterResponse where @@ -1462,6 +1484,7 @@ where key_store, operation.clone(), req, + payment_id, header_payload.clone(), ) .await?; @@ -7182,3 +7205,217 @@ impl OperationSessionSetters for PaymentStatusData { todo!() } } + +#[cfg(feature = "v2")] +impl OperationSessionGetters for PaymentUpdateData { + fn get_payment_attempt(&self) -> &storage::PaymentAttempt { + todo!() + } + + 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> { + todo!() + } + + 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> { + todo!(); + } +} + +#[cfg(feature = "v2")] +impl OperationSessionSetters for PaymentUpdateData { + // Setters Implementation + 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) { + todo!() + } + + 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!() + } + + fn push_sessions_token(&mut self, _token: api::SessionToken) { + todo!() + } + + fn set_surcharge_details(&mut self, _surcharge_details: Option) { + todo!() + } + + 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/operations.rs b/crates/router/src/core/payments/operations.rs index edf0485a77df..97b8f31b492d 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -34,6 +34,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; @@ -54,6 +56,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..1b9074dd9b62 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -0,0 +1,341 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsUpdateIntentRequest}; +use async_trait::async_trait; +use common_utils::{ + errors::CustomResult, + ext_traits::{AsyncExt, ValueExt}, + types::MinorUnit, +}; +use error_stack::ResultExt; +use hyperswitch_domain_models::payments::{ + payment_intent::PaymentIntentUpdate, AmountDetails, PaymentIntent, +}; +use masking::Secret; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payment_methods::cards::create_encrypted_data, + payments::{self, helpers, operations}, + }, + db::errors::StorageErrorExt, + routes::{app::ReqState, SessionState}, + types::{ + api, domain, + storage::{self, enums}, + }, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentUpdateIntent; + +impl Operation for &PaymentUpdateIntent { + type Data = payments::PaymentUpdateData; + 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::PaymentUpdateData; + 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::PaymentUpdateData>; + +#[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< + operations::GetTrackerResponse< + 'a, + F, + PaymentsUpdateIntentRequest, + payments::PaymentUpdateData, + >, + > { + 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)?; + + // TODO: Use Batch Encryption + let billing_address = request + .billing + .clone() + .async_map(|billing_details| { + create_encrypted_data(key_manager_state, key_store, billing_details) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt billing details")? + .map(|encrypted_value| { + encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to deserialize decrypted value to Address")?; + + let shipping_address = request + .shipping + .clone() + .async_map(|shipping_details| { + create_encrypted_data(key_manager_state, key_store, shipping_details) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt shipping details")? + .map(|encrypted_value| { + encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to deserialize decrypted value to Address")?; + + let order_details = request + .order_details + .clone() + .map(|order_details| order_details.into_iter().map(Secret::new).collect()); + + // TODO: This should most likely be created_time + session_expiry rather than now + session_expiry + let session_expiry = request.session_expiry.map(|expiry| { + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(i64::from(expiry))) + }); + + let payment_intent_update = PaymentIntentUpdate::UpdateIntent { + amount: request + .amount_details + .as_ref() + .map(|details| MinorUnit::from(details.order_amount())), + currency: request + .amount_details + .as_ref() + .map(|details| details.currency()), + customer_id: request.customer_id.clone(), + return_url: request.return_url.clone(), + setup_future_usage: request.setup_future_usage.clone(), + metadata: request.metadata.clone(), + description: request.description.clone(), + statement_descriptor: request.statement_descriptor.clone(), + order_details, + updated_by: storage_scheme.to_string(), + session_expiry: session_expiry, + request_external_three_ds_authentication: request + .request_external_three_ds_authentication + .clone(), + // TODO: Does frm_metadata need more processing? + frm_metadata: request.frm_metadata.clone(), + billing_address, + shipping_address, + }; + + let payment_data = payments::PaymentUpdateData { + flow: PhantomData, + payment_intent, + payment_intent_update, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + 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, + payment_data: payments::PaymentUpdateData, + _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::PaymentUpdateData, + )> + where + F: 'b + Send, + { + let db = &*state.store; + let key_manager_state = &state.into(); + + let new_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_data.payment_intent_update.clone(), + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not update Intent")?; + + let payment_data = payments::PaymentUpdateData { + flow: PhantomData, + payment_intent: new_payment_intent, + payment_intent_update: payment_data.payment_intent_update, + }; + + 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<( + PaymentsUpdateIntentOperation<'b, F>, + operations::ValidateResult, + )> { + Ok(( + Box::new(self), + 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::PaymentUpdateData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsUpdateIntentRequest, payments::PaymentUpdateData>, + 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::PaymentUpdateData, + _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, + // TODO: do not take the whole payment data here + payment_data: &mut payments::PaymentUpdateData, + 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::PaymentUpdateData, + ) -> CustomResult { + Ok(false) + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 97b9ddcb43a1..29394ead5775 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -531,6 +531,10 @@ impl Payments { web::resource("/get-intent") .route(web::get().to(payments::payments_get_intent)), ) + .service( + web::resource("/update-intent") + .route(web::post().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 43ddc35b8ae6..3acd1976d076 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -140,6 +140,7 @@ impl From for ApiIdentifier { | Flow::PaymentsConfirmIntent | Flow::PaymentsCreateIntent | Flow::PaymentsGetIntent + | Flow::PaymentsUpdateIntent | Flow::PaymentsPostSessionTokens => Self::Payments, Flow::PayoutsCreate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index e05fd46f4eae..bf0e9338e400 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -138,6 +138,7 @@ pub async fn payments_create_intent( auth.key_store, payments::operations::PaymentIntentCreate, req, + None, header_payload.clone(), ) }, @@ -178,6 +179,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 +201,62 @@ pub async fn payments_get_intent( auth.key_store, payments::operations::PaymentGetIntent, req, + Some(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::PaymentUpdateData; + + 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::AuthenticationDataV2, req, req_state| { + payments::payments_intent_core::< + api_types::PaymentUpdateIntent, + payment_types::PaymentsIntentResponse, + _, + _, + PaymentUpdateData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + payments::operations::PaymentUpdateIntent, + req.payload, + Some(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 57ef1d3336f5..f8b61ea05f64 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 2410be9750cb..e69d22436b9b 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -173,6 +173,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 f26cf876ade8..305c63772acc 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")]