diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index d5255b5e8c5..47460e73b71 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -6,6 +6,8 @@ use cards::CardNumber; use common_utils::{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, + errors, + ext_traits::OptionExt, id_type, link_utils, pii, types::{MinorUnit, Percentage, Surcharge}, }; @@ -2065,16 +2067,16 @@ pub struct PaymentMethodRecord { pub email: Option, pub phone: Option>, pub phone_country_code: Option, - pub merchant_id: id_type::MerchantId, + pub merchant_id: Option, pub payment_method: Option, pub payment_method_type: Option, pub nick_name: masking::Secret, - pub payment_instrument_id: masking::Secret, + pub payment_instrument_id: Option>, pub card_number_masked: masking::Secret, pub card_expiry_month: masking::Secret, pub card_expiry_year: masking::Secret, pub card_scheme: Option, - pub original_transaction_id: String, + pub original_transaction_id: Option, pub billing_address_zip: masking::Secret, pub billing_address_state: masking::Secret, pub billing_address_first_name: masking::Secret, @@ -2085,7 +2087,7 @@ pub struct PaymentMethodRecord { pub billing_address_line2: Option>, pub billing_address_line3: Option>, pub raw_card_number: Option>, - pub merchant_connector_id: id_type::MerchantConnectorAccountId, + pub merchant_connector_id: Option, pub original_transaction_amount: Option, pub original_transaction_currency: Option, pub line_number: Option, @@ -2171,31 +2173,54 @@ impl From for PaymentMethodMigrationResponse } } -impl From for PaymentMethodMigrate { - fn from(record: PaymentMethodRecord) -> Self { - let mut mandate_reference = HashMap::new(); - mandate_reference.insert( - record.merchant_connector_id, - PaymentsMandateReferenceRecord { - connector_mandate_id: record.payment_instrument_id.peek().to_string(), - payment_method_type: record.payment_method_type, - original_payment_authorized_amount: record.original_transaction_amount, - original_payment_authorized_currency: record.original_transaction_currency, - }, - ); - Self { - merchant_id: record.merchant_id, +impl + TryFrom<( + PaymentMethodRecord, + id_type::MerchantId, + Option, + )> for PaymentMethodMigrate +{ + type Error = error_stack::Report; + fn try_from( + item: ( + PaymentMethodRecord, + id_type::MerchantId, + Option, + ), + ) -> Result { + let (record, merchant_id, mca_id) = item; + + // if payment instrument id is present then only construct this + let connector_mandate_details = if record.payment_instrument_id.is_some() { + Some(PaymentsMandateReference(HashMap::from([( + mca_id.get_required_value("merchant_connector_id")?, + PaymentsMandateReferenceRecord { + connector_mandate_id: record + .payment_instrument_id + .get_required_value("payment_instrument_id")? + .peek() + .to_string(), + payment_method_type: record.payment_method_type, + original_payment_authorized_amount: record.original_transaction_amount, + original_payment_authorized_currency: record.original_transaction_currency, + }, + )]))) + } else { + None + }; + Ok(Self { + merchant_id, customer_id: Some(record.customer_id), card: Some(MigrateCardDetail { card_number: record.raw_card_number.unwrap_or(record.card_number_masked), card_exp_month: record.card_expiry_month, card_exp_year: record.card_expiry_year, - card_holder_name: record.name, + card_holder_name: record.name.clone(), card_network: None, card_type: None, card_issuer: None, card_issuing_country: None, - nick_name: Some(record.nick_name), + nick_name: Some(record.nick_name.clone()), }), payment_method: record.payment_method, payment_method_type: record.payment_method_type, @@ -2218,7 +2243,7 @@ impl From for PaymentMethodMigrate { }), email: record.email, }), - connector_mandate_details: Some(PaymentsMandateReference(mandate_reference)), + connector_mandate_details, metadata: None, payment_method_issuer_code: None, card_network: None, @@ -2227,17 +2252,18 @@ impl From for PaymentMethodMigrate { #[cfg(feature = "payouts")] wallet: None, payment_method_data: None, - network_transaction_id: record.original_transaction_id.into(), - } + network_transaction_id: record.original_transaction_id, + }) } } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -impl From for customers::CustomerRequest { - fn from(record: PaymentMethodRecord) -> Self { +impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerRequest { + fn from(value: (PaymentMethodRecord, id_type::MerchantId)) -> Self { + let (record, merchant_id) = value; Self { customer_id: Some(record.customer_id), - merchant_id: record.merchant_id, + merchant_id, name: record.name, email: record.email, phone: record.phone, diff --git a/crates/router/src/core/payment_methods/migration.rs b/crates/router/src/core/payment_methods/migration.rs index d548d03ab47..af9e01d253c 100644 --- a/crates/router/src/core/payment_methods/migration.rs +++ b/crates/router/src/core/payment_methods/migration.rs @@ -1,6 +1,7 @@ -use actix_multipart::form::{bytes::Bytes, MultipartForm}; +use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; use api_models::payment_methods::{PaymentMethodMigrationResponse, PaymentMethodRecord}; use csv::Reader; +use error_stack::ResultExt; use rdkafka::message::ToBytes; use crate::{ @@ -15,12 +16,32 @@ pub async fn migrate_payment_methods( merchant_id: &common_utils::id_type::MerchantId, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, + mca_id: Option, ) -> errors::RouterResponse> { let mut result = Vec::new(); for record in payment_methods { + let req = api::PaymentMethodMigrate::try_from(( + record.clone(), + merchant_id.clone(), + mca_id.clone(), + )) + .map_err(|err| errors::ApiErrorResponse::InvalidRequestData { + message: format!("error: {:?}", err), + }) + .attach_printable("record deserialization failed"); + match req { + Ok(_) => (), + Err(e) => { + result.push(PaymentMethodMigrationResponse::from(( + Err(e.to_string()), + record, + ))); + continue; + } + }; let res = migrate_payment_method( state.clone(), - api::PaymentMethodMigrate::from(record.clone()), + req?, merchant_id, merchant_account, key_store, @@ -42,6 +63,10 @@ pub async fn migrate_payment_methods( pub struct PaymentMethodsMigrateForm { #[multipart(limit = "1MB")] pub file: Bytes, + + pub merchant_id: Text, + + pub merchant_connector_id: Text>, } fn parse_csv(data: &[u8]) -> csv::Result> { @@ -58,26 +83,19 @@ fn parse_csv(data: &[u8]) -> csv::Result> { } pub fn get_payment_method_records( form: PaymentMethodsMigrateForm, -) -> Result<(common_utils::id_type::MerchantId, Vec), errors::ApiErrorResponse> -{ +) -> Result< + ( + common_utils::id_type::MerchantId, + Vec, + Option, + ), + errors::ApiErrorResponse, +> { match parse_csv(form.file.data.to_bytes()) { Ok(records) => { - if let Some(first_record) = records.first() { - if records - .iter() - .all(|merchant_id| merchant_id.merchant_id == first_record.merchant_id) - { - Ok((first_record.merchant_id.clone(), records)) - } else { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: "Only one merchant id can be updated at a time".to_string(), - }) - } - } else { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: "No records found".to_string(), - }) - } + let merchant_id = form.merchant_id.clone(); + let mca_id = form.merchant_connector_id.clone(); + Ok((merchant_id.clone(), records, mca_id)) } Err(e) => Err(errors::ApiErrorResponse::PreconditionFailed { message: e.to_string(), diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 0dbddbef77b..3b47a06cf7c 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -331,10 +331,13 @@ pub async fn migrate_payment_methods( MultipartForm(form): MultipartForm, ) -> HttpResponse { let flow = Flow::PaymentMethodsMigrate; - let (merchant_id, records) = match migration::get_payment_method_records(form) { - Ok((merchant_id, records)) => (merchant_id, records), - Err(e) => return api::log_and_return_error_response(e.into()), - }; + let (merchant_id, records, merchant_connector_id) = + match migration::get_payment_method_records(form) { + Ok((merchant_id, records, merchant_connector_id)) => { + (merchant_id, records, merchant_connector_id) + } + Err(e) => return api::log_and_return_error_response(e.into()), + }; Box::pin(api::server_wrap( flow, state, @@ -342,6 +345,7 @@ pub async fn migrate_payment_methods( records, |state, _, req, _| { let merchant_id = merchant_id.clone(); + let merchant_connector_id = merchant_connector_id.clone(); async move { let (key_store, merchant_account) = get_merchant_account(&state, &merchant_id).await?; @@ -349,7 +353,7 @@ pub async fn migrate_payment_methods( customers::migrate_customers( state.clone(), req.iter() - .map(|e| CustomerRequest::from(e.clone())) + .map(|e| CustomerRequest::from((e.clone(), merchant_id.clone()))) .collect(), merchant_account.clone(), key_store.clone(), @@ -362,6 +366,7 @@ pub async fn migrate_payment_methods( &merchant_id, &merchant_account, &key_store, + merchant_connector_id, )) .await }