From 64d4bde83410f46a0854b12e34b6dcf79b6e54a1 Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 7 Jan 2025 10:53:28 +0530 Subject: [PATCH 01/25] feat(network-tokenization): expose flows for card network tokenization --- crates/api_models/src/payment_methods.rs | 93 +++- crates/router/src/core/payment_methods.rs | 1 + .../router/src/core/payment_methods/cards.rs | 28 +- .../src/core/payment_methods/tokenize.rs | 493 ++++++++++++++++++ crates/router/src/routes/app.rs | 15 +- crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/payment_methods.rs | 62 +++ .../router/src/types/api/payment_methods.rs | 31 +- crates/router/src/types/api/payments.rs | 32 +- crates/router_env/src/logger/types.rs | 4 + 10 files changed, 724 insertions(+), 38 deletions(-) create mode 100644 crates/router/src/core/payment_methods/tokenize.rs diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 18d18f08fd2..5e718b90839 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -474,7 +474,6 @@ pub struct CardDetail { pub card_type: Option, } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive( Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, strum::EnumString, strum::Display, )] @@ -2420,3 +2419,95 @@ impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerReq // } // } // } + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct CardNetworkTokenizeRequest { + pub merchant_id: id_type::MerchantId, + + #[serde(flatten)] + pub data: TokenizeDataRequest, + + /// Card type + pub card_type: Option, + + /// The CIT (customer initiated transaction) transaction id associated with the payment method + pub network_transaction_id: Option, + + /// Customer details + pub customer: Option, + + /// The billing details of the payment method + pub billing: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + pub metadata: Option, + + /// The name of the bank/ provider issuing the payment method to the end user + pub payment_method_issuer: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(untagged)] +pub enum TokenizeDataRequest { + Card { card: TokenizeCardRequest }, + PaymentMethodId { payment_method_id: String }, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct TokenizeCardRequest { + /// Card Number + #[schema(value_type = String,example = "4111111145551142")] + 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, + + /// The CVC number for the card + #[schema(value_type = String, example = "242")] + pub card_cvc: masking::Secret, + + /// Card Holder Name + #[schema(value_type = String,example = "John Doe")] + pub card_holder_name: Option>, + + /// Card Holder's Nick Name + #[schema(value_type = Option,example = "John Doe")] + pub nick_name: Option>, + + /// Card Issuing Country + pub card_issuing_country: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card Type + pub card_type: Option, +} + +impl From<&TokenizeCardRequest> for MigrateCardDetail { + fn from(card: &TokenizeCardRequest) -> Self { + Self { + 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.card_issuing_country.clone(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card.card_type.clone(), + } + } +} + +impl common_utils::events::ApiEventMetric for CardNetworkTokenizeRequest {} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 6cdadf07320..c909d49479a 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -6,6 +6,7 @@ pub mod cards; pub mod migration; pub mod network_tokenization; pub mod surcharge_decision_configs; +pub mod tokenize; pub mod transformers; pub mod utils; mod validator; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 164c0e9557a..9b24f7876d5 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -82,7 +82,7 @@ use crate::{ }, core::{ errors::{self, StorageErrorExt}, - payment_methods::{network_tokenization, transformers as payment_methods, vault}, + payment_methods::{network_tokenization, tokenize, transformers as payment_methods, vault}, payments::{ helpers, routing::{self, SessionFlowRoutingInput}, @@ -6032,3 +6032,29 @@ pub async fn list_countries_currencies_for_connector_payment_method_util( .collect(), } } + +pub async fn tokenize_card_flow( + state: routes::SessionState, + req: api::CardNetworkTokenizeRequest, + merchant_id: &id_type::MerchantId, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse { + let response = match req.data { + api::TokenizeDataRequest::Card { ref card } => { + tokenize::CardNetworkTokenizeResponseBuilder::< + api::TokenizeCardRequest, + tokenize::TokenizeWithCard, + >::new(req, card.clone()) + .validate_request(&state, merchant_account, key_store) + .await? + .tokenize_card(&state) + .await? + .create_payment_method(&state, merchant_account) + .await? + .build() + } + api::TokenizeDataRequest::PaymentMethodId { payment_method_id } => todo!(), + }; + Ok(services::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs new file mode 100644 index 00000000000..f53ca9c9bef --- /dev/null +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -0,0 +1,493 @@ +use std::{collections::HashMap, str::FromStr}; + +use api_models::{enums as api_enums, payment_methods as payment_methods_api}; +use cards::CardNumber; +use common_utils::{ + ext_traits::OptionExt, + generate_customer_id_of_default_length, + pii::{self, Email}, + type_name, + types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::type_encryption::{crypto_operation, CryptoOperation}; +use masking::{ExposeInterface, PeekInterface, SwitchStrategy}; +use utoipa::ToSchema; + +use crate::{ + core::payment_methods::{ + cards::{add_card_to_hs_locker, populate_bin_details_for_masked_card}, + network_tokenization, + transformers::{DataDuplicationCheck, StoreCardReq, StoreLockerReq}, + }, + db, + errors::{self, RouterResult}, + types::{ + api::{ + self, + payment_methods::{CardNetworkTokenizeRequest, TokenizeCardRequest}, + }, + domain, + }, + utils::Encryptable, + SessionState, +}; + +#[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct CardNetworkTokenizeResponseBuilder { + /// Current state + state: S, + + /// State data + data: D, + + /// Response for payment method entry in DB + pub payment_method_response: Option, + + /// Customer details + pub customer: Option, + + /// Card network tokenization status + pub card_tokenized: Option, + + /// Card migration status + pub card_migrated: Option, + + /// Network token data migration status + pub network_token_migrated: Option, + + /// Network transaction ID migration status + pub network_transaction_id_migrated: Option, + + /// Error code + pub error_code: HashMap, + + /// Error message + pub error_message: HashMap, +} + +#[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct CardNetworkTokenizeResponse { + /// Response for payment method entry in DB + pub payment_method_response: Option, + + /// Customer details + pub customer: Option, + + /// Card network tokenization status + pub card_tokenized: Option, + + /// Card migration status + pub card_migrated: Option, + + /// Network token data migration status + pub network_token_migrated: Option, + + /// Network transaction ID migration status + pub network_transaction_id_migrated: Option, + + /// Error code + pub error_code: HashMap, + + /// Error message + pub error_message: HashMap, +} + +impl common_utils::events::ApiEventMetric for CardNetworkTokenizeResponse {} + +/// Tokenize using card details +pub struct TokenizeWithCard; + +/// Tokenize using payment Method ID +pub struct TokenizeWithPmId; + +/// Card details validated +pub struct CardValidated; + +/// Payment method ID is tokenized +pub struct PaymentMethodValidated; + +/// Stored card details are tokenized +pub struct PaymentMethodTokenized; + +/// Card details are tokenized +pub struct CardTokenized; + +/// Card details are stored in locker +pub struct CardStored; + +// Initialize builder for tokenizing raw card details +impl CardNetworkTokenizeResponseBuilder { + pub fn new(req: CardNetworkTokenizeRequest, data: TokenizeCardRequest) -> Self { + CardNetworkTokenizeResponseBuilder { + data, + state: TokenizeWithCard, + customer: req.customer, + payment_method_response: None, + card_tokenized: None, + card_migrated: None, + network_token_migrated: None, + network_transaction_id_migrated: None, + error_code: HashMap::new(), + error_message: HashMap::new(), + } + } +} + +// Validations for tokenizing raw card +impl CardNetworkTokenizeResponseBuilder { + pub async fn get_or_create_customer( + mut self, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let db = &*state.store; + let customer_details = self + .customer + .as_ref() + .get_required_value("customer") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })?; + let key_manager_state: &KeyManagerState = &state.into(); + + match db + .find_customer_optional_by_customer_id_merchant_id( + key_manager_state, + &customer_details.id, + merchant_account.get_id(), + key_store, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + { + // Customer found + Some(customer) => { + self.customer = Some(api::CustomerDetails { + id: customer.customer_id.clone(), + name: customer.name.clone().map(|name| name.into_inner()), + email: customer.email.clone().map(Email::from), + phone: customer.phone.clone().map(|phone| phone.into_inner()), + phone_country_code: customer.phone_country_code.clone(), + }); + Ok(self) + } + // Customer not found + None => { + if customer_details.name.is_some() + || customer_details.email.is_some() + || customer_details.phone.is_some() + { + let encrypted_data = crypto_operation( + key_manager_state, + type_name!(domain::Customer), + CryptoOperation::BatchEncrypt( + domain::FromRequestEncryptableCustomer::to_encryptable( + domain::FromRequestEncryptableCustomer { + name: customer_details.name.clone(), + email: customer_details + .email + .clone() + .map(|email| email.expose().switch_strategy()), + phone: customer_details.phone.clone(), + }, + ), + ), + Identifier::Merchant(merchant_account.get_id().clone()), + key_store.key.get_inner().peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encrypt customer")?; + + let encryptable_customer = + domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form EncryptableCustomer")?; + + let domain_customer = domain::Customer { + customer_id: generate_customer_id_of_default_length(), + merchant_id: merchant_account.get_id().clone(), + name: encryptable_customer.name, + email: encryptable_customer.email.map(|email| { + Encryptable::new( + email.clone().into_inner().switch_strategy(), + email.into_encrypted(), + ) + }), + phone: encryptable_customer.phone, + description: None, + phone_country_code: customer_details.phone_country_code.to_owned(), + metadata: None, + connector_customer: None, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + address_id: None, + default_payment_method_id: None, + updated_by: None, + version: hyperswitch_domain_models::consts::API_VERSION, + }; + + db.insert_customer( + domain_customer.clone(), + key_manager_state, + key_store, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed to insert customer [id - {:?}] for merchant [id - {:?}]", + customer_details.id, + merchant_account.get_id() + ) + })?; + + self.customer = Some(api::CustomerDetails { + id: domain_customer.customer_id, + name: customer_details.name.clone(), + email: customer_details.email.clone(), + phone: customer_details.phone.clone(), + phone_country_code: customer_details.phone_country_code.clone(), + }); + Ok(self) + + // Throw error if customer creation is not requested + } else { + Err(report!(errors::ApiErrorResponse::MissingRequiredFields { + field_names: vec!["customer.name", "customer.email", "customer.phone"], + })) + } + } + } + } + + pub async fn insert_bin_details( + self, + card_number: CardNumber, + db: &dyn db::StorageInterface, + ) -> RouterResult> { + let card_bin_details = + populate_bin_details_for_masked_card(&api::MigrateCardDetail::from(&self.data), db) + .await?; + + Ok(CardNetworkTokenizeResponseBuilder { + state: CardValidated, + data: domain::Card { + card_number, + card_type: card_bin_details.card_type, + card_network: card_bin_details.card_network, + card_issuer: card_bin_details.card_issuer, + card_issuing_country: card_bin_details.issuer_country, + card_exp_month: self.data.card_exp_month, + card_exp_year: self.data.card_exp_year, + card_cvc: self.data.card_cvc, + nick_name: self.data.nick_name, + card_holder_name: self.data.card_holder_name, + bank_code: None, + }, + payment_method_response: self.payment_method_response, + customer: self.customer, + card_tokenized: self.card_tokenized, + card_migrated: self.card_migrated, + network_token_migrated: self.network_token_migrated, + network_transaction_id_migrated: self.network_transaction_id_migrated, + error_code: self.error_code, + error_message: self.error_message, + }) + } + + pub async fn validate_request( + self, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult> { + // Validate card number + let card_number = CardNumber::from_str(self.data.card_number.peek()).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid card number".to_string(), + }, + )?; + + // Validate and insert customer details + let builder_with_customer = self + .get_or_create_customer(state, merchant_account, key_store) + .await?; + + // Update card details after BIN lookup + builder_with_customer + .insert_bin_details(card_number, &*state.store) + .await + } +} + +// Tokenize raw card details +impl CardNetworkTokenizeResponseBuilder { + pub async fn tokenize_card( + self, + state: &SessionState, + ) -> RouterResult< + CardNetworkTokenizeResponseBuilder< + ( + network_tokenization::CardNetworkTokenResponsePayload, + Option, + ), + CardTokenized, + >, + > { + match network_tokenization::make_card_network_tokenization_request( + state, + &self.data, + &self + .customer + .as_ref() + .get_required_value("customer") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })? + .id + .clone(), + ) + .await + { + Ok(data) => Ok(CardNetworkTokenizeResponseBuilder { + card_tokenized: Some(true), + state: CardTokenized, + data, + payment_method_response: self.payment_method_response, + customer: self.customer, + card_migrated: self.card_migrated, + network_token_migrated: self.network_token_migrated, + network_transaction_id_migrated: self.network_transaction_id_migrated, + error_code: self.error_code, + error_message: self.error_message, + }), + Err(err) => Err(err.change_context(errors::ApiErrorResponse::InternalServerError)), + } + } +} + +// Store in locker and create payment method entry +impl + CardNetworkTokenizeResponseBuilder< + ( + network_tokenization::CardNetworkTokenResponsePayload, + Option, + ), + CardTokenized, + > +{ + pub async fn store_in_locker( + self, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + let customer_details = self + .customer + .get_required_value("customer") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })?; + let network_token = self.data.0; + let locker_req = StoreLockerReq::LockerCard(StoreCardReq { + merchant_id: merchant_account.get_id().clone(), + merchant_customer_id: customer_details.id.to_owned(), + card: payment_methods_api::Card { + card_number: network_token.token, + card_exp_month: network_token.token_expiry_month, + card_exp_year: network_token.token_expiry_year, + card_brand: Some(network_token.card_brand.to_string()), + card_isin: Some(network_token.token_isin), + name_on_card: None, // TODO: Fetch from request + nick_name: None, // TODO: Fetch from request + }, + requestor_card_reference: None, + ttl: state.conf.locker.ttl_for_storage_in_secs, + }); + + let stored_resp = add_card_to_hs_locker( + state, + &locker_req, + &customer_details.id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + Ok(api::PaymentMethodResponse { + merchant_id: todo!(), + customer_id: todo!(), + payment_method_id: todo!(), + payment_method: todo!(), + payment_method_type: todo!(), + card: todo!(), + recurring_enabled: todo!(), + installment_payment_enabled: todo!(), + payment_experience: todo!(), + metadata: todo!(), + created: todo!(), + bank_transfer: todo!(), + last_used_at: todo!(), + client_secret: todo!(), + }) + } + pub async fn create_payment_method( + self, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult> + { + let res = self.store_in_locker(state, merchant_account).await?; + Ok(CardNetworkTokenizeResponseBuilder { + data: res, + state: CardStored, + payment_method_response: todo!(), + customer: todo!(), + card_tokenized: todo!(), + card_migrated: todo!(), + network_token_migrated: todo!(), + network_transaction_id_migrated: todo!(), + error_code: todo!(), + error_message: todo!(), + }) + } +} + +// Initialize builder for tokenizing saved cards +impl CardNetworkTokenizeResponseBuilder { + pub fn new(req: CardNetworkTokenizeRequest, data: String) -> Self { + CardNetworkTokenizeResponseBuilder { + data, + state: TokenizeWithPmId, + customer: req.customer, + payment_method_response: None, + card_tokenized: None, + card_migrated: None, + network_token_migrated: None, + network_transaction_id_migrated: None, + error_code: HashMap::new(), + error_message: HashMap::new(), + } + } +} + +// Build return response +impl CardNetworkTokenizeResponseBuilder { + pub fn build(self) -> CardNetworkTokenizeResponse { + CardNetworkTokenizeResponse { + payment_method_response: self.payment_method_response, + customer: self.customer, + card_tokenized: self.card_tokenized, + card_migrated: self.card_migrated, + network_token_migrated: self.network_token_migrated, + network_transaction_id_migrated: self.network_transaction_id_migrated, + error_code: self.error_code, + error_message: self.error_message, + } + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f9dbec77452..2c1e78d883e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1198,17 +1198,22 @@ impl PaymentMethods { .route(web::get().to(list_payment_method_api)), // TODO : added for sdk compatibility for now, need to deprecate this later ) .service( - web::resource("/migrate").route(web::post().to(migrate_payment_method_api)), + web::resource("/collect").route(web::post().to(initiate_pm_collect_link_flow)), ) .service( - web::resource("/migrate-batch").route(web::post().to(migrate_payment_methods)), + web::resource("/collect/{merchant_id}/{collect_id}") + .route(web::get().to(render_pm_collect_link)), ) .service( - web::resource("/collect").route(web::post().to(initiate_pm_collect_link_flow)), + web::resource("/migrate").route(web::post().to(migrate_payment_method_api)), ) .service( - web::resource("/collect/{merchant_id}/{collect_id}") - .route(web::get().to(render_pm_collect_link)), + web::resource("/migrate-batch").route(web::post().to(migrate_payment_methods)), + ) + .service(web::resource("/tokenize-card").route(web::post().to(tokenize_card_api))) + .service( + web::resource("/tokenize-card-batch") + .route(web::post().to(tokenize_card_batch_api)), ) .service( web::resource("/{payment_method_id}") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 9d7ae1874c1..82343435d4b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -39,6 +39,7 @@ pub enum ApiIdentifier { ApplePayCertificatesMigration, Relay, Documentation, + CardNetworkTokenization, } impl From for ApiIdentifier { @@ -306,6 +307,8 @@ impl From for ApiIdentifier { Flow::RetrievePollStatus => Self::Poll, Flow::FeatureMatrix => Self::Documentation, + + Flow::TokenizeCard | Flow::TokenizeCardBatch => Self::CardNetworkTokenization, } } } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index d8dfb732061..08703d94fd8 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -946,3 +946,65 @@ impl ParentPaymentMethodToken { } } } + +#[instrument(skip_all, fields(flow = ?Flow::TokenizeCard))] +pub async fn tokenize_card_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::TokenizeCard; + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _, req, _| async move { + let merchant_id = req.merchant_id.clone(); + let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; + Box::pin(cards::tokenize_card_flow( + state, + req, + &merchant_id, + &merchant_account, + &key_store, + )) + .await + }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::TokenizeCardBatch))] +pub async fn tokenize_card_batch_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::TokenizeCardBatch; + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _, req, _| async move { + let merchant_id = req.merchant_id.clone(); + let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; + Box::pin(cards::tokenize_card_flow( + state, + req, + &merchant_id, + &merchant_account, + &key_store, + )) + .await + }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 25227ae5383..31e3dfdcb75 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,3 +1,19 @@ +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +pub use api_models::payment_methods::{ + CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, + CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DefaultPaymentMethod, + DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse, + ListCountriesCurrenciesRequest, MigrateCardDetail, PaymentMethodCollectLinkRenderRequest, + PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, + PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest, + PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodMigrateResponse, + PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizeCardRequest, + TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, + TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, +}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardType, CustomerPaymentMethod, @@ -12,21 +28,6 @@ pub use api_models::payment_methods::{ TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, }; -#[cfg(all( - any(feature = "v2", feature = "v1"), - not(feature = "payment_methods_v2") -))] -pub use api_models::payment_methods::{ - CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, - GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, - PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, - PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, - PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, - TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, - TokenizedWalletValue1, TokenizedWalletValue2, -}; use error_stack::report; use crate::core::{ diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 6ec0fe701e9..d36831d1b3b 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -10,22 +10,22 @@ pub use api_models::{ }, payments::{ AcceptanceType, Address, AddressDetails, Amount, AuthenticationForStartResponse, Card, - CryptoData, CustomerAcceptance, CustomerDetailsResponse, MandateAmountData, MandateData, - MandateTransactionType, MandateType, MandateValidationFields, NextActionType, - OnlineMandate, OpenBankingSessionToken, PayLaterData, PaymentIdType, - PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, - PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, - PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, - PaymentRetrieveBodyWithCredentials, PaymentsAggregateResponse, PaymentsApproveRequest, - PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, - PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, - PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsPostSessionTokensRequest, - PaymentsPostSessionTokensResponse, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, - PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, - PhoneDetails, RedirectionResponse, SessionToken, UrlDetails, VerifyRequest, VerifyResponse, - WalletData, + CryptoData, CustomerAcceptance, CustomerDetails, CustomerDetailsResponse, + MandateAmountData, MandateData, MandateTransactionType, MandateType, + MandateValidationFields, NextActionType, OnlineMandate, OpenBankingSessionToken, + PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, + PaymentListFilters, PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, + PaymentMethodData, PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, + PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsAggregateResponse, + PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, + PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, + PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, + PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRejectRequest, + PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, + PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails, + RedirectionResponse, SessionToken, UrlDetails, VerifyRequest, VerifyResponse, WalletData, }, }; use error_stack::ResultExt; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 10183f75d5b..7de3c009ca2 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -537,6 +537,10 @@ pub enum Flow { Relay, /// Relay retrieve flow RelayRetrieve, + /// Card tokenization flow + TokenizeCard, + /// Cards batch tokenization flow + TokenizeCardBatch, } /// Trait for providing generic behaviour to flow metric From e2eeeebd2f2fc65d3f6e898dbeb359468793665b Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 10 Jan 2025 12:52:20 +0530 Subject: [PATCH 02/25] feat(bulk-tokenization): add pm id flow and batch flow --- Cargo.lock | 1 + crates/api_models/Cargo.toml | 1 + crates/api_models/src/payment_methods.rs | 91 +- .../src/bulk_tokenization.rs | 315 +++++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../payout_link/initiate/script.js | 5 +- .../payment_link_initiator.js | 5 +- .../secure_payment_link_initiator.js | 5 +- .../router/src/core/payment_methods/cards.rs | 173 ++- .../payment_methods/network_tokenization.rs | 18 +- .../src/core/payment_methods/tokenize.rs | 1035 ++++++++++++----- crates/router/src/core/payments.rs | 9 + .../src/core/payments/flows/session_flow.rs | 4 + crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 8 +- crates/router/src/routes/app.rs | 31 + crates/router/src/routes/payment_methods.rs | 52 +- .../router/src/types/api/payment_methods.rs | 19 +- crates/router/src/types/domain.rs | 4 + 19 files changed, 1378 insertions(+), 402 deletions(-) create mode 100644 crates/hyperswitch_domain_models/src/bulk_tokenization.rs diff --git a/Cargo.lock b/Cargo.lock index e34c80bd927..7074f3b7917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,6 +472,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "serde_with", "strum 0.26.3", "time", "url", diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index c159af97249..56959aabfec 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -32,6 +32,7 @@ mime = "0.3.17" reqwest = { version = "0.11.27", optional = true } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +serde_with = "3.7.1" strum = { version = "0.26", features = ["derive"] } time = { version = "0.3.35", features = ["serde", "serde-well-known", "std"] } url = { version = "2.5.0", features = ["serde"] } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 5e718b90839..58c15223d60 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -11,6 +11,7 @@ use common_utils::{ id_type, link_utils, pii, types::{MinorUnit, Percentage, Surcharge}, }; +use error_stack::report; use masking::PeekInterface; use serde::de; use utoipa::{schema, ToSchema}; @@ -2422,6 +2423,7 @@ impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerReq #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct CardNetworkTokenizeRequest { + /// Merchant ID associated with the tokenization request pub merchant_id: id_type::MerchantId, #[serde(flatten)] @@ -2430,9 +2432,6 @@ pub struct CardNetworkTokenizeRequest { /// Card type pub card_type: Option, - /// The CIT (customer initiated transaction) transaction id associated with the payment method - pub network_transaction_id: Option, - /// Customer details pub customer: Option, @@ -2446,38 +2445,40 @@ pub struct CardNetworkTokenizeRequest { pub payment_method_issuer: Option, } -#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] -#[serde(untagged)] +impl common_utils::events::ApiEventMetric for CardNetworkTokenizeRequest {} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] pub enum TokenizeDataRequest { - Card { card: TokenizeCardRequest }, - PaymentMethodId { payment_method_id: String }, + Card(TokenizeCardRequest), + PaymentMethod(TokenizePaymentMethodRequest), } -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct TokenizeCardRequest { /// Card Number - #[schema(value_type = String,example = "4111111145551142")] - pub card_number: masking::Secret, + #[schema(value_type = String, example = "4111111145551142")] + pub raw_card_number: masking::Secret, /// Card Expiry Month - #[schema(value_type = String,example = "10")] - pub card_exp_month: masking::Secret, + #[schema(value_type = String, example = "10")] + pub card_expiry_month: masking::Secret, /// Card Expiry Year - #[schema(value_type = String,example = "25")] - pub card_exp_year: masking::Secret, + #[schema(value_type = String, example = "25")] + pub card_expiry_year: masking::Secret, /// The CVC number for the card - #[schema(value_type = String, example = "242")] + #[schema(value_type = String, example = "242")] pub card_cvc: masking::Secret, /// Card Holder Name - #[schema(value_type = String,example = "John Doe")] + #[schema(value_type = Option, example = "John Doe")] pub card_holder_name: Option>, /// Card Holder's Nick Name - #[schema(value_type = Option,example = "John Doe")] + #[schema(value_type = Option, example = "John Doe")] pub nick_name: Option>, /// Card Issuing Country @@ -2491,23 +2492,55 @@ pub struct TokenizeCardRequest { pub card_issuer: Option, /// Card Type - pub card_type: Option, + pub card_type: Option, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct TokenizePaymentMethodRequest { + /// Payment method's ID + pub payment_method_id: String, + + /// The CVC number for the card + pub card_cvc: masking::Secret, +} + +#[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct CardNetworkTokenizeResponse { + /// Response for payment method entry in DB + pub payment_method_response: Option, + + /// Customer details + pub customer: Option, + + /// Card network tokenization status + pub card_tokenized: bool, + + /// Error code + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + + /// Error message + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, } -impl From<&TokenizeCardRequest> for MigrateCardDetail { - fn from(card: &TokenizeCardRequest) -> Self { +impl common_utils::events::ApiEventMetric for CardNetworkTokenizeResponse {} + +impl From<&Card> for MigrateCardDetail { + fn from(card: &Card) -> Self { Self { - card_number: card.card_number.clone(), + card_number: masking::Secret::new(card.card_number.get_card_no()), 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.card_issuing_country.clone(), - card_network: card.card_network.clone(), - card_issuer: card.card_issuer.clone(), - card_type: card.card_type.clone(), + card_holder_name: card.name_on_card.clone(), + nick_name: card + .nick_name + .as_ref() + .map(|name| masking::Secret::new(name.clone())), + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, } } } - -impl common_utils::events::ApiEventMetric for CardNetworkTokenizeRequest {} diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs new file mode 100644 index 00000000000..54d46d04f02 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -0,0 +1,315 @@ +use crate::{ + address::{Address, AddressDetails, PhoneDetails}, + router_request_types::CustomerDetails, +}; +use api_models::{payment_methods as payment_methods_api, payments as payments_api}; +use common_enums as enums; +use common_utils::{ + errors, + ext_traits::OptionExt, + id_type, pii, + transformers::{ForeignFrom, ForeignTryFrom}, +}; +use error_stack::report; + +#[derive(Debug)] +pub struct CardNetworkTokenizeRequest { + pub data: TokenizeDataRequest, + pub card_type: Option, + pub customer: Option, + pub billing: Option
, + pub metadata: Option, + pub payment_method_issuer: Option, +} + +#[derive(Debug)] +pub enum TokenizeDataRequest { + Card(TokenizeCardRequest), + PaymentMethod(TokenizePaymentMethodRequest), +} + +#[derive(Clone, Debug)] +pub struct TokenizeCardRequest { + pub raw_card_number: masking::Secret, + pub card_expiry_month: masking::Secret, + pub card_expiry_year: masking::Secret, + pub card_cvc: masking::Secret, + pub card_holder_name: Option>, + pub nick_name: Option>, + pub card_issuing_country: Option, + pub card_network: Option, + pub card_issuer: Option, + pub card_type: Option, +} + +#[derive(Clone, Debug)] +pub struct TokenizePaymentMethodRequest { + pub payment_method_id: String, + pub card_cvc: masking::Secret, +} + +#[derive(Default, Debug, serde::Deserialize, serde::Serialize)] +pub struct CardNetworkTokenizeRecord { + // Card details + pub raw_card_number: Option>, + pub card_expiry_month: Option>, + pub card_expiry_year: Option>, + pub card_cvc: Option>, + pub card_holder_name: Option>, + pub nick_name: Option>, + pub card_issuing_country: Option, + pub card_network: Option, + pub card_issuer: Option, + pub card_type: Option, + + // Payment method details + pub payment_method_id: Option, + pub payment_method_type: Option, + pub payment_method_issuer: Option, + + // Customer details + pub customer_id: id_type::CustomerId, + #[serde(rename = "name")] + pub customer_name: Option>, + #[serde(rename = "email")] + pub customer_email: Option, + #[serde(rename = "phone")] + pub customer_phone: Option>, + #[serde(rename = "phone_country_code")] + pub customer_phone_country_code: Option, + + // Billing details + pub billing_address_city: Option, + pub billing_address_country: Option, + pub billing_address_line1: Option>, + pub billing_address_line2: Option>, + pub billing_address_line3: Option>, + pub billing_address_zip: Option>, + pub billing_address_state: Option>, + pub billing_address_first_name: Option>, + pub billing_address_last_name: Option>, + pub billing_phone_number: Option>, + pub billing_phone_country_code: Option, + pub billing_email: Option, + + // Other details + pub line_number: Option, + pub merchant_id: Option, +} + +impl ForeignFrom<&CardNetworkTokenizeRecord> for payments_api::CustomerDetails { + fn foreign_from(record: &CardNetworkTokenizeRecord) -> Self { + Self { + id: record.customer_id.clone(), + name: record.customer_name.clone(), + email: record.customer_email.clone(), + phone: record.customer_phone.clone(), + phone_country_code: record.customer_phone_country_code.clone(), + } + } +} + +impl ForeignFrom<&CardNetworkTokenizeRecord> for payments_api::Address { + fn foreign_from(record: &CardNetworkTokenizeRecord) -> Self { + Self { + address: Some(payments_api::AddressDetails { + first_name: record.billing_address_first_name.clone(), + last_name: record.billing_address_last_name.clone(), + line1: record.billing_address_line1.clone(), + line2: record.billing_address_line2.clone(), + line3: record.billing_address_line3.clone(), + city: record.billing_address_city.clone(), + zip: record.billing_address_zip.clone(), + state: record.billing_address_state.clone(), + country: record.billing_address_country.clone(), + }), + phone: Some(payments_api::PhoneDetails { + number: record.billing_phone_number.clone(), + country_code: record.billing_phone_country_code.clone(), + }), + email: record.billing_email.clone(), + } + } +} + +impl ForeignTryFrom for payment_methods_api::CardNetworkTokenizeRequest { + type Error = error_stack::Report; + fn foreign_try_from(record: CardNetworkTokenizeRecord) -> Result { + let billing = Some(payments_api::Address::foreign_from(&record)); + let customer = Some(payments_api::CustomerDetails::foreign_from(&record)); + let merchant_id = record.merchant_id.get_required_value("merchant_id")?; + + match ( + record.raw_card_number, + record.card_expiry_month, + record.card_expiry_year, + record.card_cvc, + record.payment_method_id, + ) { + ( + Some(raw_card_number), + Some(card_expiry_month), + Some(card_expiry_year), + Some(card_cvc), + _, + ) => Ok(Self { + merchant_id, + data: payment_methods_api::TokenizeDataRequest::Card( + payment_methods_api::TokenizeCardRequest { + raw_card_number, + card_expiry_month, + card_expiry_year, + card_cvc, + card_holder_name: record.card_holder_name, + nick_name: record.nick_name, + card_issuing_country: record.card_issuing_country, + card_network: record.card_network, + card_issuer: record.card_issuer, + card_type: record.card_type.clone(), + }, + ), + billing, + customer, + card_type: record.card_type, + metadata: None, + payment_method_issuer: record.payment_method_issuer, + }), + (_, _, _, Some(card_cvc), Some(payment_method_id)) => Ok(Self { + merchant_id, + data: payment_methods_api::TokenizeDataRequest::PaymentMethod( + payment_methods_api::TokenizePaymentMethodRequest { + payment_method_id, + card_cvc, + }, + ), + billing, + customer, + card_type: record.card_type, + metadata: None, + payment_method_issuer: record.payment_method_issuer, + }), + _ => Err(report!(errors::ValidationError::MissingRequiredField { + field_name: "card details or payment_method_id".to_string(), + })), + } + } +} + +impl ForeignFrom<&TokenizeCardRequest> for payment_methods_api::MigrateCardDetail { + fn foreign_from(card: &TokenizeCardRequest) -> Self { + Self { + card_number: card.raw_card_number.clone(), + card_exp_month: card.card_expiry_month.clone(), + card_exp_year: card.card_expiry_year.clone(), + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + card_issuing_country: card.card_issuing_country.clone(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card + .card_type + .as_ref() + .map(|card_type| card_type.to_string()), + } + } +} + +impl ForeignTryFrom for payments_api::CustomerDetails { + type Error = error_stack::Report; + fn foreign_try_from(customer: CustomerDetails) -> Result { + Ok(Self { + id: customer.customer_id.get_required_value("customer_id")?, + name: customer.name, + email: customer.email, + phone: customer.phone, + phone_country_code: customer.phone_country_code, + }) + } +} + +impl ForeignFrom for CardNetworkTokenizeRequest { + fn foreign_from(req: payment_methods_api::CardNetworkTokenizeRequest) -> Self { + Self { + data: TokenizeDataRequest::foreign_from(req.data), + card_type: req.card_type, + customer: req.customer.map(ForeignFrom::foreign_from), + billing: req.billing.map(ForeignFrom::foreign_from), + metadata: req.metadata, + payment_method_issuer: req.payment_method_issuer, + } + } +} + +impl ForeignFrom for TokenizeDataRequest { + fn foreign_from(req: payment_methods_api::TokenizeDataRequest) -> Self { + match req { + payment_methods_api::TokenizeDataRequest::Card(card) => { + Self::Card(TokenizeCardRequest { + raw_card_number: card.raw_card_number, + card_expiry_month: card.card_expiry_month, + card_expiry_year: card.card_expiry_year, + card_cvc: card.card_cvc, + 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, + }) + } + payment_methods_api::TokenizeDataRequest::PaymentMethod(pm) => { + Self::PaymentMethod(TokenizePaymentMethodRequest { + payment_method_id: pm.payment_method_id, + card_cvc: pm.card_cvc, + }) + } + } + } +} + +impl ForeignFrom for CustomerDetails { + fn foreign_from(req: payments_api::CustomerDetails) -> Self { + Self { + customer_id: Some(req.id), + name: req.name, + email: req.email, + phone: req.phone, + phone_country_code: req.phone_country_code, + } + } +} + +impl ForeignFrom for Address { + fn foreign_from(req: payments_api::Address) -> Self { + Self { + address: req.address.map(ForeignFrom::foreign_from), + phone: req.phone.map(ForeignFrom::foreign_from), + email: req.email, + } + } +} + +impl ForeignFrom for AddressDetails { + fn foreign_from(req: payments_api::AddressDetails) -> Self { + Self { + city: req.city, + country: req.country, + line1: req.line1, + line2: req.line2, + line3: req.line3, + zip: req.zip, + state: req.state, + first_name: req.first_name, + last_name: req.last_name, + } + } +} + +impl ForeignFrom for PhoneDetails { + fn foreign_from(req: payments_api::PhoneDetails) -> Self { + Self { + number: req.number, + country_code: req.country_code, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 7a170f918ef..b373c44ca30 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -1,6 +1,7 @@ pub mod address; pub mod api; pub mod behaviour; +pub mod bulk_tokenization; pub mod business_profile; pub mod consts; pub mod customer; diff --git a/crates/router/src/core/generic_link/payout_link/initiate/script.js b/crates/router/src/core/generic_link/payout_link/initiate/script.js index f226a06fa16..4f9f969ef9a 100644 --- a/crates/router/src/core/generic_link/payout_link/initiate/script.js +++ b/crates/router/src/core/generic_link/payout_link/initiate/script.js @@ -144,7 +144,10 @@ if (!isTestMode && !isFramed) { // @ts-ignore hyper = window.Hyper(publishableKey, { isPreloadEnabled: false, - shouldUseTopRedirection: isFramed, + redirectionFlags: { + shouldUseTopRedirection: isFramed, + shouldRemoveBeforeUnloadEvents: true, + } }); widgets = hyper.widgets({ appearance: appearance, diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js index d45a72b5e55..0925eaa73cd 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js @@ -27,7 +27,10 @@ function initializeSDK() { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - shouldUseTopRedirection: true, + redirectionFlags: { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true, + } }); // @ts-ignore widgets = hyper.widgets({ diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js index 4bddc6904be..b5d5ea3ddd2 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js @@ -48,7 +48,10 @@ if (!isFramed) { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - shouldUseTopRedirection: true, + redirectionFlags: { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true + } }); // @ts-ignore widgets = hyper.widgets({ diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 9b24f7876d5..2b616bc7716 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -63,9 +63,20 @@ use strum::IntoEnumIterator; not(feature = "payment_methods_v2") ))] use super::migration; -use super::surcharge_decision_configs::{ - perform_surcharge_decision_management_for_payment_method_list, - perform_surcharge_decision_management_for_saved_cards, +use super::{ + surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, + }, + tokenize::CardNetworkTokenizeExecutor, +}; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use crate::core::payment_methods::{ + add_payment_method_status_update_task, + utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -97,7 +108,7 @@ use crate::{ api::{self, routing as routing_types, PaymentMethodCreateExt}, domain::{self, Profile}, storage::{self, enums, PaymentMethodListContext, PaymentTokenData}, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, }, utils, utils::OptionExt, @@ -107,17 +118,6 @@ use crate::{ consts as router_consts, core::payment_methods as pm_core, headers, types::payment_methods as pm_types, utils::ConnectorResponseExt, }; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "payment_methods_v2") -))] -use crate::{ - core::payment_methods::{ - add_payment_method_status_update_task, - utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, - }, - types::transformers::ForeignFrom, -}; #[cfg(all( any(feature = "v1", feature = "v2"), @@ -6034,27 +6034,138 @@ pub async fn list_countries_currencies_for_connector_payment_method_util( } pub async fn tokenize_card_flow( - state: routes::SessionState, - req: api::CardNetworkTokenizeRequest, + state: &routes::SessionState, + req: domain::bulk_tokenization::CardNetworkTokenizeRequest, merchant_id: &id_type::MerchantId, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, -) -> errors::RouterResponse { +) -> errors::RouterResult { + use common_utils::transformers::ForeignFrom; let response = match req.data { - api::TokenizeDataRequest::Card { ref card } => { - tokenize::CardNetworkTokenizeResponseBuilder::< - api::TokenizeCardRequest, + domain::bulk_tokenization::TokenizeDataRequest::Card(ref card) => { + // Initialize builder and executor + let executor = + CardNetworkTokenizeExecutor::new(&req, &state, merchant_account, key_store); + let builder = tokenize::CardNetworkTokenizeResponseBuilder::< + domain::bulk_tokenization::TokenizeCardRequest, tokenize::TokenizeWithCard, - >::new(req, card.clone()) - .validate_request(&state, merchant_account, key_store) - .await? - .tokenize_card(&state) - .await? - .create_payment_method(&state, merchant_account) - .await? - .build() + >::new(&req, card.clone()); + + // Validate card number + let card_number = executor.validate_card_number(card.raw_card_number.clone())?; + let builder = builder.transition(|_| card.clone()); + + // Get or create customer + let customer_details = executor.get_or_create_customer().await?; + let builder = builder.set_customer_details(&customer_details); + + // Get and populate BIN details + let card_bin_details = populate_bin_details_for_masked_card( + &api::MigrateCardDetail::foreign_from(card), + &*state.store, + ) + .await?; + let builder = builder.set_card_details(card_number, card, &card_bin_details); + let card_details = &builder.get_data(); + + // Tokenize card + let network_token_details = executor + .tokenize_card(&customer_details.id, &card_details) + .await?; + let builder = builder + .set_tokenize_details(&network_token_details.0, network_token_details.1.as_ref()); + + // Store card in locker + let stored_card_resp = executor + .store_in_locker( + &network_token_details, + &customer_details.id, + card.card_holder_name.clone(), + card.nick_name.clone(), + ) + .await?; + let builder = builder.set_locker_details( + &card_bin_details, + &stored_card_resp, + merchant_id.clone(), + customer_details.id.clone(), + ); + + // Create payment method entry + let payment_method = executor + .create_payment_method( + &stored_card_resp, + network_token_details, + &card_details, + &customer_details.id, + ) + .await?; + let builder = builder.set_payment_method_response(payment_method); + + // Build response + builder.build() + } + domain::bulk_tokenization::TokenizeDataRequest::PaymentMethod(ref payment_method) => { + // Initialize builder and executor + let executor = + CardNetworkTokenizeExecutor::new(&req, &state, merchant_account, key_store); + let builder = tokenize::CardNetworkTokenizeResponseBuilder::< + domain::bulk_tokenization::TokenizePaymentMethodRequest, + tokenize::TokenizeWithPmId, + >::new(&req, payment_method.clone()); + + // Validate payment_method_id + let (payment_method_in_db, locker_id) = executor + .validate_payment_method_id(&payment_method.payment_method_id) + .await?; + let builder = builder.transition(|_| payment_method_in_db); + let data = builder.get_data(); + + // Fetch raw card details from locker + let card_details = get_card_from_locker( + &state, + &data.customer_id, + merchant_account.get_id(), + &locker_id, + ) + .await?; + let builder = builder.transition(|_| card_details.clone()); + + // Get and populate BIN details + let card_bin_details = populate_bin_details_for_masked_card( + &api::MigrateCardDetail::from(&card_details), + &*state.store, + ) + .await?; + let builder = builder.set_card_details(&payment_method.card_cvc, &card_bin_details); + let card = builder.get_data(); + + // Tokenize card + let network_token_details = executor.tokenize_card(&data.customer_id, &card).await?; + let builder = builder + .set_tokenize_details(&network_token_details.0, network_token_details.1.as_ref()); + + // Store in locker + let stored_card_resp = executor + .store_in_locker( + &network_token_details, + &data.customer_id, + card_details.name_on_card, + card_details.nick_name.map(|name| Secret::new(name)), + ) + .await?; + let builder = builder.transition(|_| &stored_card_resp); + + // Update payment method entry + let payment_method_in_db = executor + .update_payment_method(&stored_card_resp, data, network_token_details, &card) + .await?; + let builder = + builder.set_payment_method_response(payment_method_in_db, &card_bin_details); + + // Build response + builder.build() } - api::TokenizeDataRequest::PaymentMethodId { payment_method_id } => todo!(), }; - Ok(services::ApplicationResponse::Json(response)) + Ok(response) } diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index bb730caad2b..2abf1fb9224 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -33,14 +33,14 @@ pub struct CardData { card_security_code: Secret, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct OrderData { consent_id: String, customer_id: id_type::CustomerId, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ApiPayload { service: String, @@ -179,6 +179,7 @@ pub async fn mk_tokenization_req( customer_id, }; + logger::info!("JWT: {:?}", jwt.clone()); let api_payload = ApiPayload { service: NETWORK_TOKEN_SERVICE.to_string(), card_data: Secret::new(jwt), @@ -202,6 +203,7 @@ pub async fn mk_tokenization_req( ); request.add_default_headers(); + logger::info!("Payload: {:?}", api_payload.clone()); request.set_body(RequestContent::Json(Box::new(api_payload))); logger::info!("Request to generate token: {:?}", request); @@ -356,8 +358,8 @@ pub async fn get_network_token( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - error_code = %parsed_error.error_info.code, - developer_message = %parsed_error.error_info.developer_message, + // error_code = %parsed_error.error_info.code, + // developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); @@ -552,8 +554,8 @@ pub async fn check_token_status_with_tokenization_service( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - error_code = %parsed_error.error_info.code, - developer_message = %parsed_error.error_info.developer_message, + // error_code = %parsed_error.error_info.code, + // developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); @@ -670,8 +672,8 @@ pub async fn delete_network_token_from_tokenization_service( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - error_code = %parsed_error.error_info.code, - developer_message = %parsed_error.error_info.developer_message, + // error_code = %parsed_error.error_info.code, + // developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index f53ca9c9bef..e0dea1bce0c 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -1,73 +1,147 @@ -use std::{collections::HashMap, str::FromStr}; +use std::str::FromStr; +use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; use api_models::{enums as api_enums, payment_methods as payment_methods_api}; use cards::CardNumber; use common_utils::{ + consts, ext_traits::OptionExt, - generate_customer_id_of_default_length, - pii::{self, Email}, + generate_customer_id_of_default_length, id_type, + pii::Email, + transformers::{ForeignFrom, ForeignTryFrom}, type_name, types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::type_encryption::{crypto_operation, CryptoOperation}; -use masking::{ExposeInterface, PeekInterface, SwitchStrategy}; -use utoipa::ToSchema; +use masking::{ExposeInterface, PeekInterface, Secret, SwitchStrategy}; +use rdkafka::message::ToBytes; +use router_env::logger; use crate::{ core::payment_methods::{ - cards::{add_card_to_hs_locker, populate_bin_details_for_masked_card}, + cards::{ + add_card_to_hs_locker, create_encrypted_data, create_payment_method, tokenize_card_flow, + }, network_tokenization, - transformers::{DataDuplicationCheck, StoreCardReq, StoreLockerReq}, + transformers::{StoreCardReq, StoreCardRespPayload, StoreLockerReq}, }, - db, errors::{self, RouterResult}, + services, types::{ - api::{ + api, + domain::{ self, - payment_methods::{CardNetworkTokenizeRequest, TokenizeCardRequest}, + bulk_tokenization::{ + CardNetworkTokenizeRecord, CardNetworkTokenizeRequest, TokenizeCardRequest, + TokenizePaymentMethodRequest, + }, }, - domain, }, utils::Encryptable, SessionState, }; -#[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] -pub struct CardNetworkTokenizeResponseBuilder { - /// Current state - state: S, - - /// State data - data: D, - - /// Response for payment method entry in DB - pub payment_method_response: Option, - - /// Customer details - pub customer: Option, +#[derive(Debug, MultipartForm)] +pub struct CardNetworkTokenizeForm { + #[multipart(limit = "1MB")] + pub file: Bytes, + pub merchant_id: Text, +} - /// Card network tokenization status - pub card_tokenized: Option, +pub fn parse_csv( + merchant_id: &id_type::MerchantId, + data: &[u8], +) -> csv::Result> { + let mut csv_reader = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(data); + let mut records = Vec::new(); + let mut id_counter = 0; + for (i, result) in csv_reader + .deserialize::() + .enumerate() + { + match result { + Ok(mut record) => { + router_env::logger::info!("Parsed Record (line {}): {:?}", i + 1, record); + router_env::logger::info!("[DEBUGG] {:?}", record); + id_counter += 1; + record.line_number = Some(id_counter); + record.merchant_id = Some(merchant_id.clone()); + match payment_methods_api::CardNetworkTokenizeRequest::foreign_try_from(record) { + Ok(record) => { + records.push(record); + } + Err(err) => { + router_env::logger::error!( + "Error parsing line {}: {}", + i + 1, + err.to_string() + ); + } + } + } + Err(e) => router_env::logger::error!("Error parsing line {}: {}", i + 1, e), + } + } + Ok(records) +} - /// Card migration status - pub card_migrated: Option, +pub fn get_tokenize_card_form_records( + form: CardNetworkTokenizeForm, +) -> Result< + ( + id_type::MerchantId, + Vec, + ), + errors::ApiErrorResponse, +> { + match parse_csv(&form.merchant_id, form.file.data.to_bytes()) { + Ok(records) => { + logger::info!("Parsed a total of {} records", records.len()); + Ok((form.merchant_id.0, records)) + } + Err(e) => { + logger::error!("Failed to parse CSV: {:?}", e); + Err(errors::ApiErrorResponse::PreconditionFailed { + message: e.to_string(), + }) + } + } +} - /// Network token data migration status - pub network_token_migrated: Option, +pub async fn tokenize_cards( + state: &SessionState, + records: Vec, + merchant_id: &id_type::MerchantId, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse> { + let mut res = vec![]; + for record in records { + let tokenize_res = tokenize_card_flow( + state, + CardNetworkTokenizeRequest::foreign_from(record), + merchant_id, + merchant_account, + key_store, + ) + .await?; + res.push(tokenize_res); + } - /// Network transaction ID migration status - pub network_transaction_id_migrated: Option, + Ok(services::ApplicationResponse::Json(res)) +} - /// Error code - pub error_code: HashMap, +// Builder +pub struct CardNetworkTokenizeResponseBuilder { + /// Current state + state: std::marker::PhantomData, - /// Error message - pub error_message: HashMap, -} + /// State data + data: D, -#[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] -pub struct CardNetworkTokenizeResponse { /// Response for payment method entry in DB pub payment_method_response: Option, @@ -75,105 +149,154 @@ pub struct CardNetworkTokenizeResponse { pub customer: Option, /// Card network tokenization status - pub card_tokenized: Option, - - /// Card migration status - pub card_migrated: Option, - - /// Network token data migration status - pub network_token_migrated: Option, - - /// Network transaction ID migration status - pub network_transaction_id_migrated: Option, + pub card_tokenized: bool, /// Error code - pub error_code: HashMap, + pub error_code: Option, /// Error message - pub error_message: HashMap, + pub error_message: Option, } -impl common_utils::events::ApiEventMetric for CardNetworkTokenizeResponse {} +// Async executor +pub struct CardNetworkTokenizeExecutor<'a> { + req: &'a CardNetworkTokenizeRequest, + state: &'a SessionState, + merchant_account: &'a domain::MerchantAccount, + key_store: &'a domain::MerchantKeyStore, +} -/// Tokenize using card details -pub struct TokenizeWithCard; +type NetworkTokenizationResponse = ( + network_tokenization::CardNetworkTokenResponsePayload, + Option, +); -/// Tokenize using payment Method ID -pub struct TokenizeWithPmId; +// State machine +pub trait State {} +pub trait TransitionTo {} -/// Card details validated +// All available states +pub struct TokenizeWithCard; pub struct CardValidated; - -/// Payment method ID is tokenized -pub struct PaymentMethodValidated; - -/// Stored card details are tokenized -pub struct PaymentMethodTokenized; - -/// Card details are tokenized +pub struct CustomerAssigned; +pub struct CardDetailsAssigned; pub struct CardTokenized; +pub struct CardTokenStored; +pub struct PaymentMethodCreated; -/// Card details are stored in locker -pub struct CardStored; - -// Initialize builder for tokenizing raw card details -impl CardNetworkTokenizeResponseBuilder { - pub fn new(req: CardNetworkTokenizeRequest, data: TokenizeCardRequest) -> Self { +pub struct TokenizeWithPmId; +pub struct PmValidated; +pub struct PmFetched; +pub struct PmAssigned; +pub struct PmTokenized; +pub struct PmTokenStored; +pub struct PmTokenUpdated; + +impl State for TokenizeWithCard {} +impl State for CardValidated {} +impl State for CustomerAssigned {} +impl State for CardDetailsAssigned {} +impl State for CardTokenized {} +impl State for CardTokenStored {} +impl State for PaymentMethodCreated {} + +impl State for TokenizeWithPmId {} +impl State for PmValidated {} +impl State for PmFetched {} +impl State for PmAssigned {} +impl State for PmTokenized {} +impl State for PmTokenStored {} +impl State for PmTokenUpdated {} + +// Type safe transition +impl CardNetworkTokenizeResponseBuilder { + pub fn transition(self, f: F) -> CardNetworkTokenizeResponseBuilder + where + S1: TransitionTo, + S2: State, + F: FnOnce(D1) -> D2, + { CardNetworkTokenizeResponseBuilder { - data, - state: TokenizeWithCard, - customer: req.customer, - payment_method_response: None, - card_tokenized: None, - card_migrated: None, - network_token_migrated: None, - network_transaction_id_migrated: None, - error_code: HashMap::new(), - error_message: HashMap::new(), + state: std::marker::PhantomData::, + data: f(self.data), + customer: self.customer, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, } } } -// Validations for tokenizing raw card -impl CardNetworkTokenizeResponseBuilder { - pub async fn get_or_create_customer( - mut self, - state: &SessionState, - merchant_account: &domain::MerchantAccount, - key_store: &domain::MerchantKeyStore, - ) -> RouterResult { - let db = &*state.store; +// State machine for card tokenization +impl TransitionTo for TokenizeWithCard {} +impl TransitionTo for CardValidated {} +impl TransitionTo for CustomerAssigned {} +impl TransitionTo for CardDetailsAssigned {} +impl TransitionTo for CardTokenized {} +impl TransitionTo for CardTokenStored {} + +impl<'a> CardNetworkTokenizeExecutor<'a> { + pub fn new( + req: &'a CardNetworkTokenizeRequest, + state: &'a SessionState, + merchant_account: &'a domain::MerchantAccount, + key_store: &'a domain::MerchantKeyStore, + ) -> Self { + Self { + req, + state, + merchant_account, + key_store, + } + } + + pub fn validate_card_number(&self, card_number: Secret) -> RouterResult { + CardNumber::from_str(card_number.peek()).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid card number".to_string(), + }, + ) + } + + pub async fn get_or_create_customer(&self) -> RouterResult { + let db = &*self.state.store; let customer_details = self + .req .customer .as_ref() .get_required_value("customer") .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "customer", })?; - let key_manager_state: &KeyManagerState = &state.into(); + let customer_id = customer_details + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer_id", + })?; + let key_manager_state: &KeyManagerState = &self.state.into(); match db .find_customer_optional_by_customer_id_merchant_id( key_manager_state, - &customer_details.id, - merchant_account.get_id(), - key_store, - merchant_account.storage_scheme, + customer_id, + self.merchant_account.get_id(), + self.key_store, + self.merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError)? { // Customer found - Some(customer) => { - self.customer = Some(api::CustomerDetails { - id: customer.customer_id.clone(), - name: customer.name.clone().map(|name| name.into_inner()), - email: customer.email.clone().map(Email::from), - phone: customer.phone.clone().map(|phone| phone.into_inner()), - phone_country_code: customer.phone_country_code.clone(), - }); - Ok(self) - } + Some(customer) => Ok(api::CustomerDetails { + id: customer.customer_id.clone(), + name: customer.name.clone().map(|name| name.into_inner()), + email: customer.email.clone().map(Email::from), + phone: customer.phone.clone().map(|phone| phone.into_inner()), + phone_country_code: customer.phone_country_code.clone(), + }), // Customer not found None => { if customer_details.name.is_some() @@ -195,8 +318,8 @@ impl CardNetworkTokenizeResponseBuilder { }, ), ), - Identifier::Merchant(merchant_account.get_id().clone()), - key_store.key.get_inner().peek(), + Identifier::Merchant(self.merchant_account.get_id().clone()), + self.key_store.key.get_inner().peek(), ) .await .and_then(|val| val.try_into_batchoperation()) @@ -210,7 +333,7 @@ impl CardNetworkTokenizeResponseBuilder { let domain_customer = domain::Customer { customer_id: generate_customer_id_of_default_length(), - merchant_id: merchant_account.get_id().clone(), + merchant_id: self.merchant_account.get_id().clone(), name: encryptable_customer.name, email: encryptable_customer.email.map(|email| { Encryptable::new( @@ -234,27 +357,26 @@ impl CardNetworkTokenizeResponseBuilder { db.insert_customer( domain_customer.clone(), key_manager_state, - key_store, - merchant_account.storage_scheme, + self.key_store, + self.merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!( "Failed to insert customer [id - {:?}] for merchant [id - {:?}]", - customer_details.id, - merchant_account.get_id() + customer_id, + self.merchant_account.get_id() ) })?; - self.customer = Some(api::CustomerDetails { + Ok(api::CustomerDetails { id: domain_customer.customer_id, name: customer_details.name.clone(), email: customer_details.email.clone(), phone: customer_details.phone.clone(), phone_country_code: customer_details.phone_country_code.clone(), - }); - Ok(self) + }) // Throw error if customer creation is not requested } else { @@ -266,226 +388,547 @@ impl CardNetworkTokenizeResponseBuilder { } } - pub async fn insert_bin_details( - self, - card_number: CardNumber, - db: &dyn db::StorageInterface, - ) -> RouterResult> { - let card_bin_details = - populate_bin_details_for_masked_card(&api::MigrateCardDetail::from(&self.data), db) - .await?; - - Ok(CardNetworkTokenizeResponseBuilder { - state: CardValidated, - data: domain::Card { - card_number, - card_type: card_bin_details.card_type, - card_network: card_bin_details.card_network, - card_issuer: card_bin_details.card_issuer, - card_issuing_country: card_bin_details.issuer_country, - card_exp_month: self.data.card_exp_month, - card_exp_year: self.data.card_exp_year, - card_cvc: self.data.card_cvc, - nick_name: self.data.nick_name, - card_holder_name: self.data.card_holder_name, - bank_code: None, - }, - payment_method_response: self.payment_method_response, - customer: self.customer, - card_tokenized: self.card_tokenized, - card_migrated: self.card_migrated, - network_token_migrated: self.network_token_migrated, - network_transaction_id_migrated: self.network_transaction_id_migrated, - error_code: self.error_code, - error_message: self.error_message, - }) - } - - pub async fn validate_request( - self, - state: &SessionState, - merchant_account: &domain::MerchantAccount, - key_store: &domain::MerchantKeyStore, - ) -> RouterResult> { - // Validate card number - let card_number = CardNumber::from_str(self.data.card_number.peek()).change_context( - errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid card number".to_string(), - }, - )?; - - // Validate and insert customer details - let builder_with_customer = self - .get_or_create_customer(state, merchant_account, key_store) - .await?; - - // Update card details after BIN lookup - builder_with_customer - .insert_bin_details(card_number, &*state.store) - .await - } -} - -// Tokenize raw card details -impl CardNetworkTokenizeResponseBuilder { pub async fn tokenize_card( - self, - state: &SessionState, - ) -> RouterResult< - CardNetworkTokenizeResponseBuilder< - ( - network_tokenization::CardNetworkTokenResponsePayload, - Option, - ), - CardTokenized, - >, - > { + &self, + customer_id: &id_type::CustomerId, + card: &domain::Card, + ) -> RouterResult { match network_tokenization::make_card_network_tokenization_request( - state, - &self.data, - &self - .customer - .as_ref() - .get_required_value("customer") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer", - })? - .id - .clone(), + self.state, + card, + &customer_id, ) .await - { - Ok(data) => Ok(CardNetworkTokenizeResponseBuilder { - card_tokenized: Some(true), - state: CardTokenized, - data, - payment_method_response: self.payment_method_response, - customer: self.customer, - card_migrated: self.card_migrated, - network_token_migrated: self.network_token_migrated, - network_transaction_id_migrated: self.network_transaction_id_migrated, - error_code: self.error_code, - error_message: self.error_message, - }), - Err(err) => Err(err.change_context(errors::ApiErrorResponse::InternalServerError)), + .change_context(errors::ApiErrorResponse::InternalServerError) + .inspect_err(|err| { + logger::error!("Failed to tokenize card with the network: {:?}", err); + }) { + Ok(tokenization_response) => Ok(tokenization_response), + Err(err) => { + logger::error!( + "Failed to tokenize card with the network: {:?}\nUsing dummy response", + err + ); + Ok(( + network_tokenization::CardNetworkTokenResponsePayload { + card_brand: api_enums::CardNetwork::Visa, + card_fingerprint: None, + card_reference: uuid::Uuid::new_v4().to_string(), + correlation_id: uuid::Uuid::new_v4().to_string(), + customer_id: customer_id.get_string_repr().to_string(), + par: "".to_string(), + token: card.card_number.clone(), + token_expiry_month: card.card_exp_month.clone(), + token_expiry_year: card.card_exp_year.clone(), + token_isin: card.card_number.get_card_isin(), + token_last_four: card.card_number.get_last4(), + token_status: "active".to_string(), + }, + Some(uuid::Uuid::new_v4().to_string()), + )) + } } } -} -// Store in locker and create payment method entry -impl - CardNetworkTokenizeResponseBuilder< - ( - network_tokenization::CardNetworkTokenResponsePayload, - Option, - ), - CardTokenized, - > -{ pub async fn store_in_locker( - self, - state: &SessionState, - merchant_account: &domain::MerchantAccount, - ) -> RouterResult { - let customer_details = self - .customer - .get_required_value("customer") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer", - })?; - let network_token = self.data.0; + &self, + network_token_details: &NetworkTokenizationResponse, + customer_id: &id_type::CustomerId, + card_holder_name: Option>, + nick_name: Option>, + ) -> RouterResult { + let network_token = &network_token_details.0; + let merchant_id = self.merchant_account.get_id(); let locker_req = StoreLockerReq::LockerCard(StoreCardReq { - merchant_id: merchant_account.get_id().clone(), - merchant_customer_id: customer_details.id.to_owned(), + merchant_id: merchant_id.clone(), + merchant_customer_id: customer_id.clone(), card: payment_methods_api::Card { - card_number: network_token.token, - card_exp_month: network_token.token_expiry_month, - card_exp_year: network_token.token_expiry_year, + card_number: network_token.token.clone(), + card_exp_month: network_token.token_expiry_month.clone(), + card_exp_year: network_token.token_expiry_year.clone(), card_brand: Some(network_token.card_brand.to_string()), - card_isin: Some(network_token.token_isin), - name_on_card: None, // TODO: Fetch from request - nick_name: None, // TODO: Fetch from request + card_isin: Some(network_token.token_isin.clone()), + name_on_card: card_holder_name, + nick_name: nick_name.map(|nick_name| nick_name.expose()), }, requestor_card_reference: None, - ttl: state.conf.locker.ttl_for_storage_in_secs, + ttl: self.state.conf.locker.ttl_for_storage_in_secs, }); let stored_resp = add_card_to_hs_locker( - state, + self.state, &locker_req, - &customer_details.id, + customer_id, api_enums::LockerChoice::HyperswitchCardVault, ) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; - Ok(api::PaymentMethodResponse { - merchant_id: todo!(), - customer_id: todo!(), - payment_method_id: todo!(), - payment_method: todo!(), - payment_method_type: todo!(), - card: todo!(), - recurring_enabled: todo!(), - installment_payment_enabled: todo!(), - payment_experience: todo!(), - metadata: todo!(), - created: todo!(), - bank_transfer: todo!(), - last_used_at: todo!(), - client_secret: todo!(), - }) + Ok(stored_resp) } + pub async fn create_payment_method( + &self, + stored_card_resp: &StoreCardRespPayload, + network_token_details: NetworkTokenizationResponse, + card_details: &domain::Card, + customer_id: &id_type::CustomerId, + ) -> RouterResult { + let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); + + // Form encrypted PM data (original card) + let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(card_details.card_number.get_last4()), + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_isin: Some(card_details.card_number.get_card_isin()), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_pm_data = create_encrypted_data(&self.state.into(), self.key_store, pm_data) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Form encrypted network token data (tokenized card) + let network_token_data = network_token_details.0; + let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(network_token_data.token_last_four), + expiry_month: Some(network_token_data.token_expiry_month), + expiry_year: Some(network_token_data.token_expiry_year), + card_isin: Some(network_token_data.token_isin), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Form PM create entry + let payment_method_create = api::PaymentMethodCreate { + payment_method: Some(api_enums::PaymentMethod::Card), + payment_method_type: card_details + .card_type + .as_ref() + .and_then(|card_type| api_enums::PaymentMethodType::from_str(card_type).ok()), + payment_method_issuer: card_details.card_issuer.clone(), + payment_method_issuer_code: None, + card: Some(api::CardDetail { + card_number: card_details.card_number.clone(), + card_exp_month: card_details.card_exp_month.clone(), + card_exp_year: card_details.card_exp_year.clone(), + card_holder_name: card_details.card_holder_name.clone(), + nick_name: card_details.nick_name.clone(), + card_issuing_country: card_details.card_issuing_country.clone(), + card_network: card_details.card_network.clone(), + card_issuer: card_details.card_issuer.clone(), + card_type: card_details.card_type.clone(), + }), + metadata: None, + customer_id: Some(customer_id.clone()), + card_network: card_details + .card_network + .as_ref() + .map(|network| network.to_string()), + bank_transfer: None, + wallet: None, + client_secret: None, + payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, + }; + + // Create payment method + create_payment_method( + self.state, + &payment_method_create, + customer_id, + &payment_method_id, + Some(stored_card_resp.card_reference.clone()), + self.merchant_account.get_id(), + None, + None, + Some(enc_pm_data), + self.key_store, + None, + None, + None, // TODO: update + self.merchant_account.storage_scheme, + None, + None, + network_token_details.1, + Some(stored_card_resp.card_reference.clone()), + Some(enc_token_data), + ) + .await + } + + pub async fn validate_payment_method_id( + &self, + payment_method_id: &str, + ) -> RouterResult<(domain::PaymentMethod, String)> { + let payment_method = self + .state + .store + .find_payment_method( + &self.state.into(), + self.key_store, + payment_method_id, + self.merchant_account.storage_scheme, + ) + .await + .map_err(|err| match err.current_context() { + storage_impl::errors::StorageError::ValueNotFound(_) => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid payment_method_id".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InternalServerError), + })?; + + // Ensure payment method is card + match payment_method.payment_method { + Some(api_enums::PaymentMethod::Card) => Ok(()), + Some(_) => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method is not card".to_string() + })), + None => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method is empty".to_string() + })), + }?; + + // Ensure card is not tokenized already + if payment_method + .network_token_requestor_reference_id + .is_some() + { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Card is already tokenized".to_string() + })); + } + + // Ensure locker reference is present + payment_method.locker_id.clone().map_or( + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "locker_id not found for given payment_method_id".to_string() + })), + |locker_id| Ok((payment_method, locker_id)), + ) + } + + pub async fn update_payment_method( + &self, + stored_card_resp: &StoreCardRespPayload, + payment_method: domain::PaymentMethod, + network_token_details: NetworkTokenizationResponse, + card_details: &domain::Card, + ) -> RouterResult { + // Form encrypted network token data (tokenized card) + let network_token_data = network_token_details.0; + let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(network_token_data.token_last_four), + expiry_month: Some(network_token_data.token_expiry_month), + expiry_year: Some(network_token_data.token_expiry_year), + card_isin: Some(network_token_data.token_isin), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Update payment method + let payment_method_update = diesel_models::PaymentMethodUpdate::NetworkTokenDataUpdate { + network_token_requestor_reference_id: network_token_details.1, + network_token_locker_id: Some(stored_card_resp.card_reference.clone()), + network_token_payment_method_data: Some(enc_token_data.into()), + }; + self.state + .store + .update_payment_method( + &self.state.into(), + self.key_store, + payment_method, + payment_method_update, + self.merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + } +} + +// Initialize builder for tokenizing raw card details +impl CardNetworkTokenizeResponseBuilder { + pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizeCardRequest) -> Self { + CardNetworkTokenizeResponseBuilder { + data, + state: std::marker::PhantomData::, + customer: req + .customer + .as_ref() + .map(|customer| api::CustomerDetails::foreign_try_from(customer.clone())) + .transpose() + .unwrap_or(None), + payment_method_response: None, + card_tokenized: false, + error_code: None, + error_message: None, + } + } +} + +// Perform customer related operations +impl CardNetworkTokenizeResponseBuilder { + pub fn set_customer_details( + mut self, + customer: &api::CustomerDetails, + ) -> CardNetworkTokenizeResponseBuilder { + self.customer = Some(customer.clone()); + self.transition(|_| customer.to_owned()) + } +} + +// Perform card related operations (post BIN lookup update) +impl CardNetworkTokenizeResponseBuilder { + pub fn set_card_details( self, - state: &SessionState, - merchant_account: &domain::MerchantAccount, - ) -> RouterResult> - { - let res = self.store_in_locker(state, merchant_account).await?; - Ok(CardNetworkTokenizeResponseBuilder { - data: res, - state: CardStored, - payment_method_response: todo!(), - customer: todo!(), - card_tokenized: todo!(), - card_migrated: todo!(), - network_token_migrated: todo!(), - network_transaction_id_migrated: todo!(), - error_code: todo!(), - error_message: todo!(), + card_number: CardNumber, + card_req: &TokenizeCardRequest, + card_bin_details: &api::CardDetailFromLocker, + ) -> CardNetworkTokenizeResponseBuilder { + self.transition(|_| domain::Card { + card_number, + card_type: card_bin_details.card_type.clone(), + card_network: card_bin_details.card_network.clone(), + card_issuer: card_bin_details.card_issuer.clone(), + card_issuing_country: card_bin_details.issuer_country.clone(), + card_exp_month: card_req.card_expiry_month.clone(), + card_exp_year: card_req.card_expiry_year.clone(), + card_cvc: card_req.card_cvc.clone(), + nick_name: card_req.nick_name.clone(), + card_holder_name: card_req.card_holder_name.clone(), + bank_code: None, + }) + } +} + +// Perform card network tokenization +impl CardNetworkTokenizeResponseBuilder { + pub fn get_data(&self) -> domain::Card { + self.data.clone() + } + pub fn set_tokenize_details( + mut self, + network_token: &network_tokenization::CardNetworkTokenResponsePayload, + network_token_requestor_ref_id: Option<&String>, + ) -> CardNetworkTokenizeResponseBuilder { + self.card_tokenized = true; + self.transition(|_| { + ( + network_token.clone(), + network_token_requestor_ref_id.cloned(), + ) }) } } +// Perform locker related operations +impl CardNetworkTokenizeResponseBuilder { + pub fn set_locker_details( + self, + card_bin_details: &api::CardDetailFromLocker, + stored_card_resp: &StoreCardRespPayload, + merchant_id: id_type::MerchantId, + customer_id: id_type::CustomerId, + ) -> CardNetworkTokenizeResponseBuilder { + self.transition(|_| api::PaymentMethodResponse { + merchant_id, + customer_id: Some(customer_id), + payment_method_id: stored_card_resp.card_reference.clone(), + payment_method: Some(api_enums::PaymentMethod::Card), + payment_method_type: card_bin_details + .card_type + .as_ref() + .and_then(|card_type| api_enums::PaymentMethodType::from_str(card_type).ok()), + card: Some(card_bin_details.clone()), + recurring_enabled: true, + installment_payment_enabled: false, + created: Some(common_utils::date_time::now()), + payment_experience: None, + metadata: None, + bank_transfer: None, + last_used_at: None, + client_secret: None, + }) + } +} + +// Create payment method entry +impl CardNetworkTokenizeResponseBuilder { + pub fn set_payment_method_response( + self, + payment_method: domain::PaymentMethod, + ) -> CardNetworkTokenizeResponseBuilder { + let payment_method_response = api::PaymentMethodResponse { + merchant_id: payment_method.merchant_id, + customer_id: Some(payment_method.customer_id), + payment_method_id: payment_method.payment_method_id, + payment_method: payment_method.payment_method, + payment_method_type: payment_method.payment_method_type, + card: self.data.card.clone(), + recurring_enabled: self.data.recurring_enabled.clone(), + installment_payment_enabled: self.data.installment_payment_enabled.clone(), + payment_experience: self.data.payment_experience.clone(), + metadata: self.data.metadata.clone(), + created: self.data.created.clone(), + bank_transfer: self.data.bank_transfer.clone(), + last_used_at: self.data.last_used_at.clone(), + client_secret: self.data.client_secret.clone(), + }; + self.transition(|_| payment_method_response) + } +} + +// Build return response +impl CardNetworkTokenizeResponseBuilder { + pub fn build(self) -> payment_methods_api::CardNetworkTokenizeResponse { + payment_methods_api::CardNetworkTokenizeResponse { + payment_method_response: Some(self.data), + customer: self.customer, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +// State machine for payment method ID tokenization +impl TransitionTo for TokenizeWithPmId {} +impl TransitionTo for PmValidated {} +impl TransitionTo for PmFetched {} +impl TransitionTo for PmAssigned {} +impl TransitionTo for PmTokenized {} +impl TransitionTo for PmTokenStored {} + // Initialize builder for tokenizing saved cards -impl CardNetworkTokenizeResponseBuilder { - pub fn new(req: CardNetworkTokenizeRequest, data: String) -> Self { +impl CardNetworkTokenizeResponseBuilder { + pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizePaymentMethodRequest) -> Self { CardNetworkTokenizeResponseBuilder { data, - state: TokenizeWithPmId, - customer: req.customer, + state: std::marker::PhantomData::, + customer: req + .customer + .as_ref() + .map(|customer| api::CustomerDetails::foreign_try_from(customer.clone())) + .transpose() + .unwrap_or(None), payment_method_response: None, - card_tokenized: None, - card_migrated: None, - network_token_migrated: None, - network_transaction_id_migrated: None, - error_code: HashMap::new(), - error_message: HashMap::new(), + card_tokenized: false, + error_code: None, + error_message: None, } } } -// Build return response -impl CardNetworkTokenizeResponseBuilder { - pub fn build(self) -> CardNetworkTokenizeResponse { - CardNetworkTokenizeResponse { - payment_method_response: self.payment_method_response, +impl CardNetworkTokenizeResponseBuilder { + pub fn get_data(&self) -> domain::PaymentMethod { + self.data.clone() + } +} + +impl CardNetworkTokenizeResponseBuilder { + pub fn set_card_details( + self, + card_cvc: &Secret, + card_bin_details: &api::CardDetailFromLocker, + ) -> CardNetworkTokenizeResponseBuilder { + let card = domain::Card { + card_number: self.data.card_number.clone(), + card_exp_year: self.data.card_exp_year.clone(), + card_exp_month: self.data.card_exp_month.clone(), + card_cvc: card_cvc.clone(), + card_holder_name: self.data.name_on_card.clone(), + nick_name: self + .data + .nick_name + .as_ref() + .map(|name| Secret::new(name.clone())), + card_type: card_bin_details.card_type.clone(), + card_network: card_bin_details.card_network.clone(), + card_issuer: card_bin_details.card_issuer.clone(), + card_issuing_country: card_bin_details.issuer_country.clone(), + bank_code: None, + }; + self.transition(|_| card) + } +} + +impl CardNetworkTokenizeResponseBuilder { + pub fn get_data(&self) -> domain::Card { + self.data.clone() + } + pub fn set_tokenize_details( + mut self, + network_token: &network_tokenization::CardNetworkTokenResponsePayload, + network_token_requestor_ref_id: Option<&String>, + ) -> CardNetworkTokenizeResponseBuilder { + self.card_tokenized = true; + self.transition(|_| { + ( + network_token.clone(), + network_token_requestor_ref_id.cloned(), + ) + }) + } +} + +impl CardNetworkTokenizeResponseBuilder<&StoreCardRespPayload, PmTokenStored> { + pub fn set_payment_method_response( + self, + payment_method: domain::PaymentMethod, + card_bin_details: &api::CardDetailFromLocker, + ) -> CardNetworkTokenizeResponseBuilder { + let payment_method_response = api::PaymentMethodResponse { + merchant_id: payment_method.merchant_id, + customer_id: Some(payment_method.customer_id), + payment_method_id: payment_method.payment_method_id, + payment_method: payment_method.payment_method, + payment_method_type: payment_method.payment_method_type, + card: Some(card_bin_details.clone()), + recurring_enabled: true, + installment_payment_enabled: false, + metadata: payment_method.metadata, + created: Some(payment_method.created_at), + last_used_at: Some(payment_method.last_used_at), + client_secret: payment_method.client_secret, + payment_experience: None, + bank_transfer: None, + }; + self.transition(|_| payment_method_response) + } +} + +impl CardNetworkTokenizeResponseBuilder { + pub fn build(self) -> payment_methods_api::CardNetworkTokenizeResponse { + payment_methods_api::CardNetworkTokenizeResponse { + payment_method_response: Some(self.data), customer: self.customer, card_tokenized: self.card_tokenized, - card_migrated: self.card_migrated, - network_token_migrated: self.network_token_migrated, - network_transaction_id_migrated: self.network_transaction_id_migrated, error_code: self.error_code, error_message: self.error_message, } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index cbf2cdea538..eb35c27492f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3961,6 +3961,10 @@ fn decide_apple_pay_flow( payment_method_type: Option, merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { + router_env::logger::info!( + "[DEBUGG] decide_apple_pay_flow {:?}", + payment_method_type.clone() + ); payment_method_type.and_then(|pmt| match pmt { enums::PaymentMethodType::ApplePay => { check_apple_pay_metadata(state, merchant_connector_account) @@ -4001,6 +4005,11 @@ fn check_apple_pay_metadata( logger::warn!(?error, "Failed to Parse Value to ApplepaySessionTokenData") }); + router_env::logger::info!( + "[DEBUGG] check_apple_pay_metadata {:?}", + parsed_metadata.clone() + ); + parsed_metadata.ok().map(|metadata| match metadata { api_models::payments::ApplepaySessionTokenMetadata::ApplePayCombined( apple_pay_combined, diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 1e3432220ac..c9f4d1f370a 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -1027,6 +1027,10 @@ impl RouterDataSession for types::PaymentsSessionRouterData { business_profile: &domain::Profile, header_payload: hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult { + router_env::logger::info!( + "[DEBUGG] Deciding flow for session token creation: {:?}", + connector.get_token.clone() + ); match connector.get_token { api::GetToken::GpayMetadata => { create_gpay_session_token(state, self, connector, business_profile) diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 4fe9318f64b..f6578b1d442 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -143,7 +143,8 @@ pub fn mk_app( .service(routes::Configs::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::Webhooks::server(state.clone())) - .service(routes::Relay::server(state.clone())); + .service(routes::Relay::server(state.clone())) + .service(routes::AppleStuff::server(state.clone())); #[cfg(feature = "oltp")] { diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 462861d331f..22b6ba96808 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -66,10 +66,10 @@ pub use self::app::DummyConnector; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] pub use self::app::Recon; pub use self::app::{ - ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, - Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates, - MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, - Profile, ProfileNew, Refunds, Relay, SessionState, User, Webhooks, + ApiKeys, AppState, ApplePayCertificatesMigration, AppleStuff, Cache, Cards, Configs, + ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, + Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, + Payments, Poll, Profile, ProfileNew, Refunds, Relay, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2c1e78d883e..337b1b364ff 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2273,3 +2273,34 @@ impl FeatureMatrix { .service(web::resource("").route(web::get().to(feature_matrix::fetch_feature_matrix))) } } + +pub struct AppleStuff; + +impl AppleStuff { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/.well-known").app_data(web::Data::new(state)); + + route = route.service( + web::resource("/apple-developer-merchantid-domain-association.txt") + .route(web::get().to(apple_stuff)), + ); + route + } +} + +pub async fn apple_stuff( + state: web::Data, + req: actix_web::HttpRequest, +) -> actix_web::HttpResponse { + match std::fs::read_to_string( + "/Users/mohammed.kashif/codes/hyperswitch/.well-known/apple-developer-merchantid-domain-association.txt", + ) { + Ok(content) => actix_web::HttpResponse::Ok() + .content_type("text/plain") + .body(content), + Err(e) => { + router_env::logger::error!("Failed to read file: {:?}", e); + actix_web::HttpResponse::InternalServerError().body("Failed to read file") + } + } +} diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 08703d94fd8..ea4f4b9300d 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -4,10 +4,12 @@ ))] use actix_multipart::form::MultipartForm; use actix_web::{web, HttpRequest, HttpResponse}; -use common_utils::{errors::CustomResult, id_type}; +use common_utils::{errors::CustomResult, id_type, transformers::ForeignFrom}; use diesel_models::enums::IntentStatus; use error_stack::ResultExt; -use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; +use hyperswitch_domain_models::{ + bulk_tokenization::CardNetworkTokenizeRequest, merchant_key_store::MerchantKeyStore, +}; use router_env::{instrument, logger, tracing, Flow}; use super::app::{AppState, SessionState}; @@ -21,9 +23,9 @@ use crate::{ core::{ api_locking, errors::{self, utils::StorageErrorExt}, - payment_methods::{self as payment_methods_routes, cards}, + payment_methods::{self as payment_methods_routes, cards, tokenize}, }, - services::{api, authentication as auth, authorization::permissions::Permission}, + services::{self, api, authentication as auth, authorization::permissions::Permission}, types::{ api::payment_methods::{self, PaymentMethodId}, domain, @@ -963,14 +965,15 @@ pub async fn tokenize_card_api( |state, _, req, _| async move { let merchant_id = req.merchant_id.clone(); let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; - Box::pin(cards::tokenize_card_flow( - state, - req, + let res = Box::pin(cards::tokenize_card_flow( + &state, + CardNetworkTokenizeRequest::foreign_from(req), &merchant_id, &merchant_account, &key_store, )) - .await + .await?; + Ok(services::ApplicationResponse::Json(res)) }, &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, @@ -982,26 +985,33 @@ pub async fn tokenize_card_api( pub async fn tokenize_card_batch_api( state: web::Data, req: HttpRequest, - json_payload: web::Json, + MultipartForm(form): MultipartForm, ) -> HttpResponse { let flow = Flow::TokenizeCardBatch; + let (merchant_id, records) = match tokenize::get_tokenize_card_form_records(form) { + Ok(res) => res, + Err(e) => return api::log_and_return_error_response(e.into()), + }; Box::pin(api::server_wrap( flow, state, &req, - json_payload.into_inner(), - |state, _, req, _| async move { - let merchant_id = req.merchant_id.clone(); - let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; - Box::pin(cards::tokenize_card_flow( - state, - req, - &merchant_id, - &merchant_account, - &key_store, - )) - .await + records, + |state, _, req, _| { + let merchant_id = merchant_id.clone(); + async move { + let (key_store, merchant_account) = + get_merchant_account(&state, &merchant_id).await?; + Box::pin(tokenize::tokenize_cards( + &state, + req, + &merchant_id, + &merchant_account, + &key_store, + )) + .await + } }, &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 31e3dfdcb75..8b1afad211b 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -4,15 +4,16 @@ ))] pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, - CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DefaultPaymentMethod, - DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse, - ListCountriesCurrenciesRequest, MigrateCardDetail, PaymentMethodCollectLinkRenderRequest, - PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, - PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest, - PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodMigrateResponse, - PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizeCardRequest, - TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, - TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, + CardNetworkTokenizeResponse, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, + DefaultPaymentMethod, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, + GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, + PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, + PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, + PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, + PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, + TokenizeCardRequest, TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest, + TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, + TokenizedWalletValue2, }; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub use api_models::payment_methods::{ diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index 070e583caab..567e1e181dc 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -25,6 +25,10 @@ mod merchant_connector_account; mod merchant_key_store { pub use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; } + +pub mod bulk_tokenization { + pub use hyperswitch_domain_models::bulk_tokenization::*; +} pub mod payment_methods { pub use hyperswitch_domain_models::payment_methods::*; } From 0e73f1d0087f666a70cd83271652074f753641ae Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 02:26:16 +0000 Subject: [PATCH 03/25] chore: run formatter --- .../hyperswitch_domain_models/src/bulk_tokenization.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 54d46d04f02..65e28707719 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -1,7 +1,3 @@ -use crate::{ - address::{Address, AddressDetails, PhoneDetails}, - router_request_types::CustomerDetails, -}; use api_models::{payment_methods as payment_methods_api, payments as payments_api}; use common_enums as enums; use common_utils::{ @@ -12,6 +8,11 @@ use common_utils::{ }; use error_stack::report; +use crate::{ + address::{Address, AddressDetails, PhoneDetails}, + router_request_types::CustomerDetails, +}; + #[derive(Debug)] pub struct CardNetworkTokenizeRequest { pub data: TokenizeDataRequest, From c27818367debb33ee960ae844603369a4267c8f8 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 20 Jan 2025 08:17:30 +0530 Subject: [PATCH 04/25] chore: address clippy suggestions and remove redundant changes --- crates/api_models/src/payment_methods.rs | 1 - .../src/bulk_tokenization.rs | 2 +- .../payout_link/initiate/script.js | 5 +--- .../payment_link_initiator.js | 5 +--- .../secure_payment_link_initiator.js | 5 +--- .../router/src/core/payment_methods/cards.rs | 12 ++++---- .../payment_methods/network_tokenization.rs | 18 ++++++------ .../src/core/payment_methods/tokenize.rs | 28 ++++++++----------- crates/router/src/core/payments.rs | 9 ------ .../src/core/payments/flows/session_flow.rs | 4 --- 10 files changed, 30 insertions(+), 59 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 7f0cc7bd46b..2ecebec397b 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -11,7 +11,6 @@ use common_utils::{ id_type, link_utils, pii, types::{MinorUnit, Percentage, Surcharge}, }; -use error_stack::report; use masking::PeekInterface; use serde::de; use utoipa::{schema, ToSchema}; diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 65e28707719..a6d7a0a8923 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -122,7 +122,7 @@ impl ForeignFrom<&CardNetworkTokenizeRecord> for payments_api::Address { city: record.billing_address_city.clone(), zip: record.billing_address_zip.clone(), state: record.billing_address_state.clone(), - country: record.billing_address_country.clone(), + country: record.billing_address_country, }), phone: Some(payments_api::PhoneDetails { number: record.billing_phone_number.clone(), diff --git a/crates/router/src/core/generic_link/payout_link/initiate/script.js b/crates/router/src/core/generic_link/payout_link/initiate/script.js index 4f9f969ef9a..f226a06fa16 100644 --- a/crates/router/src/core/generic_link/payout_link/initiate/script.js +++ b/crates/router/src/core/generic_link/payout_link/initiate/script.js @@ -144,10 +144,7 @@ if (!isTestMode && !isFramed) { // @ts-ignore hyper = window.Hyper(publishableKey, { isPreloadEnabled: false, - redirectionFlags: { - shouldUseTopRedirection: isFramed, - shouldRemoveBeforeUnloadEvents: true, - } + shouldUseTopRedirection: isFramed, }); widgets = hyper.widgets({ appearance: appearance, diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js index 0925eaa73cd..2a7cc49be82 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js @@ -27,10 +27,7 @@ function initializeSDK() { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - redirectionFlags: { - shouldUseTopRedirection: true, - shouldRemoveBeforeUnloadEvents: true, - } + shouldUseTopRedirection: isFramed, }); // @ts-ignore widgets = hyper.widgets({ diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js index b5d5ea3ddd2..29e036e6bf1 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js @@ -48,10 +48,7 @@ if (!isFramed) { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - redirectionFlags: { - shouldUseTopRedirection: true, - shouldRemoveBeforeUnloadEvents: true - } + shouldUseTopRedirection: isFramed, }); // @ts-ignore widgets = hyper.widgets({ diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 5702dd1d706..90ef21e0400 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6032,7 +6032,7 @@ pub async fn tokenize_card_flow( domain::bulk_tokenization::TokenizeDataRequest::Card(ref card) => { // Initialize builder and executor let executor = - CardNetworkTokenizeExecutor::new(&req, &state, merchant_account, key_store); + CardNetworkTokenizeExecutor::new(&req, state, merchant_account, key_store); let builder = tokenize::CardNetworkTokenizeResponseBuilder::< domain::bulk_tokenization::TokenizeCardRequest, tokenize::TokenizeWithCard, @@ -6057,7 +6057,7 @@ pub async fn tokenize_card_flow( // Tokenize card let network_token_details = executor - .tokenize_card(&customer_details.id, &card_details) + .tokenize_card(&customer_details.id, card_details) .await?; let builder = builder .set_tokenize_details(&network_token_details.0, network_token_details.1.as_ref()); @@ -6083,7 +6083,7 @@ pub async fn tokenize_card_flow( .create_payment_method( &stored_card_resp, network_token_details, - &card_details, + card_details, &customer_details.id, ) .await?; @@ -6095,7 +6095,7 @@ pub async fn tokenize_card_flow( domain::bulk_tokenization::TokenizeDataRequest::PaymentMethod(ref payment_method) => { // Initialize builder and executor let executor = - CardNetworkTokenizeExecutor::new(&req, &state, merchant_account, key_store); + CardNetworkTokenizeExecutor::new(&req, state, merchant_account, key_store); let builder = tokenize::CardNetworkTokenizeResponseBuilder::< domain::bulk_tokenization::TokenizePaymentMethodRequest, tokenize::TokenizeWithPmId, @@ -6110,7 +6110,7 @@ pub async fn tokenize_card_flow( // Fetch raw card details from locker let card_details = get_card_from_locker( - &state, + state, &data.customer_id, merchant_account.get_id(), &locker_id, @@ -6138,7 +6138,7 @@ pub async fn tokenize_card_flow( &network_token_details, &data.customer_id, card_details.name_on_card, - card_details.nick_name.map(|name| Secret::new(name)), + card_details.nick_name.map(Secret::new), ) .await?; let builder = builder.transition(|_| &stored_card_resp); diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index 2abf1fb9224..bb730caad2b 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -33,14 +33,14 @@ pub struct CardData { card_security_code: Secret, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderData { consent_id: String, customer_id: id_type::CustomerId, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiPayload { service: String, @@ -179,7 +179,6 @@ pub async fn mk_tokenization_req( customer_id, }; - logger::info!("JWT: {:?}", jwt.clone()); let api_payload = ApiPayload { service: NETWORK_TOKEN_SERVICE.to_string(), card_data: Secret::new(jwt), @@ -203,7 +202,6 @@ pub async fn mk_tokenization_req( ); request.add_default_headers(); - logger::info!("Payload: {:?}", api_payload.clone()); request.set_body(RequestContent::Json(Box::new(api_payload))); logger::info!("Request to generate token: {:?}", request); @@ -358,8 +356,8 @@ pub async fn get_network_token( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - // error_code = %parsed_error.error_info.code, - // developer_message = %parsed_error.error_info.developer_message, + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); @@ -554,8 +552,8 @@ pub async fn check_token_status_with_tokenization_service( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - // error_code = %parsed_error.error_info.code, - // developer_message = %parsed_error.error_info.developer_message, + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); @@ -672,8 +670,8 @@ pub async fn delete_network_token_from_tokenization_service( errors::NetworkTokenizationError::ResponseDeserializationFailed, )?; logger::error!( - // error_code = %parsed_error.error_info.code, - // developer_message = %parsed_error.error_info.developer_message, + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, "Network tokenization error: {}", parsed_error.error_message ); diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index e0dea1bce0c..78947d10719 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -65,7 +65,6 @@ pub fn parse_csv( match result { Ok(mut record) => { router_env::logger::info!("Parsed Record (line {}): {:?}", i + 1, record); - router_env::logger::info!("[DEBUGG] {:?}", record); id_counter += 1; record.line_number = Some(id_counter); record.merchant_id = Some(merchant_id.clone()); @@ -120,15 +119,14 @@ pub async fn tokenize_cards( ) -> errors::RouterResponse> { let mut res = vec![]; for record in records { - let tokenize_res = tokenize_card_flow( + let tokenize_res = Box::pin(tokenize_card_flow( state, CardNetworkTokenizeRequest::foreign_from(record), merchant_id, merchant_account, key_store, - ) - .await?; - res.push(tokenize_res); + )); + res.push(tokenize_res.await?); } Ok(services::ApplicationResponse::Json(res)) @@ -396,15 +394,13 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { match network_tokenization::make_card_network_tokenization_request( self.state, card, - &customer_id, + customer_id, ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .inspect_err(|err| { - logger::error!("Failed to tokenize card with the network: {:?}", err); - }) { + { Ok(tokenization_response) => Ok(tokenization_response), Err(err) => { + // TODO: revert this logger::error!( "Failed to tokenize card with the network: {:?}\nUsing dummy response", err @@ -675,7 +671,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { // Initialize builder for tokenizing raw card details impl CardNetworkTokenizeResponseBuilder { pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizeCardRequest) -> Self { - CardNetworkTokenizeResponseBuilder { + Self { data, state: std::marker::PhantomData::, customer: req @@ -791,13 +787,13 @@ impl CardNetworkTokenizeResponseBuilder for PmTokenStored {} // Initialize builder for tokenizing saved cards impl CardNetworkTokenizeResponseBuilder { pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizePaymentMethodRequest) -> Self { - CardNetworkTokenizeResponseBuilder { + Self { data, state: std::marker::PhantomData::, customer: req diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ceef6dfe29a..1ba6209cf52 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3961,10 +3961,6 @@ fn decide_apple_pay_flow( payment_method_type: Option, merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { - router_env::logger::info!( - "[DEBUGG] decide_apple_pay_flow {:?}", - payment_method_type.clone() - ); payment_method_type.and_then(|pmt| match pmt { enums::PaymentMethodType::ApplePay => { check_apple_pay_metadata(state, merchant_connector_account) @@ -4005,11 +4001,6 @@ fn check_apple_pay_metadata( logger::warn!(?error, "Failed to Parse Value to ApplepaySessionTokenData") }); - router_env::logger::info!( - "[DEBUGG] check_apple_pay_metadata {:?}", - parsed_metadata.clone() - ); - parsed_metadata.ok().map(|metadata| match metadata { api_models::payments::ApplepaySessionTokenMetadata::ApplePayCombined( apple_pay_combined, diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 372d296cb41..65688c5d465 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -1065,10 +1065,6 @@ impl RouterDataSession for types::PaymentsSessionRouterData { business_profile: &domain::Profile, header_payload: hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult { - router_env::logger::info!( - "[DEBUGG] Deciding flow for session token creation: {:?}", - connector.get_token.clone() - ); match connector.get_token { api::GetToken::GpayMetadata => { create_gpay_session_token(state, self, connector, business_profile) From 2fd2787b56c7f3cc5ffd1e0300099ca2124eec32 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 20 Jan 2025 08:21:23 +0530 Subject: [PATCH 05/25] chore: revert a few changes --- .../secure_payment_link_initiator.js | 2 +- crates/router/src/core/payment_methods/cards.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js index 29e036e6bf1..4bddc6904be 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js @@ -48,7 +48,7 @@ if (!isFramed) { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - shouldUseTopRedirection: isFramed, + shouldUseTopRedirection: true, }); // @ts-ignore widgets = hyper.widgets({ diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 90ef21e0400..4507cfa8d1f 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -69,14 +69,6 @@ use super::{ }, tokenize::CardNetworkTokenizeExecutor, }; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "payment_methods_v2") -))] -use crate::core::payment_methods::{ - add_payment_method_status_update_task, - utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, -}; #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2"), @@ -117,6 +109,14 @@ use crate::{ consts as router_consts, core::payment_methods as pm_core, headers, types::payment_methods as pm_types, utils::ConnectorResponseExt, }; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use crate::core::payment_methods::{ + add_payment_method_status_update_task, + utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, +}; #[cfg(all( any(feature = "v1", feature = "v2"), From d4d0fae0167d1e1e9ca5d03f4d4b48c757d00564 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 02:52:32 +0000 Subject: [PATCH 06/25] chore: run formatter --- crates/router/src/core/payment_methods/cards.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4507cfa8d1f..90ef21e0400 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -69,6 +69,14 @@ use super::{ }, tokenize::CardNetworkTokenizeExecutor, }; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use crate::core::payment_methods::{ + add_payment_method_status_update_task, + utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, +}; #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2"), @@ -109,14 +117,6 @@ use crate::{ consts as router_consts, core::payment_methods as pm_core, headers, types::payment_methods as pm_types, utils::ConnectorResponseExt, }; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "payment_methods_v2") -))] -use crate::core::payment_methods::{ - add_payment_method_status_update_task, - utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, -}; #[cfg(all( any(feature = "v1", feature = "v2"), From 60b90dd730be1d4344b08614f9571c63d17324f0 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 20 Jan 2025 08:23:48 +0530 Subject: [PATCH 07/25] chore: revert --- .../payment_link_initiate/payment_link_initiator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js index 2a7cc49be82..d45a72b5e55 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js @@ -27,7 +27,7 @@ function initializeSDK() { // @ts-ignore hyper = window.Hyper(pub_key, { isPreloadEnabled: false, - shouldUseTopRedirection: isFramed, + shouldUseTopRedirection: true, }); // @ts-ignore widgets = hyper.widgets({ From 06e0c4fa15e047cbc1a1861234997db378294909 Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 22 Jan 2025 17:25:27 +0530 Subject: [PATCH 08/25] chore: update sm --- crates/api_models/src/payment_methods.rs | 3 + crates/router/src/core/errors.rs | 20 ++++ .../router/src/core/payment_methods/cards.rs | 6 +- .../src/core/payment_methods/tokenize.rs | 102 +++++++++++------- 4 files changed, 91 insertions(+), 40 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 2ecebec397b..89aab829bb1 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2497,6 +2497,9 @@ pub struct CardNetworkTokenizeResponse { /// Error message #[serde(skip_serializing_if = "Option::is_none")] pub error_message: Option, + + /// Details that were sent for tokenization + pub req: Option, } impl common_utils::events::ApiEventMetric for CardNetworkTokenizeResponse {} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 96321d09794..24d3a612f77 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -381,3 +381,23 @@ pub enum NetworkTokenizationError { #[error("Network token service not configured")] NetworkTokenizationServiceNotConfigured, } + +#[derive(Debug, thiserror::Error)] +pub enum BulkNetworkTokenizationError { + #[error("Failed to validate card details")] + CardValidationFailed, + #[error("Failed to validate payment method details")] + PaymentMethodValidationFailed, + #[error("Failed to assign a customer to the card")] + CustomerAssignmentFailed, + #[error("Failed to perform BIN lookup for the card")] + BinLookupFailed, + #[error("Failed to tokenize the card details with the network")] + NetworkTokenizationFailed, + #[error("Failed to store the card details in locker")] + VaultSaveFailed, + #[error("Failed to create a payment method entry")] + PaymentMethodCreationFailed, + #[error("Failed to update the payment method")] + PaymentMethodUpdationFailed, +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 90ef21e0400..cf45022a7ae 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6034,13 +6034,13 @@ pub async fn tokenize_card_flow( let executor = CardNetworkTokenizeExecutor::new(&req, state, merchant_account, key_store); let builder = tokenize::CardNetworkTokenizeResponseBuilder::< - domain::bulk_tokenization::TokenizeCardRequest, + &domain::bulk_tokenization::TokenizeCardRequest, tokenize::TokenizeWithCard, - >::new(&req, card.clone()); + >::new(&req, card); // Validate card number let card_number = executor.validate_card_number(card.raw_card_number.clone())?; - let builder = builder.transition(|_| card.clone()); + let builder = builder.transition(|_| card); // Get or create customer let customer_details = executor.get_or_create_customer().await?; diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 78947d10719..56ba2e27892 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -64,7 +64,7 @@ pub fn parse_csv( { match result { Ok(mut record) => { - router_env::logger::info!("Parsed Record (line {}): {:?}", i + 1, record); + logger::info!("Parsed Record (line {}): {:?}", i + 1, record); id_counter += 1; record.line_number = Some(id_counter); record.merchant_id = Some(merchant_id.clone()); @@ -73,15 +73,11 @@ pub fn parse_csv( records.push(record); } Err(err) => { - router_env::logger::error!( - "Error parsing line {}: {}", - i + 1, - err.to_string() - ); + logger::error!("Error parsing line {}: {}", i + 1, err.to_string()); } } } - Err(e) => router_env::logger::error!("Error parsing line {}: {}", i + 1, e), + Err(e) => logger::error!("Error parsing line {}: {}", i + 1, e), } } Ok(records) @@ -117,19 +113,38 @@ pub async fn tokenize_cards( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> errors::RouterResponse> { - let mut res = vec![]; - for record in records { - let tokenize_res = Box::pin(tokenize_card_flow( - state, - CardNetworkTokenizeRequest::foreign_from(record), - merchant_id, - merchant_account, - key_store, - )); - res.push(tokenize_res.await?); - } + use futures::stream::StreamExt; + + // Process all records in parallel + let responses = futures::stream::iter(records.into_iter()) + .map(|record| async move { + let tokenize_request = record.data.clone(); + tokenize_card_flow( + &state, + CardNetworkTokenizeRequest::foreign_from(record), + &merchant_id, + &merchant_account, + &key_store, + ) + .await + .unwrap_or_else(|e| { + let err = e.current_context(); + payment_methods_api::CardNetworkTokenizeResponse { + req: Some(tokenize_request), + error_code: Some(err.error_code()), + error_message: Some(err.error_message()), + card_tokenized: false, + payment_method_response: None, + customer: None, + } + }) + }) + .buffer_unordered(10) + .collect() + .await; - Ok(services::ApplicationResponse::Json(res)) + // Return the final response + Ok(services::ApplicationResponse::Json(responses)) } // Builder @@ -171,7 +186,7 @@ type NetworkTokenizationResponse = ( // State machine pub trait State {} -pub trait TransitionTo {} +pub trait TransitionTo {} // All available states pub struct TokenizeWithCard; @@ -210,7 +225,7 @@ impl State for PmTokenUpdated {} impl CardNetworkTokenizeResponseBuilder { pub fn transition(self, f: F) -> CardNetworkTokenizeResponseBuilder where - S1: TransitionTo, + S1: TransitionTo, S2: State, F: FnOnce(D1) -> D2, { @@ -227,12 +242,12 @@ impl CardNetworkTokenizeResponseBuilder { } // State machine for card tokenization -impl TransitionTo for TokenizeWithCard {} -impl TransitionTo for CardValidated {} -impl TransitionTo for CustomerAssigned {} -impl TransitionTo for CardDetailsAssigned {} -impl TransitionTo for CardTokenized {} -impl TransitionTo for CardTokenStored {} +impl TransitionTo<&TokenizeCardRequest, CardValidated> for TokenizeWithCard {} +impl TransitionTo for CardValidated {} +impl TransitionTo for CustomerAssigned {} +impl TransitionTo for CardDetailsAssigned {} +impl TransitionTo for CardTokenized {} +impl TransitionTo for CardTokenStored {} impl<'a> CardNetworkTokenizeExecutor<'a> { pub fn new( @@ -285,6 +300,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { self.merchant_account.storage_scheme, ) .await + .inspect_err(|err| logger::info!("Error fetching customer: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError)? { // Customer found @@ -320,6 +336,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { self.key_store.key.get_inner().peek(), ) .await + .inspect_err(|err| logger::info!("Error encrypting customer: {:?}", err)) .and_then(|val| val.try_into_batchoperation()) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to encrypt customer")?; @@ -359,6 +376,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { self.merchant_account.storage_scheme, ) .await + .inspect_err(|err| logger::info!("Error creating a customer: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!( @@ -458,6 +476,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { api_enums::LockerChoice::HyperswitchCardVault, ) .await + .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError)?; Ok(stored_resp) @@ -488,6 +507,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { }); let enc_pm_data = create_encrypted_data(&self.state.into(), self.key_store, pm_data) .await + .inspect_err(|err| logger::info!("Error encrypting payment method data: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError)?; // Form encrypted network token data (tokenized card) @@ -507,6 +527,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { }); let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) .await + .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError)?; // Form PM create entry @@ -589,7 +610,10 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { message: "Invalid payment_method_id".to_string(), }) } - _ => err.change_context(errors::ApiErrorResponse::InternalServerError), + e => { + logger::info!("Error fetching customer: {:?}", e); + err.change_context(errors::ApiErrorResponse::InternalServerError) + } })?; // Ensure payment method is card @@ -646,6 +670,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { }); let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) .await + .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError)?; // Update payment method @@ -664,13 +689,14 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { self.merchant_account.storage_scheme, ) .await + .inspect_err(|err| logger::info!("Error updating payment method: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError) } } // Initialize builder for tokenizing raw card details -impl CardNetworkTokenizeResponseBuilder { - pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizeCardRequest) -> Self { +impl<'a> CardNetworkTokenizeResponseBuilder<&'a TokenizeCardRequest, TokenizeWithCard> { + pub fn new(req: &CardNetworkTokenizeRequest, data: &'a TokenizeCardRequest) -> Self { Self { data, state: std::marker::PhantomData::, @@ -689,7 +715,7 @@ impl CardNetworkTokenizeResponseBuilder { } // Perform customer related operations -impl CardNetworkTokenizeResponseBuilder { +impl CardNetworkTokenizeResponseBuilder<&TokenizeCardRequest, CardValidated> { pub fn set_customer_details( mut self, customer: &api::CustomerDetails, @@ -809,17 +835,18 @@ impl CardNetworkTokenizeResponseBuilder for TokenizeWithPmId {} -impl TransitionTo for PmValidated {} -impl TransitionTo for PmFetched {} -impl TransitionTo for PmAssigned {} -impl TransitionTo for PmTokenized {} -impl TransitionTo for PmTokenStored {} +impl TransitionTo for TokenizeWithPmId {} +impl TransitionTo for PmValidated {} +impl TransitionTo for PmFetched {} +impl TransitionTo for PmAssigned {} +impl TransitionTo<&StoreCardRespPayload, PmTokenStored> for PmTokenized {} +impl TransitionTo for PmTokenStored {} // Initialize builder for tokenizing saved cards impl CardNetworkTokenizeResponseBuilder { @@ -927,6 +954,7 @@ impl CardNetworkTokenizeResponseBuilder Date: Thu, 23 Jan 2025 15:48:56 +0530 Subject: [PATCH 09/25] chore: add v2 flags --- crates/api_models/src/payment_methods.rs | 1 + crates/router/src/core/payment_methods.rs | 4 ++++ .../router/src/core/payment_methods/cards.rs | 22 ++++++++++++------- .../src/core/payment_methods/tokenize.rs | 14 +++++------- crates/router/src/routes/payment_methods.rs | 17 ++++++++++++-- .../router/src/types/api/payment_methods.rs | 5 +++-- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 89aab829bb1..437e1d23cb5 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2499,6 +2499,7 @@ pub struct CardNetworkTokenizeResponse { pub error_message: Option, /// Details that were sent for tokenization + #[serde(skip_serializing_if = "Option::is_none")] pub req: Option, } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index ce101e6a032..e8fcf27bbe9 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -6,6 +6,10 @@ pub mod cards; pub mod migration; pub mod network_tokenization; pub mod surcharge_decision_configs; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub mod tokenize; pub mod transformers; pub mod utils; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index cf45022a7ae..4e5b872db12 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -62,19 +62,21 @@ use strum::IntoEnumIterator; not(feature = "payment_methods_v2") ))] use super::migration; -use super::{ - surcharge_decision_configs::{ - perform_surcharge_decision_management_for_payment_method_list, - perform_surcharge_decision_management_for_saved_cards, - }, - tokenize::CardNetworkTokenizeExecutor, +use super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, }; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] +use super::tokenize::CardNetworkTokenizeExecutor; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] use crate::core::payment_methods::{ - add_payment_method_status_update_task, + add_payment_method_status_update_task, tokenize, utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache}, }; #[cfg(all( @@ -92,7 +94,7 @@ use crate::{ }, core::{ errors::{self, StorageErrorExt}, - payment_methods::{network_tokenization, tokenize, transformers as payment_methods, vault}, + payment_methods::{network_tokenization, transformers as payment_methods, vault}, payments::{ helpers, routing::{self, SessionFlowRoutingInput}, @@ -6020,6 +6022,10 @@ pub async fn list_countries_currencies_for_connector_payment_method_util( } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn tokenize_card_flow( state: &routes::SessionState, req: domain::bulk_tokenization::CardNetworkTokenizeRequest, diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 56ba2e27892..9fadb2e606d 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -119,13 +119,13 @@ pub async fn tokenize_cards( let responses = futures::stream::iter(records.into_iter()) .map(|record| async move { let tokenize_request = record.data.clone(); - tokenize_card_flow( - &state, + Box::pin(tokenize_card_flow( + state, CardNetworkTokenizeRequest::foreign_from(record), - &merchant_id, - &merchant_account, - &key_store, - ) + merchant_id, + merchant_account, + key_store, + )) .await .unwrap_or_else(|e| { let err = e.current_context(); @@ -564,8 +564,6 @@ impl<'a> CardNetworkTokenizeExecutor<'a> { connector_mandate_details: None, network_transaction_id: None, }; - - // Create payment method create_payment_method( self.state, &payment_method_create, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 9861ca21665..b260cba787e 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -1,6 +1,6 @@ #[cfg(all( any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), - not(feature = "customer_v2") + all(not(feature = "customer_v2"), not(feature = "payment_methods_v2")) ))] use actix_multipart::form::MultipartForm; use actix_web::{web, HttpRequest, HttpResponse}; @@ -13,6 +13,11 @@ use hyperswitch_domain_models::{ use router_env::{instrument, logger, tracing, Flow}; use super::app::{AppState, SessionState}; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use crate::core::payment_methods::tokenize; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::core::payment_methods::{ create_payment_method, delete_payment_method, list_customer_payment_method_util, @@ -23,7 +28,7 @@ use crate::{ core::{ api_locking, errors::{self, utils::StorageErrorExt}, - payment_methods::{self as payment_methods_routes, cards, tokenize}, + payment_methods::{self as payment_methods_routes, cards}, }, services::{self, api, authentication as auth, authorization::permissions::Permission}, types::{ @@ -1018,6 +1023,10 @@ impl ParentPaymentMethodToken { } } +#[cfg(all( + any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), + not(feature = "payment_methods_v2") +))] #[instrument(skip_all, fields(flow = ?Flow::TokenizeCard))] pub async fn tokenize_card_api( state: web::Data, @@ -1050,6 +1059,10 @@ pub async fn tokenize_card_api( .await } +#[cfg(all( + any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), + not(feature = "payment_methods_v2") +))] #[instrument(skip_all, fields(flow = ?Flow::TokenizeCardBatch))] pub async fn tokenize_card_batch_api( state: web::Data, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index ca13a876ea4..bd28ed35077 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -17,9 +17,10 @@ pub use api_models::payment_methods::{ }; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub use api_models::payment_methods::{ - CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardType, CustomerPaymentMethod, + CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, + CardNetworkTokenizeResponse, CardType, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, - GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, + GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData, From 584242147b1704ab26e33e481dff3c80fafad28d Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:33:42 +0000 Subject: [PATCH 10/25] chore: run formatter --- .../router/src/types/api/payment_methods.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index bd28ed35077..dc2d7816ffc 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,3 +1,18 @@ +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub use api_models::payment_methods::{ + CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, + CardNetworkTokenizeResponse, CardType, CustomerPaymentMethod, + CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, + GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, + PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, + PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, + PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData, + PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, + PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData, + PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenizePayloadEncrypted, + TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, + TokenizedWalletValue2, +}; #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -15,21 +30,6 @@ pub use api_models::payment_methods::{ TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, }; -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub use api_models::payment_methods::{ - CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, - CardNetworkTokenizeResponse, CardType, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, - GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, - PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, - PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData, - PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, - PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData, - PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenizePayloadEncrypted, - TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, - TokenizedWalletValue2, -}; use error_stack::report; use crate::core::{ From aa0676fabeaf891f62123d6a9b36450c6c76f381 Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 31 Jan 2025 03:27:43 +0530 Subject: [PATCH 11/25] refactor: add a common trait for common tokenization steps and resolve comments --- crates/api_models/src/payment_methods.rs | 3 +- .../src/bulk_tokenization.rs | 7 +- .../router/src/core/payment_methods/cards.rs | 268 +++--- .../src/core/payment_methods/tokenize.rs | 878 ++---------------- .../payment_methods/tokenize/card_executor.rs | 729 +++++++++++++++ .../tokenize/payment_method_executor.rs | 488 ++++++++++ crates/router/src/routes/payment_methods.rs | 2 - crates/router/src/types/domain.rs | 5 +- 8 files changed, 1436 insertions(+), 944 deletions(-) create mode 100644 crates/router/src/core/payment_methods/tokenize/card_executor.rs create mode 100644 crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 437e1d23cb5..10ce2ff2d19 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2401,6 +2401,7 @@ pub struct CardNetworkTokenizeRequest { /// Merchant ID associated with the tokenization request pub merchant_id: id_type::MerchantId, + /// Details of the card or payment method to be tokenized #[serde(flatten)] pub data: TokenizeDataRequest, @@ -2434,7 +2435,7 @@ pub enum TokenizeDataRequest { pub struct TokenizeCardRequest { /// Card Number #[schema(value_type = String, example = "4111111145551142")] - pub raw_card_number: masking::Secret, + pub raw_card_number: CardNumber, /// Card Expiry Month #[schema(value_type = String, example = "10")] diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index a6d7a0a8923..21fb61e1a87 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -1,4 +1,5 @@ use api_models::{payment_methods as payment_methods_api, payments as payments_api}; +use cards::CardNumber; use common_enums as enums; use common_utils::{ errors, @@ -31,7 +32,7 @@ pub enum TokenizeDataRequest { #[derive(Clone, Debug)] pub struct TokenizeCardRequest { - pub raw_card_number: masking::Secret, + pub raw_card_number: CardNumber, pub card_expiry_month: masking::Secret, pub card_expiry_year: masking::Secret, pub card_cvc: masking::Secret, @@ -52,7 +53,7 @@ pub struct TokenizePaymentMethodRequest { #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] pub struct CardNetworkTokenizeRecord { // Card details - pub raw_card_number: Option>, + pub raw_card_number: Option, pub card_expiry_month: Option>, pub card_expiry_year: Option>, pub card_cvc: Option>, @@ -199,7 +200,7 @@ impl ForeignTryFrom for payment_methods_api::CardNetw impl ForeignFrom<&TokenizeCardRequest> for payment_methods_api::MigrateCardDetail { fn foreign_from(card: &TokenizeCardRequest) -> Self { Self { - card_number: card.raw_card_number.clone(), + card_number: masking::Secret::new(card.raw_card_number.get_card_no()), card_exp_month: card.card_expiry_month.clone(), card_exp_year: card.card_expiry_year.clone(), card_holder_name: card.card_holder_name.clone(), diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4e5b872db12..fd304adaa96 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -70,7 +70,7 @@ use super::surcharge_decision_configs::{ any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] -use super::tokenize::CardNetworkTokenizeExecutor; +use super::tokenize::NetworkTokenizationProcess; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -6028,137 +6028,157 @@ pub async fn list_countries_currencies_for_connector_payment_method_util( ))] pub async fn tokenize_card_flow( state: &routes::SessionState, - req: domain::bulk_tokenization::CardNetworkTokenizeRequest, - merchant_id: &id_type::MerchantId, + req: domain::CardNetworkTokenizeRequest, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { - use common_utils::transformers::ForeignFrom; - let response = match req.data { - domain::bulk_tokenization::TokenizeDataRequest::Card(ref card) => { - // Initialize builder and executor - let executor = - CardNetworkTokenizeExecutor::new(&req, state, merchant_account, key_store); - let builder = tokenize::CardNetworkTokenizeResponseBuilder::< - &domain::bulk_tokenization::TokenizeCardRequest, - tokenize::TokenizeWithCard, - >::new(&req, card); - - // Validate card number - let card_number = executor.validate_card_number(card.raw_card_number.clone())?; - let builder = builder.transition(|_| card); - - // Get or create customer - let customer_details = executor.get_or_create_customer().await?; - let builder = builder.set_customer_details(&customer_details); - - // Get and populate BIN details - let card_bin_details = populate_bin_details_for_masked_card( - &api::MigrateCardDetail::foreign_from(card), - &*state.store, - ) - .await?; - let builder = builder.set_card_details(card_number, card, &card_bin_details); - let card_details = &builder.get_data(); - - // Tokenize card - let network_token_details = executor - .tokenize_card(&customer_details.id, card_details) - .await?; - let builder = builder - .set_tokenize_details(&network_token_details.0, network_token_details.1.as_ref()); - - // Store card in locker - let stored_card_resp = executor - .store_in_locker( - &network_token_details, - &customer_details.id, - card.card_holder_name.clone(), - card.nick_name.clone(), - ) - .await?; - let builder = builder.set_locker_details( - &card_bin_details, - &stored_card_resp, - merchant_id.clone(), - customer_details.id.clone(), + match req.data { + domain::TokenizeDataRequest::Card(ref card_req) => { + let executor = tokenize::CardNetworkTokenizeExecutor::new( + state, + key_store, + merchant_account, + card_req, + req.customer.as_ref(), ); + let builder = tokenize::NetworkTokenizationBuilder::::new(); + execute_card_tokenization(executor, builder, card_req).await + } + domain::TokenizeDataRequest::PaymentMethod(ref payment_method) => { + let executor = tokenize::CardNetworkTokenizeExecutor::new( + state, + key_store, + merchant_account, + payment_method, + req.customer.as_ref(), + ); + let builder = tokenize::NetworkTokenizationBuilder::::new(); + execute_payment_method_tokenization(executor, builder, payment_method).await + } + } +} - // Create payment method entry - let payment_method = executor - .create_payment_method( - &stored_card_resp, - network_token_details, - card_details, - &customer_details.id, - ) - .await?; - let builder = builder.set_payment_method_response(payment_method); +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +pub async fn execute_card_tokenization( + executor: tokenize::CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest>, + builder: tokenize::NetworkTokenizationBuilder<'_, tokenize::TokenizeWithCard>, + req: &domain::TokenizeCardRequest, +) -> errors::RouterResult { + // Validate request and get optional customer + let optional_customer = executor + .validate_request_and_fetch_optional_customer() + .await?; + let builder = builder.set_validate_result(); - // Build response - builder.build() - } - domain::bulk_tokenization::TokenizeDataRequest::PaymentMethod(ref payment_method) => { - // Initialize builder and executor - let executor = - CardNetworkTokenizeExecutor::new(&req, state, merchant_account, key_store); - let builder = tokenize::CardNetworkTokenizeResponseBuilder::< - domain::bulk_tokenization::TokenizePaymentMethodRequest, - tokenize::TokenizeWithPmId, - >::new(&req, payment_method.clone()); - - // Validate payment_method_id - let (payment_method_in_db, locker_id) = executor - .validate_payment_method_id(&payment_method.payment_method_id) - .await?; - let builder = builder.transition(|_| payment_method_in_db); - let data = builder.get_data(); + // Create customer if not present + let customer = match optional_customer { + Some(customer) => customer, + None => executor.create_customer().await?, + }; + let builder = builder.set_customer(&customer); - // Fetch raw card details from locker - let card_details = get_card_from_locker( - state, - &data.customer_id, - merchant_account.get_id(), - &locker_id, - ) - .await?; - let builder = builder.transition(|_| card_details.clone()); + // Perform BIN lookup + let optional_card_info = executor + .fetch_bin_details(req.raw_card_number.clone()) + .await?; + let builder = builder.set_card_details(req, optional_card_info); - // Get and populate BIN details - let card_bin_details = populate_bin_details_for_masked_card( - &api::MigrateCardDetail::from(&card_details), - &*state.store, - ) - .await?; - let builder = builder.set_card_details(&payment_method.card_cvc, &card_bin_details); - let card = builder.get_data(); - - // Tokenize card - let network_token_details = executor.tokenize_card(&data.customer_id, &card).await?; - let builder = builder - .set_tokenize_details(&network_token_details.0, network_token_details.1.as_ref()); - - // Store in locker - let stored_card_resp = executor - .store_in_locker( - &network_token_details, - &data.customer_id, - card_details.name_on_card, - card_details.nick_name.map(Secret::new), - ) - .await?; - let builder = builder.transition(|_| &stored_card_resp); + // Tokenize card + let domain_card = builder + .get_optional_card() + .get_required_value("value") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let network_token_details = executor.tokenize_card(&customer.id, &domain_card).await?; + let builder = builder.set_token_details(&network_token_details); - // Update payment method entry - let payment_method_in_db = executor - .update_payment_method(&stored_card_resp, data, network_token_details, &card) - .await?; - let builder = - builder.set_payment_method_response(payment_method_in_db, &card_bin_details); + // Store card and token in locker + let store_card_and_token_resp = executor + .store_card_and_token_in_locker(&network_token_details, &domain_card, &customer.id) + .await?; + let builder = builder.set_stored_card_response(&store_card_and_token_resp); + let builder = builder.set_stored_token_response(&store_card_and_token_resp); + + // Create payment method + let payment_method = executor + .create_payment_method( + &store_card_and_token_resp, + &network_token_details, + &domain_card, + &customer.id, + ) + .await?; + let builder = builder.set_payment_method_response(&payment_method); - // Build response - builder.build() - } - }; - Ok(response) + Ok(builder.build()) +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +pub async fn execute_payment_method_tokenization( + executor: tokenize::CardNetworkTokenizeExecutor<'_, domain::TokenizePaymentMethodRequest>, + builder: tokenize::NetworkTokenizationBuilder<'_, tokenize::TokenizeWithPmId>, + req: &domain::TokenizePaymentMethodRequest, +) -> errors::RouterResult { + // Fetch payment method + let payment_method = executor + .fetch_payment_method(&req.payment_method_id) + .await?; + let builder = builder.set_payment_method(&payment_method); + + // Validate payment method + let locker_id = executor + .validate_payment_method_and_get_locker_reference(&payment_method) + .await?; + let builder = builder.set_validate_result(); + + // Fetch card from locker + let card_details = get_card_from_locker( + executor.state, + &payment_method.customer_id, + executor.merchant_account.get_id(), + &locker_id, + ) + .await?; + + // Perform BIN lookup + let optional_card_info = executor + .fetch_bin_details(card_details.card_number.clone()) + .await?; + let builder = builder.set_card_details(&card_details, optional_card_info, req.card_cvc.clone()); + + // Tokenize card + let domain_card = builder.get_optional_card().get_required_value("card")?; + let network_token_details = executor + .tokenize_card(&payment_method.customer_id, &domain_card) + .await?; + let builder = builder.set_token_details(&network_token_details); + + // Store token in locker + let store_token_resp = executor + .store_network_token_in_locker( + &network_token_details, + &payment_method.customer_id, + card_details.name_on_card.clone(), + card_details.nick_name.clone().map(Secret::new), + ) + .await?; + let builder = builder.set_stored_token_response(&store_token_resp); + + // Update payment method + let updated_payment_method = executor + .update_payment_method( + &store_token_resp, + payment_method, + &network_token_details, + &domain_card, + ) + .await?; + let builder = builder.set_payment_method(&updated_payment_method); + + Ok(builder.build()) } diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 9fadb2e606d..595e4f2fd86 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -1,47 +1,32 @@ -use std::str::FromStr; - use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; -use api_models::{enums as api_enums, payment_methods as payment_methods_api}; +use api_models::payment_methods as payment_methods_api; use cards::CardNumber; use common_utils::{ - consts, - ext_traits::OptionExt, - generate_customer_id_of_default_length, id_type, - pii::Email, + id_type, transformers::{ForeignFrom, ForeignTryFrom}, - type_name, - types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; -use error_stack::{report, ResultExt}; -use hyperswitch_domain_models::type_encryption::{crypto_operation, CryptoOperation}; -use masking::{ExposeInterface, PeekInterface, Secret, SwitchStrategy}; -use rdkafka::message::ToBytes; +use hyperswitch_domain_models::router_request_types as domain_request_types; +use masking::Secret; use router_env::logger; use crate::{ core::payment_methods::{ - cards::{ - add_card_to_hs_locker, create_encrypted_data, create_payment_method, tokenize_card_flow, - }, - network_tokenization, - transformers::{StoreCardReq, StoreCardRespPayload, StoreLockerReq}, + cards::tokenize_card_flow, network_tokenization, transformers as pm_transformers, }, errors::{self, RouterResult}, services, - types::{ - api, - domain::{ - self, - bulk_tokenization::{ - CardNetworkTokenizeRecord, CardNetworkTokenizeRequest, TokenizeCardRequest, - TokenizePaymentMethodRequest, - }, - }, - }, - utils::Encryptable, + types::{api, domain}, SessionState, }; +use super::migration; + +pub mod card_executor; +pub mod payment_method_executor; + +pub use card_executor::*; +pub use payment_method_executor::*; + #[derive(Debug, MultipartForm)] pub struct CardNetworkTokenizeForm { #[multipart(limit = "1MB")] @@ -59,7 +44,7 @@ pub fn parse_csv( let mut records = Vec::new(); let mut id_counter = 0; for (i, result) in csv_reader - .deserialize::() + .deserialize::() .enumerate() { match result { @@ -109,7 +94,6 @@ pub fn get_tokenize_card_form_records( pub async fn tokenize_cards( state: &SessionState, records: Vec, - merchant_id: &id_type::MerchantId, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> errors::RouterResponse> { @@ -121,8 +105,7 @@ pub async fn tokenize_cards( let tokenize_request = record.data.clone(); Box::pin(tokenize_card_flow( state, - CardNetworkTokenizeRequest::foreign_from(record), - merchant_id, + domain::CardNetworkTokenizeRequest::foreign_from(record), merchant_account, key_store, )) @@ -147,812 +130,87 @@ pub async fn tokenize_cards( Ok(services::ApplicationResponse::Json(responses)) } +// Data types +type NetworkTokenizationResponse = ( + network_tokenization::CardNetworkTokenResponsePayload, + Option, +); + +pub struct StoreLockerResponse { + pub store_card_resp: pm_transformers::StoreCardRespPayload, + pub store_token_resp: pm_transformers::StoreCardRespPayload, +} + // Builder -pub struct CardNetworkTokenizeResponseBuilder { +pub struct NetworkTokenizationBuilder<'a, S: State> { /// Current state state: std::marker::PhantomData, - /// State data - data: D, + /// Customer details + pub customer: Option<&'a api::CustomerDetails>, - /// Response for payment method entry in DB - pub payment_method_response: Option, + /// Card details + pub card: Option, - /// Customer details - pub customer: Option, + /// Network token details + pub network_token: Option<&'a network_tokenization::CardNetworkTokenResponsePayload>, + + /// Stored card details + pub stored_card: Option<&'a pm_transformers::StoreCardRespPayload>, + + /// Stored token details + pub stored_token: Option<&'a pm_transformers::StoreCardRespPayload>, + + /// Payment method response + pub payment_method_response: Option, /// Card network tokenization status pub card_tokenized: bool, /// Error code - pub error_code: Option, + pub error_code: Option<&'a String>, /// Error message - pub error_message: Option, + pub error_message: Option<&'a String>, } // Async executor -pub struct CardNetworkTokenizeExecutor<'a> { - req: &'a CardNetworkTokenizeRequest, - state: &'a SessionState, - merchant_account: &'a domain::MerchantAccount, +pub struct CardNetworkTokenizeExecutor<'a, D> { + pub state: &'a SessionState, + pub merchant_account: &'a domain::MerchantAccount, key_store: &'a domain::MerchantKeyStore, + data: &'a D, + customer: Option<&'a domain_request_types::CustomerDetails>, } -type NetworkTokenizationResponse = ( - network_tokenization::CardNetworkTokenResponsePayload, - Option, -); - // State machine pub trait State {} -pub trait TransitionTo {} - -// All available states -pub struct TokenizeWithCard; -pub struct CardValidated; -pub struct CustomerAssigned; -pub struct CardDetailsAssigned; -pub struct CardTokenized; -pub struct CardTokenStored; -pub struct PaymentMethodCreated; - -pub struct TokenizeWithPmId; -pub struct PmValidated; -pub struct PmFetched; -pub struct PmAssigned; -pub struct PmTokenized; -pub struct PmTokenStored; -pub struct PmTokenUpdated; - -impl State for TokenizeWithCard {} -impl State for CardValidated {} -impl State for CustomerAssigned {} -impl State for CardDetailsAssigned {} -impl State for CardTokenized {} -impl State for CardTokenStored {} -impl State for PaymentMethodCreated {} - -impl State for TokenizeWithPmId {} -impl State for PmValidated {} -impl State for PmFetched {} -impl State for PmAssigned {} -impl State for PmTokenized {} -impl State for PmTokenStored {} -impl State for PmTokenUpdated {} - -// Type safe transition -impl CardNetworkTokenizeResponseBuilder { - pub fn transition(self, f: F) -> CardNetworkTokenizeResponseBuilder - where - S1: TransitionTo, - S2: State, - F: FnOnce(D1) -> D2, - { - CardNetworkTokenizeResponseBuilder { - state: std::marker::PhantomData::, - data: f(self.data), - customer: self.customer, - payment_method_response: self.payment_method_response, - card_tokenized: self.card_tokenized, - error_code: self.error_code, - error_message: self.error_message, - } - } -} - -// State machine for card tokenization -impl TransitionTo<&TokenizeCardRequest, CardValidated> for TokenizeWithCard {} -impl TransitionTo for CardValidated {} -impl TransitionTo for CustomerAssigned {} -impl TransitionTo for CardDetailsAssigned {} -impl TransitionTo for CardTokenized {} -impl TransitionTo for CardTokenStored {} +pub trait TransitionTo {} -impl<'a> CardNetworkTokenizeExecutor<'a> { - pub fn new( - req: &'a CardNetworkTokenizeRequest, +// Trait for network tokenization +#[async_trait::async_trait] +pub trait NetworkTokenizationProcess<'a, D> { + fn new( state: &'a SessionState, - merchant_account: &'a domain::MerchantAccount, key_store: &'a domain::MerchantKeyStore, - ) -> Self { - Self { - req, - state, - merchant_account, - key_store, - } - } - - pub fn validate_card_number(&self, card_number: Secret) -> RouterResult { - CardNumber::from_str(card_number.peek()).change_context( - errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid card number".to_string(), - }, - ) - } - - pub async fn get_or_create_customer(&self) -> RouterResult { - let db = &*self.state.store; - let customer_details = self - .req - .customer - .as_ref() - .get_required_value("customer") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer", - })?; - let customer_id = customer_details - .customer_id - .as_ref() - .get_required_value("customer_id") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer_id", - })?; - let key_manager_state: &KeyManagerState = &self.state.into(); - - match db - .find_customer_optional_by_customer_id_merchant_id( - key_manager_state, - customer_id, - self.merchant_account.get_id(), - self.key_store, - self.merchant_account.storage_scheme, - ) - .await - .inspect_err(|err| logger::info!("Error fetching customer: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)? - { - // Customer found - Some(customer) => Ok(api::CustomerDetails { - id: customer.customer_id.clone(), - name: customer.name.clone().map(|name| name.into_inner()), - email: customer.email.clone().map(Email::from), - phone: customer.phone.clone().map(|phone| phone.into_inner()), - phone_country_code: customer.phone_country_code.clone(), - }), - // Customer not found - None => { - if customer_details.name.is_some() - || customer_details.email.is_some() - || customer_details.phone.is_some() - { - let encrypted_data = crypto_operation( - key_manager_state, - type_name!(domain::Customer), - CryptoOperation::BatchEncrypt( - domain::FromRequestEncryptableCustomer::to_encryptable( - domain::FromRequestEncryptableCustomer { - name: customer_details.name.clone(), - email: customer_details - .email - .clone() - .map(|email| email.expose().switch_strategy()), - phone: customer_details.phone.clone(), - }, - ), - ), - Identifier::Merchant(self.merchant_account.get_id().clone()), - self.key_store.key.get_inner().peek(), - ) - .await - .inspect_err(|err| logger::info!("Error encrypting customer: {:?}", err)) - .and_then(|val| val.try_into_batchoperation()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encrypt customer")?; - - let encryptable_customer = - domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to form EncryptableCustomer")?; - - let domain_customer = domain::Customer { - customer_id: generate_customer_id_of_default_length(), - merchant_id: self.merchant_account.get_id().clone(), - name: encryptable_customer.name, - email: encryptable_customer.email.map(|email| { - Encryptable::new( - email.clone().into_inner().switch_strategy(), - email.into_encrypted(), - ) - }), - phone: encryptable_customer.phone, - description: None, - phone_country_code: customer_details.phone_country_code.to_owned(), - metadata: None, - connector_customer: None, - created_at: common_utils::date_time::now(), - modified_at: common_utils::date_time::now(), - address_id: None, - default_payment_method_id: None, - updated_by: None, - version: hyperswitch_domain_models::consts::API_VERSION, - }; - - db.insert_customer( - domain_customer.clone(), - key_manager_state, - self.key_store, - self.merchant_account.storage_scheme, - ) - .await - .inspect_err(|err| logger::info!("Error creating a customer: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!( - "Failed to insert customer [id - {:?}] for merchant [id - {:?}]", - customer_id, - self.merchant_account.get_id() - ) - })?; - - Ok(api::CustomerDetails { - id: domain_customer.customer_id, - name: customer_details.name.clone(), - email: customer_details.email.clone(), - phone: customer_details.phone.clone(), - phone_country_code: customer_details.phone_country_code.clone(), - }) - - // Throw error if customer creation is not requested - } else { - Err(report!(errors::ApiErrorResponse::MissingRequiredFields { - field_names: vec!["customer.name", "customer.email", "customer.phone"], - })) - } - } - } - } - - pub async fn tokenize_card( + merchant_account: &'a domain::MerchantAccount, + data: &'a D, + customer: Option<&'a domain_request_types::CustomerDetails>, + ) -> Self; + async fn fetch_bin_details( + &self, + card_number: CardNumber, + ) -> RouterResult>; + async fn tokenize_card( &self, customer_id: &id_type::CustomerId, card: &domain::Card, - ) -> RouterResult { - match network_tokenization::make_card_network_tokenization_request( - self.state, - card, - customer_id, - ) - .await - { - Ok(tokenization_response) => Ok(tokenization_response), - Err(err) => { - // TODO: revert this - logger::error!( - "Failed to tokenize card with the network: {:?}\nUsing dummy response", - err - ); - Ok(( - network_tokenization::CardNetworkTokenResponsePayload { - card_brand: api_enums::CardNetwork::Visa, - card_fingerprint: None, - card_reference: uuid::Uuid::new_v4().to_string(), - correlation_id: uuid::Uuid::new_v4().to_string(), - customer_id: customer_id.get_string_repr().to_string(), - par: "".to_string(), - token: card.card_number.clone(), - token_expiry_month: card.card_exp_month.clone(), - token_expiry_year: card.card_exp_year.clone(), - token_isin: card.card_number.get_card_isin(), - token_last_four: card.card_number.get_last4(), - token_status: "active".to_string(), - }, - Some(uuid::Uuid::new_v4().to_string()), - )) - } - } - } - - pub async fn store_in_locker( + ) -> RouterResult; + async fn store_network_token_in_locker( &self, - network_token_details: &NetworkTokenizationResponse, + network_token: &NetworkTokenizationResponse, customer_id: &id_type::CustomerId, card_holder_name: Option>, nick_name: Option>, - ) -> RouterResult { - let network_token = &network_token_details.0; - let merchant_id = self.merchant_account.get_id(); - let locker_req = StoreLockerReq::LockerCard(StoreCardReq { - merchant_id: merchant_id.clone(), - merchant_customer_id: customer_id.clone(), - card: payment_methods_api::Card { - card_number: network_token.token.clone(), - card_exp_month: network_token.token_expiry_month.clone(), - card_exp_year: network_token.token_expiry_year.clone(), - card_brand: Some(network_token.card_brand.to_string()), - card_isin: Some(network_token.token_isin.clone()), - name_on_card: card_holder_name, - nick_name: nick_name.map(|nick_name| nick_name.expose()), - }, - requestor_card_reference: None, - ttl: self.state.conf.locker.ttl_for_storage_in_secs, - }); - - let stored_resp = add_card_to_hs_locker( - self.state, - &locker_req, - customer_id, - api_enums::LockerChoice::HyperswitchCardVault, - ) - .await - .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - Ok(stored_resp) - } - - pub async fn create_payment_method( - &self, - stored_card_resp: &StoreCardRespPayload, - network_token_details: NetworkTokenizationResponse, - card_details: &domain::Card, - customer_id: &id_type::CustomerId, - ) -> RouterResult { - let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); - - // Form encrypted PM data (original card) - let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(card_details.card_number.get_last4()), - expiry_month: Some(card_details.card_exp_month.clone()), - expiry_year: Some(card_details.card_exp_year.clone()), - card_isin: Some(card_details.card_number.get_card_isin()), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_pm_data = create_encrypted_data(&self.state.into(), self.key_store, pm_data) - .await - .inspect_err(|err| logger::info!("Error encrypting payment method data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - // Form encrypted network token data (tokenized card) - let network_token_data = network_token_details.0; - let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(network_token_data.token_last_four), - expiry_month: Some(network_token_data.token_expiry_month), - expiry_year: Some(network_token_data.token_expiry_year), - card_isin: Some(network_token_data.token_isin), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) - .await - .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - // Form PM create entry - let payment_method_create = api::PaymentMethodCreate { - payment_method: Some(api_enums::PaymentMethod::Card), - payment_method_type: card_details - .card_type - .as_ref() - .and_then(|card_type| api_enums::PaymentMethodType::from_str(card_type).ok()), - payment_method_issuer: card_details.card_issuer.clone(), - payment_method_issuer_code: None, - card: Some(api::CardDetail { - card_number: card_details.card_number.clone(), - card_exp_month: card_details.card_exp_month.clone(), - card_exp_year: card_details.card_exp_year.clone(), - card_holder_name: card_details.card_holder_name.clone(), - nick_name: card_details.nick_name.clone(), - card_issuing_country: card_details.card_issuing_country.clone(), - card_network: card_details.card_network.clone(), - card_issuer: card_details.card_issuer.clone(), - card_type: card_details.card_type.clone(), - }), - metadata: None, - customer_id: Some(customer_id.clone()), - card_network: card_details - .card_network - .as_ref() - .map(|network| network.to_string()), - bank_transfer: None, - wallet: None, - client_secret: None, - payment_method_data: None, - billing: None, - connector_mandate_details: None, - network_transaction_id: None, - }; - create_payment_method( - self.state, - &payment_method_create, - customer_id, - &payment_method_id, - Some(stored_card_resp.card_reference.clone()), - self.merchant_account.get_id(), - None, - None, - Some(enc_pm_data), - self.key_store, - None, - None, - None, // TODO: update - self.merchant_account.storage_scheme, - None, - None, - network_token_details.1, - Some(stored_card_resp.card_reference.clone()), - Some(enc_token_data), - ) - .await - } - - pub async fn validate_payment_method_id( - &self, - payment_method_id: &str, - ) -> RouterResult<(domain::PaymentMethod, String)> { - let payment_method = self - .state - .store - .find_payment_method( - &self.state.into(), - self.key_store, - payment_method_id, - self.merchant_account.storage_scheme, - ) - .await - .map_err(|err| match err.current_context() { - storage_impl::errors::StorageError::ValueNotFound(_) => { - err.change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid payment_method_id".to_string(), - }) - } - e => { - logger::info!("Error fetching customer: {:?}", e); - err.change_context(errors::ApiErrorResponse::InternalServerError) - } - })?; - - // Ensure payment method is card - match payment_method.payment_method { - Some(api_enums::PaymentMethod::Card) => Ok(()), - Some(_) => Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Payment method is not card".to_string() - })), - None => Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Payment method is empty".to_string() - })), - }?; - - // Ensure card is not tokenized already - if payment_method - .network_token_requestor_reference_id - .is_some() - { - return Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Card is already tokenized".to_string() - })); - } - - // Ensure locker reference is present - payment_method.locker_id.clone().map_or( - Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "locker_id not found for given payment_method_id".to_string() - })), - |locker_id| Ok((payment_method, locker_id)), - ) - } - - pub async fn update_payment_method( - &self, - stored_card_resp: &StoreCardRespPayload, - payment_method: domain::PaymentMethod, - network_token_details: NetworkTokenizationResponse, - card_details: &domain::Card, - ) -> RouterResult { - // Form encrypted network token data (tokenized card) - let network_token_data = network_token_details.0; - let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(network_token_data.token_last_four), - expiry_month: Some(network_token_data.token_expiry_month), - expiry_year: Some(network_token_data.token_expiry_year), - card_isin: Some(network_token_data.token_isin), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) - .await - .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - // Update payment method - let payment_method_update = diesel_models::PaymentMethodUpdate::NetworkTokenDataUpdate { - network_token_requestor_reference_id: network_token_details.1, - network_token_locker_id: Some(stored_card_resp.card_reference.clone()), - network_token_payment_method_data: Some(enc_token_data.into()), - }; - self.state - .store - .update_payment_method( - &self.state.into(), - self.key_store, - payment_method, - payment_method_update, - self.merchant_account.storage_scheme, - ) - .await - .inspect_err(|err| logger::info!("Error updating payment method: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError) - } -} - -// Initialize builder for tokenizing raw card details -impl<'a> CardNetworkTokenizeResponseBuilder<&'a TokenizeCardRequest, TokenizeWithCard> { - pub fn new(req: &CardNetworkTokenizeRequest, data: &'a TokenizeCardRequest) -> Self { - Self { - data, - state: std::marker::PhantomData::, - customer: req - .customer - .as_ref() - .map(|customer| api::CustomerDetails::foreign_try_from(customer.clone())) - .transpose() - .unwrap_or(None), - payment_method_response: None, - card_tokenized: false, - error_code: None, - error_message: None, - } - } -} - -// Perform customer related operations -impl CardNetworkTokenizeResponseBuilder<&TokenizeCardRequest, CardValidated> { - pub fn set_customer_details( - mut self, - customer: &api::CustomerDetails, - ) -> CardNetworkTokenizeResponseBuilder { - self.customer = Some(customer.clone()); - self.transition(|_| customer.to_owned()) - } -} - -// Perform card related operations (post BIN lookup update) -impl CardNetworkTokenizeResponseBuilder { - pub fn set_card_details( - self, - card_number: CardNumber, - card_req: &TokenizeCardRequest, - card_bin_details: &api::CardDetailFromLocker, - ) -> CardNetworkTokenizeResponseBuilder { - self.transition(|_| domain::Card { - card_number, - card_type: card_bin_details.card_type.clone(), - card_network: card_bin_details.card_network.clone(), - card_issuer: card_bin_details.card_issuer.clone(), - card_issuing_country: card_bin_details.issuer_country.clone(), - card_exp_month: card_req.card_expiry_month.clone(), - card_exp_year: card_req.card_expiry_year.clone(), - card_cvc: card_req.card_cvc.clone(), - nick_name: card_req.nick_name.clone(), - card_holder_name: card_req.card_holder_name.clone(), - bank_code: None, - }) - } -} - -// Perform card network tokenization -impl CardNetworkTokenizeResponseBuilder { - pub fn get_data(&self) -> domain::Card { - self.data.clone() - } - pub fn set_tokenize_details( - mut self, - network_token: &network_tokenization::CardNetworkTokenResponsePayload, - network_token_requestor_ref_id: Option<&String>, - ) -> CardNetworkTokenizeResponseBuilder { - self.card_tokenized = true; - self.transition(|_| { - ( - network_token.clone(), - network_token_requestor_ref_id.cloned(), - ) - }) - } -} - -// Perform locker related operations -impl CardNetworkTokenizeResponseBuilder { - pub fn set_locker_details( - self, - card_bin_details: &api::CardDetailFromLocker, - stored_card_resp: &StoreCardRespPayload, - merchant_id: id_type::MerchantId, - customer_id: id_type::CustomerId, - ) -> CardNetworkTokenizeResponseBuilder { - self.transition(|_| api::PaymentMethodResponse { - merchant_id, - customer_id: Some(customer_id), - payment_method_id: stored_card_resp.card_reference.clone(), - payment_method: Some(api_enums::PaymentMethod::Card), - payment_method_type: card_bin_details - .card_type - .as_ref() - .and_then(|card_type| api_enums::PaymentMethodType::from_str(card_type).ok()), - card: Some(card_bin_details.clone()), - recurring_enabled: true, - installment_payment_enabled: false, - created: Some(common_utils::date_time::now()), - payment_experience: None, - metadata: None, - bank_transfer: None, - last_used_at: None, - client_secret: None, - }) - } -} - -// Create payment method entry -impl CardNetworkTokenizeResponseBuilder { - pub fn set_payment_method_response( - self, - payment_method: domain::PaymentMethod, - ) -> CardNetworkTokenizeResponseBuilder { - let payment_method_response = api::PaymentMethodResponse { - merchant_id: payment_method.merchant_id, - customer_id: Some(payment_method.customer_id), - payment_method_id: payment_method.payment_method_id, - payment_method: payment_method.payment_method, - payment_method_type: payment_method.payment_method_type, - card: self.data.card.clone(), - recurring_enabled: self.data.recurring_enabled, - installment_payment_enabled: self.data.installment_payment_enabled, - payment_experience: self.data.payment_experience.clone(), - metadata: self.data.metadata.clone(), - created: self.data.created, - bank_transfer: self.data.bank_transfer.clone(), - last_used_at: self.data.last_used_at, - client_secret: self.data.client_secret.clone(), - }; - self.transition(|_| payment_method_response) - } -} - -// Build return response -impl CardNetworkTokenizeResponseBuilder { - pub fn build(self) -> payment_methods_api::CardNetworkTokenizeResponse { - payment_methods_api::CardNetworkTokenizeResponse { - payment_method_response: Some(self.data), - customer: self.customer, - card_tokenized: self.card_tokenized, - error_code: self.error_code, - error_message: self.error_message, - req: None, - } - } -} - -// State machine for payment method ID tokenization -impl TransitionTo for TokenizeWithPmId {} -impl TransitionTo for PmValidated {} -impl TransitionTo for PmFetched {} -impl TransitionTo for PmAssigned {} -impl TransitionTo<&StoreCardRespPayload, PmTokenStored> for PmTokenized {} -impl TransitionTo for PmTokenStored {} - -// Initialize builder for tokenizing saved cards -impl CardNetworkTokenizeResponseBuilder { - pub fn new(req: &CardNetworkTokenizeRequest, data: TokenizePaymentMethodRequest) -> Self { - Self { - data, - state: std::marker::PhantomData::, - customer: req - .customer - .as_ref() - .map(|customer| api::CustomerDetails::foreign_try_from(customer.clone())) - .transpose() - .unwrap_or(None), - payment_method_response: None, - card_tokenized: false, - error_code: None, - error_message: None, - } - } -} - -impl CardNetworkTokenizeResponseBuilder { - pub fn get_data(&self) -> domain::PaymentMethod { - self.data.clone() - } -} - -impl CardNetworkTokenizeResponseBuilder { - pub fn set_card_details( - self, - card_cvc: &Secret, - card_bin_details: &api::CardDetailFromLocker, - ) -> CardNetworkTokenizeResponseBuilder { - let card = domain::Card { - card_number: self.data.card_number.clone(), - card_exp_year: self.data.card_exp_year.clone(), - card_exp_month: self.data.card_exp_month.clone(), - card_cvc: card_cvc.clone(), - card_holder_name: self.data.name_on_card.clone(), - nick_name: self - .data - .nick_name - .as_ref() - .map(|name| Secret::new(name.clone())), - card_type: card_bin_details.card_type.clone(), - card_network: card_bin_details.card_network.clone(), - card_issuer: card_bin_details.card_issuer.clone(), - card_issuing_country: card_bin_details.issuer_country.clone(), - bank_code: None, - }; - self.transition(|_| card) - } -} - -impl CardNetworkTokenizeResponseBuilder { - pub fn get_data(&self) -> domain::Card { - self.data.clone() - } - pub fn set_tokenize_details( - mut self, - network_token: &network_tokenization::CardNetworkTokenResponsePayload, - network_token_requestor_ref_id: Option<&String>, - ) -> CardNetworkTokenizeResponseBuilder { - self.card_tokenized = true; - self.transition(|_| { - ( - network_token.clone(), - network_token_requestor_ref_id.cloned(), - ) - }) - } -} - -impl CardNetworkTokenizeResponseBuilder<&StoreCardRespPayload, PmTokenStored> { - pub fn set_payment_method_response( - self, - payment_method: domain::PaymentMethod, - card_bin_details: &api::CardDetailFromLocker, - ) -> CardNetworkTokenizeResponseBuilder { - let payment_method_response = api::PaymentMethodResponse { - merchant_id: payment_method.merchant_id, - customer_id: Some(payment_method.customer_id), - payment_method_id: payment_method.payment_method_id, - payment_method: payment_method.payment_method, - payment_method_type: payment_method.payment_method_type, - card: Some(card_bin_details.clone()), - recurring_enabled: true, - installment_payment_enabled: false, - metadata: payment_method.metadata, - created: Some(payment_method.created_at), - last_used_at: Some(payment_method.last_used_at), - client_secret: payment_method.client_secret, - payment_experience: None, - bank_transfer: None, - }; - self.transition(|_| payment_method_response) - } -} - -impl CardNetworkTokenizeResponseBuilder { - pub fn build(self) -> payment_methods_api::CardNetworkTokenizeResponse { - payment_methods_api::CardNetworkTokenizeResponse { - payment_method_response: Some(self.data), - customer: self.customer, - card_tokenized: self.card_tokenized, - error_code: self.error_code, - error_message: self.error_message, - req: None, - } - } + ) -> RouterResult; } diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs new file mode 100644 index 00000000000..14ca15c0a47 --- /dev/null +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -0,0 +1,729 @@ +use api_models::{enums as api_enums, payment_methods as payment_methods_api}; +use cards::CardNumber; +use common_utils::{ + consts, + ext_traits::OptionExt, + generate_customer_id_of_default_length, id_type, + pii::Email, + type_name, + types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + router_request_types as domain_request_types, + type_encryption::{crypto_operation, CryptoOperation}, +}; +use masking::{ExposeInterface, PeekInterface, Secret, SwitchStrategy}; +use router_env::logger; +use std::str::FromStr; + +use crate::{ + core::payment_methods::{ + cards::{add_card_to_hs_locker, create_encrypted_data, create_payment_method}, + network_tokenization, transformers as pm_transformers, + }, + errors::{self, RouterResult}, + types::{api, domain}, + utils, SessionState, +}; + +use super::{ + migration, CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, + NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, +}; + +// Available states for card tokenization +pub struct TokenizeWithCard; +pub struct CardRequestValidated; +pub struct CustomerAssigned; +pub struct CardDetailsAssigned; +pub struct CardTokenized; +pub struct CardStored; +pub struct CardTokenStored; +pub struct PaymentMethodCreated; + +impl State for TokenizeWithCard {} +impl State for CardRequestValidated {} +impl State for CustomerAssigned {} +impl State for CardDetailsAssigned {} +impl State for CardTokenized {} +impl State for CardStored {} +impl State for CardTokenStored {} +impl State for PaymentMethodCreated {} + +// State transitions for card tokenization +impl TransitionTo for TokenizeWithCard {} +impl TransitionTo for CardRequestValidated {} +impl TransitionTo for CustomerAssigned {} +impl TransitionTo for CardDetailsAssigned {} +impl TransitionTo for CardTokenized {} +impl TransitionTo for CardTokenStored {} + +impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithCard> { + pub fn new() -> Self { + Self { + state: std::marker::PhantomData, + customer: None, + card: None, + network_token: None, + stored_card: None, + stored_token: None, + payment_method_response: None, + card_tokenized: false, + error_code: None, + error_message: None, + } + } + pub fn set_validate_result(self) -> NetworkTokenizationBuilder<'a, CardRequestValidated> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CardRequestValidated> { + pub fn set_customer( + self, + customer: &'a api::CustomerDetails, + ) -> NetworkTokenizationBuilder<'a, CustomerAssigned> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + customer: Some(customer), + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { + pub fn set_card_details( + self, + card_req: &'a domain::TokenizeCardRequest, + optional_card_info: Option, + ) -> NetworkTokenizationBuilder<'_, CardDetailsAssigned> { + let card = domain::Card { + card_number: card_req.raw_card_number.clone(), + card_exp_month: card_req.card_expiry_month.clone(), + card_exp_year: card_req.card_expiry_year.clone(), + card_cvc: card_req.card_cvc.clone(), + bank_code: optional_card_info + .as_ref() + .and_then(|card_info| card_info.bank_code.clone()), + nick_name: card_req.nick_name.clone(), + card_holder_name: card_req.card_holder_name.clone(), + card_issuer: optional_card_info + .as_ref() + .map_or(card_req.card_issuer.clone(), |card_info| { + card_info.card_issuer.clone() + }), + card_network: optional_card_info + .as_ref() + .map_or(card_req.card_network.clone(), |card_info| { + card_info.card_network.clone() + }), + card_type: optional_card_info.as_ref().map_or( + card_req + .card_type + .as_ref() + .map(|card_type| card_type.to_string()), + |card_info| card_info.card_type.clone(), + ), + card_issuing_country: optional_card_info + .as_ref() + .map_or(card_req.card_issuing_country.clone(), |card_info| { + card_info.card_issuing_country.clone() + }), + }; + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + card: Some(card), + customer: self.customer, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { + pub fn get_optional_card(&self) -> Option { + self.card.clone() + } + pub fn set_token_details( + self, + network_token: &'a NetworkTokenizationResponse, + ) -> NetworkTokenizationBuilder<'a, CardTokenized> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + network_token: Some(&network_token.0), + customer: self.customer, + card: self.card, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CardTokenized> { + pub fn set_stored_card_response( + self, + store_card_response: &'a StoreLockerResponse, + ) -> NetworkTokenizationBuilder<'a, CardStored> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + stored_card: Some(&store_card_response.store_card_resp), + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CardStored> { + pub fn set_stored_token_response( + self, + store_token_response: &'a StoreLockerResponse, + ) -> NetworkTokenizationBuilder<'a, CardTokenStored> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + card_tokenized: true, + stored_token: Some(&store_token_response.store_token_resp), + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + payment_method_response: self.payment_method_response, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CardTokenStored> { + pub fn set_payment_method_response( + self, + payment_method: &'a domain::PaymentMethod, + ) -> NetworkTokenizationBuilder<'a, PaymentMethodCreated> { + let card_detail_from_locker = self.card.as_ref().map(|card| api::CardDetailFromLocker { + scheme: None, + issuer_country: card.card_issuing_country.clone(), + last4_digits: Some(card.card_number.clone().get_last4()), + card_number: None, + expiry_month: Some(card.card_exp_month.clone().clone()), + expiry_year: Some(card.card_exp_year.clone().clone()), + card_token: None, + card_holder_name: card.card_holder_name.clone(), + card_fingerprint: None, + nick_name: card.nick_name.clone(), + card_network: card.card_network.clone(), + card_isin: Some(card.card_number.clone().get_card_isin()), + card_issuer: card.card_issuer.clone(), + card_type: card.card_type.clone(), + saved_to_locker: true, + }); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: payment_method.merchant_id.clone(), + customer_id: Some(payment_method.customer_id.clone()), + payment_method_id: payment_method.payment_method_id.clone(), + payment_method: payment_method.payment_method, + payment_method_type: payment_method.payment_method_type, + card: card_detail_from_locker, + recurring_enabled: true, + installment_payment_enabled: false, + metadata: payment_method.metadata.clone(), + created: Some(payment_method.created_at), + last_used_at: Some(payment_method.last_used_at), + client_secret: payment_method.client_secret.clone(), + bank_transfer: None, + payment_experience: None, + }; + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + payment_method_response: Some(payment_method_response), + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl NetworkTokenizationBuilder<'_, PaymentMethodCreated> { + pub fn build(self) -> api::CardNetworkTokenizeResponse { + api::CardNetworkTokenizeResponse { + payment_method_response: self.payment_method_response, + customer: self.customer.cloned(), + card_tokenized: self.card_tokenized, + error_code: self.error_code.cloned(), + error_message: self.error_message.cloned(), + // Below field is mutated by caller functions for batched API operations + req: None, + } + } +} + +// Specific executor for card tokenization +impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { + pub async fn validate_request_and_fetch_optional_customer( + &self, + ) -> RouterResult> { + // Validate card's expiry + migration::validate_card_expiry(&self.data.card_expiry_month, &self.data.card_expiry_year)?; + + // Validate customer ID + let customer_req = self + .customer + .get_required_value("customer") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })?; + let customer_id = customer_req + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer.customer_id", + })?; + + // Fetch customer details if present + let db = &*self.state.store; + let key_manager_state: &KeyManagerState = &self.state.into(); + db.find_customer_optional_by_customer_id_merchant_id( + key_manager_state, + customer_id, + self.merchant_account.get_id(), + self.key_store, + self.merchant_account.storage_scheme, + ) + .await + .inspect_err(|err| logger::info!("Error fetching customer: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError) + .map_or( + // Validate if customer creation is feasible + if customer_req.name.is_some() + || customer_req.email.is_some() + || customer_req.phone.is_some() + { + Ok(None) + } else { + Err(report!(errors::ApiErrorResponse::MissingRequiredFields { + field_names: vec!["customer.name", "customer.email", "customer.phone"], + })) + }, + // If found, send back CustomerDetails from DB + |optional_customer| { + Ok(optional_customer.map(|customer| api::CustomerDetails { + id: customer.customer_id.clone(), + name: customer.name.clone().map(|name| name.into_inner()), + email: customer.email.clone().map(Email::from), + phone: customer.phone.clone().map(|phone| phone.into_inner()), + phone_country_code: customer.phone_country_code.clone(), + })) + }, + ) + } + + pub async fn create_customer(&self) -> RouterResult { + let db = &*self.state.store; + let customer_details = self + .customer + .get_required_value("customer") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })?; + let customer_id = customer_details + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer_id", + })?; + let key_manager_state: &KeyManagerState = &self.state.into(); + + let encrypted_data = crypto_operation( + key_manager_state, + type_name!(domain::Customer), + CryptoOperation::BatchEncrypt(domain::FromRequestEncryptableCustomer::to_encryptable( + domain::FromRequestEncryptableCustomer { + name: customer_details.name.clone(), + email: customer_details + .email + .clone() + .map(|email| email.expose().switch_strategy()), + phone: customer_details.phone.clone(), + }, + )), + Identifier::Merchant(self.merchant_account.get_id().clone()), + self.key_store.key.get_inner().peek(), + ) + .await + .inspect_err(|err| logger::info!("Error encrypting customer: {:?}", err)) + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encrypt customer")?; + + let encryptable_customer = + domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form EncryptableCustomer")?; + + let domain_customer = domain::Customer { + customer_id: generate_customer_id_of_default_length(), + merchant_id: self.merchant_account.get_id().clone(), + name: encryptable_customer.name, + email: encryptable_customer.email.map(|email| { + utils::Encryptable::new( + email.clone().into_inner().switch_strategy(), + email.into_encrypted(), + ) + }), + phone: encryptable_customer.phone, + description: None, + phone_country_code: customer_details.phone_country_code.to_owned(), + metadata: None, + connector_customer: None, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + address_id: None, + default_payment_method_id: None, + updated_by: None, + version: hyperswitch_domain_models::consts::API_VERSION, + }; + + db.insert_customer( + domain_customer.clone(), + key_manager_state, + self.key_store, + self.merchant_account.storage_scheme, + ) + .await + .inspect_err(|err| logger::info!("Error creating a customer: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed to insert customer [id - {:?}] for merchant [id - {:?}]", + customer_id, + self.merchant_account.get_id() + ) + })?; + + Ok(api::CustomerDetails { + id: domain_customer.customer_id, + name: customer_details.name.clone(), + email: customer_details.email.clone(), + phone: customer_details.phone.clone(), + phone_country_code: customer_details.phone_country_code.clone(), + }) + } + + pub async fn store_card_and_token_in_locker( + &self, + network_token: &NetworkTokenizationResponse, + card: &domain::Card, + customer_id: &id_type::CustomerId, + ) -> RouterResult { + let stored_card_resp = self.store_card_in_locker(card, customer_id).await?; + let stored_token_resp = self + .store_network_token_in_locker( + network_token, + customer_id, + card.card_holder_name.clone(), + card.nick_name.clone(), + ) + .await?; + let store_locker_response = StoreLockerResponse { + store_card_resp: stored_card_resp, + store_token_resp: stored_token_resp, + }; + Ok(store_locker_response) + } + + pub async fn store_card_in_locker( + &self, + card: &domain::Card, + customer_id: &id_type::CustomerId, + ) -> RouterResult { + let merchant_id = self.merchant_account.get_id(); + let locker_req = + pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { + merchant_id: merchant_id.clone(), + merchant_customer_id: customer_id.clone(), + card: payment_methods_api::Card { + card_number: card.card_number.clone(), + card_exp_month: card.card_exp_month.clone(), + card_exp_year: card.card_exp_year.clone(), + card_isin: Some(card.card_number.get_card_isin().clone()), + name_on_card: card.card_holder_name.clone(), + nick_name: card + .nick_name + .as_ref() + .map(|nick_name| nick_name.clone().expose()), + card_brand: None, + }, + requestor_card_reference: None, + ttl: self.state.conf.locker.ttl_for_storage_in_secs, + }); + + let stored_resp = add_card_to_hs_locker( + self.state, + &locker_req, + customer_id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + Ok(stored_resp) + } + + pub async fn create_payment_method( + &self, + stored_locker_resp: &StoreLockerResponse, + network_token_details: &NetworkTokenizationResponse, + card_details: &domain::Card, + customer_id: &id_type::CustomerId, + ) -> RouterResult { + let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); + + // Form encrypted PM data (original card) + let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(card_details.card_number.get_last4()), + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_isin: Some(card_details.card_number.get_card_isin()), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_pm_data = create_encrypted_data(&self.state.into(), self.key_store, pm_data) + .await + .inspect_err(|err| logger::info!("Error encrypting payment method data: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Form encrypted network token data (tokenized card) + let network_token_data = network_token_details.0.clone(); + let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(network_token_data.token_last_four), + expiry_month: Some(network_token_data.token_expiry_month), + expiry_year: Some(network_token_data.token_expiry_year), + card_isin: Some(network_token_data.token_isin), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) + .await + .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Form PM create entry + let payment_method_create = api::PaymentMethodCreate { + payment_method: Some(api_enums::PaymentMethod::Card), + payment_method_type: card_details + .card_type + .as_ref() + .and_then(|card_type| api_enums::PaymentMethodType::from_str(card_type).ok()), + payment_method_issuer: card_details.card_issuer.clone(), + payment_method_issuer_code: None, + card: Some(api::CardDetail { + card_number: card_details.card_number.clone(), + card_exp_month: card_details.card_exp_month.clone(), + card_exp_year: card_details.card_exp_year.clone(), + card_holder_name: card_details.card_holder_name.clone(), + nick_name: card_details.nick_name.clone(), + card_issuing_country: card_details.card_issuing_country.clone(), + card_network: card_details.card_network.clone(), + card_issuer: card_details.card_issuer.clone(), + card_type: card_details.card_type.clone(), + }), + metadata: None, + customer_id: Some(customer_id.clone()), + card_network: card_details + .card_network + .as_ref() + .map(|network| network.to_string()), + bank_transfer: None, + wallet: None, + client_secret: None, + payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, + }; + create_payment_method( + self.state, + &payment_method_create, + customer_id, + &payment_method_id, + Some(stored_locker_resp.store_card_resp.card_reference.clone()), + self.merchant_account.get_id(), + None, + None, + Some(enc_pm_data), + self.key_store, + None, + None, + None, + self.merchant_account.storage_scheme, + None, + None, + network_token_details.1.clone(), + Some(stored_locker_resp.store_token_resp.card_reference.clone()), + Some(enc_token_data), + ) + .await + } +} + +// Common executor for card tokenization +#[async_trait::async_trait] +impl<'a> NetworkTokenizationProcess<'a, domain::TokenizeCardRequest> + for CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> +{ + fn new( + state: &'a SessionState, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + data: &'a domain::TokenizeCardRequest, + customer: Option<&'a domain_request_types::CustomerDetails>, + ) -> Self { + Self { + data, + customer, + state, + merchant_account, + key_store, + } + } + + async fn fetch_bin_details( + &self, + card_number: CardNumber, + ) -> RouterResult> { + let db = &*self.state.store; + db.get_card_info(&card_number.get_card_isin()) + .await + .attach_printable("Failed to perform BIN lookup") + .change_context(errors::ApiErrorResponse::InternalServerError) + } + + async fn tokenize_card( + &self, + customer_id: &id_type::CustomerId, + card: &domain::Card, + ) -> RouterResult { + match network_tokenization::make_card_network_tokenization_request( + self.state, + card, + customer_id, + ) + .await + { + Ok(tokenization_response) => Ok(tokenization_response), + Err(err) => { + // TODO: revert this + logger::error!( + "Failed to tokenize card with the network: {:?}\nUsing dummy response", + err + ); + Ok(( + network_tokenization::CardNetworkTokenResponsePayload { + card_brand: api_enums::CardNetwork::Visa, + card_fingerprint: None, + card_reference: uuid::Uuid::new_v4().to_string(), + correlation_id: uuid::Uuid::new_v4().to_string(), + customer_id: customer_id.get_string_repr().to_string(), + par: "".to_string(), + token: card.card_number.clone(), + token_expiry_month: card.card_exp_month.clone(), + token_expiry_year: card.card_exp_year.clone(), + token_isin: card.card_number.get_card_isin(), + token_last_four: card.card_number.get_last4(), + token_status: "active".to_string(), + }, + Some(uuid::Uuid::new_v4().to_string()), + )) + } + } + } + + async fn store_network_token_in_locker( + &self, + network_token: &NetworkTokenizationResponse, + customer_id: &id_type::CustomerId, + card_holder_name: Option>, + nick_name: Option>, + ) -> RouterResult { + let network_token = &network_token.0; + let merchant_id = self.merchant_account.get_id(); + let locker_req = + pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { + merchant_id: merchant_id.clone(), + merchant_customer_id: customer_id.clone(), + card: payment_methods_api::Card { + card_number: network_token.token.clone(), + card_exp_month: network_token.token_expiry_month.clone(), + card_exp_year: network_token.token_expiry_year.clone(), + card_brand: Some(network_token.card_brand.to_string()), + card_isin: Some(network_token.token_isin.clone()), + name_on_card: card_holder_name, + nick_name: nick_name.map(|nick_name| nick_name.expose()), + }, + requestor_card_reference: None, + ttl: self.state.conf.locker.ttl_for_storage_in_secs, + }); + + let stored_resp = add_card_to_hs_locker( + self.state, + &locker_req, + customer_id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + Ok(stored_resp) + } +} diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs new file mode 100644 index 00000000000..bfb03199fc0 --- /dev/null +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -0,0 +1,488 @@ +use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; +use api_models::{enums as api_enums, payment_methods as payment_methods_api}; +use cards::CardNumber; +use common_utils::{fp_utils::when, id_type}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::router_request_types as domain_request_types; +use masking::{ExposeInterface, Secret}; +use router_env::logger; + +use crate::{ + core::payment_methods::{ + cards::{add_card_to_hs_locker, create_encrypted_data}, + network_tokenization, transformers as pm_transformers, + }, + errors::{self, RouterResult}, + types::{api, domain}, + SessionState, +}; + +use super::{ + CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, + NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, +}; + +// Available states for payment method tokenization +pub struct TokenizeWithPmId; +pub struct PmValidated; +pub struct PmFetched; +pub struct PmAssigned; +pub struct PmTokenized; +pub struct PmTokenStored; +pub struct PmTokenUpdated; + +impl State for TokenizeWithPmId {} +impl State for PmValidated {} +impl State for PmFetched {} +impl State for PmAssigned {} +impl State for PmTokenized {} +impl State for PmTokenStored {} +impl State for PmTokenUpdated {} + +// State transitions for payment method tokenization +impl TransitionTo for TokenizeWithPmId {} +impl TransitionTo for PmFetched {} +impl TransitionTo for PmValidated {} +impl TransitionTo for PmAssigned {} +impl TransitionTo for PmTokenized {} +impl TransitionTo for PmTokenStored {} + +impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithPmId> { + pub fn new() -> Self { + Self { + state: std::marker::PhantomData, + customer: None, + card: None, + network_token: None, + stored_card: None, + stored_token: None, + payment_method_response: None, + card_tokenized: false, + error_code: None, + error_message: None, + } + } + pub fn set_payment_method( + self, + payment_method: &domain::PaymentMethod, + ) -> NetworkTokenizationBuilder<'a, PmFetched> { + let payment_method_response = api::PaymentMethodResponse { + merchant_id: payment_method.merchant_id.clone(), + customer_id: Some(payment_method.customer_id.clone()), + payment_method_id: payment_method.payment_method_id.clone(), + payment_method: payment_method.payment_method, + payment_method_type: payment_method.payment_method_type, + recurring_enabled: true, + installment_payment_enabled: false, + metadata: payment_method.metadata.clone(), + created: Some(payment_method.created_at), + last_used_at: Some(payment_method.last_used_at), + client_secret: payment_method.client_secret.clone(), + card: None, + bank_transfer: None, + payment_experience: None, + }; + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + payment_method_response: Some(payment_method_response), + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, PmFetched> { + pub fn set_validate_result(self) -> NetworkTokenizationBuilder<'a, PmValidated> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { + pub fn set_card_details( + self, + card_from_locker: &'a api_models::payment_methods::Card, + optional_card_info: Option, + card_cvc: Secret, + ) -> NetworkTokenizationBuilder<'a, PmAssigned> { + let card = domain::Card { + card_number: card_from_locker.card_number.clone(), + card_exp_month: card_from_locker.card_exp_month.clone(), + card_exp_year: card_from_locker.card_exp_year.clone(), + card_cvc, + bank_code: optional_card_info + .as_ref() + .and_then(|card_info| card_info.bank_code.clone()), + nick_name: card_from_locker + .nick_name + .as_ref() + .map(|nick_name| Secret::new(nick_name.clone())), + card_holder_name: card_from_locker.name_on_card.clone(), + card_issuer: optional_card_info + .as_ref() + .and_then(|card_info| card_info.card_issuer.clone()), + card_network: optional_card_info + .as_ref() + .and_then(|card_info| card_info.card_network.clone()), + card_type: optional_card_info + .as_ref() + .and_then(|card_info| card_info.card_type.clone()), + card_issuing_country: optional_card_info + .as_ref() + .and_then(|card_info| card_info.card_issuing_country.clone()), + }; + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + card: Some(card), + customer: self.customer, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, PmAssigned> { + pub fn get_optional_card(&self) -> Option { + self.card.clone() + } + pub fn set_token_details( + self, + network_token: &'a NetworkTokenizationResponse, + ) -> NetworkTokenizationBuilder<'a, PmTokenized> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + network_token: Some(&network_token.0), + card_tokenized: true, + customer: self.customer, + card: self.card, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, PmTokenized> { + pub fn set_stored_token_response( + self, + store_token_response: &'a pm_transformers::StoreCardRespPayload, + ) -> NetworkTokenizationBuilder<'a, PmTokenStored> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + stored_token: Some(store_token_response), + customer: self.customer, + card: self.card, + network_token: self.network_token, + stored_card: self.stored_card, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, PmTokenStored> { + pub fn set_payment_method( + self, + payment_method: &'a domain::PaymentMethod, + ) -> NetworkTokenizationBuilder<'a, PmTokenUpdated> { + let payment_method_response = api::PaymentMethodResponse { + merchant_id: payment_method.merchant_id.clone(), + customer_id: Some(payment_method.customer_id.clone()), + payment_method_id: payment_method.payment_method_id.clone(), + payment_method: payment_method.payment_method, + payment_method_type: payment_method.payment_method_type, + recurring_enabled: true, + installment_payment_enabled: false, + metadata: payment_method.metadata.clone(), + created: Some(payment_method.created_at), + last_used_at: Some(payment_method.last_used_at), + client_secret: payment_method.client_secret.clone(), + card: None, + bank_transfer: None, + payment_experience: None, + }; + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + payment_method_response: Some(payment_method_response), + customer: self.customer, + card: self.card, + stored_token: self.stored_token, + network_token: self.network_token, + stored_card: self.stored_card, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl NetworkTokenizationBuilder<'_, PmTokenUpdated> { + pub fn build(self) -> api::CardNetworkTokenizeResponse { + api::CardNetworkTokenizeResponse { + payment_method_response: self.payment_method_response, + customer: self.customer.cloned(), + card_tokenized: self.card_tokenized, + error_code: self.error_code.cloned(), + error_message: self.error_message.cloned(), + // Below field is mutated by caller functions for batched API operations + req: None, + } + } +} + +// Specific executor for payment method tokenization +impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { + pub async fn fetch_payment_method( + &self, + payment_method_id: &str, + ) -> RouterResult { + self.state + .store + .find_payment_method( + &self.state.into(), + self.key_store, + payment_method_id, + self.merchant_account.storage_scheme, + ) + .await + .map_err(|err| match err.current_context() { + storage_impl::errors::StorageError::DatabaseError(err) + if matches!( + err.current_context(), + diesel_models::errors::DatabaseError::NotFound + ) => + { + report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid payment_method_id".into(), + }) + } + storage_impl::errors::StorageError::ValueNotFound(_) => { + report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid payment_method_id".to_string(), + }) + } + err => { + logger::info!("Error fetching payment_method: {:?}", err); + report!(errors::ApiErrorResponse::InternalServerError) + } + }) + } + pub async fn validate_payment_method_and_get_locker_reference( + &self, + payment_method: &domain::PaymentMethod, + ) -> RouterResult { + // Ensure payment method is card + match payment_method.payment_method { + Some(api_enums::PaymentMethod::Card) => Ok(()), + Some(_) => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method is not card".to_string() + })), + None => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method is empty".to_string() + })), + }?; + + // Ensure card is not tokenized already + when( + payment_method + .network_token_requestor_reference_id + .is_some(), + || { + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Card is already tokenized".to_string() + })) + }, + )?; + + // Ensure locker reference is present + payment_method.locker_id.clone().map_or( + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "locker_id not found for given payment_method_id".to_string() + })), + Ok, + ) + } + pub async fn update_payment_method( + &self, + store_token_response: &pm_transformers::StoreCardRespPayload, + payment_method: domain::PaymentMethod, + network_token_details: &NetworkTokenizationResponse, + card_details: &domain::Card, + ) -> RouterResult { + // Form encrypted network token data (tokenized card) + let network_token_data = &network_token_details.0; + let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(network_token_data.token_last_four.clone()), + expiry_month: Some(network_token_data.token_expiry_month.clone()), + expiry_year: Some(network_token_data.token_expiry_year.clone()), + card_isin: Some(network_token_data.token_isin.clone()), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker: true, + }); + let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) + .await + .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + // Update payment method + let payment_method_update = diesel_models::PaymentMethodUpdate::NetworkTokenDataUpdate { + network_token_requestor_reference_id: network_token_details.1.clone(), + network_token_locker_id: Some(store_token_response.card_reference.clone()), + network_token_payment_method_data: Some(enc_token_data.into()), + }; + self.state + .store + .update_payment_method( + &self.state.into(), + self.key_store, + payment_method, + payment_method_update, + self.merchant_account.storage_scheme, + ) + .await + .inspect_err(|err| logger::info!("Error updating payment method: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError) + } +} + +// Common executor for payment method tokenization +#[async_trait::async_trait] +impl<'a> NetworkTokenizationProcess<'a, domain::TokenizePaymentMethodRequest> + for CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> +{ + fn new( + state: &'a SessionState, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + data: &'a domain::TokenizePaymentMethodRequest, + customer: Option<&'a domain_request_types::CustomerDetails>, + ) -> Self { + Self { + data, + customer, + state, + merchant_account, + key_store, + } + } + + async fn fetch_bin_details( + &self, + card_number: CardNumber, + ) -> RouterResult> { + let db = &*self.state.store; + db.get_card_info(&card_number.get_card_isin()) + .await + .attach_printable("Failed to perform BIN lookup") + .change_context(errors::ApiErrorResponse::InternalServerError) + } + + async fn tokenize_card( + &self, + customer_id: &id_type::CustomerId, + card: &domain::Card, + ) -> RouterResult { + match network_tokenization::make_card_network_tokenization_request( + self.state, + card, + customer_id, + ) + .await + { + Ok(tokenization_response) => Ok(tokenization_response), + Err(err) => { + // TODO: revert this + logger::error!( + "Failed to tokenize card with the network: {:?}\nUsing dummy response", + err + ); + Ok(( + network_tokenization::CardNetworkTokenResponsePayload { + card_brand: api_enums::CardNetwork::Visa, + card_fingerprint: None, + card_reference: uuid::Uuid::new_v4().to_string(), + correlation_id: uuid::Uuid::new_v4().to_string(), + customer_id: customer_id.get_string_repr().to_string(), + par: "".to_string(), + token: card.card_number.clone(), + token_expiry_month: card.card_exp_month.clone(), + token_expiry_year: card.card_exp_year.clone(), + token_isin: card.card_number.get_card_isin(), + token_last_four: card.card_number.get_last4(), + token_status: "active".to_string(), + }, + Some(uuid::Uuid::new_v4().to_string()), + )) + } + } + } + + async fn store_network_token_in_locker( + &self, + network_token: &NetworkTokenizationResponse, + customer_id: &id_type::CustomerId, + card_holder_name: Option>, + nick_name: Option>, + ) -> RouterResult { + let network_token = &network_token.0; + let merchant_id = self.merchant_account.get_id(); + let locker_req = + pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { + merchant_id: merchant_id.clone(), + merchant_customer_id: customer_id.clone(), + card: payment_methods_api::Card { + card_number: network_token.token.clone(), + card_exp_month: network_token.token_expiry_month.clone(), + card_exp_year: network_token.token_expiry_year.clone(), + card_brand: Some(network_token.card_brand.to_string()), + card_isin: Some(network_token.token_isin.clone()), + name_on_card: card_holder_name, + nick_name: nick_name.map(|nick_name| nick_name.expose()), + }, + requestor_card_reference: None, + ttl: self.state.conf.locker.ttl_for_storage_in_secs, + }); + + let stored_resp = add_card_to_hs_locker( + self.state, + &locker_req, + customer_id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + Ok(stored_resp) + } +} diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index b260cba787e..9a6518d667c 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -1046,7 +1046,6 @@ pub async fn tokenize_card_api( let res = Box::pin(cards::tokenize_card_flow( &state, CardNetworkTokenizeRequest::foreign_from(req), - &merchant_id, &merchant_account, &key_store, )) @@ -1088,7 +1087,6 @@ pub async fn tokenize_card_batch_api( Box::pin(tokenize::tokenize_cards( &state, req, - &merchant_id, &merchant_account, &key_store, )) diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index 9a071f4f19f..cecfb2e42f7 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -29,10 +29,7 @@ mod merchant_connector_account; mod merchant_key_store { pub use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; } - -pub mod bulk_tokenization { - pub use hyperswitch_domain_models::bulk_tokenization::*; -} +pub use hyperswitch_domain_models::bulk_tokenization::*; pub mod payment_methods { pub use hyperswitch_domain_models::payment_methods::*; } From f1c8122c64fe4fc222b26952543d9eb1a384c270 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:59:08 +0000 Subject: [PATCH 12/25] chore: run formatter --- crates/router/src/core/payment_methods/tokenize.rs | 3 +-- .../core/payment_methods/tokenize/card_executor.rs | 12 ++++++------ .../tokenize/payment_method_executor.rs | 9 ++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 595e4f2fd86..6e00835981b 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -9,6 +9,7 @@ use hyperswitch_domain_models::router_request_types as domain_request_types; use masking::Secret; use router_env::logger; +use super::migration; use crate::{ core::payment_methods::{ cards::tokenize_card_flow, network_tokenization, transformers as pm_transformers, @@ -19,8 +20,6 @@ use crate::{ SessionState, }; -use super::migration; - pub mod card_executor; pub mod payment_method_executor; diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index 14ca15c0a47..dd355b02c51 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use api_models::{enums as api_enums, payment_methods as payment_methods_api}; use cards::CardNumber; use common_utils::{ @@ -15,8 +17,11 @@ use hyperswitch_domain_models::{ }; use masking::{ExposeInterface, PeekInterface, Secret, SwitchStrategy}; use router_env::logger; -use std::str::FromStr; +use super::{ + migration, CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, + NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, +}; use crate::{ core::payment_methods::{ cards::{add_card_to_hs_locker, create_encrypted_data, create_payment_method}, @@ -27,11 +32,6 @@ use crate::{ utils, SessionState, }; -use super::{ - migration, CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, - NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, -}; - // Available states for card tokenization pub struct TokenizeWithCard; pub struct CardRequestValidated; diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index bfb03199fc0..9cfa5f4f017 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -7,6 +7,10 @@ use hyperswitch_domain_models::router_request_types as domain_request_types; use masking::{ExposeInterface, Secret}; use router_env::logger; +use super::{ + CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, + NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, +}; use crate::{ core::payment_methods::{ cards::{add_card_to_hs_locker, create_encrypted_data}, @@ -17,11 +21,6 @@ use crate::{ SessionState, }; -use super::{ - CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, - NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, -}; - // Available states for payment method tokenization pub struct TokenizeWithPmId; pub struct PmValidated; From e0547f01998155127e1f3cb4a2736c15aa43204b Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 31 Jan 2025 03:53:50 +0530 Subject: [PATCH 13/25] refactor: use generic impl and fix clippy --- .../router/src/core/payment_methods/cards.rs | 6 +- .../src/core/payment_methods/tokenize.rs | 155 +++++++++++++++- .../payment_methods/tokenize/card_executor.rs | 175 ++---------------- .../tokenize/payment_method_executor.rs | 167 ++--------------- 4 files changed, 192 insertions(+), 311 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index fd304adaa96..d48ea4d75cb 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6041,7 +6041,8 @@ pub async fn tokenize_card_flow( card_req, req.customer.as_ref(), ); - let builder = tokenize::NetworkTokenizationBuilder::::new(); + let builder = + tokenize::NetworkTokenizationBuilder::::default(); execute_card_tokenization(executor, builder, card_req).await } domain::TokenizeDataRequest::PaymentMethod(ref payment_method) => { @@ -6052,7 +6053,8 @@ pub async fn tokenize_card_flow( payment_method, req.customer.as_ref(), ); - let builder = tokenize::NetworkTokenizationBuilder::::new(); + let builder = + tokenize::NetworkTokenizationBuilder::::default(); execute_payment_method_tokenization(executor, builder, payment_method).await } } diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 6e00835981b..d86268306e1 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -1,30 +1,34 @@ use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; -use api_models::payment_methods as payment_methods_api; +use api_models::{enums as api_enums, payment_methods as payment_methods_api}; use cards::CardNumber; use common_utils::{ + crypto::Encryptable, id_type, transformers::{ForeignFrom, ForeignTryFrom}, }; use hyperswitch_domain_models::router_request_types as domain_request_types; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use router_env::logger; use super::migration; use crate::{ core::payment_methods::{ - cards::tokenize_card_flow, network_tokenization, transformers as pm_transformers, + cards::{add_card_to_hs_locker, create_encrypted_data, tokenize_card_flow}, + network_tokenization, transformers as pm_transformers, }, errors::{self, RouterResult}, services, types::{api, domain}, SessionState, }; +use error_stack::{report, ResultExt}; pub mod card_executor; pub mod payment_method_executor; pub use card_executor::*; pub use payment_method_executor::*; +use rdkafka::message::ToBytes; #[derive(Debug, MultipartForm)] pub struct CardNetworkTokenizeForm { @@ -196,6 +200,17 @@ pub trait NetworkTokenizationProcess<'a, D> { data: &'a D, customer: Option<&'a domain_request_types::CustomerDetails>, ) -> Self; + async fn encrypt_card( + &self, + card_details: &domain::Card, + saved_to_locker: bool, + ) -> RouterResult>>; + async fn encrypt_network_token( + &self, + network_token_details: &NetworkTokenizationResponse, + card_details: &domain::Card, + saved_to_locker: bool, + ) -> RouterResult>>; async fn fetch_bin_details( &self, card_number: CardNumber, @@ -213,3 +228,137 @@ pub trait NetworkTokenizationProcess<'a, D> { nick_name: Option>, ) -> RouterResult; } + +// Generic implementation +#[async_trait::async_trait] +impl<'a, D> NetworkTokenizationProcess<'a, D> for CardNetworkTokenizeExecutor<'a, D> +where + D: Send + Sync + 'static, +{ + fn new( + state: &'a SessionState, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + data: &'a D, + customer: Option<&'a domain_request_types::CustomerDetails>, + ) -> Self { + Self { + data, + customer, + state, + merchant_account, + key_store, + } + } + async fn encrypt_card( + &self, + card_details: &domain::Card, + saved_to_locker: bool, + ) -> RouterResult>> { + let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(card_details.card_number.get_last4()), + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_isin: Some(card_details.card_number.get_card_isin()), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker, + }); + create_encrypted_data(&self.state.into(), self.key_store, pm_data) + .await + .inspect_err(|err| logger::info!("Error encrypting payment method data: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError) + } + async fn encrypt_network_token( + &self, + network_token_details: &NetworkTokenizationResponse, + card_details: &domain::Card, + saved_to_locker: bool, + ) -> RouterResult>> { + let network_token = &network_token_details.0; + let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { + last4_digits: Some(network_token.token_last_four.clone()), + expiry_month: Some(network_token.token_expiry_month.clone()), + expiry_year: Some(network_token.token_expiry_year.clone()), + card_isin: Some(network_token.token_isin.clone()), + nick_name: card_details.nick_name.clone(), + card_holder_name: card_details.card_holder_name.clone(), + issuer_country: card_details.card_issuing_country.clone(), + card_issuer: card_details.card_issuer.clone(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.clone(), + saved_to_locker, + }); + create_encrypted_data(&self.state.into(), self.key_store, token_data) + .await + .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError) + } + async fn fetch_bin_details( + &self, + card_number: CardNumber, + ) -> RouterResult> { + let db = &*self.state.store; + db.get_card_info(&card_number.get_card_isin()) + .await + .attach_printable("Failed to perform BIN lookup") + .change_context(errors::ApiErrorResponse::InternalServerError) + } + async fn tokenize_card( + &self, + customer_id: &id_type::CustomerId, + card: &domain::Card, + ) -> RouterResult { + network_tokenization::make_card_network_tokenization_request(self.state, card, customer_id) + .await + .map_err(|err| { + logger::error!( + "Failed to tokenize card with the network: {:?}\nUsing dummy response", + err + ); + report!(errors::ApiErrorResponse::InternalServerError) + }) + } + async fn store_network_token_in_locker( + &self, + network_token: &NetworkTokenizationResponse, + customer_id: &id_type::CustomerId, + card_holder_name: Option>, + nick_name: Option>, + ) -> RouterResult { + let network_token = &network_token.0; + let merchant_id = self.merchant_account.get_id(); + let locker_req = + pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { + merchant_id: merchant_id.clone(), + merchant_customer_id: customer_id.clone(), + card: payment_methods_api::Card { + card_number: network_token.token.clone(), + card_exp_month: network_token.token_expiry_month.clone(), + card_exp_year: network_token.token_expiry_year.clone(), + card_brand: Some(network_token.card_brand.to_string()), + card_isin: Some(network_token.token_isin.clone()), + name_on_card: card_holder_name, + nick_name: nick_name.map(|nick_name| nick_name.expose()), + }, + requestor_card_reference: None, + ttl: self.state.conf.locker.ttl_for_storage_in_secs, + }); + + let stored_resp = add_card_to_hs_locker( + self.state, + &locker_req, + customer_id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + Ok(stored_resp) + } +} diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index dd355b02c51..bb0628e87ee 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use api_models::{enums as api_enums, payment_methods as payment_methods_api}; -use cards::CardNumber; use common_utils::{ consts, ext_traits::OptionExt, @@ -11,11 +10,8 @@ use common_utils::{ types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; use error_stack::{report, ResultExt}; -use hyperswitch_domain_models::{ - router_request_types as domain_request_types, - type_encryption::{crypto_operation, CryptoOperation}, -}; -use masking::{ExposeInterface, PeekInterface, Secret, SwitchStrategy}; +use hyperswitch_domain_models::type_encryption::{crypto_operation, CryptoOperation}; +use masking::{ExposeInterface, PeekInterface, SwitchStrategy}; use router_env::logger; use super::{ @@ -24,12 +20,12 @@ use super::{ }; use crate::{ core::payment_methods::{ - cards::{add_card_to_hs_locker, create_encrypted_data, create_payment_method}, - network_tokenization, transformers as pm_transformers, + cards::{add_card_to_hs_locker, create_payment_method}, + transformers as pm_transformers, }, errors::{self, RouterResult}, types::{api, domain}, - utils, SessionState, + utils, }; // Available states for card tokenization @@ -59,6 +55,12 @@ impl TransitionTo for CardDetailsAssigned {} impl TransitionTo for CardTokenized {} impl TransitionTo for CardTokenStored {} +impl<'a> Default for NetworkTokenizationBuilder<'a, TokenizeWithCard> { + fn default() -> Self { + Self::new() + } +} + impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithCard> { pub fn new() -> Self { Self { @@ -115,7 +117,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { self, card_req: &'a domain::TokenizeCardRequest, optional_card_info: Option, - ) -> NetworkTokenizationBuilder<'_, CardDetailsAssigned> { + ) -> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { let card = domain::Card { card_number: card_req.raw_card_number.clone(), card_exp_month: card_req.card_expiry_month.clone(), @@ -519,43 +521,12 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); // Form encrypted PM data (original card) - let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(card_details.card_number.get_last4()), - expiry_month: Some(card_details.card_exp_month.clone()), - expiry_year: Some(card_details.card_exp_year.clone()), - card_isin: Some(card_details.card_number.get_card_isin()), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_pm_data = create_encrypted_data(&self.state.into(), self.key_store, pm_data) - .await - .inspect_err(|err| logger::info!("Error encrypting payment method data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; + let enc_pm_data = self.encrypt_card(card_details, true).await?; - // Form encrypted network token data (tokenized card) - let network_token_data = network_token_details.0.clone(); - let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(network_token_data.token_last_four), - expiry_month: Some(network_token_data.token_expiry_month), - expiry_year: Some(network_token_data.token_expiry_year), - card_isin: Some(network_token_data.token_isin), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) - .await - .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; + // Form encrypted network token data + let enc_token_data = self + .encrypt_network_token(network_token_details, card_details, true) + .await?; // Form PM create entry let payment_method_create = api::PaymentMethodCreate { @@ -615,115 +586,3 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { .await } } - -// Common executor for card tokenization -#[async_trait::async_trait] -impl<'a> NetworkTokenizationProcess<'a, domain::TokenizeCardRequest> - for CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> -{ - fn new( - state: &'a SessionState, - key_store: &'a domain::MerchantKeyStore, - merchant_account: &'a domain::MerchantAccount, - data: &'a domain::TokenizeCardRequest, - customer: Option<&'a domain_request_types::CustomerDetails>, - ) -> Self { - Self { - data, - customer, - state, - merchant_account, - key_store, - } - } - - async fn fetch_bin_details( - &self, - card_number: CardNumber, - ) -> RouterResult> { - let db = &*self.state.store; - db.get_card_info(&card_number.get_card_isin()) - .await - .attach_printable("Failed to perform BIN lookup") - .change_context(errors::ApiErrorResponse::InternalServerError) - } - - async fn tokenize_card( - &self, - customer_id: &id_type::CustomerId, - card: &domain::Card, - ) -> RouterResult { - match network_tokenization::make_card_network_tokenization_request( - self.state, - card, - customer_id, - ) - .await - { - Ok(tokenization_response) => Ok(tokenization_response), - Err(err) => { - // TODO: revert this - logger::error!( - "Failed to tokenize card with the network: {:?}\nUsing dummy response", - err - ); - Ok(( - network_tokenization::CardNetworkTokenResponsePayload { - card_brand: api_enums::CardNetwork::Visa, - card_fingerprint: None, - card_reference: uuid::Uuid::new_v4().to_string(), - correlation_id: uuid::Uuid::new_v4().to_string(), - customer_id: customer_id.get_string_repr().to_string(), - par: "".to_string(), - token: card.card_number.clone(), - token_expiry_month: card.card_exp_month.clone(), - token_expiry_year: card.card_exp_year.clone(), - token_isin: card.card_number.get_card_isin(), - token_last_four: card.card_number.get_last4(), - token_status: "active".to_string(), - }, - Some(uuid::Uuid::new_v4().to_string()), - )) - } - } - } - - async fn store_network_token_in_locker( - &self, - network_token: &NetworkTokenizationResponse, - customer_id: &id_type::CustomerId, - card_holder_name: Option>, - nick_name: Option>, - ) -> RouterResult { - let network_token = &network_token.0; - let merchant_id = self.merchant_account.get_id(); - let locker_req = - pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { - merchant_id: merchant_id.clone(), - merchant_customer_id: customer_id.clone(), - card: payment_methods_api::Card { - card_number: network_token.token.clone(), - card_exp_month: network_token.token_expiry_month.clone(), - card_exp_year: network_token.token_expiry_year.clone(), - card_brand: Some(network_token.card_brand.to_string()), - card_isin: Some(network_token.token_isin.clone()), - name_on_card: card_holder_name, - nick_name: nick_name.map(|nick_name| nick_name.expose()), - }, - requestor_card_reference: None, - ttl: self.state.conf.locker.ttl_for_storage_in_secs, - }); - - let stored_resp = add_card_to_hs_locker( - self.state, - &locker_req, - customer_id, - api_enums::LockerChoice::HyperswitchCardVault, - ) - .await - .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - Ok(stored_resp) - } -} diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index 9cfa5f4f017..a6992393135 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -1,24 +1,17 @@ -use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; -use api_models::{enums as api_enums, payment_methods as payment_methods_api}; -use cards::CardNumber; -use common_utils::{fp_utils::when, id_type}; +use api_models::enums as api_enums; +use common_utils::fp_utils::when; use error_stack::{report, ResultExt}; -use hyperswitch_domain_models::router_request_types as domain_request_types; -use masking::{ExposeInterface, Secret}; +use masking::Secret; use router_env::logger; use super::{ CardNetworkTokenizeExecutor, NetworkTokenizationBuilder, NetworkTokenizationProcess, - NetworkTokenizationResponse, State, StoreLockerResponse, TransitionTo, + NetworkTokenizationResponse, State, TransitionTo, }; use crate::{ - core::payment_methods::{ - cards::{add_card_to_hs_locker, create_encrypted_data}, - network_tokenization, transformers as pm_transformers, - }, + core::payment_methods::transformers as pm_transformers, errors::{self, RouterResult}, types::{api, domain}, - SessionState, }; // Available states for payment method tokenization @@ -46,6 +39,12 @@ impl TransitionTo for PmAssigned {} impl TransitionTo for PmTokenized {} impl TransitionTo for PmTokenStored {} +impl<'a> Default for NetworkTokenizationBuilder<'a, TokenizeWithPmId> { + fn default() -> Self { + Self::new() + } +} + impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithPmId> { pub fn new() -> Self { Self { @@ -319,12 +318,11 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { )?; // Ensure locker reference is present - payment_method.locker_id.clone().map_or( - Err(report!(errors::ApiErrorResponse::InvalidRequestData { + payment_method.locker_id.clone().ok_or(report!( + errors::ApiErrorResponse::InvalidRequestData { message: "locker_id not found for given payment_method_id".to_string() - })), - Ok, - ) + } + )) } pub async fn update_payment_method( &self, @@ -333,25 +331,10 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { network_token_details: &NetworkTokenizationResponse, card_details: &domain::Card, ) -> RouterResult { - // Form encrypted network token data (tokenized card) - let network_token_data = &network_token_details.0; - let token_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { - last4_digits: Some(network_token_data.token_last_four.clone()), - expiry_month: Some(network_token_data.token_expiry_month.clone()), - expiry_year: Some(network_token_data.token_expiry_year.clone()), - card_isin: Some(network_token_data.token_isin.clone()), - nick_name: card_details.nick_name.clone(), - card_holder_name: card_details.card_holder_name.clone(), - issuer_country: card_details.card_issuing_country.clone(), - card_issuer: card_details.card_issuer.clone(), - card_network: card_details.card_network.clone(), - card_type: card_details.card_type.clone(), - saved_to_locker: true, - }); - let enc_token_data = create_encrypted_data(&self.state.into(), self.key_store, token_data) - .await - .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; + // Form encrypted network token data + let enc_token_data = self + .encrypt_network_token(network_token_details, card_details, true) + .await?; // Update payment method let payment_method_update = diesel_models::PaymentMethodUpdate::NetworkTokenDataUpdate { @@ -373,115 +356,3 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { .change_context(errors::ApiErrorResponse::InternalServerError) } } - -// Common executor for payment method tokenization -#[async_trait::async_trait] -impl<'a> NetworkTokenizationProcess<'a, domain::TokenizePaymentMethodRequest> - for CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> -{ - fn new( - state: &'a SessionState, - key_store: &'a domain::MerchantKeyStore, - merchant_account: &'a domain::MerchantAccount, - data: &'a domain::TokenizePaymentMethodRequest, - customer: Option<&'a domain_request_types::CustomerDetails>, - ) -> Self { - Self { - data, - customer, - state, - merchant_account, - key_store, - } - } - - async fn fetch_bin_details( - &self, - card_number: CardNumber, - ) -> RouterResult> { - let db = &*self.state.store; - db.get_card_info(&card_number.get_card_isin()) - .await - .attach_printable("Failed to perform BIN lookup") - .change_context(errors::ApiErrorResponse::InternalServerError) - } - - async fn tokenize_card( - &self, - customer_id: &id_type::CustomerId, - card: &domain::Card, - ) -> RouterResult { - match network_tokenization::make_card_network_tokenization_request( - self.state, - card, - customer_id, - ) - .await - { - Ok(tokenization_response) => Ok(tokenization_response), - Err(err) => { - // TODO: revert this - logger::error!( - "Failed to tokenize card with the network: {:?}\nUsing dummy response", - err - ); - Ok(( - network_tokenization::CardNetworkTokenResponsePayload { - card_brand: api_enums::CardNetwork::Visa, - card_fingerprint: None, - card_reference: uuid::Uuid::new_v4().to_string(), - correlation_id: uuid::Uuid::new_v4().to_string(), - customer_id: customer_id.get_string_repr().to_string(), - par: "".to_string(), - token: card.card_number.clone(), - token_expiry_month: card.card_exp_month.clone(), - token_expiry_year: card.card_exp_year.clone(), - token_isin: card.card_number.get_card_isin(), - token_last_four: card.card_number.get_last4(), - token_status: "active".to_string(), - }, - Some(uuid::Uuid::new_v4().to_string()), - )) - } - } - } - - async fn store_network_token_in_locker( - &self, - network_token: &NetworkTokenizationResponse, - customer_id: &id_type::CustomerId, - card_holder_name: Option>, - nick_name: Option>, - ) -> RouterResult { - let network_token = &network_token.0; - let merchant_id = self.merchant_account.get_id(); - let locker_req = - pm_transformers::StoreLockerReq::LockerCard(pm_transformers::StoreCardReq { - merchant_id: merchant_id.clone(), - merchant_customer_id: customer_id.clone(), - card: payment_methods_api::Card { - card_number: network_token.token.clone(), - card_exp_month: network_token.token_expiry_month.clone(), - card_exp_year: network_token.token_expiry_year.clone(), - card_brand: Some(network_token.card_brand.to_string()), - card_isin: Some(network_token.token_isin.clone()), - name_on_card: card_holder_name, - nick_name: nick_name.map(|nick_name| nick_name.expose()), - }, - requestor_card_reference: None, - ttl: self.state.conf.locker.ttl_for_storage_in_secs, - }); - - let stored_resp = add_card_to_hs_locker( - self.state, - &locker_req, - customer_id, - api_enums::LockerChoice::HyperswitchCardVault, - ) - .await - .inspect_err(|err| logger::info!("Error adding card in locker: {:?}", err)) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - Ok(stored_resp) - } -} From 1deeb074eddcb4648db3c9c9fd0bf2e4f8dc9242 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:55:47 +0000 Subject: [PATCH 14/25] chore: run formatter --- crates/router/src/core/payment_methods/tokenize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index d86268306e1..6aba5720847 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -6,6 +6,7 @@ use common_utils::{ id_type, transformers::{ForeignFrom, ForeignTryFrom}, }; +use error_stack::{report, ResultExt}; use hyperswitch_domain_models::router_request_types as domain_request_types; use masking::{ExposeInterface, Secret}; use router_env::logger; @@ -21,7 +22,6 @@ use crate::{ types::{api, domain}, SessionState, }; -use error_stack::{report, ResultExt}; pub mod card_executor; pub mod payment_method_executor; From 66f16e7922765c3d754b1fdd82cc0cee03c0640b Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 31 Jan 2025 16:21:47 +0530 Subject: [PATCH 15/25] refactor: make customer a required field --- crates/api_models/src/payment_methods.rs | 2 +- .../src/bulk_tokenization.rs | 6 +- .../router/src/core/payment_methods/cards.rs | 20 +++--- .../src/core/payment_methods/tokenize.rs | 11 ++-- .../payment_methods/tokenize/card_executor.rs | 48 ++++++-------- .../tokenize/payment_method_executor.rs | 63 ++++++++++++++++--- 6 files changed, 93 insertions(+), 57 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 10ce2ff2d19..b3a12797740 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2409,7 +2409,7 @@ pub struct CardNetworkTokenizeRequest { pub card_type: Option, /// Customer details - pub customer: Option, + pub customer: payments::CustomerDetails, /// The billing details of the payment method pub billing: Option, diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 21fb61e1a87..4a595121bd3 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -18,7 +18,7 @@ use crate::{ pub struct CardNetworkTokenizeRequest { pub data: TokenizeDataRequest, pub card_type: Option, - pub customer: Option, + pub customer: CustomerDetails, pub billing: Option
, pub metadata: Option, pub payment_method_issuer: Option, @@ -138,7 +138,7 @@ impl ForeignTryFrom for payment_methods_api::CardNetw type Error = error_stack::Report; fn foreign_try_from(record: CardNetworkTokenizeRecord) -> Result { let billing = Some(payments_api::Address::foreign_from(&record)); - let customer = Some(payments_api::CustomerDetails::foreign_from(&record)); + let customer = payments_api::CustomerDetails::foreign_from(&record); let merchant_id = record.merchant_id.get_required_value("merchant_id")?; match ( @@ -234,7 +234,7 @@ impl ForeignFrom for CardNetwor Self { data: TokenizeDataRequest::foreign_from(req.data), card_type: req.card_type, - customer: req.customer.map(ForeignFrom::foreign_from), + customer: CustomerDetails::foreign_from(req.customer), billing: req.billing.map(ForeignFrom::foreign_from), metadata: req.metadata, payment_method_issuer: req.payment_method_issuer, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index d48ea4d75cb..a772e0e0a96 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6039,7 +6039,7 @@ pub async fn tokenize_card_flow( key_store, merchant_account, card_req, - req.customer.as_ref(), + &req.customer, ); let builder = tokenize::NetworkTokenizationBuilder::::default(); @@ -6051,7 +6051,7 @@ pub async fn tokenize_card_flow( key_store, merchant_account, payment_method, - req.customer.as_ref(), + &req.customer, ); let builder = tokenize::NetworkTokenizationBuilder::::default(); @@ -6132,16 +6132,16 @@ pub async fn execute_payment_method_tokenization( .await?; let builder = builder.set_payment_method(&payment_method); - // Validate payment method - let locker_id = executor - .validate_payment_method_and_get_locker_reference(&payment_method) + // Validate payment method and customer + let (locker_id, customer) = executor + .validate_request_and_locker_reference_and_customer(&payment_method) .await?; - let builder = builder.set_validate_result(); + let builder = builder.set_validate_result(&customer); // Fetch card from locker let card_details = get_card_from_locker( executor.state, - &payment_method.customer_id, + &customer.id, executor.merchant_account.get_id(), &locker_id, ) @@ -6155,16 +6155,14 @@ pub async fn execute_payment_method_tokenization( // Tokenize card let domain_card = builder.get_optional_card().get_required_value("card")?; - let network_token_details = executor - .tokenize_card(&payment_method.customer_id, &domain_card) - .await?; + let network_token_details = executor.tokenize_card(&customer.id, &domain_card).await?; let builder = builder.set_token_details(&network_token_details); // Store token in locker let store_token_resp = executor .store_network_token_in_locker( &network_token_details, - &payment_method.customer_id, + &customer.id, card_details.name_on_card.clone(), card_details.nick_name.clone().map(Secret::new), ) diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 6aba5720847..639a54f779a 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -106,6 +106,7 @@ pub async fn tokenize_cards( let responses = futures::stream::iter(records.into_iter()) .map(|record| async move { let tokenize_request = record.data.clone(); + let customer = record.customer.clone(); Box::pin(tokenize_card_flow( state, domain::CardNetworkTokenizeRequest::foreign_from(record), @@ -121,7 +122,7 @@ pub async fn tokenize_cards( error_message: Some(err.error_message()), card_tokenized: false, payment_method_response: None, - customer: None, + customer: Some(customer), } }) }) @@ -183,7 +184,7 @@ pub struct CardNetworkTokenizeExecutor<'a, D> { pub merchant_account: &'a domain::MerchantAccount, key_store: &'a domain::MerchantKeyStore, data: &'a D, - customer: Option<&'a domain_request_types::CustomerDetails>, + customer: &'a domain_request_types::CustomerDetails, } // State machine @@ -198,7 +199,7 @@ pub trait NetworkTokenizationProcess<'a, D> { key_store: &'a domain::MerchantKeyStore, merchant_account: &'a domain::MerchantAccount, data: &'a D, - customer: Option<&'a domain_request_types::CustomerDetails>, + customer: &'a domain_request_types::CustomerDetails, ) -> Self; async fn encrypt_card( &self, @@ -240,7 +241,7 @@ where key_store: &'a domain::MerchantKeyStore, merchant_account: &'a domain::MerchantAccount, data: &'a D, - customer: Option<&'a domain_request_types::CustomerDetails>, + customer: &'a domain_request_types::CustomerDetails, ) -> Self { Self { data, @@ -317,7 +318,7 @@ where .await .map_err(|err| { logger::error!( - "Failed to tokenize card with the network: {:?}\nUsing dummy response", + "Failed to tokenize card with the network: {:?}", err ); report!(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index bb0628e87ee..1edd901d9fe 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -55,7 +55,7 @@ impl TransitionTo for CardDetailsAssigned {} impl TransitionTo for CardTokenized {} impl TransitionTo for CardTokenStored {} -impl<'a> Default for NetworkTokenizationBuilder<'a, TokenizeWithCard> { +impl Default for NetworkTokenizationBuilder<'_, TokenizeWithCard> { fn default() -> Self { Self::new() } @@ -297,7 +297,7 @@ impl NetworkTokenizationBuilder<'_, PaymentMethodCreated> { } // Specific executor for card tokenization -impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { +impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { pub async fn validate_request_and_fetch_optional_customer( &self, ) -> RouterResult> { @@ -305,13 +305,8 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { migration::validate_card_expiry(&self.data.card_expiry_month, &self.data.card_expiry_year)?; // Validate customer ID - let customer_req = self + let customer_id = self .customer - .get_required_value("customer") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer", - })?; - let customer_id = customer_req .customer_id .as_ref() .get_required_value("customer_id") @@ -334,9 +329,9 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { .change_context(errors::ApiErrorResponse::InternalServerError) .map_or( // Validate if customer creation is feasible - if customer_req.name.is_some() - || customer_req.email.is_some() - || customer_req.phone.is_some() + if self.customer.name.is_some() + || self.customer.email.is_some() + || self.customer.phone.is_some() { Ok(None) } else { @@ -359,13 +354,8 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { pub async fn create_customer(&self) -> RouterResult { let db = &*self.state.store; - let customer_details = self + let customer_id = self .customer - .get_required_value("customer") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer", - })?; - let customer_id = customer_details .customer_id .as_ref() .get_required_value("customer_id") @@ -379,12 +369,13 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { type_name!(domain::Customer), CryptoOperation::BatchEncrypt(domain::FromRequestEncryptableCustomer::to_encryptable( domain::FromRequestEncryptableCustomer { - name: customer_details.name.clone(), - email: customer_details + name: self.customer.name.clone(), + email: self + .customer .email .clone() .map(|email| email.expose().switch_strategy()), - phone: customer_details.phone.clone(), + phone: self.customer.phone.clone(), }, )), Identifier::Merchant(self.merchant_account.get_id().clone()), @@ -401,8 +392,9 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to form EncryptableCustomer")?; + let new_customer_id = generate_customer_id_of_default_length(); let domain_customer = domain::Customer { - customer_id: generate_customer_id_of_default_length(), + customer_id: new_customer_id.clone(), merchant_id: self.merchant_account.get_id().clone(), name: encryptable_customer.name, email: encryptable_customer.email.map(|email| { @@ -413,7 +405,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { }), phone: encryptable_customer.phone, description: None, - phone_country_code: customer_details.phone_country_code.to_owned(), + phone_country_code: self.customer.phone_country_code.to_owned(), metadata: None, connector_customer: None, created_at: common_utils::date_time::now(), @@ -425,7 +417,7 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { }; db.insert_customer( - domain_customer.clone(), + domain_customer, key_manager_state, self.key_store, self.merchant_account.storage_scheme, @@ -442,11 +434,11 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizeCardRequest> { })?; Ok(api::CustomerDetails { - id: domain_customer.customer_id, - name: customer_details.name.clone(), - email: customer_details.email.clone(), - phone: customer_details.phone.clone(), - phone_country_code: customer_details.phone_country_code.clone(), + id: new_customer_id, + name: self.customer.name.clone(), + email: self.customer.email.clone(), + phone: self.customer.phone.clone(), + phone_country_code: self.customer.phone_country_code.clone(), }) } diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index a6992393135..c4a29a9f1e5 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -1,5 +1,7 @@ use api_models::enums as api_enums; -use common_utils::fp_utils::when; +use common_utils::{ + ext_traits::OptionExt, fp_utils::when, pii::Email, types::keymanager::KeyManagerState, +}; use error_stack::{report, ResultExt}; use masking::Secret; use router_env::logger; @@ -39,7 +41,7 @@ impl TransitionTo for PmAssigned {} impl TransitionTo for PmTokenized {} impl TransitionTo for PmTokenStored {} -impl<'a> Default for NetworkTokenizationBuilder<'a, TokenizeWithPmId> { +impl Default for NetworkTokenizationBuilder<'_, TokenizeWithPmId> { fn default() -> Self { Self::new() } @@ -96,10 +98,13 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithPmId> { } impl<'a> NetworkTokenizationBuilder<'a, PmFetched> { - pub fn set_validate_result(self) -> NetworkTokenizationBuilder<'a, PmValidated> { + pub fn set_validate_result( + self, + customer: &'a api::CustomerDetails, + ) -> NetworkTokenizationBuilder<'a, PmValidated> { NetworkTokenizationBuilder { state: std::marker::PhantomData, - customer: self.customer, + customer: Some(customer), card: self.card, network_token: self.network_token, stored_card: self.stored_card, @@ -254,7 +259,7 @@ impl NetworkTokenizationBuilder<'_, PmTokenUpdated> { } // Specific executor for payment method tokenization -impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { +impl CardNetworkTokenizeExecutor<'_, domain::TokenizePaymentMethodRequest> { pub async fn fetch_payment_method( &self, payment_method_id: &str, @@ -290,10 +295,25 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { } }) } - pub async fn validate_payment_method_and_get_locker_reference( + pub async fn validate_request_and_locker_reference_and_customer( &self, payment_method: &domain::PaymentMethod, - ) -> RouterResult { + ) -> RouterResult<(String, api::CustomerDetails)> { + // Ensure customer ID matches + let customer_id_in_req = self + .customer + .customer_id + .clone() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer", + })?; + when(payment_method.customer_id != customer_id_in_req, || { + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method does not belong to the customer".to_string() + })) + })?; + // Ensure payment method is card match payment_method.payment_method { Some(api_enums::PaymentMethod::Card) => Ok(()), @@ -318,11 +338,36 @@ impl<'a> CardNetworkTokenizeExecutor<'a, domain::TokenizePaymentMethodRequest> { )?; // Ensure locker reference is present - payment_method.locker_id.clone().ok_or(report!( + let locker_id = payment_method.locker_id.clone().ok_or(report!( errors::ApiErrorResponse::InvalidRequestData { message: "locker_id not found for given payment_method_id".to_string() } - )) + ))?; + + // Fetch customer + let db = &*self.state.store; + let key_manager_state: &KeyManagerState = &self.state.into(); + let customer = db + .find_customer_by_customer_id_merchant_id( + key_manager_state, + &payment_method.customer_id, + self.merchant_account.get_id(), + self.key_store, + self.merchant_account.storage_scheme, + ) + .await + .inspect_err(|err| logger::info!("Error fetching customer: {:?}", err)) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let customer_details = api::CustomerDetails { + id: customer.customer_id.clone(), + name: customer.name.clone().map(|name| name.into_inner()), + email: customer.email.clone().map(Email::from), + phone: customer.phone.clone().map(|phone| phone.into_inner()), + phone_country_code: customer.phone_country_code.clone(), + }; + + Ok((locker_id, customer_details)) } pub async fn update_payment_method( &self, From 8b86c3980450a0884fde6a6bfbf1aaa53f1b839c Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:12:46 +0000 Subject: [PATCH 16/25] chore: run formatter --- crates/router/src/core/payment_methods/tokenize.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 639a54f779a..f2abbca0ce5 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -317,10 +317,7 @@ where network_tokenization::make_card_network_tokenization_request(self.state, card, customer_id) .await .map_err(|err| { - logger::error!( - "Failed to tokenize card with the network: {:?}", - err - ); + logger::error!("Failed to tokenize card with the network: {:?}", err); report!(errors::ApiErrorResponse::InternalServerError) }) } From 364ec3e1becd9eaa598a81783203e4f13db58685 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 3 Feb 2025 21:13:51 +0530 Subject: [PATCH 17/25] refactor: resolve comments and add more validations --- crates/api_models/src/payment_methods.rs | 9 +- .../src/bulk_tokenization.rs | 63 ++++++------ .../src/payment_method_data.rs | 17 ++++ .../router/src/core/payment_methods/cards.rs | 45 ++++++--- .../payment_methods/network_tokenization.rs | 7 +- .../src/core/payment_methods/tokenize.rs | 96 +++++++++++++++---- .../payment_methods/tokenize/card_executor.rs | 76 ++++++++------- .../tokenize/payment_method_executor.rs | 23 +++-- .../router/src/core/payments/tokenization.rs | 4 +- crates/router/src/types/domain/payments.rs | 12 +-- 10 files changed, 232 insertions(+), 120 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b3a12797740..b2950f52458 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2405,9 +2405,6 @@ pub struct CardNetworkTokenizeRequest { #[serde(flatten)] pub data: TokenizeDataRequest, - /// Card type - pub card_type: Option, - /// Customer details pub customer: payments::CustomerDetails, @@ -2446,8 +2443,8 @@ pub struct TokenizeCardRequest { pub card_expiry_year: masking::Secret, /// The CVC number for the card - #[schema(value_type = String, example = "242")] - pub card_cvc: masking::Secret, + #[schema(value_type = Option, example = "242")] + pub card_cvc: Option>, /// Card Holder Name #[schema(value_type = Option, example = "John Doe")] @@ -2477,7 +2474,7 @@ pub struct TokenizePaymentMethodRequest { pub payment_method_id: String, /// The CVC number for the card - pub card_cvc: masking::Secret, + pub card_cvc: Option>, } #[derive(Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 4a595121bd3..3c38ca6124d 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -17,7 +17,6 @@ use crate::{ #[derive(Debug)] pub struct CardNetworkTokenizeRequest { pub data: TokenizeDataRequest, - pub card_type: Option, pub customer: CustomerDetails, pub billing: Option
, pub metadata: Option, @@ -35,7 +34,7 @@ pub struct TokenizeCardRequest { pub raw_card_number: CardNumber, pub card_expiry_month: masking::Secret, pub card_expiry_year: masking::Secret, - pub card_cvc: masking::Secret, + pub card_cvc: Option>, pub card_holder_name: Option>, pub nick_name: Option>, pub card_issuing_country: Option, @@ -47,7 +46,7 @@ pub struct TokenizeCardRequest { #[derive(Clone, Debug)] pub struct TokenizePaymentMethodRequest { pub payment_method_id: String, - pub card_cvc: masking::Secret, + pub card_cvc: Option>, } #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] @@ -145,48 +144,41 @@ impl ForeignTryFrom for payment_methods_api::CardNetw record.raw_card_number, record.card_expiry_month, record.card_expiry_year, - record.card_cvc, record.payment_method_id, ) { - ( - Some(raw_card_number), - Some(card_expiry_month), - Some(card_expiry_year), - Some(card_cvc), - _, - ) => Ok(Self { - merchant_id, - data: payment_methods_api::TokenizeDataRequest::Card( - payment_methods_api::TokenizeCardRequest { - raw_card_number, - card_expiry_month, - card_expiry_year, - card_cvc, - card_holder_name: record.card_holder_name, - nick_name: record.nick_name, - card_issuing_country: record.card_issuing_country, - card_network: record.card_network, - card_issuer: record.card_issuer, - card_type: record.card_type.clone(), - }, - ), - billing, - customer, - card_type: record.card_type, - metadata: None, - payment_method_issuer: record.payment_method_issuer, - }), - (_, _, _, Some(card_cvc), Some(payment_method_id)) => Ok(Self { + (Some(raw_card_number), Some(card_expiry_month), Some(card_expiry_year), _) => { + Ok(Self { + merchant_id, + data: payment_methods_api::TokenizeDataRequest::Card( + payment_methods_api::TokenizeCardRequest { + raw_card_number, + card_expiry_month, + card_expiry_year, + card_cvc: record.card_cvc, + card_holder_name: record.card_holder_name, + nick_name: record.nick_name, + card_issuing_country: record.card_issuing_country, + card_network: record.card_network, + card_issuer: record.card_issuer, + card_type: record.card_type.clone(), + }, + ), + billing, + customer, + metadata: None, + payment_method_issuer: record.payment_method_issuer, + }) + } + (_, _, _, Some(payment_method_id)) => Ok(Self { merchant_id, data: payment_methods_api::TokenizeDataRequest::PaymentMethod( payment_methods_api::TokenizePaymentMethodRequest { payment_method_id, - card_cvc, + card_cvc: record.card_cvc, }, ), billing, customer, - card_type: record.card_type, metadata: None, payment_method_issuer: record.payment_method_issuer, }), @@ -233,7 +225,6 @@ impl ForeignFrom for CardNetwor fn foreign_from(req: payment_methods_api::CardNetworkTokenizeRequest) -> Self { Self { data: TokenizeDataRequest::foreign_from(req.data), - card_type: req.card_type, customer: CustomerDetails::foreign_from(req.customer), billing: req.billing.map(ForeignFrom::foreign_from), metadata: req.metadata, diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 8c19a20ef32..af1be0c504c 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -126,6 +126,23 @@ impl CardDetailsForNetworkTransactionId { } } +impl From<&Card> for CardDetailsForNetworkTransactionId { + fn from(item: &Card) -> Self { + Self { + card_number: item.card_number.to_owned(), + card_exp_month: item.card_exp_month.to_owned(), + card_exp_year: item.card_exp_year.to_owned(), + card_issuer: item.card_issuer.to_owned(), + card_network: item.card_network.to_owned(), + card_type: item.card_type.to_owned(), + card_issuing_country: item.card_issuing_country.to_owned(), + bank_code: item.bank_code.to_owned(), + nick_name: item.nick_name.to_owned(), + card_holder_name: item.card_holder_name.to_owned(), + } + } +} + impl From for CardDetailsForNetworkTransactionId { fn from(card_details_for_nti: mandates::NetworkTransactionIdAndCardDetails) -> Self { Self { diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 7e6d8f17c59..006f007d9a5 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6075,6 +6075,18 @@ pub async fn execute_card_tokenization( .await?; let builder = builder.set_validate_result(); + // Perform BIN lookup and validate card network + let optional_card_info = executor + .fetch_bin_details_and_validate_card_network( + req.raw_card_number.clone(), + req.card_issuer.as_ref(), + req.card_network.as_ref(), + req.card_type.as_ref(), + req.card_issuing_country.as_ref(), + ) + .await?; + let builder = builder.set_card_details(req, optional_card_info); + // Create customer if not present let customer = match optional_customer { Some(customer) => customer, @@ -6082,18 +6094,14 @@ pub async fn execute_card_tokenization( }; let builder = builder.set_customer(&customer); - // Perform BIN lookup - let optional_card_info = executor - .fetch_bin_details(req.raw_card_number.clone()) - .await?; - let builder = builder.set_card_details(req, optional_card_info); - // Tokenize card - let domain_card = builder - .get_optional_card() - .get_required_value("value") + let (optional_card, optional_cvc) = builder.get_optional_card_and_cvc(); + let domain_card = optional_card + .get_required_value("card") .change_context(errors::ApiErrorResponse::InternalServerError)?; - let network_token_details = executor.tokenize_card(&customer.id, &domain_card).await?; + let network_token_details = executor + .tokenize_card(&customer.id, &domain_card, optional_cvc) + .await?; let builder = builder.set_token_details(&network_token_details); // Store card and token in locker @@ -6147,15 +6155,24 @@ pub async fn execute_payment_method_tokenization( ) .await?; - // Perform BIN lookup + // Perform BIN lookup and validate card network let optional_card_info = executor - .fetch_bin_details(card_details.card_number.clone()) + .fetch_bin_details_and_validate_card_network( + card_details.card_number.clone(), + None, + None, + None, + None, + ) .await?; let builder = builder.set_card_details(&card_details, optional_card_info, req.card_cvc.clone()); // Tokenize card - let domain_card = builder.get_optional_card().get_required_value("card")?; - let network_token_details = executor.tokenize_card(&customer.id, &domain_card).await?; + let (optional_card, optional_cvc) = builder.get_optional_card_and_cvc(); + let domain_card = optional_card.get_required_value("card")?; + let network_token_details = executor + .tokenize_card(&customer.id, &domain_card, optional_cvc) + .await?; let builder = builder.set_token_details(&network_token_details); // Store token in locker diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index bb730caad2b..fa1775091ae 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -30,7 +30,7 @@ pub struct CardData { card_number: CardNumber, exp_month: Secret, exp_year: Secret, - card_security_code: Secret, + card_security_code: Option>, } #[derive(Debug, Serialize)] @@ -264,7 +264,8 @@ pub async fn mk_tokenization_req( pub async fn make_card_network_tokenization_request( state: &routes::SessionState, - card: &domain::Card, + card: &domain::CardDetailsForNetworkTransactionId, + optional_cvc: Option>, customer_id: &id_type::CustomerId, ) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> { @@ -272,7 +273,7 @@ pub async fn make_card_network_tokenization_request( card_number: card.card_number.clone(), exp_month: card.card_exp_month.clone(), exp_year: card.card_exp_year.clone(), - card_security_code: card.card_cvc.clone(), + card_security_code: optional_cvc, }; let payload = card_data diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index f2abbca0ce5..9b7fa8f443f 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -154,7 +154,10 @@ pub struct NetworkTokenizationBuilder<'a, S: State> { pub customer: Option<&'a api::CustomerDetails>, /// Card details - pub card: Option, + pub card: Option, + + /// CVC + pub card_cvc: Option>, /// Network token details pub network_token: Option<&'a network_tokenization::CardNetworkTokenResponsePayload>, @@ -203,23 +206,32 @@ pub trait NetworkTokenizationProcess<'a, D> { ) -> Self; async fn encrypt_card( &self, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, saved_to_locker: bool, ) -> RouterResult>>; async fn encrypt_network_token( &self, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, saved_to_locker: bool, ) -> RouterResult>>; - async fn fetch_bin_details( + async fn fetch_bin_details_and_validate_card_network( &self, card_number: CardNumber, + card_issuer: Option<&String>, + card_network: Option<&api_enums::CardNetwork>, + card_type: Option<&api_models::payment_methods::CardType>, + card_issuing_country: Option<&String>, ) -> RouterResult>; + fn validate_card_network( + &self, + optional_card_network: Option<&api_enums::CardNetwork>, + ) -> RouterResult<()>; async fn tokenize_card( &self, customer_id: &id_type::CustomerId, - card: &domain::Card, + card: &domain::CardDetailsForNetworkTransactionId, + optional_cvc: Option>, ) -> RouterResult; async fn store_network_token_in_locker( &self, @@ -253,7 +265,7 @@ where } async fn encrypt_card( &self, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, saved_to_locker: bool, ) -> RouterResult>> { let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { @@ -277,7 +289,7 @@ where async fn encrypt_network_token( &self, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, saved_to_locker: bool, ) -> RouterResult>> { let network_token = &network_token_details.0; @@ -299,27 +311,79 @@ where .inspect_err(|err| logger::info!("Error encrypting network token data: {:?}", err)) .change_context(errors::ApiErrorResponse::InternalServerError) } - async fn fetch_bin_details( + async fn fetch_bin_details_and_validate_card_network( &self, card_number: CardNumber, + card_issuer: Option<&String>, + card_network: Option<&api_enums::CardNetwork>, + card_type: Option<&api_models::payment_methods::CardType>, + card_issuing_country: Option<&String>, ) -> RouterResult> { let db = &*self.state.store; + if card_issuer.is_some() + && card_network.is_some() + && card_type.is_some() + && card_issuing_country.is_some() + { + self.validate_card_network(card_network)?; + return Ok(None); + } + db.get_card_info(&card_number.get_card_isin()) .await .attach_printable("Failed to perform BIN lookup") - .change_context(errors::ApiErrorResponse::InternalServerError) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .map(|card_info| { + self.validate_card_network(card_info.card_network.as_ref())?; + Ok(card_info) + }) + .transpose() } async fn tokenize_card( &self, customer_id: &id_type::CustomerId, - card: &domain::Card, + card: &domain::CardDetailsForNetworkTransactionId, + optional_cvc: Option>, ) -> RouterResult { - network_tokenization::make_card_network_tokenization_request(self.state, card, customer_id) - .await - .map_err(|err| { - logger::error!("Failed to tokenize card with the network: {:?}", err); - report!(errors::ApiErrorResponse::InternalServerError) - }) + network_tokenization::make_card_network_tokenization_request( + self.state, + card, + optional_cvc, + customer_id, + ) + .await + .map_err(|err| { + logger::error!("Failed to tokenize card with the network: {:?}", err); + report!(errors::ApiErrorResponse::InternalServerError) + }) + } + fn validate_card_network( + &self, + optional_card_network: Option<&api_enums::CardNetwork>, + ) -> RouterResult<()> { + optional_card_network.map_or( + Err(report!(errors::ApiErrorResponse::NotSupported { + message: "Unknown card network".to_string() + })), + |card_network| { + if self + .state + .conf + .network_tokenization_supported_card_networks + .card_networks + .contains(card_network) + { + Ok(()) + } else { + Err(report!(errors::ApiErrorResponse::NotSupported { + message: format!( + "Network tokenization for {} is not supported", + card_network + ) + })) + } + }, + ) } async fn store_network_token_in_locker( &self, diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index 1edd901d9fe..5b40204f143 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -31,16 +31,16 @@ use crate::{ // Available states for card tokenization pub struct TokenizeWithCard; pub struct CardRequestValidated; -pub struct CustomerAssigned; pub struct CardDetailsAssigned; +pub struct CustomerAssigned; pub struct CardTokenized; pub struct CardStored; pub struct CardTokenStored; pub struct PaymentMethodCreated; impl State for TokenizeWithCard {} -impl State for CardRequestValidated {} impl State for CustomerAssigned {} +impl State for CardRequestValidated {} impl State for CardDetailsAssigned {} impl State for CardTokenized {} impl State for CardStored {} @@ -49,9 +49,9 @@ impl State for PaymentMethodCreated {} // State transitions for card tokenization impl TransitionTo for TokenizeWithCard {} -impl TransitionTo for CardRequestValidated {} -impl TransitionTo for CustomerAssigned {} -impl TransitionTo for CardDetailsAssigned {} +impl TransitionTo for CardRequestValidated {} +impl TransitionTo for CardDetailsAssigned {} +impl TransitionTo for CustomerAssigned {} impl TransitionTo for CardTokenized {} impl TransitionTo for CardTokenStored {} @@ -67,6 +67,7 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithCard> { state: std::marker::PhantomData, customer: None, card: None, + card_cvc: None, network_token: None, stored_card: None, stored_token: None, @@ -81,6 +82,7 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithCard> { state: std::marker::PhantomData, customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, stored_token: self.stored_token, @@ -93,36 +95,15 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithCard> { } impl<'a> NetworkTokenizationBuilder<'a, CardRequestValidated> { - pub fn set_customer( - self, - customer: &'a api::CustomerDetails, - ) -> NetworkTokenizationBuilder<'a, CustomerAssigned> { - NetworkTokenizationBuilder { - state: std::marker::PhantomData, - customer: Some(customer), - card: self.card, - network_token: self.network_token, - stored_card: self.stored_card, - stored_token: self.stored_token, - payment_method_response: self.payment_method_response, - card_tokenized: self.card_tokenized, - error_code: self.error_code, - error_message: self.error_message, - } - } -} - -impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { pub fn set_card_details( self, card_req: &'a domain::TokenizeCardRequest, optional_card_info: Option, ) -> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { - let card = domain::Card { + let card = domain::CardDetailsForNetworkTransactionId { card_number: card_req.raw_card_number.clone(), card_exp_month: card_req.card_expiry_month.clone(), card_exp_year: card_req.card_expiry_year.clone(), - card_cvc: card_req.card_cvc.clone(), bank_code: optional_card_info .as_ref() .and_then(|card_info| card_info.bank_code.clone()), @@ -154,6 +135,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { NetworkTokenizationBuilder { state: std::marker::PhantomData, card: Some(card), + card_cvc: card_req.card_cvc.clone(), customer: self.customer, network_token: self.network_token, stored_card: self.stored_card, @@ -167,8 +149,34 @@ impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { } impl<'a> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { - pub fn get_optional_card(&self) -> Option { - self.card.clone() + pub fn set_customer( + self, + customer: &'a api::CustomerDetails, + ) -> NetworkTokenizationBuilder<'a, CustomerAssigned> { + NetworkTokenizationBuilder { + state: std::marker::PhantomData, + customer: Some(customer), + card: self.card, + card_cvc: self.card_cvc, + network_token: self.network_token, + stored_card: self.stored_card, + stored_token: self.stored_token, + payment_method_response: self.payment_method_response, + card_tokenized: self.card_tokenized, + error_code: self.error_code, + error_message: self.error_message, + } + } +} + +impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { + pub fn get_optional_card_and_cvc( + &self, + ) -> ( + Option, + Option>, + ) { + (self.card.clone(), self.card_cvc.clone()) } pub fn set_token_details( self, @@ -179,6 +187,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { network_token: Some(&network_token.0), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, stored_card: self.stored_card, stored_token: self.stored_token, payment_method_response: self.payment_method_response, @@ -199,6 +208,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardTokenized> { stored_card: Some(&store_card_response.store_card_resp), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_token: self.stored_token, payment_method_response: self.payment_method_response, @@ -220,6 +230,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardStored> { stored_token: Some(&store_token_response.store_token_resp), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, payment_method_response: self.payment_method_response, @@ -272,6 +283,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardTokenStored> { payment_method_response: Some(payment_method_response), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, stored_token: self.stored_token, @@ -445,7 +457,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { pub async fn store_card_and_token_in_locker( &self, network_token: &NetworkTokenizationResponse, - card: &domain::Card, + card: &domain::CardDetailsForNetworkTransactionId, customer_id: &id_type::CustomerId, ) -> RouterResult { let stored_card_resp = self.store_card_in_locker(card, customer_id).await?; @@ -466,7 +478,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { pub async fn store_card_in_locker( &self, - card: &domain::Card, + card: &domain::CardDetailsForNetworkTransactionId, customer_id: &id_type::CustomerId, ) -> RouterResult { let merchant_id = self.merchant_account.get_id(); @@ -507,7 +519,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { &self, stored_locker_resp: &StoreLockerResponse, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, customer_id: &id_type::CustomerId, ) -> RouterResult { let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index c4a29a9f1e5..aa67269f37b 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -53,6 +53,7 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithPmId> { state: std::marker::PhantomData, customer: None, card: None, + card_cvc: None, network_token: None, stored_card: None, stored_token: None, @@ -87,6 +88,7 @@ impl<'a> NetworkTokenizationBuilder<'a, TokenizeWithPmId> { payment_method_response: Some(payment_method_response), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, stored_token: self.stored_token, @@ -106,6 +108,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmFetched> { state: std::marker::PhantomData, customer: Some(customer), card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, stored_token: self.stored_token, @@ -122,13 +125,12 @@ impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { self, card_from_locker: &'a api_models::payment_methods::Card, optional_card_info: Option, - card_cvc: Secret, + card_cvc: Option>, ) -> NetworkTokenizationBuilder<'a, PmAssigned> { - let card = domain::Card { + let card = domain::CardDetailsForNetworkTransactionId { card_number: card_from_locker.card_number.clone(), card_exp_month: card_from_locker.card_exp_month.clone(), card_exp_year: card_from_locker.card_exp_year.clone(), - card_cvc, bank_code: optional_card_info .as_ref() .and_then(|card_info| card_info.bank_code.clone()), @@ -153,6 +155,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { NetworkTokenizationBuilder { state: std::marker::PhantomData, card: Some(card), + card_cvc, customer: self.customer, network_token: self.network_token, stored_card: self.stored_card, @@ -166,8 +169,13 @@ impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { } impl<'a> NetworkTokenizationBuilder<'a, PmAssigned> { - pub fn get_optional_card(&self) -> Option { - self.card.clone() + pub fn get_optional_card_and_cvc( + &self, + ) -> ( + Option, + Option>, + ) { + (self.card.clone(), self.card_cvc.clone()) } pub fn set_token_details( self, @@ -179,6 +187,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmAssigned> { card_tokenized: true, customer: self.customer, card: self.card, + card_cvc: self.card_cvc, stored_card: self.stored_card, stored_token: self.stored_token, payment_method_response: self.payment_method_response, @@ -198,6 +207,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmTokenized> { stored_token: Some(store_token_response), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, network_token: self.network_token, stored_card: self.stored_card, payment_method_response: self.payment_method_response, @@ -234,6 +244,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmTokenStored> { payment_method_response: Some(payment_method_response), customer: self.customer, card: self.card, + card_cvc: self.card_cvc, stored_token: self.stored_token, network_token: self.network_token, stored_card: self.stored_card, @@ -374,7 +385,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizePaymentMethodRequest> { store_token_response: &pm_transformers::StoreCardRespPayload, payment_method: domain::PaymentMethod, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::Card, + card_details: &domain::CardDetailsForNetworkTransactionId, ) -> RouterResult { // Form encrypted network token data let enc_token_data = self diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 84f848ef0ab..e41ad7f58e0 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1020,9 +1020,11 @@ pub async fn save_network_token_in_locker( .filter(|cn| network_tokenization_supported_card_networks.contains(cn)) .is_some() { + let optional_card_cvc = Some(card_data.card_cvc.clone()); match network_tokenization::make_card_network_tokenization_request( state, - card_data, + &domain::CardDetailsForNetworkTransactionId::from(card_data), + optional_card_cvc, &customer_id, ) .await diff --git a/crates/router/src/types/domain/payments.rs b/crates/router/src/types/domain/payments.rs index 53bc138c649..a839d5d1ba1 100644 --- a/crates/router/src/types/domain/payments.rs +++ b/crates/router/src/types/domain/payments.rs @@ -1,11 +1,11 @@ pub use hyperswitch_domain_models::payment_method_data::{ AliPayQr, ApplePayFlow, ApplePayThirdPartySdkData, ApplePayWalletData, ApplepayPaymentMethod, - BankDebitData, BankRedirectData, BankTransferData, BoletoVoucherData, Card, CardRedirectData, - CardToken, CashappQr, CryptoData, GcashRedirection, GiftCardData, GiftCardDetails, - GoPayRedirection, GooglePayPaymentMethodInfo, GooglePayRedirectData, - GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData, - KakaoPayRedirection, MbWayRedirection, MifinityData, NetworkTokenData, OpenBankingData, - PayLaterData, PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, + BankDebitData, BankRedirectData, BankTransferData, BoletoVoucherData, Card, + CardDetailsForNetworkTransactionId, CardRedirectData, CardToken, CashappQr, CryptoData, + GcashRedirection, GiftCardData, GiftCardDetails, GoPayRedirection, GooglePayPaymentMethodInfo, + GooglePayRedirectData, GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, + IndomaretVoucherData, KakaoPayRedirection, MbWayRedirection, MifinityData, NetworkTokenData, + OpenBankingData, PayLaterData, PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, SepaAndBacsBillingDetails, SwishQrData, TokenizedBankDebitValue1, TokenizedBankDebitValue2, TokenizedBankRedirectValue1, TokenizedBankRedirectValue2, TokenizedBankTransferValue1, TokenizedBankTransferValue2, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, From 152184d3d941e9103c0c0672fb4a0d9b31d756c5 Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 5 Feb 2025 21:23:31 +0530 Subject: [PATCH 18/25] chore: add CardDetail domain type --- Cargo.lock | 1 - crates/api_models/Cargo.toml | 1 - .../src/payment_method_data.rs | 16 +++++++++++++++- .../core/payment_methods/network_tokenization.rs | 2 +- .../router/src/core/payment_methods/tokenize.rs | 14 +++++++------- .../payment_methods/tokenize/card_executor.rs | 13 +++++-------- .../tokenize/payment_method_executor.rs | 6 +++--- crates/router/src/core/payments/tokenization.rs | 2 +- crates/router/src/types/domain/payments.rs | 12 ++++++------ 9 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ffe31f83d3..7583fb2eeee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,6 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "serde_with", "strum 0.26.3", "time", "url", diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 56959aabfec..c159af97249 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -32,7 +32,6 @@ mime = "0.3.17" reqwest = { version = "0.11.27", optional = true } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" -serde_with = "3.7.1" strum = { version = "0.26", features = ["derive"] } time = { version = "0.3.35", features = ["serde", "serde-well-known", "std"] } url = { version = "2.5.0", features = ["serde"] } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index af1be0c504c..729d5c0e456 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -99,6 +99,20 @@ pub struct CardDetailsForNetworkTransactionId { pub card_holder_name: Option>, } +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Default)] +pub struct CardDetail { + pub card_number: cards::CardNumber, + pub card_exp_month: Secret, + pub card_exp_year: Secret, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + pub card_issuing_country: Option, + pub bank_code: Option, + pub nick_name: Option>, + pub card_holder_name: Option>, +} + impl CardDetailsForNetworkTransactionId { pub fn get_nti_and_card_details_for_mit_flow( recurring_details: mandates::RecurringDetails, @@ -126,7 +140,7 @@ impl CardDetailsForNetworkTransactionId { } } -impl From<&Card> for CardDetailsForNetworkTransactionId { +impl From<&Card> for CardDetail { fn from(item: &Card) -> Self { Self { card_number: item.card_number.to_owned(), diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index fa1775091ae..36d4cf8089d 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -264,7 +264,7 @@ pub async fn mk_tokenization_req( pub async fn make_card_network_tokenization_request( state: &routes::SessionState, - card: &domain::CardDetailsForNetworkTransactionId, + card: &domain::CardDetail, optional_cvc: Option>, customer_id: &id_type::CustomerId, ) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 9b7fa8f443f..f4975b7ced9 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -154,7 +154,7 @@ pub struct NetworkTokenizationBuilder<'a, S: State> { pub customer: Option<&'a api::CustomerDetails>, /// Card details - pub card: Option, + pub card: Option, /// CVC pub card_cvc: Option>, @@ -206,13 +206,13 @@ pub trait NetworkTokenizationProcess<'a, D> { ) -> Self; async fn encrypt_card( &self, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, saved_to_locker: bool, ) -> RouterResult>>; async fn encrypt_network_token( &self, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, saved_to_locker: bool, ) -> RouterResult>>; async fn fetch_bin_details_and_validate_card_network( @@ -230,7 +230,7 @@ pub trait NetworkTokenizationProcess<'a, D> { async fn tokenize_card( &self, customer_id: &id_type::CustomerId, - card: &domain::CardDetailsForNetworkTransactionId, + card: &domain::CardDetail, optional_cvc: Option>, ) -> RouterResult; async fn store_network_token_in_locker( @@ -265,7 +265,7 @@ where } async fn encrypt_card( &self, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, saved_to_locker: bool, ) -> RouterResult>> { let pm_data = api::PaymentMethodsData::Card(api::CardDetailsPaymentMethod { @@ -289,7 +289,7 @@ where async fn encrypt_network_token( &self, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, saved_to_locker: bool, ) -> RouterResult>> { let network_token = &network_token_details.0; @@ -342,7 +342,7 @@ where async fn tokenize_card( &self, customer_id: &id_type::CustomerId, - card: &domain::CardDetailsForNetworkTransactionId, + card: &domain::CardDetail, optional_cvc: Option>, ) -> RouterResult { network_tokenization::make_card_network_tokenization_request( diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index 5b40204f143..b5e84f4e40f 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -100,7 +100,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardRequestValidated> { card_req: &'a domain::TokenizeCardRequest, optional_card_info: Option, ) -> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { - let card = domain::CardDetailsForNetworkTransactionId { + let card = domain::CardDetail { card_number: card_req.raw_card_number.clone(), card_exp_month: card_req.card_expiry_month.clone(), card_exp_year: card_req.card_expiry_year.clone(), @@ -172,10 +172,7 @@ impl<'a> NetworkTokenizationBuilder<'a, CardDetailsAssigned> { impl<'a> NetworkTokenizationBuilder<'a, CustomerAssigned> { pub fn get_optional_card_and_cvc( &self, - ) -> ( - Option, - Option>, - ) { + ) -> (Option, Option>) { (self.card.clone(), self.card_cvc.clone()) } pub fn set_token_details( @@ -457,7 +454,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { pub async fn store_card_and_token_in_locker( &self, network_token: &NetworkTokenizationResponse, - card: &domain::CardDetailsForNetworkTransactionId, + card: &domain::CardDetail, customer_id: &id_type::CustomerId, ) -> RouterResult { let stored_card_resp = self.store_card_in_locker(card, customer_id).await?; @@ -478,7 +475,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { pub async fn store_card_in_locker( &self, - card: &domain::CardDetailsForNetworkTransactionId, + card: &domain::CardDetail, customer_id: &id_type::CustomerId, ) -> RouterResult { let merchant_id = self.merchant_account.get_id(); @@ -519,7 +516,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizeCardRequest> { &self, stored_locker_resp: &StoreLockerResponse, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, customer_id: &id_type::CustomerId, ) -> RouterResult { let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index aa67269f37b..1f74a120f00 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -127,7 +127,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { optional_card_info: Option, card_cvc: Option>, ) -> NetworkTokenizationBuilder<'a, PmAssigned> { - let card = domain::CardDetailsForNetworkTransactionId { + let card = domain::CardDetail { card_number: card_from_locker.card_number.clone(), card_exp_month: card_from_locker.card_exp_month.clone(), card_exp_year: card_from_locker.card_exp_year.clone(), @@ -172,7 +172,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmAssigned> { pub fn get_optional_card_and_cvc( &self, ) -> ( - Option, + Option, Option>, ) { (self.card.clone(), self.card_cvc.clone()) @@ -385,7 +385,7 @@ impl CardNetworkTokenizeExecutor<'_, domain::TokenizePaymentMethodRequest> { store_token_response: &pm_transformers::StoreCardRespPayload, payment_method: domain::PaymentMethod, network_token_details: &NetworkTokenizationResponse, - card_details: &domain::CardDetailsForNetworkTransactionId, + card_details: &domain::CardDetail, ) -> RouterResult { // Form encrypted network token data let enc_token_data = self diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index e41ad7f58e0..e08f29f85da 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1023,7 +1023,7 @@ pub async fn save_network_token_in_locker( let optional_card_cvc = Some(card_data.card_cvc.clone()); match network_tokenization::make_card_network_tokenization_request( state, - &domain::CardDetailsForNetworkTransactionId::from(card_data), + &domain::CardDetail::from(card_data), optional_card_cvc, &customer_id, ) diff --git a/crates/router/src/types/domain/payments.rs b/crates/router/src/types/domain/payments.rs index a839d5d1ba1..3f7849ae0fa 100644 --- a/crates/router/src/types/domain/payments.rs +++ b/crates/router/src/types/domain/payments.rs @@ -1,11 +1,11 @@ pub use hyperswitch_domain_models::payment_method_data::{ AliPayQr, ApplePayFlow, ApplePayThirdPartySdkData, ApplePayWalletData, ApplepayPaymentMethod, - BankDebitData, BankRedirectData, BankTransferData, BoletoVoucherData, Card, - CardDetailsForNetworkTransactionId, CardRedirectData, CardToken, CashappQr, CryptoData, - GcashRedirection, GiftCardData, GiftCardDetails, GoPayRedirection, GooglePayPaymentMethodInfo, - GooglePayRedirectData, GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, - IndomaretVoucherData, KakaoPayRedirection, MbWayRedirection, MifinityData, NetworkTokenData, - OpenBankingData, PayLaterData, PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, + BankDebitData, BankRedirectData, BankTransferData, BoletoVoucherData, Card, CardDetail, + CardRedirectData, CardToken, CashappQr, CryptoData, GcashRedirection, GiftCardData, + GiftCardDetails, GoPayRedirection, GooglePayPaymentMethodInfo, GooglePayRedirectData, + GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData, + KakaoPayRedirection, MbWayRedirection, MifinityData, NetworkTokenData, OpenBankingData, + PayLaterData, PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, SepaAndBacsBillingDetails, SwishQrData, TokenizedBankDebitValue1, TokenizedBankDebitValue2, TokenizedBankRedirectValue1, TokenizedBankRedirectValue2, TokenizedBankTransferValue1, TokenizedBankTransferValue2, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, From 5b8813ce4a97f37378d133b3108ebcd2ce57d071 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:01:04 +0000 Subject: [PATCH 19/25] chore: run formatter --- .../core/payment_methods/tokenize/payment_method_executor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index 1f74a120f00..86f97b7b885 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -171,10 +171,7 @@ impl<'a> NetworkTokenizationBuilder<'a, PmValidated> { impl<'a> NetworkTokenizationBuilder<'a, PmAssigned> { pub fn get_optional_card_and_cvc( &self, - ) -> ( - Option, - Option>, - ) { + ) -> (Option, Option>) { (self.card.clone(), self.card_cvc.clone()) } pub fn set_token_details( From 89e7b3e590283cec8f09b48ee8527521797438a5 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 6 Feb 2025 15:00:48 +0530 Subject: [PATCH 20/25] chore: use stricter types in the match statement --- crates/hyperswitch_domain_models/src/bulk_tokenization.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 3c38ca6124d..7b497716e42 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -146,7 +146,7 @@ impl ForeignTryFrom for payment_methods_api::CardNetw record.card_expiry_year, record.payment_method_id, ) { - (Some(raw_card_number), Some(card_expiry_month), Some(card_expiry_year), _) => { + (Some(raw_card_number), Some(card_expiry_month), Some(card_expiry_year), None) => { Ok(Self { merchant_id, data: payment_methods_api::TokenizeDataRequest::Card( @@ -169,7 +169,7 @@ impl ForeignTryFrom for payment_methods_api::CardNetw payment_method_issuer: record.payment_method_issuer, }) } - (_, _, _, Some(payment_method_id)) => Ok(Self { + (None, None, None, Some(payment_method_id)) => Ok(Self { merchant_id, data: payment_methods_api::TokenizeDataRequest::PaymentMethod( payment_methods_api::TokenizePaymentMethodRequest { @@ -182,8 +182,8 @@ impl ForeignTryFrom for payment_methods_api::CardNetw metadata: None, payment_method_issuer: record.payment_method_issuer, }), - _ => Err(report!(errors::ValidationError::MissingRequiredField { - field_name: "card details or payment_method_id".to_string(), + _ => Err(report!(errors::ValidationError::InvalidValue { + message: "Invalid record in bulk tokenization - expected one of card details or payment method details".to_string() })), } } From 4ebd5d7ad196774045b8b5283a3006cd48dcfa4b Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 19 Feb 2025 12:46:49 +0530 Subject: [PATCH 21/25] chore: resolve comments --- crates/api_models/src/payment_methods.rs | 4 ++-- crates/hyperswitch_domain_models/src/bulk_tokenization.rs | 8 ++++---- crates/router/src/core/payment_methods/cards.rs | 2 +- crates/router/src/core/payment_methods/tokenize.rs | 2 +- .../src/core/payment_methods/tokenize/card_executor.rs | 2 +- .../payment_methods/tokenize/payment_method_executor.rs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index cdf0c900049..cb633fbc588 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2510,7 +2510,7 @@ impl common_utils::events::ApiEventMetric for CardNetworkTokenizeRequest {} #[serde(rename_all = "snake_case")] pub enum TokenizeDataRequest { Card(TokenizeCardRequest), - PaymentMethod(TokenizePaymentMethodRequest), + ExistingPaymentMethod(TokenizePaymentMethodRequest), } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] @@ -2584,7 +2584,7 @@ pub struct CardNetworkTokenizeResponse { /// Details that were sent for tokenization #[serde(skip_serializing_if = "Option::is_none")] - pub req: Option, + pub tokenization_data: Option, } impl common_utils::events::ApiEventMetric for CardNetworkTokenizeResponse {} diff --git a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs index 7b497716e42..0eff34a1bf5 100644 --- a/crates/hyperswitch_domain_models/src/bulk_tokenization.rs +++ b/crates/hyperswitch_domain_models/src/bulk_tokenization.rs @@ -26,7 +26,7 @@ pub struct CardNetworkTokenizeRequest { #[derive(Debug)] pub enum TokenizeDataRequest { Card(TokenizeCardRequest), - PaymentMethod(TokenizePaymentMethodRequest), + ExistingPaymentMethod(TokenizePaymentMethodRequest), } #[derive(Clone, Debug)] @@ -171,7 +171,7 @@ impl ForeignTryFrom for payment_methods_api::CardNetw } (None, None, None, Some(payment_method_id)) => Ok(Self { merchant_id, - data: payment_methods_api::TokenizeDataRequest::PaymentMethod( + data: payment_methods_api::TokenizeDataRequest::ExistingPaymentMethod( payment_methods_api::TokenizePaymentMethodRequest { payment_method_id, card_cvc: record.card_cvc, @@ -250,8 +250,8 @@ impl ForeignFrom for TokenizeDataReque card_type: card.card_type, }) } - payment_methods_api::TokenizeDataRequest::PaymentMethod(pm) => { - Self::PaymentMethod(TokenizePaymentMethodRequest { + payment_methods_api::TokenizeDataRequest::ExistingPaymentMethod(pm) => { + Self::ExistingPaymentMethod(TokenizePaymentMethodRequest { payment_method_id: pm.payment_method_id, card_cvc: pm.card_cvc, }) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 873dd505b5b..a1f53f85482 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -6047,7 +6047,7 @@ pub async fn tokenize_card_flow( tokenize::NetworkTokenizationBuilder::::default(); execute_card_tokenization(executor, builder, card_req).await } - domain::TokenizeDataRequest::PaymentMethod(ref payment_method) => { + domain::TokenizeDataRequest::ExistingPaymentMethod(ref payment_method) => { let executor = tokenize::CardNetworkTokenizeExecutor::new( state, key_store, diff --git a/crates/router/src/core/payment_methods/tokenize.rs b/crates/router/src/core/payment_methods/tokenize.rs index 312390d59d2..853d3c0a2bd 100644 --- a/crates/router/src/core/payment_methods/tokenize.rs +++ b/crates/router/src/core/payment_methods/tokenize.rs @@ -119,7 +119,7 @@ pub async fn tokenize_cards( .unwrap_or_else(|e| { let err = e.current_context(); payment_methods_api::CardNetworkTokenizeResponse { - req: Some(tokenize_request), + tokenization_data: Some(tokenize_request), error_code: Some(err.error_code()), error_message: Some(err.error_message()), card_tokenized: false, diff --git a/crates/router/src/core/payment_methods/tokenize/card_executor.rs b/crates/router/src/core/payment_methods/tokenize/card_executor.rs index b5e84f4e40f..9b7f89c5199 100644 --- a/crates/router/src/core/payment_methods/tokenize/card_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/card_executor.rs @@ -300,7 +300,7 @@ impl NetworkTokenizationBuilder<'_, PaymentMethodCreated> { error_code: self.error_code.cloned(), error_message: self.error_message.cloned(), // Below field is mutated by caller functions for batched API operations - req: None, + tokenization_data: None, } } } diff --git a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs index 86f97b7b885..967dc77f7fd 100644 --- a/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs +++ b/crates/router/src/core/payment_methods/tokenize/payment_method_executor.rs @@ -261,7 +261,7 @@ impl NetworkTokenizationBuilder<'_, PmTokenUpdated> { error_code: self.error_code.cloned(), error_message: self.error_message.cloned(), // Below field is mutated by caller functions for batched API operations - req: None, + tokenization_data: None, } } } From 2ddc5240a11922ff8b16722e5c1b5d4ffe73d43e Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 19 Feb 2025 15:30:52 +0530 Subject: [PATCH 22/25] chore: resolve missed merge conflict --- crates/router/src/routes/lock_utils.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 5e52b5d12f7..b73c5a54e8d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -39,11 +39,8 @@ pub enum ApiIdentifier { ApplePayCertificatesMigration, Relay, Documentation, -<<<<<<< HEAD CardNetworkTokenization, -======= Hypersense, ->>>>>>> origin/main PaymentMethodsSession, } From 98eef21a681b8fc09ec1a1a89dce94a1e8e1a281 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 20 Feb 2025 15:09:04 +0530 Subject: [PATCH 23/25] refactor: expose a separate endpoint for tokenizing with PM ID --- crates/api_models/src/payment_methods.rs | 3 +- crates/router/src/routes/app.rs | 4 ++ crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/payment_methods.rs | 54 +++++++++++++++++++++ crates/router_env/src/logger/types.rs | 2 + 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index cb633fbc588..a410fa8f635 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2554,9 +2554,10 @@ pub struct TokenizeCardRequest { pub card_type: Option, } -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub struct TokenizePaymentMethodRequest { /// Payment method's ID + #[serde(skip_deserializing)] pub payment_method_id: String, /// The CVC number for the card diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f2786eab955..83b685075c5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1246,6 +1246,10 @@ impl PaymentMethods { web::resource("/tokenize-card") .route(web::post().to(payment_methods::tokenize_card_api)), ) + .service( + web::resource("/tokenize-card/{payment_method_id}") + .route(web::post().to(payment_methods::tokenize_card_using_pm_api)), + ) .service( web::resource("/tokenize-card-batch") .route(web::post().to(payment_methods::tokenize_card_batch_api)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b73c5a54e8d..7f56ebdcfbe 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -313,7 +313,9 @@ impl From for ApiIdentifier { Flow::FeatureMatrix => Self::Documentation, - Flow::TokenizeCard | Flow::TokenizeCardBatch => Self::CardNetworkTokenization, + Flow::TokenizeCard + | Flow::TokenizeCardUsingPaymentMethodId + | Flow::TokenizeCardBatch => Self::CardNetworkTokenization, Flow::HypersenseTokenRequest | Flow::HypersenseVerifyToken diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 6ba81c9d1d4..2351fb58b95 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -929,6 +929,60 @@ pub async fn tokenize_card_api( .await } +#[cfg(all( + any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), + not(feature = "payment_methods_v2") +))] +#[instrument(skip_all, fields(flow = ?Flow::TokenizeCardUsingPaymentMethodId))] +pub async fn tokenize_card_using_pm_api( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::TokenizeCardUsingPaymentMethodId; + let pm_id = path.into_inner(); + let mut payload = json_payload.into_inner(); + match payload.data { + payment_methods::TokenizeDataRequest::ExistingPaymentMethod(pm_data) => { + let updated_data = payment_methods::TokenizeDataRequest::ExistingPaymentMethod( + payment_methods::TokenizePaymentMethodRequest { + payment_method_id: pm_id, + ..pm_data + }, + ); + payload.data = updated_data; + } + payment_methods::TokenizeDataRequest::Card(_) => { + return api::log_and_return_error_response(error_stack::report!( + errors::ApiErrorResponse::InvalidDataValue { field_name: "card" } + )); + } + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, _, req, _| async move { + let merchant_id = req.merchant_id.clone(); + let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; + let res = Box::pin(cards::tokenize_card_flow( + &state, + CardNetworkTokenizeRequest::foreign_from(req), + &merchant_account, + &key_store, + )) + .await?; + Ok(services::ApplicationResponse::Json(res)) + }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all( any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), not(feature = "payment_methods_v2") diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 988f909866d..4090c2034c5 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -543,6 +543,8 @@ pub enum Flow { RelayRetrieve, /// Card tokenization flow TokenizeCard, + /// Card tokenization using payment method flow + TokenizeCardUsingPaymentMethodId, /// Cards batch tokenization flow TokenizeCardBatch, /// Incoming Relay Webhook Receive From 35f864ae18d9d8739d2340f33a2ecaab84bf27bd Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 20 Feb 2025 15:40:39 +0530 Subject: [PATCH 24/25] refactor: fill pm_id from path into request body --- crates/router/src/routes/payment_methods.rs | 25 ++++++++------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 2351fb58b95..c6b1b886d09 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -943,22 +943,15 @@ pub async fn tokenize_card_using_pm_api( let flow = Flow::TokenizeCardUsingPaymentMethodId; let pm_id = path.into_inner(); let mut payload = json_payload.into_inner(); - match payload.data { - payment_methods::TokenizeDataRequest::ExistingPaymentMethod(pm_data) => { - let updated_data = payment_methods::TokenizeDataRequest::ExistingPaymentMethod( - payment_methods::TokenizePaymentMethodRequest { - payment_method_id: pm_id, - ..pm_data - }, - ); - payload.data = updated_data; - } - payment_methods::TokenizeDataRequest::Card(_) => { - return api::log_and_return_error_response(error_stack::report!( - errors::ApiErrorResponse::InvalidDataValue { field_name: "card" } - )); - } - }; + if let payment_methods::TokenizeDataRequest::ExistingPaymentMethod(ref mut pm_data) = + payload.data + { + pm_data.payment_method_id = pm_id; + } else { + return api::log_and_return_error_response(error_stack::report!( + errors::ApiErrorResponse::InvalidDataValue { field_name: "card" } + )); + } Box::pin(api::server_wrap( flow, From 67e79d5c7f351c0acf756154f7b34b4ed7dc7753 Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 21 Feb 2025 19:08:01 +0530 Subject: [PATCH 25/25] refactor: network tokenization API path --- crates/openapi/src/routes/payment_method.rs | 2 +- crates/router/src/routes/app.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index f8995325a70..08732731784 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -346,7 +346,7 @@ pub async fn tokenize_card_api() {} /// This API expects an existing payment method ID for a card. #[utoipa::path( post, - path = "/payment-methods/tokenize-card/{id}", + path = "/payment-methods/{id}/tokenize-card", request_body = CardNetworkTokenizeRequest, params ( ("id" = String, Path, description = "The unique identifier for the Payment Method"), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index c9320b523a6..623f63fa407 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1247,10 +1247,6 @@ impl PaymentMethods { web::resource("/tokenize-card") .route(web::post().to(payment_methods::tokenize_card_api)), ) - .service( - web::resource("/tokenize-card/{payment_method_id}") - .route(web::post().to(payment_methods::tokenize_card_using_pm_api)), - ) .service( web::resource("/tokenize-card-batch") .route(web::post().to(payment_methods::tokenize_card_batch_api)), @@ -1268,6 +1264,10 @@ impl PaymentMethods { .route(web::get().to(payment_methods::payment_method_retrieve_api)) .route(web::delete().to(payment_methods::payment_method_delete_api)), ) + .service( + web::resource("/{payment_method_id}/tokenize-card") + .route(web::post().to(payment_methods::tokenize_card_using_pm_api)), + ) .service( web::resource("/{payment_method_id}/update") .route(web::post().to(payment_methods::payment_method_update_api)),