From 2c35639763982bfa2291cda0c2b6b0fd91f43d27 Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Fri, 6 Jun 2025 20:54:31 +0530 Subject: [PATCH] feat(router): Save payment method on payments confirm (V2) (#8090) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 179 +++++++++++++---- crates/api_models/src/events/payment.rs | 33 ++-- crates/api_models/src/payment_methods.rs | 71 ++++++- crates/api_models/src/payments.rs | 9 +- crates/diesel_models/src/payment_attempt.rs | 2 +- .../src/payment_method_data.rs | 50 +++++ .../hyperswitch_domain_models/src/payments.rs | 19 +- .../src/payments/payment_attempt.rs | 62 +++++- crates/openapi/src/openapi_v2.rs | 7 +- crates/openapi/src/routes/payment_method.rs | 2 +- crates/router/src/core/payment_methods.rs | 187 +++++++++++++++--- .../router/src/core/payment_methods/cards.rs | 2 +- .../src/core/payment_methods/transformers.rs | 70 ++++++- .../router/src/core/payment_methods/utils.rs | 72 +++++++ .../router/src/core/payment_methods/vault.rs | 89 ++++++++- crates/router/src/core/payments.rs | 23 +++ crates/router/src/core/payments/helpers.rs | 4 + crates/router/src/core/payments/operations.rs | 11 ++ .../operations/payment_confirm_intent.rs | 168 +++++++++++++++- .../payments/operations/payment_response.rs | 46 +++++ .../operations/proxy_payments_intent.rs | 1 + .../src/core/payments/payment_methods.rs | 25 ++- crates/router/src/routes/payment_methods.rs | 11 ++ .../router/src/types/api/payment_methods.rs | 25 ++- .../src/types/storage/payment_method.rs | 22 ++- .../router/src/workflows/revenue_recovery.rs | 4 +- 26 files changed, 1069 insertions(+), 125 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 26d482bba9..ab487354ac 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3043,7 +3043,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodListResponse" + "$ref": "#/components/schemas/PaymentMethodListResponseForSession" } } } @@ -9473,18 +9473,19 @@ } } }, - "CustomerPaymentMethod": { + "CustomerPaymentMethodResponseItem": { "type": "object", "required": [ "id", + "payment_token", "customer_id", "payment_method_type", "payment_method_subtype", "recurring_enabled", "created", "requires_cvv", - "is_default", - "psp_tokenization_enabled" + "last_used_at", + "is_default" ], "properties": { "id": { @@ -9492,6 +9493,11 @@ "description": "The unique identifier of the payment method.", "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8" }, + "payment_token": { + "type": "string", + "description": "Temporary Token for payment method in vault which gets refreshed for every payment", + "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" + }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -9555,18 +9561,6 @@ } ], "nullable": true - }, - "network_tokenization": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTokenResponse" - } - ], - "nullable": true - }, - "psp_tokenization_enabled": { - "type": "boolean", - "description": "Whether psp_tokenization is enabled for the payment_method, this will be true when at least\none multi-use token with status `Active` is available for the payment method" } } }, @@ -9579,7 +9573,7 @@ "customer_payment_methods": { "type": "array", "items": { - "$ref": "#/components/schemas/CustomerPaymentMethod" + "$ref": "#/components/schemas/PaymentMethodResponseItem" }, "description": "List of payment methods for customer" } @@ -16788,29 +16782,6 @@ }, "additionalProperties": false }, - "PaymentMethodListResponse": { - "type": "object", - "required": [ - "payment_methods_enabled", - "customer_payment_methods" - ], - "properties": { - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResponsePaymentMethodTypes" - }, - "description": "The list of payment methods that are enabled for the business profile" - }, - "customer_payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerPaymentMethod" - }, - "description": "The list of saved payment methods of the customer" - } - } - }, "PaymentMethodListResponseForPayments": { "type": "object", "required": [ @@ -16827,13 +16798,36 @@ "customer_payment_methods": { "type": "array", "items": { - "$ref": "#/components/schemas/CustomerPaymentMethod" + "$ref": "#/components/schemas/CustomerPaymentMethodResponseItem" }, "description": "The list of payment methods that are saved by the given customer\nThis field is only returned if the customer_id is provided in the request", "nullable": true } } }, + "PaymentMethodListResponseForSession": { + "type": "object", + "required": [ + "payment_methods_enabled", + "customer_payment_methods" + ], + "properties": { + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponsePaymentMethodTypes" + }, + "description": "The list of payment methods that are enabled for the business profile" + }, + "customer_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerPaymentMethodResponseItem" + }, + "description": "The list of saved payment methods of the customer" + } + } + }, "PaymentMethodResponse": { "type": "object", "required": [ @@ -16932,6 +16926,103 @@ } ] }, + "PaymentMethodResponseItem": { + "type": "object", + "required": [ + "id", + "customer_id", + "payment_method_type", + "payment_method_subtype", + "recurring_enabled", + "created", + "requires_cvv", + "is_default", + "psp_tokenization_enabled" + ], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the payment method.", + "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8" + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true + }, + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodListData" + } + ], + "nullable": true + }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", + "example": "2023-01-18T11:04:09.922Z" + }, + "requires_cvv": { + "type": "boolean", + "description": "Whether this payment method requires CVV to be collected", + "example": true + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was last used", + "example": "2024-02-24T11:04:09.922Z" + }, + "is_default": { + "type": "boolean", + "description": "Indicates if the payment method has been set to default or not", + "example": true + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "network_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTokenResponse" + } + ], + "nullable": true + }, + "psp_tokenization_enabled": { + "type": "boolean", + "description": "Whether psp_tokenization is enabled for the payment_method, this will be true when at least\none multi-use token with status `Active` is available for the payment method" + } + } + }, "PaymentMethodSessionConfirmRequest": { "type": "object", "required": [ @@ -17654,6 +17745,12 @@ "type": "string", "description": "The payment_method_id to be associated with the payment", "nullable": true + }, + "payment_token": { + "type": "string", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "nullable": true } }, "additionalProperties": false diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 76d1201c2e..851e2500f7 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -5,23 +5,29 @@ use super::{ PaymentStartRedirectionRequest, PaymentsCreateIntentRequest, PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, }; +#[cfg(feature = "v2")] +use crate::payment_methods::PaymentMethodListResponseForSession; #[cfg(feature = "v1")] -use crate::payments::{ - ExtendedCardInfoResponse, PaymentIdType, PaymentListFilterConstraints, PaymentListResponseV2, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, - PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, - PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, - PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, PaymentsPostSessionTokensRequest, - PaymentsPostSessionTokensResponse, PaymentsRejectRequest, PaymentsRetrieveRequest, - PaymentsStartRequest, PaymentsUpdateMetadataRequest, PaymentsUpdateMetadataResponse, +use crate::{ + payment_methods::PaymentMethodListResponse, + payments::{ + ExtendedCardInfoResponse, PaymentIdType, PaymentListFilterConstraints, + PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, + PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, + PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsManualUpdateResponse, PaymentsPostSessionTokensRequest, + PaymentsPostSessionTokensResponse, PaymentsRejectRequest, PaymentsRetrieveRequest, + PaymentsStartRequest, PaymentsUpdateMetadataRequest, PaymentsUpdateMetadataResponse, + }, }; use crate::{ payment_methods::{ self, ListCountriesCurrenciesRequest, ListCountriesCurrenciesResponse, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, - PaymentMethodCollectLinkResponse, PaymentMethodListRequest, PaymentMethodListResponse, - PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, + PaymentMethodCollectLinkResponse, PaymentMethodListRequest, PaymentMethodMigrateResponse, + PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ self, PaymentListConstraints, PaymentListFilters, PaymentListFiltersV2, @@ -304,6 +310,8 @@ impl ApiEventMetric for PaymentMethodListRequest { impl ApiEventMetric for ListCountriesCurrenciesRequest {} impl ApiEventMetric for ListCountriesCurrenciesResponse {} + +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentMethodListResponse {} #[cfg(feature = "v1")] @@ -454,6 +462,9 @@ impl ApiEventMetric for payments::PaymentMethodListResponseForPayments { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentMethodListResponseForSession {} + #[cfg(feature = "v2")] impl ApiEventMetric for payments::PaymentsCaptureResponse { fn get_api_event_type(&self) -> Option { diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 92761192d1..2153bda732 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -1225,12 +1225,12 @@ impl From for payments::AdditionalCardInfo { #[cfg(feature = "v2")] #[derive(Debug, serde::Serialize, ToSchema)] -pub struct PaymentMethodListResponse { +pub struct PaymentMethodListResponseForSession { /// The list of payment methods that are enabled for the business profile pub payment_methods_enabled: Vec, /// The list of saved payment methods of the customer - pub customer_payment_methods: Vec, + pub customer_payment_methods: Vec, } #[cfg(all( @@ -1930,11 +1930,12 @@ pub struct CustomerPaymentMethodsListResponse { pub is_guest_customer: Option, } +// OLAP PML Response #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethodsListResponse { /// List of payment methods for customer - pub customer_payment_methods: Vec, + pub customer_payment_methods: Vec, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -2073,7 +2074,7 @@ pub struct CustomerDefaultPaymentMethodResponse { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, ToSchema)] -pub struct CustomerPaymentMethod { +pub struct PaymentMethodResponseItem { /// The unique identifier of the payment method. #[schema(value_type = String, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] pub id: id_type::GlobalPaymentMethodId, @@ -2136,6 +2137,68 @@ pub struct CustomerPaymentMethod { pub psp_tokenization_enabled: bool, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +pub struct CustomerPaymentMethodResponseItem { + /// The unique identifier of the payment method. + #[schema(value_type = String, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] + pub id: id_type::GlobalPaymentMethodId, + + /// Temporary Token for payment method in vault which gets refreshed for every payment + #[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")] + pub payment_token: String, + + /// The unique identifier of the customer. + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: id_type::GlobalCustomerId, + + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method_type: api_enums::PaymentMethod, + + /// This is a sub-category of payment method. + #[schema(value_type = PaymentMethodType,example = "credit")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// Indicates whether the payment method is eligible for recurring payments + #[schema(example = true)] + pub recurring_enabled: bool, + + /// PaymentMethod Data from locker + pub payment_method_data: Option, + + /// Masked bank details from PM auth services + #[schema(example = json!({"mask": "0000"}))] + pub bank: Option, + + /// A timestamp (ISO 8601 code) that determines when the payment method was created + #[schema(value_type = PrimitiveDateTime, example = "2023-01-18T11:04:09.922Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: time::PrimitiveDateTime, + + /// Whether this payment method requires CVV to be collected + #[schema(example = true)] + pub requires_cvv: bool, + + /// A timestamp (ISO 8601 code) that determines when the payment method was last used + #[schema(value_type = PrimitiveDateTime,example = "2024-02-24T11:04:09.922Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_used_at: time::PrimitiveDateTime, + + /// Indicates if the payment method has been set to default or not + #[schema(example = true)] + pub is_default: bool, + + /// The billing details of the payment method + #[schema(value_type = Option
)] + pub billing: Option, +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0ff988fc9b..9dd1ac971a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5328,6 +5328,10 @@ pub struct PaymentsConfirmIntentRequest { /// The payment_method_id to be associated with the payment #[schema(value_type = Option)] pub payment_method_id: Option, + + /// Provide a reference to a stored payment method + #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] + pub payment_token: Option, } #[cfg(feature = "v2")] @@ -5550,6 +5554,7 @@ impl From<&PaymentsRequest> for PaymentsConfirmIntentRequest { customer_acceptance: request.customer_acceptance.clone(), browser_info: request.browser_info.clone(), payment_method_id: request.payment_method_id.clone(), + payment_token: None, } } } @@ -7624,8 +7629,8 @@ pub struct PaymentMethodListResponseForPayments { /// The list of payment methods that are saved by the given customer /// This field is only returned if the customer_id is provided in the request - #[schema(value_type = Option>)] - pub customer_payment_methods: Option>, + #[schema(value_type = Option>)] + pub customer_payment_methods: Option>, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f351c884a0..a694f3fc0c 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -833,7 +833,7 @@ pub struct PaymentAttemptUpdateInternal { pub authentication_type: Option, pub error_message: Option, pub connector_payment_id: Option, - // payment_method_id: Option, + pub payment_method_id: Option, // cancellation_reason: Option, pub modified_at: PrimitiveDateTime, pub browser_info: Option, diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 2240df7241..6b93394d13 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -843,6 +843,56 @@ impl } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl + From<( + payment_methods::CardDetail, + Secret, + Option>, + )> for Card +{ + fn from( + (card_detail, card_cvc, card_holder_name): ( + payment_methods::CardDetail, + Secret, + Option>, + ), + ) -> Self { + Self { + card_number: card_detail.card_number, + card_exp_month: card_detail.card_exp_month, + card_exp_year: card_detail.card_exp_year, + card_cvc, + card_issuer: card_detail.card_issuer, + card_network: card_detail.card_network, + card_type: card_detail.card_type.map(|val| val.to_string()), + card_issuing_country: card_detail.card_issuing_country.map(|val| val.to_string()), + bank_code: None, + nick_name: card_detail.nick_name, + card_holder_name: card_holder_name.or(card_detail.card_holder_name), + co_badged_card_data: None, + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for payment_methods::CardDetail { + fn from(card: Card) -> Self { + Self { + card_number: card.card_number, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_holder_name: card.card_holder_name, + nick_name: card.nick_name, + card_issuing_country: None, + card_network: card.card_network, + card_issuer: card.card_issuer, + card_type: None, + card_cvc: Some(card.card_cvc), + } + } +} + impl From for CardRedirectData { fn from(value: api_models::payments::CardRedirectData) -> Self { match value { diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 8d95ae17bc..e8ec7041f6 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -43,7 +43,7 @@ use self::payment_attempt::PaymentAttempt; use crate::{ address::Address, business_profile, customer, errors, merchant_account, merchant_connector_account, merchant_context, payment_address, payment_method_data, - revenue_recovery, routing, ApiModelToDieselModelConvertor, + payment_methods, revenue_recovery, routing, ApiModelToDieselModelConvertor, }; #[cfg(feature = "v1")] use crate::{payment_method_data, RemoteStorageObject}; @@ -855,6 +855,7 @@ where pub payment_method_data: Option, pub payment_address: payment_address::PaymentAddress, pub mandate_data: Option, + pub payment_method: Option, } #[cfg(feature = "v2")] @@ -873,6 +874,22 @@ impl PaymentConfirmData { .get_connector_customer_id_from_feature_metadata(), } } + + pub fn update_payment_method_data( + &mut self, + payment_method_data: payment_method_data::PaymentMethodData, + ) { + self.payment_method_data = Some(payment_method_data); + } + + pub fn update_payment_method_and_pm_id( + &mut self, + payment_method_id: id_type::GlobalPaymentMethodId, + payment_method: payment_methods::PaymentMethod, + ) { + self.payment_attempt.payment_method_id = Some(payment_method_id); + self.payment_method = Some(payment_method); + } } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index c7342590e3..fb655280fe 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -6,7 +6,9 @@ use common_types::primitive_wrappers::{ }; #[cfg(feature = "v2")] use common_utils::{ - crypto::Encryptable, encryption::Encryption, ext_traits::ValueExt, + crypto::Encryptable, + encryption::Encryption, + ext_traits::{Encode, ValueExt}, types::keymanager::ToEncryptable, }; use common_utils::{ @@ -562,7 +564,7 @@ impl PaymentAttempt { last_synced: None, cancellation_reason: None, browser_info: request.browser_info.clone(), - payment_token: None, + payment_token: request.payment_token.clone(), connector_metadata: None, payment_experience: None, payment_method_data: None, @@ -581,7 +583,14 @@ impl PaymentAttempt { charges: None, client_source: None, client_version: None, - customer_acceptance: None, + customer_acceptance: request + .customer_acceptance + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encode customer_acceptance")? + .map(Secret::new), profile_id: payment_intent.profile_id.clone(), organization_id: payment_intent.organization_id.clone(), payment_method_type: request.payment_method_type, @@ -1747,6 +1756,15 @@ pub enum PaymentAttemptUpdate { merchant_connector_id: id_type::MerchantConnectorAccountId, authentication_type: storage_enums::AuthenticationType, }, + /// Update the payment attempt on confirming the intent, before calling the connector, when payment_method_id is present + ConfirmIntentTokenized { + status: storage_enums::AttemptStatus, + updated_by: String, + connector: String, + merchant_connector_id: id_type::MerchantConnectorAccountId, + authentication_type: storage_enums::AuthenticationType, + payment_method_id: id_type::GlobalPaymentMethodId, + }, /// Update the payment attempt on confirming the intent, after calling the connector on success response ConfirmIntentResponse(Box), /// Update the payment attempt after force syncing with the connector @@ -2523,6 +2541,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal authentication_type, } => Self { status: Some(status), + payment_method_id: None, error_message: None, modified_at: common_utils::date_time::now(), browser_info: None, @@ -2553,6 +2572,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, } => Self { status: Some(status), + payment_method_id: None, error_message: Some(error.message), error_code: Some(error.code), modified_at: common_utils::date_time::now(), @@ -2587,6 +2607,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal } = *confirm_intent_response_update; Self { status: Some(status), + payment_method_id: None, amount_capturable, error_message: None, error_code: None, @@ -2617,6 +2638,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, } => Self { status: Some(status), + payment_method_id: None, amount_capturable, error_message: None, error_code: None, @@ -2645,6 +2667,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, } => Self { status: Some(status), + payment_method_id: None, amount_capturable, amount_to_capture: None, error_message: None, @@ -2672,6 +2695,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, } => Self { amount_to_capture, + payment_method_id: None, error_message: None, modified_at: common_utils::date_time::now(), browser_info: None, @@ -2694,6 +2718,38 @@ impl From for diesel_models::PaymentAttemptUpdateInternal network_decline_code: None, network_error_message: None, }, + PaymentAttemptUpdate::ConfirmIntentTokenized { + status, + updated_by, + connector, + merchant_connector_id, + authentication_type, + payment_method_id, + } => Self { + status: Some(status), + payment_method_id: Some(payment_method_id), + error_message: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_code: None, + error_reason: None, + updated_by, + merchant_connector_id: Some(merchant_connector_id), + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: Some(connector), + redirection_data: None, + connector_metadata: None, + amount_capturable: None, + amount_to_capture: None, + connector_token_details: None, + authentication_type: Some(authentication_type), + feature_metadata: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + }, } } } diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index deb966797d..87615691f0 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -232,9 +232,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::AuthenticationDetails, api_models::payment_methods::PaymentMethodResponse, api_models::payment_methods::PaymentMethodResponseData, - api_models::payment_methods::CustomerPaymentMethod, + api_models::payment_methods::CustomerPaymentMethodResponseItem, + api_models::payment_methods::PaymentMethodResponseItem, api_models::payment_methods::PaymentMethodListRequest, - api_models::payment_methods::PaymentMethodListResponse, + api_models::payment_methods::PaymentMethodListResponseForSession, + api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::ResponsePaymentMethodsEnabled, api_models::payment_methods::PaymentMethodSubtypeSpecificData, api_models::payment_methods::ResponsePaymentMethodTypes, @@ -242,7 +244,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::CardNetworkTypes, api_models::payment_methods::BankDebitTypes, api_models::payment_methods::BankTransferTypes, - api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::PaymentMethodDeleteResponse, api_models::payment_methods::PaymentMethodUpdate, api_models::payment_methods::PaymentMethodUpdateData, diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index fd16b2ee2b..2e99106364 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -401,7 +401,7 @@ pub fn payment_method_session_retrieve() {} ("id" = String, Path, description = "The unique identifier for the Payment Method Session"), ), responses( - (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodListResponse), + (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodListResponseForSession), (status = 404, description = "The request is invalid") ), tag = "Payment Method Session", diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index d3d8b31cb0..d17f0cd234 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -37,6 +37,8 @@ use diesel_models::{ enums, GenericLinkNew, PaymentMethodCollectLink, PaymentMethodCollectLinkData, }; use error_stack::{report, ResultExt}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use futures::TryStreamExt; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; #[cfg(all( @@ -899,7 +901,8 @@ pub async fn create_payment_method( merchant_context: &domain::MerchantContext, profile: &domain::Profile, ) -> RouterResponse { - let response = + // payment_method is for internal use, can never be populated in response + let (response, _payment_method) = create_payment_method_core(state, request_state, req, merchant_context, profile).await?; Ok(services::ApplicationResponse::Json(response)) @@ -913,7 +916,7 @@ pub async fn create_payment_method_core( req: api::PaymentMethodCreate, merchant_context: &domain::MerchantContext, profile: &domain::Profile, -) -> RouterResult { +) -> RouterResult<(api::PaymentMethodResponse, domain::PaymentMethod)> { use common_utils::ext_traits::ValueExt; req.validate()?; @@ -1055,7 +1058,7 @@ pub async fn create_payment_method_core( } }?; - Ok(response) + Ok((response, payment_method)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1345,7 +1348,7 @@ pub async fn list_payment_methods_for_session( merchant_context: domain::MerchantContext, profile: domain::Profile, payment_method_session_id: id_type::GlobalPaymentMethodSessionId, -) -> RouterResponse { +) -> RouterResponse { let key_manager_state = &(&state).into(); let db = &*state.store; @@ -1371,7 +1374,7 @@ pub async fn list_payment_methods_for_session( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error when fetching merchant connector accounts")?; - let customer_payment_methods = list_customer_payment_method_core( + let customer_payment_methods = list_customer_payment_methods_core( &state, &merchant_context, &payment_method_session.customer_id, @@ -1382,7 +1385,7 @@ pub async fn list_payment_methods_for_session( hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts) .perform_filtering() .get_required_fields(RequiredFieldsInput::new(state.conf.required_fields.clone())) - .generate_response(customer_payment_methods.customer_payment_methods); + .generate_response_for_session(customer_payment_methods); Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( response, @@ -1395,9 +1398,9 @@ pub async fn list_saved_payment_methods_for_customer( state: SessionState, merchant_context: domain::MerchantContext, customer_id: id_type::GlobalCustomerId, -) -> RouterResponse { +) -> RouterResponse { let customer_payment_methods = - list_customer_payment_method_core(&state, &merchant_context, &customer_id).await?; + list_payment_methods_core(&state, &merchant_context, &customer_id).await?; Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( customer_payment_methods, @@ -1628,10 +1631,10 @@ struct RequiredFieldsForEnabledPaymentMethodTypes(Vec, - ) -> payment_methods::PaymentMethodListResponse { + customer_payment_methods: Vec, + ) -> payment_methods::PaymentMethodListResponseForSession { let response_payment_methods = self .0 .into_iter() @@ -1645,7 +1648,7 @@ impl RequiredFieldsForEnabledPaymentMethodTypes { ) .collect(); - payment_methods::PaymentMethodListResponse { + payment_methods::PaymentMethodListResponseForSession { payment_methods_enabled: response_payment_methods, customer_payment_methods, } @@ -1798,7 +1801,9 @@ pub async fn create_pm_additional_data_update( .map(From::from); let pm_update = storage::PaymentMethodUpdate::GenericUpdate { - status: Some(enums::PaymentMethodStatus::Active), + // A new payment method is created with inactive state + // It will be marked active after payment succeeds + status: Some(enums::PaymentMethodStatus::Inactive), locker_id: vault_id, payment_method_type_v2: payment_method_type, payment_method_subtype, @@ -2008,8 +2013,6 @@ pub async fn vault_payment_method( } } -// TODO: check if this function will be used for listing the customer payment methods for payments -#[allow(unused)] #[cfg(all( feature = "v2", feature = "payment_methods_v2", @@ -2031,7 +2034,7 @@ fn get_pm_list_context( card_details: api::CardDetailFromLocker::from(card), token_data: is_payment_associated.then_some( storage::PaymentTokenData::permanent_card( - Some(payment_method.get_id().clone()), + payment_method.get_id().clone(), payment_method .locker_id .as_ref() @@ -2097,12 +2100,38 @@ fn get_pm_list_context( Ok(payment_method_retrieval_context) } +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +fn get_pm_list_token_data( + payment_method_type: enums::PaymentMethod, + payment_method: &domain::PaymentMethod, +) -> Result, error_stack::Report> { + let pm_list_context = get_pm_list_context(payment_method_type, payment_method, true)? + .get_required_value("PaymentMethodListContext")?; + + match pm_list_context { + PaymentMethodListContext::Card { + card_details: _, + token_data, + } => Ok(token_data), + PaymentMethodListContext::Bank { token_data } => Ok(token_data), + PaymentMethodListContext::BankTransfer { + bank_transfer_details: _, + token_data, + } => Ok(token_data), + PaymentMethodListContext::TemporaryToken { token_data } => Ok(token_data), + } +} + #[cfg(all(feature = "v2", feature = "olap"))] -pub async fn list_customer_payment_method_core( +pub async fn list_payment_methods_core( state: &SessionState, merchant_context: &domain::MerchantContext, customer_id: &id_type::GlobalCustomerId, -) -> RouterResult { +) -> RouterResult { let db = &*state.store; let key_manager_state = &(state).into(); @@ -2122,16 +2151,84 @@ pub async fn list_customer_payment_method_core( let customer_payment_methods = saved_payment_methods .into_iter() .map(ForeignTryFrom::foreign_try_from) - .collect::, _>>() + .collect::, _>>() .change_context(errors::ApiErrorResponse::InternalServerError)?; - let response = api::CustomerPaymentMethodsListResponse { + let response = payment_methods::CustomerPaymentMethodsListResponse { customer_payment_methods, }; Ok(response) } +#[cfg(all(feature = "v2", feature = "oltp"))] +pub async fn list_customer_payment_methods_core( + state: &SessionState, + merchant_context: &domain::MerchantContext, + customer_id: &id_type::GlobalCustomerId, +) -> RouterResult> { + let db = &*state.store; + let key_manager_state = &(state).into(); + + let saved_payment_methods = db + .find_payment_method_by_global_customer_id_merchant_id_status( + key_manager_state, + merchant_context.get_merchant_key_store(), + customer_id, + merchant_context.get_merchant_account().get_id(), + common_enums::PaymentMethodStatus::Active, + None, + merchant_context.get_merchant_account().storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let mut customer_payment_methods = Vec::new(); + + let payment_method_results: Result, error_stack::Report> = + saved_payment_methods + .into_iter() + .map(|pm| async move { + let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); + + // For payment methods that are active we should always have the payment method type + let payment_method_type = pm + .payment_method_type + .get_required_value("payment_method_type")?; + + let intent_fulfillment_time = common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME; + + let token_data = get_pm_list_token_data(payment_method_type, &pm)?; + + if let Some(token_data) = token_data { + pm_routes::ParentPaymentMethodToken::create_key_for_token(( + &parent_payment_method_token, + payment_method_type, + )) + .insert(intent_fulfillment_time, token_data, state) + .await?; + + let final_pm = api::CustomerPaymentMethodResponseItem::foreign_try_from(( + pm, + parent_payment_method_token, + )) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert payment method to response format")?; + + Ok(Some(final_pm)) + } else { + Ok(None) + } + }) + .collect::>() + .try_collect::>() + .await; + + customer_payment_methods.extend(payment_method_results?.into_iter().flatten()); + + Ok(customer_payment_methods) +} + #[cfg(all(feature = "v2", feature = "olap"))] pub async fn get_total_payment_method_count_core( state: &SessionState, @@ -2186,6 +2283,48 @@ pub async fn retrieve_payment_method( .map(services::ApplicationResponse::Json) } +// TODO: When we separate out microservices, this function will be an endpoint in payment_methods +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn update_payment_method_status_internal( + state: &SessionState, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + status: enums::PaymentMethodStatus, + payment_method_id: &id_type::GlobalPaymentMethodId, +) -> RouterResult { + let db = &*state.store; + let key_manager_state = &state.into(); + + let payment_method = db + .find_payment_method( + &((state).into()), + key_store, + payment_method_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let pm_update = storage::PaymentMethodUpdate::StatusUpdate { + status: Some(status), + }; + + let updated_pm = db + .update_payment_method( + key_manager_state, + key_store, + payment_method.clone(), + pm_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + Ok(updated_pm) +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn update_payment_method( @@ -2898,7 +3037,7 @@ pub async fn payment_methods_session_confirm( ) .attach_printable("Failed to create payment method request")?; - let payment_method = create_payment_method_core( + let (payment_method_response, _payment_method) = create_payment_method_core( &state, &req_state, create_payment_method_request.clone(), @@ -2915,7 +3054,7 @@ pub async fn payment_methods_session_confirm( let zero_auth_request = construct_zero_auth_payments_request( &request, &payment_method_session, - &payment_method, + &payment_method_response, )?; let payments_response = Box::pin(create_zero_auth_payment( state.clone(), @@ -2938,7 +3077,7 @@ pub async fn payment_methods_session_confirm( merchant_context.clone(), profile.clone(), &create_payment_method_request.clone(), - &payment_method, + &payment_method_response, &payment_method_session, )) .await?; @@ -3015,7 +3154,7 @@ impl pm_types::SavedPMLPaymentsInfo { &self, state: &SessionState, parent_payment_method_token: Option, - pma: &api::CustomerPaymentMethod, + pma: &api::CustomerPaymentMethodResponseItem, pm_list_context: PaymentMethodListContext, ) -> RouterResult<()> { let token = parent_payment_method_token diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 47d74d29de..539298ac53 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -4591,7 +4591,7 @@ pub async fn perform_surcharge_ops( _state: &routes::SessionState, _merchant_context: &domain::MerchantContext, _business_profile: Option, - _response: &mut api::CustomerPaymentMethodsListResponse, + _response: &mut api_models::payment_methods::CustomerPaymentMethodsListResponse, ) -> Result<(), error_stack::Report> { todo!() } diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index fd36f6d0ab..3e64e84615 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -1,4 +1,6 @@ pub use ::payment_methods::controller::{DataDuplicationCheck, DeleteCardResp}; +#[cfg(feature = "v2")] +use api_models::payment_methods::PaymentMethodResponseItem; use api_models::{enums as api_enums, payment_methods::Card}; use common_utils::{ ext_traits::{Encode, StringExt}, @@ -930,7 +932,73 @@ pub fn mk_card_value2( } #[cfg(feature = "v2")] -impl transformers::ForeignTryFrom for api::CustomerPaymentMethod { +impl transformers::ForeignTryFrom<(domain::PaymentMethod, String)> + for api::CustomerPaymentMethodResponseItem +{ + type Error = error_stack::Report; + + fn foreign_try_from( + (item, payment_token): (domain::PaymentMethod, String), + ) -> Result { + // For payment methods that are active we should always have the payment method subtype + let payment_method_subtype = + item.payment_method_subtype + .ok_or(errors::ValidationError::MissingRequiredField { + field_name: "payment_method_subtype".to_string(), + })?; + + // For payment methods that are active we should always have the payment method type + let payment_method_type = + item.payment_method_type + .ok_or(errors::ValidationError::MissingRequiredField { + field_name: "payment_method_type".to_string(), + })?; + + let payment_method_data = item + .payment_method_data + .map(|payment_method_data| payment_method_data.into_inner()) + .map(|payment_method_data| match payment_method_data { + api_models::payment_methods::PaymentMethodsData::Card( + card_details_payment_method, + ) => { + let card_details = api::CardDetailFromLocker::from(card_details_payment_method); + api_models::payment_methods::PaymentMethodListData::Card(card_details) + } + api_models::payment_methods::PaymentMethodsData::BankDetails(..) => todo!(), + api_models::payment_methods::PaymentMethodsData::WalletDetails(..) => { + todo!() + } + }); + + let payment_method_billing = item + .payment_method_billing_address + .clone() + .map(|billing| billing.into_inner()) + .map(From::from); + + // TODO: check how we can get this field + let recurring_enabled = true; + + Ok(Self { + id: item.id, + customer_id: item.customer_id, + payment_method_type, + payment_method_subtype, + created: item.created_at, + last_used_at: item.last_used_at, + recurring_enabled, + payment_method_data, + bank: None, + requires_cvv: true, + is_default: false, + billing: payment_method_billing, + payment_token, + }) + } +} + +#[cfg(feature = "v2")] +impl transformers::ForeignTryFrom for PaymentMethodResponseItem { type Error = error_stack::Report; fn foreign_try_from(item: domain::PaymentMethod) -> Result { diff --git a/crates/router/src/core/payment_methods/utils.rs b/crates/router/src/core/payment_methods/utils.rs index 5885b830e7..512c40fc8d 100644 --- a/crates/router/src/core/payment_methods/utils.rs +++ b/crates/router/src/core/payment_methods/utils.rs @@ -6,6 +6,10 @@ use api_models::{ payment_methods::RequestPaymentMethodTypes, }; use common_enums::enums; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::ext_traits::{OptionExt, StringExt}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use error_stack::ResultExt; use euclid::frontend::dir; use hyperswitch_constraint_graph as cgraph; use kgraph_utils::{error::KgraphError, transformers::IntoDirValue}; @@ -13,6 +17,14 @@ use masking::ExposeInterface; use storage_impl::redis::cache::{CacheKey, PM_FILTERS_CGRAPH_CACHE}; use crate::{configs::settings, routes::SessionState}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::{ + db::{ + errors, + storage::{self, enums as storage_enums}, + }, + services::logger, +}; pub fn make_pm_graph( builder: &mut cgraph::ConstraintGraphBuilder, @@ -798,3 +810,63 @@ fn compile_accepted_currency_for_mca( .map_err(KgraphError::GraphConstructionError)?, )) } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub(super) async fn retrieve_payment_token_data( + state: &SessionState, + token: String, + payment_method: Option<&storage_enums::PaymentMethod>, +) -> errors::RouterResult { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let key = format!( + "pm_token_{}_{}_hyperswitch", + token, + payment_method + .get_required_value("payment_method") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Payment method is required")? + ); + + let token_data_string = redis_conn + .get_key::>(&key.into()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the token from redis")? + .ok_or(error_stack::Report::new( + errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".to_owned(), + }, + ))?; + + token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data") +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub(super) async fn delete_payment_token_data( + state: &SessionState, + key_for_token: &str, +) -> errors::RouterResult<()> { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + match redis_conn.delete_key(&key_for_token.into()).await { + Ok(_) => Ok(()), + Err(err) => { + { + logger::error!("Error while deleting redis key: {:?}", err) + }; + Ok(()) + } + } +} diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 3c231ad3f5..236072b0cf 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -23,8 +23,8 @@ use crate::types::api::payouts; use crate::{ consts, core::errors::{self, CustomResult, RouterResult}, - db, logger, routes, - routes::metrics, + db, logger, + routes::{self, metrics}, types::{ api, domain, storage::{self, enums}, @@ -35,7 +35,8 @@ use crate::{ use crate::{ core::{ errors::ConnectorErrorExt, - payment_methods::transformers as pm_transforms, + errors::StorageErrorExt, + payment_methods::{transformers as pm_transforms, utils}, payments::{self as payments_core, helpers as payment_helpers}, utils as core_utils, }, @@ -45,6 +46,7 @@ use crate::{ types::{self, payment_methods as pm_types}, utils::{ext_traits::OptionExt, ConnectorResponseExt}, }; + const VAULT_SERVICE_NAME: &str = "CARD"; pub struct SupplementaryVaultData { @@ -1465,6 +1467,87 @@ pub fn get_vault_response_for_retrieve_payment_method_data( } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn retrieve_payment_method_from_vault_using_payment_token( + state: &routes::SessionState, + merchant_context: &domain::MerchantContext, + profile: &domain::Profile, + payment_token: &String, + payment_method_type: &common_enums::PaymentMethod, +) -> RouterResult<(domain::PaymentMethod, domain::PaymentMethodVaultingData)> { + let pm_token_data = utils::retrieve_payment_token_data( + state, + payment_token.to_string(), + Some(payment_method_type), + ) + .await?; + + let payment_method_id = match pm_token_data { + storage::PaymentTokenData::PermanentCard(card_token_data) => { + card_token_data.payment_method_id + } + storage::PaymentTokenData::TemporaryGeneric(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Reason( + "TemporaryGeneric Token not implemented".to_string(), + ), + })? + } + storage::PaymentTokenData::AuthBankDebit(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Reason( + "AuthBankDebit Token not implemented".to_string(), + ), + })? + } + }; + let db = &*state.store; + let key_manager_state = &state.into(); + + let storage_scheme = merchant_context.get_merchant_account().storage_scheme; + + let payment_method = db + .find_payment_method( + key_manager_state, + merchant_context.get_merchant_key_store(), + &payment_method_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let vault_data = + retrieve_payment_method_from_vault(state, merchant_context, profile, &payment_method) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to retrieve payment method from vault")? + .data; + + Ok((payment_method, vault_data)) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn delete_payment_token( + state: &routes::SessionState, + key_for_token: &str, + intent_status: enums::IntentStatus, +) -> RouterResult<()> { + if ![ + enums::IntentStatus::RequiresCustomerAction, + enums::IntentStatus::RequiresMerchantAction, + ] + .contains(&intent_status) + { + utils::delete_payment_token_data(state, key_for_token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to delete payment_token")?; + } + Ok(()) +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn retrieve_payment_method_from_vault( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8c21739cde..2fd7e9fa4e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -94,6 +94,8 @@ use super::{ use crate::core::debit_routing; #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; +#[cfg(feature = "v2")] +use crate::core::payment_methods::vault; #[cfg(feature = "v1")] use crate::core::routing::helpers as routing_helpers; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] @@ -171,6 +173,11 @@ where // Get the trackers related to track the state of the payment let operations::GetTrackerResponse { mut payment_data } = get_tracker_response; + operation + .to_domain()? + .create_or_fetch_payment_method(state, &merchant_context, profile, &mut payment_data) + .await?; + let (_operation, customer) = operation .to_domain()? .get_customer_details( @@ -296,6 +303,22 @@ where ConnectorCallType::Skip => payment_data, }; + let payment_intent_status = payment_data.get_payment_intent().status; + + // Delete the tokens after payment + payment_data + .get_payment_attempt() + .payment_token + .as_ref() + .zip(Some(payment_data.get_payment_attempt().payment_method_type)) + .map(ParentPaymentMethodToken::return_key_for_token) + .async_map(|key_for_token| async move { + let _ = vault::delete_payment_token(state, &key_for_token, payment_intent_status) + .await + .inspect_err(|err| logger::error!("Failed to delete payment_token: {:?}", err)); + }) + .await; + Ok((payment_data, req, customer, None, None)) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 94a02248aa..3099e8c54d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2482,6 +2482,10 @@ pub async fn retrieve_payment_method_from_db_with_token_data( } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn retrieve_payment_token_data( state: &SessionState, token: String, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index ebbece0685..ff873b1377 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -373,6 +373,17 @@ pub trait Domain: Send + Sync { Ok(()) } + #[cfg(feature = "v2")] + async fn create_or_fetch_payment_method<'a>( + &'a self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + business_profile: &domain::Profile, + payment_data: &mut D, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } + // #[cfg(feature = "v2")] // async fn call_connector<'a, RouterDataReq>( // &'a self, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 7084fc14bb..8805710c07 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -1,9 +1,9 @@ use api_models::{enums::FrmSuggestion, payments::PaymentsConfirmIntentRequest}; use async_trait::async_trait; -use common_utils::{ext_traits::Encode, types::keymanager::ToEncryptable}; +use common_utils::{ext_traits::Encode, fp_utils::when, types::keymanager::ToEncryptable}; use error_stack::ResultExt; use hyperswitch_domain_models::payments::PaymentConfirmData; -use masking::PeekInterface; +use masking::{ExposeOptionInterface, PeekInterface}; use router_env::{instrument, tracing}; use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; @@ -11,6 +11,7 @@ use crate::{ core::{ admin, errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payment_methods, payments::{ self, call_decision_manager, helpers, operations::{self, ValidateStatusForOperation}, @@ -225,6 +226,23 @@ impl GetTracker, PaymentsConfir .clone() .map(hyperswitch_domain_models::payment_method_data::PaymentMethodData::from); + if request.payment_token.is_some() { + when( + !matches!( + payment_method_data, + Some(domain::payment_method_data::PaymentMethodData::CardToken(_)) + ), + || { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + }) + .attach_printable( + "payment_method_data should be card_token when a token is passed", + ) + }, + )?; + }; + let payment_address = hyperswitch_domain_models::payment_address::PaymentAddress::new( payment_intent .shipping_address @@ -248,6 +266,7 @@ impl GetTracker, PaymentsConfir payment_method_data, payment_address, mandate_data: None, + payment_method: None, }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; @@ -347,6 +366,125 @@ impl Domain( + &'a self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + business_profile: &domain::Profile, + payment_data: &mut PaymentConfirmData, + ) -> CustomResult<(), errors::ApiErrorResponse> { + let (payment_method, payment_method_data) = match ( + &payment_data.payment_attempt.payment_token, + &payment_data.payment_method_data, + &payment_data.payment_attempt.customer_acceptance, + ) { + ( + Some(payment_token), + Some(domain::payment_method_data::PaymentMethodData::CardToken(card_token)), + None, + ) => { + let (card_cvc, card_holder_name) = { + ( + card_token + .card_cvc + .clone() + .ok_or(errors::ApiErrorResponse::InvalidDataValue { + field_name: "card_cvc", + }) + .attach_printable("card_cvc not provided")?, + card_token.card_holder_name.clone(), + ) + }; + + let (payment_method, vault_data) = + payment_methods::vault::retrieve_payment_method_from_vault_using_payment_token( + state, + merchant_context, + business_profile, + payment_token, + &payment_data.payment_attempt.payment_method_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to retrieve payment method from vault")?; + + match vault_data { + domain::vault::PaymentMethodVaultingData::Card(card_detail) => { + let pm_data_from_vault = + domain::payment_method_data::PaymentMethodData::Card( + domain::payment_method_data::Card::from(( + card_detail, + card_cvc, + card_holder_name, + )), + ); + + (Some(payment_method), Some(pm_data_from_vault)) + } + _ => Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Reason( + "Non-card Tokenization not implemented".to_string(), + ), + })?, + } + } + + (Some(_payment_token), _, _) => Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + }) + .attach_printable("payment_method_data should be card_token when a token is passed")?, + + (None, Some(domain::PaymentMethodData::Card(card)), Some(_customer_acceptance)) => { + let customer_id = match &payment_data.payment_intent.customer_id { + Some(customer_id) => customer_id.clone(), + None => { + return Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "customer_id", + }) + .attach_printable("customer_id not provided"); + } + }; + + let pm_create_data = + api::PaymentMethodCreateData::Card(api::CardDetail::from(card.clone())); + + let req = api::PaymentMethodCreate { + payment_method_type: payment_data.payment_attempt.payment_method_type, + payment_method_subtype: payment_data.payment_attempt.payment_method_subtype, + metadata: None, + customer_id, + payment_method_data: pm_create_data, + billing: None, + psp_tokenization: None, + network_tokenization: None, + }; + + let (_pm_response, payment_method) = payment_methods::create_payment_method_core( + state, + &state.get_req_state(), + req, + merchant_context, + business_profile, + ) + .await?; + + // Don't modify payment_method_data in this case, only the payment_method and payment_method_id + (Some(payment_method), None) + } + _ => (None, None), // Pass payment_data unmodified for any other case + }; + + if let Some(pm_data) = payment_method_data { + payment_data.update_payment_method_data(pm_data); + } + if let Some(pm) = payment_method { + payment_data.update_payment_method_and_pm_id(pm.get_id().clone(), pm); + } + + Ok(()) + } } #[async_trait] @@ -400,12 +538,26 @@ impl UpdateTracker, PaymentsConfirmInt let authentication_type = payment_data.payment_attempt.authentication_type; - let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { - status: attempt_status, - updated_by: storage_scheme.to_string(), - connector, - merchant_connector_id, - authentication_type, + let payment_attempt_update = match &payment_data.payment_method { + Some(payment_method) => { + hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntentTokenized { + status: attempt_status, + updated_by: storage_scheme.to_string(), + connector, + merchant_connector_id, + authentication_type, + payment_method_id : payment_method.get_id().clone() + } + } + None => { + hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { + status: attempt_status, + updated_by: storage_scheme.to_string(), + connector, + merchant_connector_id, + authentication_type, + } + } }; let updated_payment_intent = db diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 40285ee5ce..41d494989b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2541,9 +2541,55 @@ impl PostUpdateTracker, types::PaymentsAuthor .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to update payment attempt")?; + let attempt_status = updated_payment_attempt.status; + payment_data.payment_intent = updated_payment_intent; payment_data.payment_attempt = updated_payment_attempt; + if let Some(payment_method) = &payment_data.payment_method { + match attempt_status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::Failure => (), + + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => { + let pm_update_status = enums::PaymentMethodStatus::Active; + + // payment_methods microservice call + payment_methods::update_payment_method_status_internal( + state, + key_store, + storage_scheme, + pm_update_status, + payment_method.get_id(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method status")?; + } + } + } + Ok(payment_data) } } diff --git a/crates/router/src/core/payments/operations/proxy_payments_intent.rs b/crates/router/src/core/payments/operations/proxy_payments_intent.rs index 1d7fb108b3..9829c10944 100644 --- a/crates/router/src/core/payments/operations/proxy_payments_intent.rs +++ b/crates/router/src/core/payments/operations/proxy_payments_intent.rs @@ -260,6 +260,7 @@ impl GetTracker, ProxyPaymentsR payment_method_data: Some(PaymentMethodData::MandatePayment), payment_address, mandate_data: Some(mandate_data_input), + payment_method: None, }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/payment_methods.rs b/crates/router/src/core/payments/payment_methods.rs index 208333ba0b..1464ed4c5f 100644 --- a/crates/router/src/core/payments/payment_methods.rs +++ b/crates/router/src/core/payments/payment_methods.rs @@ -5,7 +5,7 @@ use common_utils::{ext_traits::OptionExt, id_type}; use error_stack::ResultExt; use super::errors; -use crate::{db::errors::StorageErrorExt, routes, types::domain}; +use crate::{core::payment_methods, db::errors::StorageErrorExt, routes, types::domain}; #[cfg(all( feature = "v2", @@ -46,12 +46,24 @@ pub async fn list_payment_methods( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error when fetching merchant connector accounts")?; + let customer_payment_methods = match &payment_intent.customer_id { + Some(customer_id) => Some( + payment_methods::list_customer_payment_methods_core( + &state, + &merchant_context, + customer_id, + ) + .await?, + ), + None => None, + }; + let response = hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts) .perform_filtering() .get_required_fields(RequiredFieldsInput::new()) .perform_surcharge_calculation() - .generate_response(); + .generate_response(customer_payment_methods); Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( response, @@ -124,7 +136,12 @@ struct RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes( ); impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes { - fn generate_response(self) -> api_models::payments::PaymentMethodListResponseForPayments { + fn generate_response( + self, + customer_payment_methods: Option< + Vec, + >, + ) -> api_models::payments::PaymentMethodListResponseForPayments { let response_payment_methods = self .0 .into_iter() @@ -142,7 +159,7 @@ impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes { api_models::payments::PaymentMethodListResponseForPayments { payment_methods_enabled: response_payment_methods, - customer_payment_methods: None, + customer_payment_methods, } } } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 0efbd65577..1e8969d372 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -1044,6 +1044,17 @@ impl ParentPaymentMethodToken { ), } } + + #[cfg(feature = "v2")] + pub fn return_key_for_token( + (parent_pm_token, payment_method): (&String, api_models::enums::PaymentMethod), + ) -> String { + format!( + "pm_token_{}_{}_hyperswitch", + parent_pm_token, payment_method + ) + } + pub async fn insert( &self, fulfillment_time: i64, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 09dd783e48..ecc3ec35ed 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,19 +1,18 @@ #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, - CardNetworkTokenizeResponse, CardType, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, - GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, - NetworkTokenDetailsPaymentMethod, NetworkTokenDetailsResponse, NetworkTokenResponse, - PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, - PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData, - PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, - PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData, - PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenDataResponse, - TokenDetailsResponse, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, - TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, - TotalPaymentMethodCountResponse, + CardNetworkTokenizeResponse, CardType, CustomerPaymentMethodResponseItem, + DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse, + ListCountriesCurrenciesRequest, MigrateCardDetail, NetworkTokenDetailsPaymentMethod, + NetworkTokenDetailsResponse, NetworkTokenResponse, PaymentMethodCollectLinkRenderRequest, + PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, + PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, + PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest, + PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodMigrateResponse, + PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, + PaymentMethodsData, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted, + TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, + TokenizedWalletValue2, TotalPaymentMethodCountResponse, }; #[cfg(all( any(feature = "v2", feature = "v1"), diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 21c15c23b3..ff12d890df 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -29,7 +29,7 @@ pub struct CardTokenData { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CardTokenData { - pub payment_method_id: Option, + pub payment_method_id: common_utils::id_type::GlobalPaymentMethodId, pub locker_id: Option, pub token: String, } @@ -53,6 +53,7 @@ pub struct WalletTokenData { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] +#[cfg(feature = "v1")] pub enum PaymentTokenData { // The variants 'Temporary' and 'Permanent' are added for backwards compatibility // with any tokenized data present in Redis at the time of deployment of this change @@ -64,6 +65,15 @@ pub enum PaymentTokenData { WalletToken(WalletTokenData), } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub enum PaymentTokenData { + TemporaryGeneric(GenericTokenData), + PermanentCard(CardTokenData), + AuthBankDebit(payment_methods::BankAccountTokenData), +} + impl PaymentTokenData { #[cfg(all( any(feature = "v1", feature = "v2"), @@ -85,7 +95,7 @@ impl PaymentTokenData { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn permanent_card( - payment_method_id: Option, + payment_method_id: common_utils::id_type::GlobalPaymentMethodId, locker_id: Option, token: String, ) -> Self { @@ -100,13 +110,20 @@ impl PaymentTokenData { Self::TemporaryGeneric(GenericTokenData { token }) } + #[cfg(feature = "v1")] pub fn wallet_token(payment_method_id: String) -> Self { Self::WalletToken(WalletTokenData { payment_method_id }) } + #[cfg(feature = "v1")] pub fn is_permanent_card(&self) -> bool { matches!(self, Self::PermanentCard(_) | Self::Permanent(_)) } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn is_permanent_card(&self) -> bool { + matches!(self, Self::PermanentCard(_)) + } } #[cfg(all( @@ -126,6 +143,7 @@ pub struct PaymentMethodListContext { pub enum PaymentMethodListContext { Card { card_details: api::CardDetailFromLocker, + // TODO: Why can't these fields be mandatory? token_data: Option, }, Bank { diff --git a/crates/router/src/workflows/revenue_recovery.rs b/crates/router/src/workflows/revenue_recovery.rs index af27d6f45d..792251de49 100644 --- a/crates/router/src/workflows/revenue_recovery.rs +++ b/crates/router/src/workflows/revenue_recovery.rs @@ -85,13 +85,13 @@ impl ProcessTrackerWorkflow for ExecutePcrWorkflow { match process.name.as_deref() { Some("EXECUTE_WORKFLOW") => { - pcr::perform_execute_payment( + Box::pin(pcr::perform_execute_payment( state, &process, &tracking_data, &revenue_recovery_payment_data, &payment_data.payment_intent, - ) + )) .await } Some("PSYNC_WORKFLOW") => {