diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index b34623cd79..00d83f7965 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -17165,6 +17165,17 @@ "$ref": "#/components/schemas/CardDetail" } } + }, + { + "type": "object", + "required": [ + "proxy_card" + ], + "properties": { + "proxy_card": { + "$ref": "#/components/schemas/ProxyCardDetails" + } + } } ] }, @@ -22168,6 +22179,82 @@ } } }, + "ProxyCardDetails": { + "type": "object", + "required": [ + "card_number", + "card_exp_month", + "card_exp_year", + "card_holder_name", + "card_cvc" + ], + "properties": { + "card_number": { + "type": "string", + "description": "Tokenized Card Number", + "example": "tok_sjfowhoejsldj" + }, + "card_exp_month": { + "type": "string", + "description": "Card Expiry Month", + "example": "10" + }, + "card_exp_year": { + "type": "string", + "description": "Card Expiry Year", + "example": "25" + }, + "bin_number": { + "type": "string", + "description": "First Six Digit of Card Number", + "nullable": true + }, + "last_four": { + "type": "string", + "description": "Last Four Digit of Card Number", + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "Issuer Bank for Card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card Type", + "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Issuing Country of the Card", + "nullable": true + }, + "nick_name": { + "type": "string", + "description": "Card Holder's Nick Name", + "example": "John Doe", + "nullable": true + }, + "card_holder_name": { + "type": "string", + "description": "Card Holder Name", + "example": "John Doe" + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card\nThis is optional in case the card needs to be vaulted", + "example": "242" + } + } + }, "ProxyRequest": { "type": "object", "required": [ diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 2e9478dc25..135e0ddc24 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -497,6 +497,7 @@ pub enum PaymentMethodUpdateData { #[serde(rename = "payment_method_data")] pub enum PaymentMethodCreateData { Card(CardDetail), + ProxyCard(ProxyCardDetails), } #[cfg(feature = "v1")] @@ -612,6 +613,57 @@ pub struct CardDetail { pub card_cvc: Option>, } +// This struct is for collecting Proxy Card Data +// All card related data present in this struct are tokenzied +// No strict type is present to accept tokenized data +#[cfg(feature = "v2")] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct ProxyCardDetails { + /// Tokenized Card Number + #[schema(value_type = String,example = "tok_sjfowhoejsldj")] + pub card_number: masking::Secret, + + /// Card Expiry Month + #[schema(value_type = String,example = "10")] + pub card_exp_month: masking::Secret, + + /// Card Expiry Year + #[schema(value_type = String,example = "25")] + pub card_exp_year: masking::Secret, + + /// First Six Digit of Card Number + pub bin_number: Option, + + ///Last Four Digit of Card Number + pub last_four: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Card Type + pub card_type: Option, + + /// Issuing Country of the Card + pub card_issuing_country: Option, + + /// Card Holder's Nick Name + #[schema(value_type = Option,example = "John Doe")] + pub nick_name: Option>, + + /// Card Holder Name + #[schema(value_type = String,example = "John Doe")] + pub card_holder_name: Option>, + + /// The CVC number for the card + /// This is optional in case the card needs to be vaulted + #[schema(value_type = String, example = "242")] + pub card_cvc: Option>, +} + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct MigrateCardDetail { @@ -943,6 +995,12 @@ pub enum PaymentMethodsData { WalletDetails(PaymentMethodDataWalletInfo), } +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ExternalVaultTokenData { + /// Tokenized reference for Card Number + pub tokenized_card_number: masking::Secret, +} + #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CardDetailsPaymentMethod { pub last4_digits: Option, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 723fbae3ff..e50c37821b 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2611,7 +2611,8 @@ pub struct ProxyPaymentMethodDataRequest { #[serde(rename_all = "snake_case")] pub enum ProxyPaymentMethodData { #[schema(title = "ProxyCardData")] - VaultDataCard(ProxyCardData), + VaultDataCard(Box), + VaultToken(VaultToken), } #[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -2655,6 +2656,25 @@ pub struct ProxyCardData { /// The card holder's nick name #[schema(value_type = Option, example = "John Test")] pub nick_name: Option>, + + /// The first six digit of the card number + #[schema(value_type = String, example = "424242")] + pub bin_number: Option, + + /// The last four digit of the card number + #[schema(value_type = String, example = "4242")] + pub last_four: Option, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct VaultToken { + /// The tokenized CVC number for the card + #[schema(value_type = String, example = "242")] + pub card_cvc: Secret, + + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 998c60b550..8fbc29c696 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -194,6 +194,10 @@ impl AttemptStatus { | Self::IntegrityFailure => false, } } + + pub fn is_success(self) -> bool { + matches!(self, Self::Charged | Self::PartialCharged) + } } #[derive( diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 472a56700c..79d6865b72 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -89,6 +89,7 @@ pub struct PaymentMethod { pub payment_method_subtype: Option, pub id: common_utils::id_type::GlobalPaymentMethodId, pub external_vault_source: Option, + pub external_vault_token_data: Option, } impl PaymentMethod { @@ -167,6 +168,7 @@ pub struct PaymentMethodNew { pub network_token_requestor_reference_id: Option, pub network_token_locker_id: Option, pub network_token_payment_method_data: Option, + pub external_vault_token_data: Option, pub locker_fingerprint_id: Option, pub payment_method_type_v2: Option, pub payment_method_subtype: Option, @@ -364,6 +366,7 @@ impl PaymentMethodUpdateInternal { network_token_payment_method_data: network_token_payment_method_data .or(source.network_token_payment_method_data), external_vault_source: external_vault_source.or(source.external_vault_source), + external_vault_token_data: source.external_vault_token_data, } } } @@ -905,6 +908,7 @@ impl From<&PaymentMethodNew> for PaymentMethod { .network_token_payment_method_data .clone(), external_vault_source: None, + external_vault_token_data: payment_method_new.external_vault_token_data.clone(), } } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 192d6e72e6..1837903f3a 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1112,6 +1112,7 @@ diesel::table! { id -> Varchar, #[max_length = 64] external_vault_source -> Nullable, + external_vault_token_data -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index de07d67f47..139889daf4 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -48,7 +48,8 @@ pub enum PaymentMethodData { #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ExternalVaultPaymentMethodData { - Card(ExternalVaultCard), + Card(Box), + VaultToken(VaultToken), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -144,6 +145,8 @@ pub struct ExternalVaultCard { pub card_exp_month: Secret, pub card_exp_year: Secret, pub card_cvc: Secret, + pub bin_number: Option, + pub last_four: Option, pub card_issuer: Option, pub card_network: Option, pub card_type: Option, @@ -154,6 +157,12 @@ pub struct ExternalVaultCard { pub co_badged_card_data: Option, } +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Default)] +pub struct VaultToken { + pub card_cvc: Secret, + pub card_holder_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] pub struct CardDetailsForNetworkTransactionId { pub card_number: cards::CardNumber, @@ -849,6 +858,12 @@ impl TryFrom for PaymentMethodData { card_holder_name, co_badged_card_data: None, })), + payment_methods::PaymentMethodCreateData::ProxyCard(_) => Err( + common_utils::errors::ValidationError::IncorrectValueProvided { + field_name: "Payment method data", + } + .into(), + ), } } } @@ -911,7 +926,10 @@ impl From for ExternalVaultPayment fn from(api_model_payment_method_data: api_models::payments::ProxyPaymentMethodData) -> Self { match api_model_payment_method_data { api_models::payments::ProxyPaymentMethodData::VaultDataCard(card_data) => { - Self::Card(ExternalVaultCard::from(card_data)) + Self::Card(Box::new(ExternalVaultCard::from(*card_data))) + } + api_models::payments::ProxyPaymentMethodData::VaultToken(vault_data) => { + Self::VaultToken(VaultToken::from(vault_data)) } } } @@ -924,6 +942,8 @@ impl From for ExternalVaultCard { card_exp_year, card_holder_name, card_cvc, + bin_number, + last_four, card_issuer, card_network, card_type, @@ -937,6 +957,8 @@ impl From for ExternalVaultCard { card_exp_month, card_exp_year, card_cvc, + bin_number, + last_four, card_issuer, card_network, card_type, @@ -948,6 +970,19 @@ impl From for ExternalVaultCard { } } } +impl From for VaultToken { + fn from(value: api_models::payments::VaultToken) -> Self { + let api_models::payments::VaultToken { + card_cvc, + card_holder_name, + } = value; + + Self { + card_cvc, + card_holder_name, + } + } +} impl From<( api_models::payments::Card, @@ -1041,6 +1076,26 @@ impl From for payment_methods::CardDetail { } } +#[cfg(feature = "v2")] +impl From for payment_methods::ProxyCardDetails { + fn from(card: ExternalVaultCard) -> 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: card.card_issuing_country, + card_network: card.card_network, + card_issuer: card.card_issuer, + card_type: card.card_type, + card_cvc: Some(card.card_cvc), + bin_number: card.bin_number, + last_four: card.last_four, + } + } +} + impl From for CardRedirectData { fn from(value: api_models::payments::CardRedirectData) -> Self { match value { diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index ff4d8bc0ae..0fa3d5b4e0 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -125,6 +125,9 @@ pub struct PaymentMethod { #[encrypt(ty = Value)] pub network_token_payment_method_data: Option>, + #[encrypt(ty = Value)] + pub external_vault_token_data: + Option>, } impl PaymentMethod { @@ -468,6 +471,7 @@ impl super::behaviour::Conversion for PaymentMethod { .network_token_payment_method_data .map(|val| val.into()), external_vault_source: self.external_vault_source, + external_vault_token_data: self.external_vault_token_data.map(|val| val.into()), }) } @@ -493,6 +497,7 @@ impl super::behaviour::Conversion for PaymentMethod { .payment_method_billing_address, network_token_payment_method_data: storage_model .network_token_payment_method_data, + external_vault_token_data: storage_model.external_vault_token_data, }, )), key_manager_identifier, @@ -535,6 +540,17 @@ impl super::behaviour::Conversion for PaymentMethod { .change_context(common_utils::errors::CryptoError::DecodingFailed) .attach_printable("Error while deserializing Network token Payment Method Data")?; + let external_vault_token_data = data + .external_vault_token_data + .map(|external_vault_token_data| { + external_vault_token_data.deserialize_inner_value(|value| { + value.parse_value("External Vault Token Data") + }) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing External Vault Token Data")?; + Ok::>(Self { customer_id: storage_model.customer_id, merchant_id: storage_model.merchant_id, @@ -560,6 +576,7 @@ impl super::behaviour::Conversion for PaymentMethod { network_token_locker_id: storage_model.network_token_locker_id, network_token_payment_method_data, external_vault_source: storage_model.external_vault_source, + external_vault_token_data, }) } .await @@ -596,6 +613,7 @@ impl super::behaviour::Conversion for PaymentMethod { network_token_payment_method_data: self .network_token_payment_method_data .map(|val| val.into()), + external_vault_token_data: self.external_vault_token_data.map(|val| val.into()), }) } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index a1ff44f533..c29c4d6020 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -767,7 +767,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, diff --git a/crates/hyperswitch_domain_models/src/vault.rs b/crates/hyperswitch_domain_models/src/vault.rs index b68e1d1543..9ba63f5023 100644 --- a/crates/hyperswitch_domain_models/src/vault.rs +++ b/crates/hyperswitch_domain_models/src/vault.rs @@ -1,6 +1,10 @@ use api_models::payment_methods; +#[cfg(feature = "v2")] +use error_stack; use serde::{Deserialize, Serialize}; +#[cfg(feature = "v2")] +use crate::errors; use crate::payment_method_data; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -49,10 +53,18 @@ impl VaultingDataInterface for PaymentMethodVaultingData { } } -impl From for PaymentMethodVaultingData { - fn from(item: payment_methods::PaymentMethodCreateData) -> Self { +#[cfg(feature = "v2")] +impl TryFrom for PaymentMethodVaultingData { + type Error = error_stack::Report; + fn try_from(item: payment_methods::PaymentMethodCreateData) -> Result { match item { - payment_methods::PaymentMethodCreateData::Card(card) => Self::Card(card), + payment_methods::PaymentMethodCreateData::Card(card) => Ok(Self::Card(card)), + payment_methods::PaymentMethodCreateData::ProxyCard(card) => Err( + errors::api_error_response::ApiErrorResponse::UnprocessableEntity { + message: "Proxy Card for PaymentMethodCreateData".to_string(), + } + .into(), + ), } } } diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 69de6450c9..553c7f174c 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -263,6 +263,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodUpdateData, api_models::payment_methods::CardDetailFromLocker, api_models::payment_methods::PaymentMethodCreateData, + api_models::payment_methods::ProxyCardDetails, api_models::payment_methods::CardDetail, api_models::payment_methods::CardDetailUpdate, api_models::payment_methods::CardType, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index c8320bcc63..1ea747d810 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -885,8 +885,14 @@ pub async fn create_payment_method( profile: &domain::Profile, ) -> RouterResponse { // 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?; + let (response, _payment_method) = Box::pin(create_payment_method_core( + state, + request_state, + req, + merchant_context, + profile, + )) + .await?; Ok(services::ApplicationResponse::Json(response)) } @@ -945,10 +951,57 @@ pub async fn create_payment_method_core( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; + match &req.payment_method_data { + api::PaymentMethodCreateData::Card(_) => { + create_payment_method_card_core( + state, + req, + merchant_context, + profile, + merchant_id, + &customer_id, + payment_method_id, + payment_method_billing_address, + ) + .await + } + api::PaymentMethodCreateData::ProxyCard(_) => { + create_payment_method_proxy_card_core( + state, + req, + merchant_context, + profile, + merchant_id, + &customer_id, + payment_method_id, + payment_method_billing_address, + ) + .await + } + } +} + +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn create_payment_method_card_core( + state: &SessionState, + req: api::PaymentMethodCreate, + merchant_context: &domain::MerchantContext, + profile: &domain::Profile, + merchant_id: &id_type::MerchantId, + customer_id: &id_type::GlobalCustomerId, + payment_method_id: id_type::GlobalPaymentMethodId, + payment_method_billing_address: Option< + Encryptable, + >, +) -> RouterResult<(api::PaymentMethodResponse, domain::PaymentMethod)> { + let db = &*state.store; + let payment_method = create_payment_method_for_intent( state, req.metadata.clone(), - &customer_id, + customer_id, payment_method_id, merchant_id, merchant_context.get_merchant_key_store(), @@ -958,7 +1011,7 @@ pub async fn create_payment_method_core( .await .attach_printable("failed to add payment method to db")?; - let payment_method_data = domain::PaymentMethodVaultingData::from(req.payment_method_data) + let payment_method_data = domain::PaymentMethodVaultingData::try_from(req.payment_method_data)? .populate_bin_details_for_payment_method(state) .await; @@ -968,7 +1021,7 @@ pub async fn create_payment_method_core( merchant_context, profile, None, - &customer_id, + customer_id, ) .await; @@ -978,7 +1031,7 @@ pub async fn create_payment_method_core( merchant_context, req.network_tokenization.clone(), profile.is_network_tokenization_enabled, - &customer_id, + customer_id, ) .await; @@ -1043,6 +1096,99 @@ pub async fn create_payment_method_core( Ok((response, payment_method)) } +// network tokenization and vaulting to locker is not required for proxy card since the card is already tokenized +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn create_payment_method_proxy_card_core( + state: &SessionState, + req: api::PaymentMethodCreate, + merchant_context: &domain::MerchantContext, + profile: &domain::Profile, + merchant_id: &id_type::MerchantId, + customer_id: &id_type::GlobalCustomerId, + payment_method_id: id_type::GlobalPaymentMethodId, + payment_method_billing_address: Option< + Encryptable, + >, +) -> RouterResult<(api::PaymentMethodResponse, domain::PaymentMethod)> { + let key_manager_state = &(state).into(); + + let external_vault_source = profile + .external_vault_connector_details + .clone() + .map(|details| details.vault_connector_id); + + let additional_payment_method_data = Some( + req.payment_method_data + .populate_bin_details_for_payment_method(state) + .await + .convert_to_additional_payment_method_data()?, + ); + + let encrypted_payment_method_data = additional_payment_method_data + .async_map(|payment_method_data| { + cards::create_encrypted_data( + key_manager_state, + merchant_context.get_merchant_key_store(), + payment_method_data, + ) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method data")? + .map(|encoded_pmd| { + encoded_pmd.deserialize_inner_value(|value| value.parse_value("PaymentMethodsData")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to parse Payment method data")?; + + let external_vault_token_data = req.payment_method_data.get_external_vault_token_data(); + + let encrypted_external_vault_token_data = external_vault_token_data + .async_map(|external_vault_token_data| { + cards::create_encrypted_data( + key_manager_state, + merchant_context.get_merchant_key_store(), + external_vault_token_data, + ) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt External vault token data")? + .map(|encoded_data| { + encoded_data + .deserialize_inner_value(|value| value.parse_value("ExternalVaultTokenData")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to parse External vault token data")?; + + let payment_method = create_payment_method_for_confirm( + state, + customer_id, + payment_method_id, + external_vault_source, + merchant_id, + merchant_context.get_merchant_key_store(), + merchant_context.get_merchant_account().storage_scheme, + req.payment_method_type, + req.payment_method_subtype, + payment_method_billing_address, + encrypted_payment_method_data, + encrypted_external_vault_token_data, + ) + .await?; + + let payment_method_response = + pm_transforms::generate_payment_method_response(&payment_method, &None)?; + + Ok((payment_method_response, payment_method)) +} + #[cfg(feature = "v2")] #[derive(Clone, Debug)] pub struct NetworkTokenPaymentMethodDetails { @@ -1183,6 +1329,14 @@ pub async fn populate_bin_details_for_payment_method( #[async_trait::async_trait] pub trait PaymentMethodExt { async fn populate_bin_details_for_payment_method(&self, state: &SessionState) -> Self; + + // convert to data format compatible to save in payment method table + fn convert_to_additional_payment_method_data( + &self, + ) -> RouterResult; + + // get tokens generated from external vault + fn get_external_vault_token_data(&self) -> Option; } #[cfg(feature = "v2")] @@ -1239,6 +1393,120 @@ impl PaymentMethodExt for domain::PaymentMethodVaultingData { _ => self.clone(), } } + + // Not implement because it is saved in locker and not in payment method table + fn convert_to_additional_payment_method_data( + &self, + ) -> RouterResult { + Err(report!(errors::ApiErrorResponse::UnprocessableEntity { + message: "Payment method data is not supported".to_string() + }) + .attach_printable("Payment method data is not supported")) + } + + fn get_external_vault_token_data(&self) -> Option { + None + } +} + +#[cfg(feature = "v2")] +#[async_trait::async_trait] +impl PaymentMethodExt for payment_methods::PaymentMethodCreateData { + async fn populate_bin_details_for_payment_method(&self, state: &SessionState) -> Self { + match self { + Self::ProxyCard(card) => { + let card_isin = card.bin_number.clone(); + + if card.card_issuer.is_some() + && card.card_network.is_some() + && card.card_type.is_some() + && card.card_issuing_country.is_some() + { + Self::ProxyCard(card.clone()) + } else if let Some(card_isin) = card_isin { + let card_info = state + .store + .get_card_info(&card_isin) + .await + .map_err(|error| services::logger::error!(card_info_error=?error)) + .ok() + .flatten(); + + Self::ProxyCard(payment_methods::ProxyCardDetails { + card_number: card.card_number.clone(), + card_exp_month: card.card_exp_month.clone(), + card_exp_year: card.card_exp_year.clone(), + card_holder_name: card.card_holder_name.clone(), + bin_number: card.bin_number.clone(), + last_four: card.last_four.clone(), + nick_name: card.nick_name.clone(), + card_issuing_country: card_info + .as_ref() + .and_then(|val| val.card_issuing_country.clone()), + card_network: card_info.as_ref().and_then(|val| val.card_network.clone()), + card_issuer: card_info.as_ref().and_then(|val| val.card_issuer.clone()), + card_type: card_info.as_ref().and_then(|val| val.card_type.clone()), + card_cvc: card.card_cvc.clone(), + }) + } else { + Self::ProxyCard(card.clone()) + } + } + _ => self.clone(), + } + } + + fn convert_to_additional_payment_method_data( + &self, + ) -> RouterResult { + match self.clone() { + Self::ProxyCard(card_details) => Ok(payment_methods::PaymentMethodsData::Card( + payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details.last_four, + expiry_month: Some(card_details.card_exp_month), + expiry_year: Some(card_details.card_exp_year), + card_holder_name: card_details.card_holder_name, + nick_name: card_details.nick_name, + issuer_country: card_details.card_issuing_country, + card_network: card_details.card_network, + card_issuer: card_details.card_issuer, + card_type: card_details.card_type, + card_isin: card_details.bin_number, + saved_to_locker: false, + co_badged_card_data: None, + }, + )), + Self::Card(card_details) => Ok(payment_methods::PaymentMethodsData::Card( + payment_methods::CardDetailsPaymentMethod { + expiry_month: Some(card_details.card_exp_month), + expiry_year: Some(card_details.card_exp_year), + card_holder_name: card_details.card_holder_name, + nick_name: card_details.nick_name, + issuer_country: card_details + .card_issuing_country + .map(|country| country.to_string()), + card_network: card_details.card_network, + card_issuer: card_details.card_issuer, + card_type: card_details + .card_type + .map(|card_type| card_type.to_string()), + saved_to_locker: false, + card_isin: None, + last4_digits: None, + co_badged_card_data: None, + }, + )), + } + } + + fn get_external_vault_token_data(&self) -> Option { + match self.clone() { + Self::ProxyCard(card_details) => Some(payment_methods::ExternalVaultTokenData { + tokenized_card_number: card_details.card_number, + }), + Self::Card(_) => None, + } + } } #[cfg(feature = "v2")] @@ -1684,6 +1952,7 @@ pub async fn create_payment_method_for_intent( network_token_payment_method_data: None, network_token_requestor_reference_id: None, external_vault_source: None, + external_vault_token_data: None, }, storage_scheme, ) @@ -1694,6 +1963,180 @@ pub async fn create_payment_method_for_intent( Ok(response) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn create_payment_method_for_confirm( + state: &SessionState, + customer_id: &id_type::GlobalCustomerId, + payment_method_id: id_type::GlobalPaymentMethodId, + external_vault_source: Option, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + payment_method_type: storage_enums::PaymentMethod, + payment_method_subtype: storage_enums::PaymentMethodType, + encrypted_payment_method_billing_address: Option< + Encryptable, + >, + encrypted_payment_method_data: Option>, + encrypted_external_vault_token_data: Option< + Encryptable, + >, +) -> CustomResult { + let db = &*state.store; + let key_manager_state = &state.into(); + let current_time = common_utils::date_time::now(); + + let response = db + .insert_payment_method( + key_manager_state, + key_store, + domain::PaymentMethod { + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + id: payment_method_id, + locker_id: None, + payment_method_type: Some(payment_method_type), + payment_method_subtype: Some(payment_method_subtype), + payment_method_data: encrypted_payment_method_data, + connector_mandate_details: None, + customer_acceptance: None, + client_secret: None, + status: enums::PaymentMethodStatus::Inactive, + network_transaction_id: None, + created_at: current_time, + last_modified: current_time, + last_used_at: current_time, + payment_method_billing_address: encrypted_payment_method_billing_address, + updated_by: None, + version: common_types::consts::API_VERSION, + locker_fingerprint_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, + network_token_requestor_reference_id: None, + external_vault_source, + external_vault_token_data: encrypted_external_vault_token_data, + }, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + + Ok(response) +} + +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn get_external_vault_token( + state: &SessionState, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + payment_token: String, + vault_token: payment_method_data::VaultToken, + payment_method_type: &storage_enums::PaymentMethod, +) -> CustomResult { + let db = &*state.store; + + let pm_token_data = + utils::retrieve_payment_token_data(state, payment_token, 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 payment_method = db + .find_payment_method(&state.into(), key_store, &payment_method_id, storage_scheme) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Payment method not found")?; + + let external_vault_token_data = payment_method + .external_vault_token_data + .clone() + .map(Encryptable::into_inner) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Missing vault token data")?; + + let decrypted_addtional_payment_method_data = payment_method + .payment_method_data + .clone() + .map(Encryptable::into_inner) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert payment method data")?; + + convert_from_saved_payment_method_data( + decrypted_addtional_payment_method_data, + external_vault_token_data, + vault_token, + ) + .attach_printable("Failed to convert payment method data") +} + +#[cfg(feature = "v2")] +fn convert_from_saved_payment_method_data( + vault_additional_data: payment_methods::PaymentMethodsData, + external_vault_token_data: payment_methods::ExternalVaultTokenData, + vault_token: payment_method_data::VaultToken, +) -> RouterResult { + match vault_additional_data { + payment_methods::PaymentMethodsData::Card(card_details) => { + Ok(payment_method_data::ExternalVaultPaymentMethodData::Card( + Box::new(domain::ExternalVaultCard { + card_number: external_vault_token_data.tokenized_card_number, + card_exp_month: card_details.expiry_month.ok_or( + errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_details.expiry_month", + }, + )?, + card_exp_year: card_details.expiry_year.ok_or( + errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_details.expiry_year", + }, + )?, + card_holder_name: card_details.card_holder_name, + bin_number: card_details.card_isin, + last_four: card_details.last4_digits, + nick_name: card_details.nick_name, + card_issuing_country: card_details.issuer_country, + card_network: card_details.card_network, + card_issuer: card_details.card_issuer, + card_type: card_details.card_type, + card_cvc: vault_token.card_cvc, + co_badged_card_data: None, // Co-badged data is not supported in external vault + bank_code: None, // Bank code is not stored in external vault + }), + )) + } + payment_methods::PaymentMethodsData::BankDetails(_) + | payment_methods::PaymentMethodsData::WalletDetails(_) => { + Err(errors::ApiErrorResponse::UnprocessableEntity { + message: "External vaulting is not supported for this payment method type" + .to_string(), + } + .into()) + } + } +} + #[cfg(feature = "v2")] /// Update the connector_mandate_details of the payment method with /// new token details for the payment @@ -3014,13 +3457,13 @@ pub async fn payment_methods_session_confirm( ) .attach_printable("Failed to create payment method request")?; - let (payment_method_response, payment_method) = create_payment_method_core( + let (payment_method_response, payment_method) = Box::pin(create_payment_method_core( &state, &req_state, create_payment_method_request.clone(), &merchant_context, &profile, - ) + )) .await?; let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 133fea4101..7e2c5876ac 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1544,7 +1544,7 @@ pub async fn insert_cvc_using_payment_token( fulfillment_time: i64, encryption_key: &masking::Secret>, ) -> RouterResult<()> { - let card_cvc = domain::PaymentMethodVaultingData::from(payment_method_data) + let card_cvc = domain::PaymentMethodVaultingData::try_from(payment_method_data)? .get_card() .and_then(|card| card.card_cvc.clone()); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2dfb3d25e3..d1d7c33d8f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1563,6 +1563,11 @@ where .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + operation + .to_domain()? + .create_or_fetch_payment_method(state, &merchant_context, &profile, &mut payment_data) + .await?; + // consume the req merchant_connector_id and set it in the payment_data let connector = operation .to_domain()? @@ -1613,6 +1618,15 @@ where updated_customer, ) .await?; + + // update payment method if its a successful transaction + if router_data.status.is_success() { + operation + .to_domain()? + .update_payment_method(state, &merchant_context, &mut payment_data) + .await; + } + let payments_response_operation = Box::new(PaymentResponse); payments_response_operation diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 1c793209a7..b9c36a1f79 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -403,6 +403,17 @@ pub trait Domain: Send + Sync { Ok(()) } + // does not propagate error to not affect the payment flow + // must add debugger in case of internal error + #[cfg(feature = "v2")] + async fn update_payment_method<'a>( + &'a self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + payment_data: &mut D, + ) { + } + /// This function is used to apply the 3DS authentication strategy async fn apply_three_ds_authentication_strategy<'a>( &'a self, diff --git a/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs b/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs index 7e58dc95c1..56cf72a622 100644 --- a/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs +++ b/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs @@ -1,8 +1,12 @@ use api_models::payments::ExternalVaultProxyPaymentsRequest; use async_trait::async_trait; use common_enums::enums; -use common_utils::types::keymanager::ToEncryptable; -use error_stack::ResultExt; +use common_utils::{ + crypto::Encryptable, + ext_traits::{AsyncExt, ValueExt}, + types::keymanager::ToEncryptable, +}; +use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, payments::PaymentConfirmData, }; @@ -14,6 +18,7 @@ use super::{Domain, GetTracker, Operation, PostUpdateTracker, UpdateTracker, Val use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payment_methods::{self, PaymentMethodExt}, payments::{ self, operations::{self, ValidateStatusForOperation}, @@ -336,6 +341,95 @@ impl Domain( + &'a self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + business_profile: &domain::Profile, + payment_data: &mut PaymentConfirmData, + ) -> CustomResult<(), errors::ApiErrorResponse> { + match ( + payment_data.payment_intent.customer_id.clone(), + payment_data.external_vault_pmd.clone(), + payment_data.payment_attempt.customer_acceptance.clone(), + payment_data.payment_attempt.payment_token.clone(), + ) { + (Some(customer_id), Some(hyperswitch_domain_models::payment_method_data::ExternalVaultPaymentMethodData::Card(card_details)), Some(_), None) => { + + let payment_method_data = + api::PaymentMethodCreateData::ProxyCard(api::ProxyCardDetails::from(*card_details)); + let billing = payment_data + .payment_address + .get_payment_method_billing() + .cloned() + .map(From::from); + + 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, + billing, + psp_tokenization: None, + network_tokenization: None, + }; + + let (_pm_response, payment_method) = Box::pin(payment_methods::create_payment_method_core( + state, + &state.get_req_state(), + req, + merchant_context, + business_profile, + )) + .await?; + + payment_data.payment_method = Some(payment_method); + } + (_, Some(hyperswitch_domain_models::payment_method_data::ExternalVaultPaymentMethodData::VaultToken(vault_token)), None, Some(payment_token)) => { + payment_data.external_vault_pmd = Some(payment_methods::get_external_vault_token( + state, + merchant_context.get_merchant_key_store(), + merchant_context.get_merchant_account().storage_scheme, + payment_token.clone(), + vault_token.clone(), + &payment_data.payment_attempt.payment_method_type + ) + .await?); + } + _ => { + router_env::logger::debug!( + "No payment method to create or fetch for external vault proxy payment intent" + ); + } + } + + Ok(()) + } + + #[cfg(feature = "v2")] + async fn update_payment_method<'a>( + &'a self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + payment_data: &mut PaymentConfirmData, + ) { + if let (true, Some(payment_method_id)) = ( + payment_data.payment_attempt.customer_acceptance.is_some(), + payment_data.payment_attempt.payment_method_id.clone(), + ) { + payment_methods::update_payment_method_status_internal( + state, + merchant_context.get_merchant_key_store(), + merchant_context.get_merchant_account().storage_scheme, + common_enums::PaymentMethodStatus::Active, + &payment_method_id, + ) + .await + .map_err(|err| router_env::logger::error!(err=?err)); + }; + } + #[instrument(skip_all)] async fn populate_payment_data<'a>( &'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 c2f3e414fa..ccbe540dfc 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -496,14 +496,15 @@ impl Domain { + Err(UnifiedConnectorServiceError::NotImplemented(format!( + "Unimplemented payment method subtype: {payment_method_type:?}" + )) + .into()) + } } } pub fn build_unified_connector_service_auth_metadata( diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index df3f4d466c..ed05d5aebe 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -10,9 +10,9 @@ pub use api_models::payment_methods::{ PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, - RequestPaymentMethodTypes, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted, - TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, - TokenizedWalletValue2, TotalPaymentMethodCountResponse, + ProxyCardDetails, RequestPaymentMethodTypes, TokenDataResponse, TokenDetailsResponse, + TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, + TokenizedWalletValue1, TokenizedWalletValue2, TotalPaymentMethodCountResponse, }; #[cfg(feature = "v1")] pub use api_models::payment_methods::{ diff --git a/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/down.sql b/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/down.sql new file mode 100644 index 0000000000..322b2e59ed --- /dev/null +++ b/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_methods DROP COLUMN IF EXISTS external_vault_token_data; \ No newline at end of file diff --git a/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/up.sql b/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/up.sql new file mode 100644 index 0000000000..516ff653d0 --- /dev/null +++ b/v2_compatible_migrations/2025-07-29-080133_add-external_vault_token_data-column-to-payment-methods/up.sql @@ -0,0 +1 @@ +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS external_vault_token_data BYTEA; \ No newline at end of file