diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 04a108389121..4183e04be4d9 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -13763,6 +13763,14 @@ } ], "nullable": true + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true } }, "additionalProperties": false diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index cb268262fbcf..5b360e399569 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1229,6 +1229,8 @@ pub struct ConnectorAgnosticMitChoice { impl common_utils::events::ApiEventMetric for ConnectorAgnosticMitChoice {} +impl common_utils::events::ApiEventMetric for payment_methods::PaymentMethodMigrate {} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ExtendedCardInfoConfig { /// Merchant public key diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c8826c02089f..7b33e3dfeab8 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -75,6 +75,128 @@ pub struct PaymentMethodCreate { /// Payment method data to be passed in case of client /// based flow pub payment_method_data: Option, + + /// The billing details of the payment method + #[schema(value_type = Option
)] + pub billing: Option, + + #[serde(skip_deserializing)] + /// The connector mandate details of the payment method, this is added only for cards migration + /// api and is skipped during deserialization of the payment method create request as this + /// it should not be passed in the request + pub connector_mandate_details: Option, + + #[serde(skip_deserializing)] + /// The transaction id of a CIT (customer initiated transaction) associated with the payment method, + /// this is added only for cards migration api and is skipped during deserialization of the + /// payment method create request as it should not be passed in the request + pub network_transaction_id: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +/// This struct is only used by and internal api to migrate payment method +pub struct PaymentMethodMigrate { + /// Merchant id + pub merchant_id: String, + + /// The type of payment method use for the payment. + pub payment_method: Option, + + /// This is a sub-category of payment method. + pub payment_method_type: Option, + + /// The name of the bank/ provider issuing the payment method to the end user + pub payment_method_issuer: Option, + + /// A standard code representing the issuer of payment method + pub payment_method_issuer_code: Option, + + /// Card Details + pub card: 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 unique identifier of the customer. + pub customer_id: Option, + + /// The card network + pub card_network: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + pub bank_transfer: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + pub wallet: Option, + + /// Payment method data to be passed in case of client + /// based flow + pub payment_method_data: Option, + + /// The billing details of the payment method + pub billing: Option, + + /// The connector mandate details of the payment method + pub connector_mandate_details: Option, + + // The CIT (customer initiated transaction) transaction id associated with the payment method + pub network_transaction_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReference(pub HashMap); + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_type: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, +} + +impl PaymentMethodCreate { + pub fn get_payment_method_create_from_payment_method_migrate( + card_number: CardNumber, + payment_method_migrate: &PaymentMethodMigrate, + ) -> Self { + let card_details = + payment_method_migrate + .card + .as_ref() + .map(|payment_method_migrate_card| CardDetail { + card_number, + card_exp_month: payment_method_migrate_card.card_exp_month.clone(), + card_exp_year: payment_method_migrate_card.card_exp_year.clone(), + card_holder_name: payment_method_migrate_card.card_holder_name.clone(), + nick_name: payment_method_migrate_card.nick_name.clone(), + card_issuing_country: payment_method_migrate_card.card_issuing_country.clone(), + card_network: payment_method_migrate_card.card_network.clone(), + card_issuer: payment_method_migrate_card.card_issuer.clone(), + card_type: payment_method_migrate_card.card_type.clone(), + }); + + Self { + customer_id: payment_method_migrate.customer_id.clone(), + payment_method: payment_method_migrate.payment_method, + payment_method_type: payment_method_migrate.payment_method_type, + payment_method_issuer: payment_method_migrate.payment_method_issuer.clone(), + payment_method_issuer_code: payment_method_migrate.payment_method_issuer_code, + metadata: payment_method_migrate.metadata.clone(), + payment_method_data: payment_method_migrate.payment_method_data.clone(), + connector_mandate_details: payment_method_migrate.connector_mandate_details.clone(), + client_secret: None, + billing: payment_method_migrate.billing.clone(), + card: card_details, + card_network: payment_method_migrate.card_network.clone(), + #[cfg(feature = "payouts")] + bank_transfer: payment_method_migrate.bank_transfer.clone(), + #[cfg(feature = "payouts")] + wallet: payment_method_migrate.wallet.clone(), + network_transaction_id: payment_method_migrate.network_transaction_id.clone(), + } + } } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -139,6 +261,43 @@ pub struct CardDetail { pub card_type: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct MigrateCardDetail { + /// 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, + + /// 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, +} + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct CardDetailUpdate { diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index d968e62bea88..24ba06bcf362 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -130,6 +130,9 @@ pub async fn call_to_locker( card_network: card.card_brand, client_secret: None, payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }; let add_card_result = cards::add_card_hs( diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 495d85d5b36b..ef1fa51442bf 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -95,6 +95,7 @@ pub async fn create_payment_method( network_transaction_id: Option, storage_scheme: MerchantStorageScheme, payment_method_billing_address: Option, + card_scheme: Option, ) -> errors::CustomResult { let customer = db .find_customer_by_customer_id_merchant_id( @@ -123,7 +124,7 @@ pub async fn create_payment_method( payment_method: req.payment_method, payment_method_type: req.payment_method_type, payment_method_issuer: req.payment_method_issuer.clone(), - scheme: req.card_network.clone(), + scheme: req.card_network.clone().or(card_scheme), metadata: pm_metadata.map(Secret::new), payment_method_data, connector_mandate_details, @@ -244,7 +245,7 @@ pub async fn get_or_insert_payment_method( insert_payment_method( db, resp, - req, + &req, key_store, &merchant_account.merchant_id, customer_id, @@ -252,7 +253,7 @@ pub async fn get_or_insert_payment_method( None, locker_id, None, - None, + req.network_transaction_id.clone(), merchant_account.storage_scheme, None, ) @@ -266,6 +267,314 @@ pub async fn get_or_insert_payment_method( } } +pub async fn migrate_payment_method( + state: routes::SessionState, + req: api::PaymentMethodMigrate, +) -> errors::RouterResponse { + let merchant_id = &req.merchant_id; + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant_account = state + .store + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let card_details = req.card.as_ref().get_required_value("card")?; + + let card_number_validation_result = + cards::CardNumber::from_str(card_details.card_number.peek()); + + if let Some(connector_mandate_details) = &req.connector_mandate_details { + helpers::validate_merchant_connector_ids_in_connector_mandate_details( + &*state.store, + &key_store, + connector_mandate_details, + merchant_id, + ) + .await?; + }; + + match card_number_validation_result { + Ok(card_number) => { + let payment_method_create_request = + api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate( + card_number, + &req, + ); + get_client_secret_or_add_payment_method( + state, + payment_method_create_request, + &merchant_account, + &key_store, + ) + .await + } + Err(card_validation_error) => { + logger::debug!("Card number to be migrated is invalid, skip saving in locker {card_validation_error}"); + skip_locker_call_and_migrate_payment_method( + state, + &req, + merchant_id.into(), + &key_store, + &merchant_account, + ) + .await + } + } +} + +pub async fn skip_locker_call_and_migrate_payment_method( + state: routes::SessionState, + req: &api::PaymentMethodMigrate, + merchant_id: String, + key_store: &domain::MerchantKeyStore, + merchant_account: &domain::MerchantAccount, +) -> errors::RouterResponse { + let db = &*state.store; + let customer_id = req.customer_id.clone().get_required_value("customer_id")?; + + // In this case, since we do not have valid card details, recurring payments can only be done through connector mandate details. + let connector_mandate_details_req = req + .connector_mandate_details + .clone() + .get_required_value("connector mandate details")?; + + let connector_mandate_details = serde_json::to_value(&connector_mandate_details_req) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse connector mandate details")?; + + let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone()) + .await + .map(|details| details.into()); + + let customer = db + .find_customer_by_customer_id_merchant_id( + &customer_id, + &merchant_id, + key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let card = if let Some(card_details) = &req.card { + helpers::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?; + let card_number = card_details.card_number.clone(); + + let (card_isin, last4_digits) = get_card_bin_and_last4_digits_for_masked_card( + card_number.peek(), + ) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid card number".to_string(), + })?; + + if card_details.card_issuer.is_some() + && card_details.card_network.is_some() + && card_details.card_type.is_some() + && card_details.card_issuing_country.is_some() + { + Some(api::CardDetailFromLocker { + scheme: card_details + .card_network + .clone() + .or(card_details.card_network.clone()) + .map(|card_network| card_network.to_string()), + last4_digits: Some(last4_digits.clone()), + issuer_country: card_details + .card_issuing_country + .clone() + .or(card_details.card_issuing_country.clone()), + card_number: None, + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card_details.card_holder_name.clone(), + nick_name: card_details.nick_name.clone(), + card_isin: Some(card_isin.clone()), + card_issuer: card_details + .card_issuer + .clone() + .or(card_details.card_issuer.clone()), + card_network: card_details + .card_network + .clone() + .or(card_details.card_network.clone()), + card_type: card_details + .card_type + .clone() + .or(card_details.card_type.clone()), + saved_to_locker: false, + }) + } else { + Some( + db.get_card_info(&card_isin) + .await + .map_err(|error| services::logger::error!(card_info_error=?error)) + .ok() + .flatten() + .map(|card_info| { + logger::debug!("Fetching bin details"); + api::CardDetailFromLocker { + scheme: card_details + .card_network + .clone() + .or(card_info.card_network.clone()) + .map(|card_network| card_network.to_string()), + last4_digits: Some(last4_digits.clone()), + issuer_country: card_details + .card_issuing_country + .clone() + .or(card_info.card_issuing_country), + card_number: None, + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card_details.card_holder_name.clone(), + nick_name: card_details.nick_name.clone(), + card_isin: Some(card_isin.clone()), + card_issuer: card_details.card_issuer.clone().or(card_info.card_issuer), + card_network: card_details + .card_network + .clone() + .or(card_info.card_network), + card_type: card_details.card_type.clone().or(card_info.card_type), + saved_to_locker: false, + } + }) + .unwrap_or_else(|| { + logger::debug!("Failed to fetch bin details"); + api::CardDetailFromLocker { + scheme: card_details + .card_network + .clone() + .map(|card_network| card_network.to_string()), + last4_digits: Some(last4_digits.clone()), + issuer_country: card_details.card_issuing_country.clone(), + card_number: None, + expiry_month: Some(card_details.card_exp_month.clone()), + expiry_year: Some(card_details.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card_details.card_holder_name.clone(), + nick_name: card_details.nick_name.clone(), + card_isin: Some(card_isin.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: false, + } + }), + ) + } + } else { + None + }; + + let payment_method_card_details = card + .as_ref() + .map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()))); + + let payment_method_data_encrypted = + create_encrypted_data(key_store, payment_method_card_details) + .await + .map(|details| details.into()); + + let payment_method_metadata: Option = + req.metadata.as_ref().map(|data| data.peek()).cloned(); + + let payment_method_id = generate_id(consts::ID_LENGTH, "pm"); + + let current_time = common_utils::date_time::now(); + + let response = db + .insert_payment_method( + storage::PaymentMethodNew { + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_string(), + payment_method_id: payment_method_id.to_string(), + locker_id: None, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + payment_method_issuer: req.payment_method_issuer.clone(), + scheme: req.card_network.clone(), + metadata: payment_method_metadata.map(Secret::new), + payment_method_data: payment_method_data_encrypted, + connector_mandate_details: Some(connector_mandate_details), + customer_acceptance: None, + client_secret: None, + status: enums::PaymentMethodStatus::Active, + network_transaction_id: None, + payment_method_issuer_code: None, + accepted_currency: None, + token: None, + cardholder_name: None, + issuer_name: None, + issuer_country: None, + payer_country: None, + is_stored: None, + swift_code: None, + direct_debit_token: None, + created_at: current_time, + last_modified: current_time, + last_used_at: current_time, + payment_method_billing_address, + updated_by: None, + }, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + + logger::debug!("Payment method inserted in db"); + + if customer.default_payment_method_id.is_none() && req.payment_method.is_some() { + let _ = set_default_payment_method( + &*state.store, + merchant_id.to_string(), + key_store.clone(), + &customer_id, + payment_method_id.to_owned(), + merchant_account.storage_scheme, + ) + .await + .map_err(|err| logger::error!(error=?err,"Failed to set the payment method as default")); + } + Ok(services::api::ApplicationResponse::Json( + api::PaymentMethodResponse::foreign_from((card, response)), + )) +} + +pub fn get_card_bin_and_last4_digits_for_masked_card( + masked_card_number: &str, +) -> Result<(String, String), cards::CardNumberValidationErr> { + let last4_digits = masked_card_number + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::(); + + let card_isin = masked_card_number.chars().take(6).collect::(); + + cards::validate::validate_card_number_chars(&card_isin) + .and_then(|_| cards::validate::validate_card_number_chars(&last4_digits))?; + + Ok((card_isin, last4_digits)) +} + #[instrument(skip_all)] pub async fn get_client_secret_or_add_payment_method( state: routes::SessionState, @@ -282,6 +591,17 @@ pub async fn get_client_secret_or_add_payment_method( #[cfg(feature = "payouts")] let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some(); + let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone()) + .await + .map(|details| details.into()); + + let connector_mandate_details = req + .connector_mandate_details + .clone() + .map(serde_json::to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError)?; + if condition { Box::pin(add_payment_method(state, req, merchant_account, key_store)).await } else { @@ -298,10 +618,11 @@ pub async fn get_client_secret_or_add_payment_method( None, None, key_store, - None, + connector_mandate_details, Some(enums::PaymentMethodStatus::AwaitingData), None, merchant_account.storage_scheme, + payment_method_billing_address, None, ) .await?; @@ -322,7 +643,7 @@ pub async fn get_client_secret_or_add_payment_method( } Ok(services::api::ApplicationResponse::Json( - api::PaymentMethodResponse::foreign_from(res), + api::PaymentMethodResponse::foreign_from((None, res)), )) } } @@ -544,6 +865,16 @@ pub async fn add_payment_method( let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; let payment_method = req.payment_method.get_required_value("payment_method")?; + let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone()) + .await + .map(|details| details.into()); + + let connector_mandate_details = req + .connector_mandate_details + .clone() + .map(serde_json::to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError)?; let response = match payment_method { #[cfg(feature = "payouts")] @@ -567,11 +898,20 @@ pub async fn add_payment_method( }, api_enums::PaymentMethod::Card => match req.card.clone() { Some(card) => { - helpers::validate_card_expiry(&card.card_exp_month, &card.card_exp_year)?; + let mut card_details = card; + card_details = helpers::populate_bin_details_for_payment_method_create( + card_details.clone(), + db, + ) + .await; + helpers::validate_card_expiry( + &card_details.card_exp_month, + &card_details.card_exp_year, + )?; Box::pin(add_card_to_locker( &state, req.clone(), - &card, + &card_details, &customer_id, merchant_account, None, @@ -731,17 +1071,17 @@ pub async fn add_payment_method( let pm = insert_payment_method( db, &resp, - req, + &req, key_store, merchant_id, &customer_id, pm_metadata.cloned(), None, locker_id, - None, - None, + connector_mandate_details, + req.network_transaction_id.clone(), merchant_account.storage_scheme, - None, + payment_method_billing_address, ) .await?; @@ -756,7 +1096,7 @@ pub async fn add_payment_method( pub async fn insert_payment_method( db: &dyn db::StorageInterface, resp: &api::PaymentMethodResponse, - req: api::PaymentMethodCreate, + req: &api::PaymentMethodCreate, key_store: &domain::MerchantKeyStore, merchant_id: &str, customer_id: &id_type::CustomerId, @@ -770,7 +1110,7 @@ pub async fn insert_payment_method( ) -> errors::RouterResult { let pm_card_details = resp .card - .as_ref() + .clone() .map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()))); let pm_data_encrypted = create_encrypted_data(key_store, pm_card_details) .await @@ -778,7 +1118,7 @@ pub async fn insert_payment_method( create_payment_method( db, - &req, + req, customer_id, &resp.payment_method_id, locker_id, @@ -792,6 +1132,10 @@ pub async fn insert_payment_method( network_transaction_id, storage_scheme, payment_method_billing_address, + resp.card.clone().and_then(|card| { + card.card_network + .map(|card_network| card_network.to_string()) + }), ) .await } @@ -904,6 +1248,9 @@ pub async fn update_customer_payment_method( client_secret: pm.client_secret.clone(), payment_method_data: None, card_network: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }; new_pm.validate()?; @@ -1135,7 +1482,7 @@ pub async fn add_card_to_locker( errors::VaultError, > { metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]); - let add_card_to_hs_resp = common_utils::metrics::utils::record_operation_time( + let add_card_to_hs_resp = Box::pin(common_utils::metrics::utils::record_operation_time( async { add_card_hs( state, @@ -1162,7 +1509,7 @@ pub async fn add_card_to_locker( &metrics::CARD_ADD_TIME, &metrics::CONTEXT, &[router_env::opentelemetry::KeyValue::new("locker", "rust")], - ) + )) .await?; logger::debug!("card added to hyperswitch-card-vault"); @@ -3677,12 +4024,8 @@ pub async fn get_card_details_with_locker_fallback( }); Ok(if let Some(mut crd) = card_decrypted { - if crd.saved_to_locker { - crd.scheme.clone_from(&pm.scheme); - Some(crd) - } else { - None - } + crd.scheme.clone_from(&pm.scheme); + Some(crd) } else { logger::debug!( "Getting card details from locker as it is not found in payment methods table" diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 887c02dd0237..6bfadabdb35b 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -348,7 +348,10 @@ pub fn mk_add_card_response_hs( let card_isin = card_number.get_card_isin(); let card = api::CardDetailFromLocker { - scheme: None, + scheme: card + .card_network + .clone() + .map(|card_network| card_network.to_string()), last4_digits: Some(last4_digits), issuer_country: card.card_issuing_country, card_number: Some(card.card_number.clone()), diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c19480f1d566..745a69a1f563 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1385,6 +1385,9 @@ pub(crate) async fn get_payment_method_create_request( .map(|card_network| card_network.to_string()), client_secret: None, payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }; Ok(payment_method_request) } @@ -1404,6 +1407,9 @@ pub(crate) async fn get_payment_method_create_request( card_network: None, client_secret: None, payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }; Ok(payment_method_request) @@ -4074,6 +4080,63 @@ pub async fn get_additional_payment_data( } } +pub async fn populate_bin_details_for_payment_method_create( + card_details: api_models::payment_methods::CardDetail, + db: &dyn StorageInterface, +) -> api_models::payment_methods::CardDetail { + let card_isin: Option<_> = Some(card_details.card_number.get_card_isin()); + if card_details.card_issuer.is_some() + && card_details.card_network.is_some() + && card_details.card_type.is_some() + && card_details.card_issuing_country.is_some() + { + api::CardDetail { + card_issuer: card_details.card_issuer.to_owned(), + card_network: card_details.card_network.clone(), + card_type: card_details.card_type.to_owned(), + card_issuing_country: card_details.card_issuing_country.to_owned(), + 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(), + card_number: card_details.card_number.clone(), + nick_name: card_details.nick_name.clone(), + } + } else { + let card_info = card_isin + .clone() + .async_and_then(|card_isin| async move { + db.get_card_info(&card_isin) + .await + .map_err(|error| services::logger::error!(card_info_error=?error)) + .ok() + }) + .await + .flatten() + .map(|card_info| api::CardDetail { + card_issuer: card_info.card_issuer, + card_network: card_info.card_network.clone(), + card_type: card_info.card_type, + card_issuing_country: card_info.card_issuing_country, + 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(), + card_number: card_details.card_number.clone(), + nick_name: card_details.nick_name.clone(), + }); + card_info.unwrap_or_else(|| api::CardDetail { + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + 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(), + card_number: card_details.card_number.clone(), + nick_name: card_details.nick_name.clone(), + }) + } +} + pub fn validate_customer_access( payment_intent: &PaymentIntent, auth_flow: services::AuthFlow, @@ -5057,3 +5120,36 @@ pub async fn override_setup_future_usage_to_on_session( }; Ok(()) } + +pub async fn validate_merchant_connector_ids_in_connector_mandate_details( + db: &dyn StorageInterface, + key_store: &domain::MerchantKeyStore, + connector_mandate_details: &api_models::payment_methods::PaymentsMandateReference, + merchant_id: &str, +) -> CustomResult<(), errors::ApiErrorResponse> { + let merchant_connector_account_list = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?; + + let merchant_connector_ids: Vec = merchant_connector_account_list + .iter() + .map(|merchant_connector_account| merchant_connector_account.merchant_connector_id.clone()) + .collect(); + + for merchant_connector_id in connector_mandate_details.0.keys() { + if !merchant_connector_ids.contains(merchant_connector_id) { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_connector_id", + }) + .attach_printable_lazy(|| { + format!("{merchant_connector_id} invalid merchant connector id in connector_mandate_details") + })? + } + } + Ok(()) +} diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f63278cddc4d..3fb67abd57e4 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -318,6 +318,10 @@ where network_transaction_id, merchant_account.storage_scheme, encrypted_payment_method_billing_address, + resp.card.and_then(|card| { + card.card_network + .map(|card_network| card_network.to_string()) + }), ) .await } else { @@ -397,7 +401,7 @@ where payment_methods::cards::insert_payment_method( db, &resp, - payment_method_create_request.clone(), + &payment_method_create_request.clone(), key_store, &merchant_account.merchant_id, &customer_id, @@ -602,6 +606,10 @@ where network_transaction_id, merchant_account.storage_scheme, encrypted_payment_method_billing_address, + resp.card.and_then(|card| { + card.card_network + .map(|card_network| card_network.to_string()) + }), ) .await?; }; diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index ba00370e881f..f68a5271fd74 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -391,6 +391,9 @@ pub async fn save_payout_data_to_locker( card_network: None, client_secret: None, payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }; let pm_data = card_isin @@ -472,6 +475,9 @@ pub async fn save_payout_data_to_locker( card_network: None, client_secret: None, payment_method_data: None, + billing: None, + connector_mandate_details: None, + network_transaction_id: None, }, ) }; @@ -495,6 +501,7 @@ pub async fn save_payout_data_to_locker( None, merchant_account.storage_scheme, None, + None, ) .await?; } diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 508b9e8ef8ba..d3393b5b1f78 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -478,7 +478,6 @@ async fn store_bank_details_in_payment_methods( last_used_at: now, connector_mandate_details: None, customer_acceptance: None, - network_transaction_id: None, client_secret: None, payment_method_billing_address: None, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4fad6f863e93..ba94879cd68d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -931,6 +931,9 @@ impl PaymentMethods { .route(web::post().to(create_payment_method_api)) .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)), + ) .service( web::resource("/collect").route(web::post().to(initiate_pm_collect_link_flow)), ) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index fb5be71b5217..56e8751a9794 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -93,6 +93,7 @@ impl From for ApiIdentifier { Flow::MandatesRetrieve | Flow::MandatesRevoke | Flow::MandatesList => Self::Mandates, Flow::PaymentMethodsCreate + | Flow::PaymentMethodsMigrate | Flow::PaymentMethodsList | Flow::CustomerPaymentMethodsList | Flow::PaymentMethodsRetrieve diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index bc748572150d..8ff6e48237c2 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -46,6 +46,26 @@ pub async fn create_payment_method_api( .await } +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsMigrate))] +pub async fn migrate_payment_method_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodsMigrate; + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _, req, _| async move { Box::pin(cards::migrate_payment_method(state, req)).await }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSave))] pub async fn save_payment_method_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 ef92c099735b..77873503d6e4 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -4,9 +4,10 @@ pub use api_models::payment_methods::{ GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, - PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, - PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, - TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, + PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, + PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, + TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, + TokenizedWalletValue2, }; use error_stack::report; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 8f3ef929b1d3..49afaf2e474b 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -80,15 +80,25 @@ impl ForeignFrom for storage_enums::RefundType } } -impl ForeignFrom for payment_methods::PaymentMethodResponse { - fn foreign_from(item: diesel_models::PaymentMethod) -> Self { +impl + ForeignFrom<( + Option, + diesel_models::PaymentMethod, + )> for payment_methods::PaymentMethodResponse +{ + fn foreign_from( + (card_details, item): ( + Option, + diesel_models::PaymentMethod, + ), + ) -> Self { Self { merchant_id: item.merchant_id, customer_id: Some(item.customer_id), payment_method_id: item.payment_method_id, payment_method: item.payment_method, payment_method_type: item.payment_method_type, - card: None, + card: card_details, recurring_enabled: false, installment_payment_enabled: false, payment_experience: None, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 733876d0ee60..2fef21d420cb 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -111,6 +111,8 @@ pub enum Flow { MandatesList, /// Payment methods create flow. PaymentMethodsCreate, + /// Payment methods migrate flow. + PaymentMethodsMigrate, /// Payment methods list flow. PaymentMethodsList, /// Payment method save flow