From 0b972e38abd08380b75165dfd755087769f35a62 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:10:42 +0530 Subject: [PATCH] feat(payment_methods_v2): add support for network tokenization (#7145) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 8 + crates/api_models/src/payment_methods.rs | 14 +- crates/cards/src/lib.rs | 2 +- crates/cards/src/validate.rs | 80 +++ crates/hyperswitch_connectors/Cargo.toml | 1 + .../connectors/cybersource/transformers.rs | 11 +- crates/hyperswitch_connectors/src/utils.rs | 61 +++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../src/network_tokenization.rs | 231 ++++++++ .../src/payment_method_data.rs | 120 ++++- .../src/router_data.rs | 7 +- crates/router/Cargo.toml | 4 +- .../src/connector/adyen/transformers.rs | 36 +- crates/router/src/connector/utils.rs | 79 +++ crates/router/src/core/errors.rs | 2 + crates/router/src/core/payment_methods.rs | 188 ++++++- .../payment_methods/network_tokenization.rs | 492 ++++++++++++------ .../src/core/payment_methods/transformers.rs | 1 + crates/router/src/routes/payment_methods.rs | 1 + crates/router/src/types/domain.rs | 5 + crates/router/src/types/payment_methods.rs | 4 + 21 files changed, 1159 insertions(+), 189 deletions(-) create mode 100644 crates/hyperswitch_domain_models/src/network_tokenization.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 3a43b1bfca..cedd4abcca 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13781,6 +13781,14 @@ } ], "nullable": true + }, + "network_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTokenization" + } + ], + "nullable": true } }, "additionalProperties": false diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index ba18cbba50..a228f5fbda 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -135,6 +135,10 @@ pub struct PaymentMethodCreate { /// The billing details of the payment method #[schema(value_type = Option
)] pub billing: Option, + + /// The network tokenization configuration if applicable + #[schema(value_type = Option)] + pub network_tokenization: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -547,7 +551,15 @@ pub struct CardDetail { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive( - Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, strum::EnumString, strum::Display, + Debug, + serde::Deserialize, + serde::Serialize, + Clone, + ToSchema, + strum::EnumString, + strum::Display, + Eq, + PartialEq, )] #[serde(rename_all = "snake_case")] pub enum CardType { diff --git a/crates/cards/src/lib.rs b/crates/cards/src/lib.rs index 0c0fb9d47a..765fa7c6f8 100644 --- a/crates/cards/src/lib.rs +++ b/crates/cards/src/lib.rs @@ -7,7 +7,7 @@ use masking::{PeekInterface, StrongSecret}; use serde::{de, Deserialize, Serialize}; use time::{util::days_in_year_month, Date, Duration, PrimitiveDateTime, Time}; -pub use crate::validate::{CardNumber, CardNumberStrategy, CardNumberValidationErr}; +pub use crate::validate::{CardNumber, CardNumberStrategy, CardNumberValidationErr, NetworkToken}; #[derive(Serialize)] pub struct CardSecurityCode(StrongSecret); diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index 725d05b480..b8b9cdbf18 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -24,6 +24,10 @@ pub struct CardNumberValidationErr(&'static str); #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] pub struct CardNumber(StrongSecret); +//Network Token +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct NetworkToken(StrongSecret); + impl CardNumber { pub fn get_card_isin(&self) -> String { self.0.peek().chars().take(6).collect::() @@ -102,6 +106,30 @@ impl CardNumber { } } +impl NetworkToken { + pub fn get_card_isin(&self) -> String { + self.0.peek().chars().take(6).collect::() + } + + pub fn get_extended_card_bin(&self) -> String { + self.0.peek().chars().take(8).collect::() + } + pub fn get_card_no(&self) -> String { + self.0.peek().chars().collect::() + } + pub fn get_last4(&self) -> String { + self.0 + .peek() + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::() + } +} + impl FromStr for CardNumber { type Err = CardNumberValidationErr; @@ -131,6 +159,35 @@ impl FromStr for CardNumber { } } +impl FromStr for NetworkToken { + type Err = CardNumberValidationErr; + + fn from_str(network_token: &str) -> Result { + // Valid test cards for threedsecureio + let valid_test_network_tokens = vec![ + "4000100511112003", + "6000100611111203", + "3000100811111072", + "9000100111111111", + ]; + #[cfg(not(target_arch = "wasm32"))] + let valid_test_network_tokens = match router_env_which() { + Env::Development | Env::Sandbox => valid_test_network_tokens, + Env::Production => vec![], + }; + + let network_token = network_token.split_whitespace().collect::(); + + let is_network_token_valid = sanitize_card_number(&network_token)?; + + if valid_test_network_tokens.contains(&network_token.as_str()) || is_network_token_valid { + Ok(Self(StrongSecret::new(network_token))) + } else { + Err(CardNumberValidationErr("network token invalid")) + } + } +} + pub fn sanitize_card_number(card_number: &str) -> Result { let is_card_number_valid = Ok(card_number) .and_then(validate_card_number_chars) @@ -195,6 +252,14 @@ impl TryFrom for CardNumber { } } +impl TryFrom for NetworkToken { + type Error = CardNumberValidationErr; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + impl Deref for CardNumber { type Target = StrongSecret; @@ -203,6 +268,14 @@ impl Deref for CardNumber { } } +impl Deref for NetworkToken { + type Target = StrongSecret; + + fn deref(&self) -> &StrongSecret { + &self.0 + } +} + impl<'de> Deserialize<'de> for CardNumber { fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; @@ -210,6 +283,13 @@ impl<'de> Deserialize<'de> for CardNumber { } } +impl<'de> Deserialize<'de> for NetworkToken { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + pub enum CardNumberStrategy {} impl Strategy for CardNumberStrategy diff --git a/crates/hyperswitch_connectors/Cargo.toml b/crates/hyperswitch_connectors/Cargo.toml index 6b76e6ac19..13d5266dab 100644 --- a/crates/hyperswitch_connectors/Cargo.toml +++ b/crates/hyperswitch_connectors/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true frm = ["hyperswitch_domain_models/frm", "hyperswitch_interfaces/frm"] payouts = ["hyperswitch_domain_models/payouts", "api_models/payouts", "hyperswitch_interfaces/payouts"] v1 = ["api_models/v1", "hyperswitch_domain_models/v1", "common_utils/v1"] +v2 = ["api_models/v2", "hyperswitch_domain_models/v2", "common_utils/v2"] [dependencies] actix-http = "3.6.0" diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index b42c02606c..b953fa7375 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -18,6 +18,7 @@ use hyperswitch_domain_models::{ types::PayoutsRouterData, }; use hyperswitch_domain_models::{ + network_tokenization::NetworkTokenNumber, payment_method_data::{ ApplePayWalletData, GooglePayWalletData, NetworkTokenData, PaymentMethodData, SamsungPayWalletData, WalletData, @@ -430,7 +431,7 @@ pub struct CaptureOptions { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct NetworkTokenizedCard { - number: cards::CardNumber, + number: NetworkTokenNumber, expiration_month: Secret, expiration_year: Secret, cryptogram: Option>, @@ -1400,10 +1401,10 @@ impl let payment_information = PaymentInformation::NetworkToken(Box::new(NetworkTokenPaymentInformation { tokenized_card: NetworkTokenizedCard { - number: token_data.token_number, - expiration_month: token_data.token_exp_month, - expiration_year: token_data.token_exp_year, - cryptogram: token_data.token_cryptogram.clone(), + number: token_data.get_network_token(), + expiration_month: token_data.get_network_token_expiry_month(), + expiration_year: token_data.get_network_token_expiry_year(), + cryptogram: token_data.get_cryptogram().clone(), transaction_type: "1".to_string(), }, })); diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 6c39667de4..b14abec9d3 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -19,6 +19,7 @@ use common_utils::{ use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ address::{Address, AddressDetails, PhoneDetails}, + network_tokenization::NetworkTokenNumber, payment_method_data::{self, Card, CardDetailsForNetworkTransactionId, PaymentMethodData}, router_data::{ ApplePayPredecryptData, ErrorResponse, PaymentMethodToken, RecurringMandatePaymentData, @@ -2957,13 +2958,24 @@ impl CardData for api_models::payouts::CardPayout { pub trait NetworkTokenData { fn get_card_issuer(&self) -> Result; fn get_expiry_year_4_digit(&self) -> Secret; + fn get_network_token(&self) -> NetworkTokenNumber; + fn get_network_token_expiry_month(&self) -> Secret; + fn get_network_token_expiry_year(&self) -> Secret; + fn get_cryptogram(&self) -> Option>; } impl NetworkTokenData for payment_method_data::NetworkTokenData { + #[cfg(feature = "v1")] fn get_card_issuer(&self) -> Result { get_card_issuer(self.token_number.peek()) } + #[cfg(feature = "v2")] + fn get_card_issuer(&self) -> Result { + get_card_issuer(self.network_token.peek()) + } + + #[cfg(feature = "v1")] fn get_expiry_year_4_digit(&self) -> Secret { let mut year = self.token_exp_year.peek().clone(); if year.len() == 2 { @@ -2971,4 +2983,53 @@ impl NetworkTokenData for payment_method_data::NetworkTokenData { } Secret::new(year) } + + #[cfg(feature = "v2")] + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.network_token_exp_year.peek().clone(); + if year.len() == 2 { + year = format!("20{}", year); + } + Secret::new(year) + } + + #[cfg(feature = "v1")] + fn get_network_token(&self) -> NetworkTokenNumber { + self.token_number.clone() + } + + #[cfg(feature = "v2")] + fn get_network_token(&self) -> NetworkTokenNumber { + self.network_token.clone() + } + + #[cfg(feature = "v1")] + fn get_network_token_expiry_month(&self) -> Secret { + self.token_exp_month.clone() + } + + #[cfg(feature = "v2")] + fn get_network_token_expiry_month(&self) -> Secret { + self.network_token_exp_month.clone() + } + + #[cfg(feature = "v1")] + fn get_network_token_expiry_year(&self) -> Secret { + self.token_exp_year.clone() + } + + #[cfg(feature = "v2")] + fn get_network_token_expiry_year(&self) -> Secret { + self.network_token_exp_year.clone() + } + + #[cfg(feature = "v1")] + fn get_cryptogram(&self) -> Option> { + self.token_cryptogram.clone() + } + + #[cfg(feature = "v2")] + fn get_cryptogram(&self) -> Option> { + self.cryptogram.clone() + } } diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index b2546e3a04..6c8f499388 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -11,6 +11,7 @@ pub mod mandates; pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; +pub mod network_tokenization; pub mod payment_address; pub mod payment_method_data; pub mod payment_methods; diff --git a/crates/hyperswitch_domain_models/src/network_tokenization.rs b/crates/hyperswitch_domain_models/src/network_tokenization.rs new file mode 100644 index 0000000000..bf87c75050 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/network_tokenization.rs @@ -0,0 +1,231 @@ +use std::fmt::Debug; + +use api_models::enums as api_enums; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use cards::CardNumber; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use cards::{CardNumber, NetworkToken}; +use common_utils::id_type; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardData { + pub card_number: CardNumber, + pub exp_month: Secret, + pub exp_year: Secret, + pub card_security_code: Secret, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardData { + pub card_number: CardNumber, + pub exp_month: Secret, + pub exp_year: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + pub card_security_code: Option>, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderData { + pub consent_id: String, + pub customer_id: id_type::CustomerId, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderData { + pub consent_id: String, + pub customer_id: id_type::GlobalCustomerId, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiPayload { + pub service: String, + pub card_data: Secret, //encrypted card data + pub order_data: OrderData, + pub key_id: String, + pub should_send_token: bool, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct CardNetworkTokenResponse { + pub payload: Secret, //encrypted payload +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardNetworkTokenResponsePayload { + pub card_brand: api_enums::CardNetwork, + pub card_fingerprint: Option>, + pub card_reference: String, + pub correlation_id: String, + pub customer_id: String, + pub par: String, + pub token: CardNumber, + pub token_expiry_month: Secret, + pub token_expiry_year: Secret, + pub token_isin: String, + pub token_last_four: String, + pub token_status: String, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateNetworkTokenResponsePayload { + pub card_brand: api_enums::CardNetwork, + pub card_fingerprint: Option>, + pub card_reference: String, + pub correlation_id: String, + pub customer_id: String, + pub par: String, + pub token: NetworkToken, + pub token_expiry_month: Secret, + pub token_expiry_year: Secret, + pub token_isin: String, + pub token_last_four: String, + pub token_status: String, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Serialize)] +pub struct GetCardToken { + pub card_reference: String, + pub customer_id: id_type::CustomerId, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Serialize)] +pub struct GetCardToken { + pub card_reference: String, + pub customer_id: id_type::GlobalCustomerId, +} +#[derive(Debug, Deserialize)] +pub struct AuthenticationDetails { + pub cryptogram: Secret, + pub token: CardNumber, //network token +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenDetails { + pub exp_month: Secret, + pub exp_year: Secret, +} + +#[derive(Debug, Deserialize)] +pub struct TokenResponse { + pub authentication_details: AuthenticationDetails, + pub network: api_enums::CardNetwork, + pub token_details: TokenDetails, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteCardToken { + pub card_reference: String, //network token requestor ref id + pub customer_id: id_type::CustomerId, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteCardToken { + pub card_reference: String, //network token requestor ref id + pub customer_id: id_type::GlobalCustomerId, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum DeleteNetworkTokenStatus { + Success, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkTokenErrorInfo { + pub code: String, + pub developer_message: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkTokenErrorResponse { + pub error_message: String, + pub error_info: NetworkTokenErrorInfo, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct DeleteNetworkTokenResponse { + pub status: DeleteNetworkTokenStatus, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckTokenStatus { + pub card_reference: String, + pub customer_id: id_type::CustomerId, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckTokenStatus { + pub card_reference: String, + pub customer_id: id_type::GlobalCustomerId, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum TokenStatus { + Active, + Inactive, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckTokenStatusResponsePayload { + pub token_expiry_month: Secret, + pub token_expiry_year: Secret, + pub token_status: TokenStatus, +} + +#[derive(Debug, Deserialize)] +pub struct CheckTokenStatusResponse { + pub payload: CheckTokenStatusResponsePayload, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +pub type NetworkTokenNumber = CardNumber; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub type NetworkTokenNumber = NetworkToken; diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index e96368080d..2f8375b47c 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -1,5 +1,6 @@ use api_models::{ - mandates, payment_methods, + mandates, + payment_methods::{self}, payments::{ additional_info as payment_additional_types, AmazonPayRedirectData, ExtendedCardInfo, }, @@ -587,6 +588,10 @@ pub struct SepaAndBacsBillingDetails { pub name: Secret, } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] pub struct NetworkTokenData { pub token_number: cards::CardNumber, @@ -602,6 +607,37 @@ pub struct NetworkTokenData { pub eci: Option, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] +pub struct NetworkTokenData { + pub network_token: cards::NetworkToken, + pub network_token_exp_month: Secret, + pub network_token_exp_year: Secret, + pub cryptogram: Option>, + pub card_issuer: Option, //since network token is tied to card, so its issuer will be same as card issuer + pub card_network: Option, + pub card_type: Option, + pub card_issuing_country: Option, + pub bank_code: Option, + pub card_holder_name: Option>, + pub nick_name: Option>, + pub eci: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] +pub struct NetworkTokenDetails { + pub network_token: cards::NetworkToken, + pub network_token_exp_month: Secret, + pub network_token_exp_year: Secret, + pub card_issuer: Option, //since network token is tied to card, so its issuer will be same as card issuer + pub card_network: Option, + pub card_type: Option, + pub card_issuing_country: Option, + pub card_holder_name: Option>, + pub nick_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MobilePaymentData { @@ -1726,3 +1762,85 @@ impl From for payment_methods::PaymentMethodDataWalletInfo } } } + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum PaymentMethodsData { + Card(CardDetailsPaymentMethod), + BankDetails(payment_methods::PaymentMethodDataBankCreds), //PaymentMethodDataBankCreds and its transformations should be moved to the domain models + WalletDetails(payment_methods::PaymentMethodDataWalletInfo), //PaymentMethodDataWalletInfo and its transformations should be moved to the domain models + NetworkToken(NetworkTokenDetailsPaymentMethod), +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct NetworkTokenDetailsPaymentMethod { + pub last4_digits: Option, + pub issuer_country: Option, + pub network_token_expiry_month: Option>, + pub network_token_expiry_year: Option>, + pub nick_name: Option>, + pub card_holder_name: Option>, + pub card_isin: Option, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + #[serde(default = "saved_in_locker_default")] + pub saved_to_locker: bool, +} + +fn saved_in_locker_default() -> bool { + true +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct CardDetailsPaymentMethod { + pub last4_digits: Option, + pub issuer_country: Option, + pub expiry_month: Option>, + pub expiry_year: Option>, + pub nick_name: Option>, + pub card_holder_name: Option>, + pub card_isin: Option, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + #[serde(default = "saved_in_locker_default")] + pub saved_to_locker: bool, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for CardDetailsPaymentMethod { + fn from(item: payment_methods::CardDetail) -> Self { + Self { + issuer_country: item.card_issuing_country.map(|c| c.to_string()), + last4_digits: Some(item.card_number.get_last4()), + expiry_month: Some(item.card_exp_month), + expiry_year: Some(item.card_exp_year), + card_holder_name: item.card_holder_name, + nick_name: item.nick_name, + card_isin: None, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type.map(|card| card.to_string()), + saved_to_locker: true, + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for NetworkTokenDetailsPaymentMethod { + fn from(item: NetworkTokenDetails) -> Self { + Self { + issuer_country: item.card_issuing_country.map(|c| c.to_string()), + last4_digits: Some(item.network_token.get_last4()), + network_token_expiry_month: Some(item.network_token_exp_month), + network_token_expiry_year: Some(item.network_token_exp_year), + card_holder_name: item.card_holder_name, + nick_name: item.nick_name, + card_isin: None, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type.map(|card| card.to_string()), + saved_to_locker: true, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 0c90b0d024..e3a8264217 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -9,7 +9,10 @@ use common_utils::{ use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; -use crate::{payment_address::PaymentAddress, payment_method_data, payments}; +use crate::{ + network_tokenization::NetworkTokenNumber, payment_address::PaymentAddress, payment_method_data, + payments, +}; #[cfg(feature = "v2")] use crate::{ payments::{ @@ -294,7 +297,7 @@ pub struct PazeDecryptedData { #[derive(Debug, Clone, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct PazeToken { - pub payment_token: cards::CardNumber, + pub payment_token: NetworkTokenNumber, pub token_expiration_month: Secret, pub token_expiration_year: Secret, pub payment_account_reference: Secret, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 6257d3e083..d590797d64 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -33,8 +33,8 @@ payouts = ["api_models/payouts", "common_enums/payouts", "hyperswitch_connectors payout_retry = ["payouts"] recon = ["email", "api_models/recon"] retry = [] -v2 = ["customer_v2", "payment_methods_v2", "common_default", "api_models/v2", "diesel_models/v2", "hyperswitch_domain_models/v2", "storage_impl/v2", "kgraph_utils/v2", "common_utils/v2"] -v1 = ["common_default", "api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "storage_impl/v1", "hyperswitch_interfaces/v1", "kgraph_utils/v1", "common_utils/v1"] +v2 = ["customer_v2", "payment_methods_v2", "common_default", "api_models/v2", "diesel_models/v2", "hyperswitch_domain_models/v2", "storage_impl/v2", "kgraph_utils/v2", "common_utils/v2", "hyperswitch_connectors/v2"] +v1 = ["common_default", "api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "storage_impl/v1", "hyperswitch_interfaces/v1", "kgraph_utils/v1", "common_utils/v1", "hyperswitch_connectors/v1"] customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2", "storage_impl/customer_v2"] payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2", "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2", "common_utils/payment_methods_v2"] dynamic_routing = ["external_services/dynamic_routing", "storage_impl/dynamic_routing", "api_models/dynamic_routing"] diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 465730cca6..c9bfdbf3d4 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -4,7 +4,9 @@ use api_models::{enums, payments, webhooks}; use cards::CardNumber; use common_utils::{errors::ParsingError, ext_traits::Encode, id_type, pii, types::MinorUnit}; use error_stack::{report, ResultExt}; -use hyperswitch_domain_models::router_request_types::SubmitEvidenceRequestData; +use hyperswitch_domain_models::{ + network_tokenization::NetworkTokenNumber, router_request_types::SubmitEvidenceRequestData, +}; use masking::{ExposeInterface, PeekInterface}; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -637,6 +639,7 @@ pub enum AdyenPaymentMethod<'a> { PayEasy(Box), Pix(Box), NetworkToken(Box), + AdyenPaze(Box), } #[derive(Debug, Clone, Serialize)] @@ -1142,6 +1145,21 @@ pub struct AdyenCard { network_payment_reference: Option>, } +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenPazeData { + #[serde(rename = "type")] + payment_type: PaymentType, + number: NetworkTokenNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Option>, + holder_name: Option>, + brand: Option, //Mandatory for mandate using network_txns_id + network_payment_reference: Option>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CardBrand { @@ -1241,7 +1259,7 @@ pub struct AdyenApplePay { pub struct AdyenNetworkTokenData { #[serde(rename = "type")] payment_type: PaymentType, - number: CardNumber, + number: NetworkTokenNumber, expiry_month: Secret, expiry_year: Secret, holder_name: Option>, @@ -2200,7 +2218,7 @@ impl TryFrom<(&domain::WalletData, &types::PaymentsAuthorizeRouterData)> } domain::WalletData::Paze(_) => match item.payment_method_token.clone() { Some(types::PaymentMethodToken::PazeDecrypt(paze_decrypted_data)) => { - let data = AdyenCard { + let data = AdyenPazeData { payment_type: PaymentType::NetworkToken, number: paze_decrypted_data.token.payment_token, expiry_month: paze_decrypted_data.token.token_expiration_month, @@ -2214,7 +2232,7 @@ impl TryFrom<(&domain::WalletData, &types::PaymentsAuthorizeRouterData)> .and_then(get_adyen_card_network), network_payment_reference: None, }; - Ok(AdyenPaymentMethod::AdyenCard(Box::new(data))) + Ok(AdyenPaymentMethod::AdyenPaze(Box::new(data))) } _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), @@ -2725,8 +2743,8 @@ impl let card_holder_name = item.router_data.get_optional_billing_full_name(); let adyen_network_token = AdyenNetworkTokenData { payment_type: PaymentType::NetworkToken, - number: token_data.token_number.clone(), - expiry_month: token_data.token_exp_month.clone(), + number: token_data.get_network_token(), + expiry_month: token_data.get_network_token_expiry_month(), expiry_year: token_data.get_expiry_year_4_digit(), holder_name: card_holder_name, brand: Some(brand), // FIXME: Remove hardcoding @@ -5577,8 +5595,8 @@ impl TryFrom<(&domain::NetworkTokenData, Option>)> for AdyenPayme ) -> Result { let adyen_network_token = AdyenNetworkTokenData { payment_type: PaymentType::NetworkToken, - number: token_data.token_number.clone(), - expiry_month: token_data.token_exp_month.clone(), + number: token_data.get_network_token(), + expiry_month: token_data.get_network_token_expiry_month(), expiry_year: token_data.get_expiry_year_4_digit(), holder_name: card_holder_name, brand: None, // FIXME: Remove hardcoding @@ -5627,7 +5645,7 @@ impl directory_response: "Y".to_string(), authentication_response: "Y".to_string(), token_authentication_verification_value: token_data - .token_cryptogram + .get_cryptogram() .clone() .unwrap_or_default(), eci: Some("02".to_string()), diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 28f5d016e5..0aca263b5c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -22,6 +22,7 @@ use diesel_models::{enums, types::OrderDetailsWithAmount}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ mandates, + network_tokenization::NetworkTokenNumber, payments::payment_attempt::PaymentAttempt, router_request_types::{ AuthoriseIntegrityObject, CaptureIntegrityObject, RefundIntegrityObject, @@ -3181,13 +3182,30 @@ pub fn get_refund_integrity_object( pub trait NetworkTokenData { fn get_card_issuer(&self) -> Result; fn get_expiry_year_4_digit(&self) -> Secret; + fn get_network_token(&self) -> NetworkTokenNumber; + fn get_network_token_expiry_month(&self) -> Secret; + fn get_network_token_expiry_year(&self) -> Secret; + fn get_cryptogram(&self) -> Option>; } impl NetworkTokenData for domain::NetworkTokenData { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] fn get_card_issuer(&self) -> Result { get_card_issuer(self.token_number.peek()) } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_card_issuer(&self) -> Result { + get_card_issuer(self.network_token.peek()) + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] fn get_expiry_year_4_digit(&self) -> Secret { let mut year = self.token_exp_year.peek().clone(); if year.len() == 2 { @@ -3195,6 +3213,67 @@ impl NetworkTokenData for domain::NetworkTokenData { } Secret::new(year) } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.network_token_exp_year.peek().clone(); + if year.len() == 2 { + year = format!("20{}", year); + } + Secret::new(year) + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + fn get_network_token(&self) -> NetworkTokenNumber { + self.token_number.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_network_token(&self) -> NetworkTokenNumber { + self.network_token.clone() + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + fn get_network_token_expiry_month(&self) -> Secret { + self.token_exp_month.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_network_token_expiry_month(&self) -> Secret { + self.network_token_exp_month.clone() + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + fn get_network_token_expiry_year(&self) -> Secret { + self.token_exp_year.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_network_token_expiry_year(&self) -> Secret { + self.network_token_exp_year.clone() + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + fn get_cryptogram(&self) -> Option> { + self.token_cryptogram.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_cryptogram(&self) -> Option> { + self.cryptogram.clone() + } } pub fn convert_uppercase<'de, D, T>(v: D) -> Result diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 506ce55719..3dbaa99921 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -442,4 +442,6 @@ pub enum NetworkTokenizationError { DeleteNetworkTokenFailed, #[error("Network token service not configured")] NetworkTokenizationServiceNotConfigured, + #[error("Failed while calling Network Token Service API")] + ApiError, } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 5bdb0f0819..6ebc7bde5e 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -47,9 +47,9 @@ use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; feature = "customer_v2" ))] use hyperswitch_domain_models::mandates::CommonMandateReference; -use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use masking::ExposeInterface; +use hyperswitch_domain_models::payment_method_data; +use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing}; use time::Duration; @@ -737,6 +737,7 @@ pub(crate) async fn get_payment_method_create_request( card_detail, ), billing: None, + network_tokenization: None, }; Ok(payment_method_request) } @@ -853,6 +854,7 @@ pub async fn create_payment_method( req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, + profile: &domain::Profile, ) -> RouterResponse { use common_utils::ext_traits::ValueExt; @@ -910,6 +912,9 @@ pub async fn create_payment_method( let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data); + let payment_method_data = + populate_bin_details_for_payment_method(state, &payment_method_data).await; + let vaulting_result = vault_payment_method( state, &payment_method_data, @@ -919,6 +924,25 @@ pub async fn create_payment_method( ) .await; + let network_tokenization_resp = network_tokenize_and_vault_the_pmd( + state, + &payment_method_data, + merchant_account, + key_store, + req.network_tokenization, + profile.is_network_tokenization_enabled, + &customer_id, + ) + .await + .map_err(|e| { + services::logger::error!( + "Failed to network tokenize the payment method for customer: {}. Error: {} ", + customer_id.get_string_repr(), + e + ); + }) + .ok(); + let response = match vaulting_result { Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( @@ -929,6 +953,7 @@ pub async fn create_payment_method( Some(req.payment_method_type), Some(req.payment_method_subtype), Some(fingerprint_id), + network_tokenization_resp, ) .await .attach_printable("Unable to create Payment method data")?; @@ -972,6 +997,145 @@ pub async fn create_payment_method( Ok(services::ApplicationResponse::Json(response)) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Clone, Debug)] +pub struct NetworkTokenPaymentMethodDetails { + network_token_requestor_reference_id: String, + network_token_locker_id: String, + network_token_pmd: Encryptable>, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn network_tokenize_and_vault_the_pmd( + state: &SessionState, + payment_method_data: &pm_types::PaymentMethodVaultingData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + network_tokenization: Option, + network_tokenization_enabled_for_profile: bool, + customer_id: &id_type::GlobalCustomerId, +) -> RouterResult { + when(!network_tokenization_enabled_for_profile, || { + Err(report!(errors::ApiErrorResponse::NotSupported { + message: "Network Tokenization is not enabled for this payment method".to_string() + })) + })?; + + let is_network_tokenization_enabled_for_pm = network_tokenization + .as_ref() + .map(|nt| matches!(nt.enable, common_enums::NetworkTokenizationToggle::Enable)) + .unwrap_or(false); + + let card_data = match payment_method_data { + pm_types::PaymentMethodVaultingData::Card(data) + if is_network_tokenization_enabled_for_pm => + { + Ok(data) + } + _ => Err(report!(errors::ApiErrorResponse::NotSupported { + message: "Network Tokenization is not supported for this payment method".to_string() + })), + }?; + + let (resp, network_token_req_ref_id) = + network_tokenization::make_card_network_tokenization_request(state, card_data, customer_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to generate network token")?; + + let network_token_vaulting_data = pm_types::PaymentMethodVaultingData::NetworkToken(resp); + let vaulting_resp = vault::add_payment_method_to_vault( + state, + merchant_account, + &network_token_vaulting_data, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to vault the network token data")?; + + let key_manager_state = &(state).into(); + let network_token = match network_token_vaulting_data { + pm_types::PaymentMethodVaultingData::Card(card) => { + payment_method_data::PaymentMethodsData::Card( + payment_method_data::CardDetailsPaymentMethod::from(card.clone()), + ) + } + pm_types::PaymentMethodVaultingData::NetworkToken(network_token) => { + payment_method_data::PaymentMethodsData::NetworkToken( + payment_method_data::NetworkTokenDetailsPaymentMethod::from(network_token.clone()), + ) + } + }; + + let network_token_pmd = + cards::create_encrypted_data(key_manager_state, key_store, network_token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method data")?; + + Ok(NetworkTokenPaymentMethodDetails { + network_token_requestor_reference_id: network_token_req_ref_id, + network_token_locker_id: vaulting_resp.vault_id.get_string_repr().clone(), + network_token_pmd, + }) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn populate_bin_details_for_payment_method( + state: &SessionState, + payment_method_data: &pm_types::PaymentMethodVaultingData, +) -> pm_types::PaymentMethodVaultingData { + match payment_method_data { + pm_types::PaymentMethodVaultingData::Card(card) => { + let card_isin = card.card_number.get_card_isin(); + + if card.card_issuer.is_some() + && card.card_network.is_some() + && card.card_type.is_some() + && card.card_issuing_country.is_some() + { + pm_types::PaymentMethodVaultingData::Card(card.clone()) + } else { + let card_info = state + .store + .get_card_info(&card_isin) + .await + .map_err(|error| services::logger::error!(card_info_error=?error)) + .ok() + .flatten(); + + pm_types::PaymentMethodVaultingData::Card(payment_methods::CardDetail { + 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(), + nick_name: card.nick_name.clone(), + card_issuing_country: card_info.as_ref().and_then(|val| { + val.card_issuing_country + .as_ref() + .map(|c| api_enums::CountryAlpha2::from_str(c)) + .transpose() + .ok() + .flatten() + }), + 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 + .as_ref() + .map(|c| payment_methods::CardType::from_str(c)) + .transpose() + .ok() + .flatten() + }), + }) + } + } + _ => payment_method_data.clone(), + } +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn payment_method_intent_create( @@ -1293,6 +1457,7 @@ pub async fn create_payment_method_for_intent( Ok(response) } +#[allow(clippy::too_many_arguments)] #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub async fn create_pm_additional_data_update( pmd: &pm_types::PaymentMethodVaultingData, @@ -1302,10 +1467,18 @@ pub async fn create_pm_additional_data_update( payment_method_type: Option, payment_method_subtype: Option, vault_fingerprint_id: Option, + nt_data: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { - api::PaymentMethodsData::Card(card.clone().into()) + payment_method_data::PaymentMethodsData::Card( + payment_method_data::CardDetailsPaymentMethod::from(card.clone()), + ) + } + pm_types::PaymentMethodVaultingData::NetworkToken(network_token) => { + payment_method_data::PaymentMethodsData::NetworkToken( + payment_method_data::NetworkTokenDetailsPaymentMethod::from(network_token.clone()), + ) } }; let key_manager_state = &(state).into(); @@ -1321,9 +1494,11 @@ pub async fn create_pm_additional_data_update( payment_method_type_v2: payment_method_type, payment_method_subtype, payment_method_data: Some(pmd.into()), - network_token_requestor_reference_id: None, - network_token_locker_id: None, - network_token_payment_method_data: None, + network_token_requestor_reference_id: nt_data + .clone() + .map(|data| data.network_token_requestor_reference_id), + network_token_locker_id: nt_data.clone().map(|data| data.network_token_locker_id), + network_token_payment_method_data: nt_data.map(|data| data.network_token_pmd.into()), locker_fingerprint_id: vault_fingerprint_id, }; @@ -1633,6 +1808,7 @@ pub async fn update_payment_method_core( payment_method.get_payment_method_type(), payment_method.get_payment_method_subtype(), Some(fingerprint_id), + None, ) .await .attach_printable("Unable to create Payment method data")?; diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index bb730caad2..6799a1cf5b 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -1,7 +1,9 @@ +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use std::fmt::Debug; -use api_models::{enums as api_enums, payment_methods::PaymentMethodsData}; -use cards::CardNumber; +use api_models::payment_methods as api_payment_methods; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use cards::{CardNumber, NetworkToken}; use common_utils::{ errors::CustomResult, ext_traits::{BytesExt, Encode}, @@ -9,10 +11,17 @@ use common_utils::{ metrics::utils::record_operation_time, request::RequestContent, }; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] use error_stack::ResultExt; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use error_stack::{report, ResultExt}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use hyperswitch_domain_models::payment_method_data::NetworkTokenDetails; use josekit::jwe; use masking::{ExposeInterface, Mask, PeekInterface, Secret}; -use serde::{Deserialize, Serialize}; use super::transformers::DeleteCardResp; use crate::{ @@ -24,142 +33,21 @@ use crate::{ types::{api, domain}, }; -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CardData { - card_number: CardNumber, - exp_month: Secret, - exp_year: Secret, - card_security_code: Secret, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct OrderData { - consent_id: String, - customer_id: id_type::CustomerId, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ApiPayload { - service: String, - card_data: Secret, //encrypted card data - order_data: OrderData, - key_id: String, - should_send_token: bool, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub struct CardNetworkTokenResponse { - payload: Secret, //encrypted payload -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CardNetworkTokenResponsePayload { - pub card_brand: api_enums::CardNetwork, - pub card_fingerprint: Option>, - pub card_reference: String, - pub correlation_id: String, - pub customer_id: String, - pub par: String, - pub token: CardNumber, - pub token_expiry_month: Secret, - pub token_expiry_year: Secret, - pub token_isin: String, - pub token_last_four: String, - pub token_status: String, -} - -#[derive(Debug, Serialize)] -pub struct GetCardToken { - card_reference: String, - customer_id: id_type::CustomerId, -} -#[derive(Debug, Deserialize)] -pub struct AuthenticationDetails { - cryptogram: Secret, - token: CardNumber, //network token -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenDetails { - exp_month: Secret, - exp_year: Secret, -} - -#[derive(Debug, Deserialize)] -pub struct TokenResponse { - authentication_details: AuthenticationDetails, - network: api_enums::CardNetwork, - token_details: TokenDetails, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DeleteCardToken { - card_reference: String, //network token requestor ref id - customer_id: id_type::CustomerId, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "UPPERCASE")] -pub enum DeleteNetworkTokenStatus { - Success, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub struct NetworkTokenErrorInfo { - code: String, - developer_message: String, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub struct NetworkTokenErrorResponse { - error_message: String, - error_info: NetworkTokenErrorInfo, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub struct DeleteNetworkTokenResponse { - status: DeleteNetworkTokenStatus, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct CheckTokenStatus { - card_reference: String, - customer_id: id_type::CustomerId, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "UPPERCASE")] -pub enum TokenStatus { - Active, - Inactive, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CheckTokenStatusResponsePayload { - token_expiry_month: Secret, - token_expiry_year: Secret, - token_status: TokenStatus, -} - -#[derive(Debug, Deserialize)] -pub struct CheckTokenStatusResponse { - payload: CheckTokenStatusResponsePayload, -} - pub const NETWORK_TOKEN_SERVICE: &str = "NETWORK_TOKEN"; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn mk_tokenization_req( state: &routes::SessionState, payload_bytes: &[u8], customer_id: id_type::CustomerId, tokenization_service: &settings::NetworkTokenizationService, -) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> -{ +) -> CustomResult< + (domain::CardNetworkTokenResponsePayload, Option), + errors::NetworkTokenizationError, +> { let enc_key = tokenization_service.public_key.peek().clone(); let key_id = tokenization_service.key_id.clone(); @@ -174,12 +62,12 @@ pub async fn mk_tokenization_req( .change_context(errors::NetworkTokenizationError::SaveNetworkTokenFailed) .attach_printable("Error on jwe encrypt")?; - let order_data = OrderData { + let order_data = domain::OrderData { consent_id: uuid::Uuid::new_v4().to_string(), customer_id, }; - let api_payload = ApiPayload { + let api_payload = domain::ApiPayload { service: NETWORK_TOKEN_SERVICE.to_string(), card_data: Secret::new(jwt), order_data, @@ -208,14 +96,14 @@ pub async fn mk_tokenization_req( let response = services::call_connector_api(state, request, "generate_token") .await - .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + .change_context(errors::NetworkTokenizationError::ApiError); let res = response .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) .attach_printable("Error while receiving response") .and_then(|inner| match inner { Err(err_res) => { - let parsed_error: NetworkTokenErrorResponse = err_res + let parsed_error: domain::NetworkTokenErrorResponse = err_res .response .parse_struct("Card Network Tokenization Response") .change_context( @@ -236,11 +124,10 @@ pub async fn mk_tokenization_req( logger::error!("Error while deserializing response: {:?}", err); })?; - let network_response: CardNetworkTokenResponse = res + let network_response: domain::CardNetworkTokenResponse = res .response .parse_struct("Card Network Tokenization Response") .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; - logger::debug!("Network Token Response: {:?}", network_response); //added for debugging, will be removed let dec_key = tokenization_service.private_key.peek().clone(); @@ -256,19 +143,137 @@ pub async fn mk_tokenization_req( "Failed to decrypt the tokenization response from the tokenization service", )?; - let cn_response: CardNetworkTokenResponsePayload = + let cn_response: domain::CardNetworkTokenResponsePayload = serde_json::from_str(&card_network_token_response) .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; Ok((cn_response.clone(), Some(cn_response.card_reference))) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn generate_network_token( + state: &routes::SessionState, + payload_bytes: &[u8], + customer_id: id_type::GlobalCustomerId, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult< + (domain::GenerateNetworkTokenResponsePayload, String), + errors::NetworkTokenizationError, +> { + let enc_key = tokenization_service.public_key.peek().clone(); + + let key_id = tokenization_service.key_id.clone(); + + let jwt = encryption::encrypt_jwe( + payload_bytes, + enc_key, + services::EncryptionAlgorithm::A128GCM, + Some(key_id.as_str()), + ) + .await + .change_context(errors::NetworkTokenizationError::SaveNetworkTokenFailed) + .attach_printable("Error on jwe encrypt")?; + + let order_data = domain::OrderData { + consent_id: uuid::Uuid::new_v4().to_string(), + customer_id, + }; + + let api_payload = domain::ApiPayload { + service: NETWORK_TOKEN_SERVICE.to_string(), + card_data: Secret::new(jwt), + order_data, + key_id, + should_send_token: true, + }; + + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.generate_token_url.as_str(), + ); + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + + request.set_body(RequestContent::Json(Box::new(api_payload))); + + logger::info!("Request to generate token: {:?}", request); + + let response = services::call_connector_api(state, request, "generate_token") + .await + .change_context(errors::NetworkTokenizationError::ApiError); + + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: domain::NetworkTokenErrorResponse = err_res + .response + .parse_struct("Card Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + }) + .inspect_err(|err| { + logger::error!("Error while deserializing response: {:?}", err); + })?; + + let network_response: domain::CardNetworkTokenResponse = res + .response + .parse_struct("Card Network Tokenization Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + logger::debug!("Network Token Response: {:?}", network_response); + + let dec_key = tokenization_service.private_key.peek().clone(); + + let card_network_token_response = services::decrypt_jwe( + network_response.payload.peek(), + services::KeyIdCheck::SkipKeyIdCheck, + dec_key, + jwe::RSA_OAEP_256, + ) + .await + .change_context(errors::NetworkTokenizationError::SaveNetworkTokenFailed) + .attach_printable( + "Failed to decrypt the tokenization response from the tokenization service", + )?; + + let cn_response: domain::GenerateNetworkTokenResponsePayload = + serde_json::from_str(&card_network_token_response) + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + Ok((cn_response.clone(), cn_response.card_reference)) +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn make_card_network_tokenization_request( state: &routes::SessionState, card: &domain::Card, customer_id: &id_type::CustomerId, -) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> -{ - let card_data = CardData { +) -> CustomResult< + (domain::CardNetworkTokenResponsePayload, Option), + errors::NetworkTokenizationError, +> { + let card_data = domain::CardData { card_number: card.card_number.clone(), exp_month: card.card_exp_month.clone(), exp_year: card.card_exp_year.clone(), @@ -308,18 +313,74 @@ pub async fn make_card_network_tokenization_request( } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn make_card_network_tokenization_request( + state: &routes::SessionState, + card: &api_payment_methods::CardDetail, + customer_id: &id_type::GlobalCustomerId, +) -> CustomResult<(NetworkTokenDetails, String), errors::NetworkTokenizationError> { + let card_data = domain::CardData { + card_number: card.card_number.clone(), + exp_month: card.card_exp_month.clone(), + exp_year: card.card_exp_year.clone(), + card_security_code: None, + }; + + let payload = card_data + .encode_to_string_of_json() + .and_then(|x| x.encode_to_string_of_json()) + .change_context(errors::NetworkTokenizationError::RequestEncodingFailed)?; + + let payload_bytes = payload.as_bytes(); + let network_tokenization_service = match &state.conf.network_tokenization_service { + Some(nt_service) => Ok(nt_service.get_inner()), + None => Err(report!( + errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured + )), + }?; + + let (resp, network_token_req_ref_id) = record_operation_time( + async { + generate_network_token( + state, + payload_bytes, + customer_id.clone(), + network_tokenization_service, + ) + .await + .inspect_err(|e| logger::error!(error=?e, "Error while making tokenization request")) + }, + &metrics::GENERATE_NETWORK_TOKEN_TIME, + router_env::metric_attributes!(("locker", "rust")), + ) + .await?; + + let network_token_details = NetworkTokenDetails { + network_token: resp.token, + network_token_exp_month: resp.token_expiry_month, + network_token_exp_year: resp.token_expiry_year, + card_issuer: card.card_issuer.clone(), + card_network: Some(resp.card_brand), + card_type: card.card_type.clone(), + card_issuing_country: card.card_issuing_country, + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + }; + Ok((network_token_details, network_token_req_ref_id)) +} + #[cfg(feature = "v1")] pub async fn get_network_token( state: &routes::SessionState, customer_id: id_type::CustomerId, network_token_requestor_ref_id: String, tokenization_service: &settings::NetworkTokenizationService, -) -> CustomResult { +) -> CustomResult { let mut request = services::Request::new( services::Method::Post, tokenization_service.fetch_token_url.as_str(), ); - let payload = GetCardToken { + let payload = domain::GetCardToken { card_reference: network_token_requestor_ref_id, customer_id, }; @@ -342,14 +403,14 @@ pub async fn get_network_token( // Send the request using `call_connector_api` let response = services::call_connector_api(state, request, "get network token") .await - .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + .change_context(errors::NetworkTokenizationError::ApiError); let res = response .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) .attach_printable("Error while receiving response") .and_then(|inner| match inner { Err(err_res) => { - let parsed_error: NetworkTokenErrorResponse = err_res + let parsed_error: domain::NetworkTokenErrorResponse = err_res .response .parse_struct("Card Network Tokenization Response") .change_context( @@ -367,7 +428,75 @@ pub async fn get_network_token( Ok(res) => Ok(res), })?; - let token_response: TokenResponse = res + let token_response: domain::TokenResponse = res + .response + .parse_struct("Get Network Token Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + logger::info!("Fetch Network Token Response: {:?}", token_response); + + Ok(token_response) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn get_network_token( + state: &routes::SessionState, + customer_id: &id_type::GlobalCustomerId, + network_token_requestor_ref_id: String, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult { + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.fetch_token_url.as_str(), + ); + let payload = domain::GetCardToken { + card_reference: network_token_requestor_ref_id, + customer_id: customer_id.clone(), + }; + + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .clone() + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + request.set_body(RequestContent::Json(Box::new(payload))); + + logger::info!("Request to fetch network token: {:?}", request); + + // Send the request using `call_connector_api` + let response = services::call_connector_api(state, request, "get network token") + .await + .change_context(errors::NetworkTokenizationError::ApiError); + + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: domain::NetworkTokenErrorResponse = err_res + .response + .parse_struct("Card Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + })?; + + let token_response: domain::TokenResponse = res .response .parse_struct("Get Network Token Response") .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; @@ -415,9 +544,11 @@ pub async fn get_token_from_tokenization_service( .network_token_payment_method_data .clone() .map(|x| x.into_inner().expose()) - .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|v| serde_json::from_value::(v).ok()) .and_then(|pmd| match pmd { - PaymentMethodsData::Card(token) => Some(api::CardDetailFromLocker::from(token)), + api_payment_methods::PaymentMethodsData::Card(token) => { + Some(api::CardDetailFromLocker::from(token)) + } _ => None, }) .ok_or(errors::ApiErrorResponse::InternalServerError) @@ -452,9 +583,11 @@ pub async fn do_status_check_for_network_token( .network_token_payment_method_data .clone() .map(|x| x.into_inner().expose()) - .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|v| serde_json::from_value::(v).ok()) .and_then(|pmd| match pmd { - PaymentMethodsData::Card(token) => Some(api::CardDetailFromLocker::from(token)), + api_payment_methods::PaymentMethodsData::Card(token) => { + Some(api::CardDetailFromLocker::from(token)) + } _ => None, }); let network_token_requestor_reference_id = payment_method_info @@ -507,6 +640,10 @@ pub async fn do_status_check_for_network_token( } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn check_token_status_with_tokenization_service( state: &routes::SessionState, customer_id: &id_type::CustomerId, @@ -518,7 +655,7 @@ pub async fn check_token_status_with_tokenization_service( services::Method::Post, tokenization_service.check_token_status_url.as_str(), ); - let payload = CheckTokenStatus { + let payload = domain::CheckTokenStatus { card_reference: network_token_requestor_reference_id, customer_id: customer_id.clone(), }; @@ -539,13 +676,13 @@ pub async fn check_token_status_with_tokenization_service( // Send the request using `call_connector_api` let response = services::call_connector_api(state, request, "Check Network token Status") .await - .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + .change_context(errors::NetworkTokenizationError::ApiError); let res = response .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) .attach_printable("Error while receiving response") .and_then(|inner| match inner { Err(err_res) => { - let parsed_error: NetworkTokenErrorResponse = err_res + let parsed_error: domain::NetworkTokenErrorResponse = err_res .response .parse_struct("Delete Network Tokenization Response") .change_context( @@ -566,20 +703,35 @@ pub async fn check_token_status_with_tokenization_service( logger::error!("Error while deserializing response: {:?}", err); })?; - let check_token_status_response: CheckTokenStatusResponse = res + let check_token_status_response: domain::CheckTokenStatusResponse = res .response .parse_struct("Delete Network Tokenization Response") .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; match check_token_status_response.payload.token_status { - TokenStatus::Active => Ok(( + domain::TokenStatus::Active => Ok(( Some(check_token_status_response.payload.token_expiry_month), Some(check_token_status_response.payload.token_expiry_year), )), - TokenStatus::Inactive => Ok((None, None)), + domain::TokenStatus::Inactive => Ok((None, None)), } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn check_token_status_with_tokenization_service( + _state: &routes::SessionState, + _customer_id: &id_type::GlobalCustomerId, + _network_token_requestor_reference_id: String, + _tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult<(Option>, Option>), errors::NetworkTokenizationError> +{ + todo!() +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn delete_network_token_from_locker_and_token_service( state: &routes::SessionState, customer_id: &id_type::CustomerId, @@ -624,6 +776,10 @@ pub async fn delete_network_token_from_locker_and_token_service( Ok(resp) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn delete_network_token_from_tokenization_service( state: &routes::SessionState, network_token_requestor_reference_id: String, @@ -634,7 +790,7 @@ pub async fn delete_network_token_from_tokenization_service( services::Method::Post, tokenization_service.delete_token_url.as_str(), ); - let payload = DeleteCardToken { + let payload = domain::DeleteCardToken { card_reference: network_token_requestor_reference_id, customer_id: customer_id.clone(), }; @@ -657,13 +813,13 @@ pub async fn delete_network_token_from_tokenization_service( // Send the request using `call_connector_api` let response = services::call_connector_api(state, request, "delete network token") .await - .change_context(errors::NetworkTokenizationError::DeleteNetworkTokenFailed); + .change_context(errors::NetworkTokenizationError::ApiError); let res = response .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) .attach_printable("Error while receiving response") .and_then(|inner| match inner { Err(err_res) => { - let parsed_error: NetworkTokenErrorResponse = err_res + let parsed_error: domain::NetworkTokenErrorResponse = err_res .response .parse_struct("Delete Network Tokenization Response") .change_context( @@ -684,17 +840,29 @@ pub async fn delete_network_token_from_tokenization_service( logger::error!("Error while deserializing response: {:?}", err); })?; - let delete_token_response: DeleteNetworkTokenResponse = res + let delete_token_response: domain::DeleteNetworkTokenResponse = res .response .parse_struct("Delete Network Tokenization Response") .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; logger::info!("Delete Network Token Response: {:?}", delete_token_response); - if delete_token_response.status == DeleteNetworkTokenStatus::Success { + if delete_token_response.status == domain::DeleteNetworkTokenStatus::Success { Ok(true) } else { Err(errors::NetworkTokenizationError::DeleteNetworkTokenFailed) .attach_printable("Delete Token at Token service failed") } } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn delete_network_token_from_locker_and_token_service( + _state: &routes::SessionState, + _customer_id: &id_type::GlobalCustomerId, + _merchant_id: &id_type::MerchantId, + _payment_method_id: String, + _network_token_locker_id: Option, + _network_token_requestor_reference_id: String, +) -> errors::RouterResult { + todo!() +} diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index c4d3eafe46..947ae97b11 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -545,6 +545,7 @@ pub fn generate_pm_vaulting_req_from_update_request( card_holder_name: update_card.card_holder_name, nick_name: update_card.nick_name, }), + _ => todo!(), //todo! - since support for network tokenization is not added PaymentMethodUpdateData. should be handled later. } } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 0b6246d448..036646f719 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -85,6 +85,7 @@ pub async fn create_payment_method_api( req, &auth.merchant_account, &auth.key_store, + &auth.profile, )) .await }, diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index 9a87b6d28b..270ed3ca64 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -20,6 +20,10 @@ mod callback_mapper { pub use hyperswitch_domain_models::callback_mapper::CallbackMapper; } +mod network_tokenization { + pub use hyperswitch_domain_models::network_tokenization::*; +} + pub use customers::*; pub use merchant_account::*; @@ -48,6 +52,7 @@ pub use consts::*; pub use event::*; pub use merchant_connector_account::*; pub use merchant_key_store::*; +pub use network_tokenization::*; pub use payment_methods::*; pub use payments::*; #[cfg(feature = "olap")] diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index c40d6fedfe..d639bc44bc 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -1,6 +1,8 @@ #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use common_utils::generate_id; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use hyperswitch_domain_models::payment_method_data::NetworkTokenDetails; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use masking::Secret; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -115,6 +117,7 @@ impl VaultingInterface for VaultDelete { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub enum PaymentMethodVaultingData { Card(api::CardDetail), + NetworkToken(NetworkTokenDetails), } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -122,6 +125,7 @@ impl VaultingDataInterface for PaymentMethodVaultingData { fn get_vaulting_data_key(&self) -> String { match &self { Self::Card(card) => card.card_number.to_string(), + Self::NetworkToken(network_token) => network_token.network_token.to_string(), } } }