feat(data-migration): add connector customer and mandate details support for multiple profiles (#8473)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2025-07-01 15:02:24 +05:30
committed by GitHub
parent 1ae30247ca
commit ce2b90b3d3
9 changed files with 437 additions and 174 deletions

View File

@ -19,7 +19,7 @@ use utoipa::{schema, ToSchema};
#[cfg(feature = "payouts")] #[cfg(feature = "payouts")]
use crate::payouts; use crate::payouts;
use crate::{ use crate::{
admin, customers, enums as api_enums, admin, enums as api_enums,
payments::{self, BankCodeResponse}, payments::{self, BankCodeResponse},
}; };
@ -2517,6 +2517,7 @@ pub struct PaymentMethodRecord {
pub billing_address_line3: Option<masking::Secret<String>>, pub billing_address_line3: Option<masking::Secret<String>>,
pub raw_card_number: Option<masking::Secret<String>>, pub raw_card_number: Option<masking::Secret<String>>,
pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>, pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
pub merchant_connector_ids: Option<String>,
pub original_transaction_amount: Option<i64>, pub original_transaction_amount: Option<i64>,
pub original_transaction_currency: Option<common_enums::Currency>, pub original_transaction_currency: Option<common_enums::Currency>,
pub line_number: Option<i64>, pub line_number: Option<i64>,
@ -2526,18 +2527,6 @@ pub struct PaymentMethodRecord {
pub network_token_requestor_ref_id: Option<String>, pub network_token_requestor_ref_id: Option<String>,
} }
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ConnectorCustomerDetails {
pub connector_customer_id: String,
pub merchant_connector_id: id_type::MerchantConnectorAccountId,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PaymentMethodCustomerMigrate {
pub customer: customers::CustomerRequest,
pub connector_customer_details: Option<ConnectorCustomerDetails>,
}
#[derive(Debug, Default, serde::Serialize)] #[derive(Debug, Default, serde::Serialize)]
pub struct PaymentMethodMigrationResponse { pub struct PaymentMethodMigrationResponse {
pub line_number: Option<i64>, pub line_number: Option<i64>,
@ -2654,47 +2643,56 @@ impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse
impl impl
TryFrom<( TryFrom<(
PaymentMethodRecord, &PaymentMethodRecord,
id_type::MerchantId, id_type::MerchantId,
Option<id_type::MerchantConnectorAccountId>, Option<&Vec<id_type::MerchantConnectorAccountId>>,
)> for PaymentMethodMigrate )> for PaymentMethodMigrate
{ {
type Error = error_stack::Report<errors::ValidationError>; type Error = error_stack::Report<errors::ValidationError>;
fn try_from( fn try_from(
item: ( item: (
PaymentMethodRecord, &PaymentMethodRecord,
id_type::MerchantId, id_type::MerchantId,
Option<id_type::MerchantConnectorAccountId>, Option<&Vec<id_type::MerchantConnectorAccountId>>,
), ),
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let (record, merchant_id, mca_id) = item; let (record, merchant_id, mca_ids) = item;
let billing = record.create_billing(); let billing = record.create_billing();
let connector_mandate_details = if let Some(payment_instrument_id) =
// if payment instrument id is present then only construct this &record.payment_instrument_id
let connector_mandate_details = if record.payment_instrument_id.is_some() { {
Some(PaymentsMandateReference(HashMap::from([( let ids = mca_ids.get_required_value("mca_ids")?;
mca_id.get_required_value("merchant_connector_id")?, let mandate_map: HashMap<_, _> = ids
PaymentsMandateReferenceRecord { .iter()
connector_mandate_id: record .map(|mca_id| {
.payment_instrument_id (
.get_required_value("payment_instrument_id")? mca_id.clone(),
.peek() PaymentsMandateReferenceRecord {
.to_string(), connector_mandate_id: payment_instrument_id.peek().to_string(),
payment_method_type: record.payment_method_type, payment_method_type: record.payment_method_type,
original_payment_authorized_amount: record.original_transaction_amount, original_payment_authorized_amount: record.original_transaction_amount,
original_payment_authorized_currency: record.original_transaction_currency, original_payment_authorized_currency: record
}, .original_transaction_currency,
)]))) },
)
})
.collect();
Some(PaymentsMandateReference(mandate_map))
} else { } else {
None None
}; };
Ok(Self { Ok(Self {
merchant_id, merchant_id,
customer_id: Some(record.customer_id), customer_id: Some(record.customer_id.clone()),
card: Some(MigrateCardDetail { card: Some(MigrateCardDetail {
card_number: record.raw_card_number.unwrap_or(record.card_number_masked), card_number: record
card_exp_month: record.card_expiry_month, .raw_card_number
card_exp_year: record.card_expiry_year, .clone()
.unwrap_or_else(|| record.card_number_masked.clone()),
card_exp_month: record.card_expiry_month.clone(),
card_exp_year: record.card_expiry_year.clone(),
card_holder_name: record.name.clone(), card_holder_name: record.name.clone(),
card_network: None, card_network: None,
card_type: None, card_type: None,
@ -2704,10 +2702,16 @@ impl
}), }),
network_token: Some(MigrateNetworkTokenDetail { network_token: Some(MigrateNetworkTokenDetail {
network_token_data: MigrateNetworkTokenData { network_token_data: MigrateNetworkTokenData {
network_token_number: record.network_token_number.unwrap_or_default(), network_token_number: record.network_token_number.clone().unwrap_or_default(),
network_token_exp_month: record.network_token_expiry_month.unwrap_or_default(), network_token_exp_month: record
network_token_exp_year: record.network_token_expiry_year.unwrap_or_default(), .network_token_expiry_month
card_holder_name: record.name, .clone()
.unwrap_or_default(),
network_token_exp_year: record
.network_token_expiry_year
.clone()
.unwrap_or_default(),
card_holder_name: record.name.clone(),
nick_name: record.nick_name.clone(), nick_name: record.nick_name.clone(),
card_issuing_country: None, card_issuing_country: None,
card_network: None, card_network: None,
@ -2716,6 +2720,7 @@ impl
}, },
network_token_requestor_ref_id: record network_token_requestor_ref_id: record
.network_token_requestor_ref_id .network_token_requestor_ref_id
.clone()
.unwrap_or_default(), .unwrap_or_default(),
}), }),
payment_method: record.payment_method, payment_method: record.payment_method,
@ -2740,45 +2745,6 @@ impl
} }
} }
#[cfg(feature = "v1")]
impl From<(PaymentMethodRecord, id_type::MerchantId)> for PaymentMethodCustomerMigrate {
fn from(value: (PaymentMethodRecord, id_type::MerchantId)) -> Self {
let (record, merchant_id) = value;
Self {
customer: customers::CustomerRequest {
customer_id: Some(record.customer_id),
merchant_id,
name: record.name,
email: record.email,
phone: record.phone,
description: None,
phone_country_code: record.phone_country_code,
address: Some(payments::AddressDetails {
city: record.billing_address_city,
country: record.billing_address_country,
line1: record.billing_address_line1,
line2: record.billing_address_line2,
state: record.billing_address_state,
line3: record.billing_address_line3,
zip: record.billing_address_zip,
first_name: record.billing_address_first_name,
last_name: record.billing_address_last_name,
}),
metadata: None,
},
connector_customer_details: record
.connector_customer_id
.zip(record.merchant_connector_id)
.map(
|(connector_customer_id, merchant_connector_id)| ConnectorCustomerDetails {
connector_customer_id,
merchant_connector_id,
},
),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct CardNetworkTokenizeRequest { pub struct CardNetworkTokenizeRequest {
/// Merchant ID associated with the tokenization request /// Merchant ID associated with the tokenization request

View File

@ -36,6 +36,7 @@ pub mod router_response_types;
pub mod routing; pub mod routing;
#[cfg(feature = "tokenization_v2")] #[cfg(feature = "tokenization_v2")]
pub mod tokenization; pub mod tokenization;
pub mod transformers;
pub mod type_encryption; pub mod type_encryption;
pub mod types; pub mod types;
pub mod vault; pub mod vault;

View File

@ -1,5 +1,6 @@
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
use api_models::payment_methods::PaymentMethodsData; use api_models::payment_methods::PaymentMethodsData;
use api_models::{customers, payment_methods, payments};
// specific imports because of using the macro // specific imports because of using the macro
use common_enums::enums::MerchantStorageScheme; use common_enums::enums::MerchantStorageScheme;
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
@ -27,11 +28,12 @@ use time::PrimitiveDateTime;
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
use crate::address::Address; use crate::address::Address;
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
use crate::{mandates, type_encryption::AsyncLift}; use crate::type_encryption::AsyncLift;
use crate::{ use crate::{
mandates::CommonMandateReference, mandates::{self, CommonMandateReference},
merchant_key_store::MerchantKeyStore, merchant_key_store::MerchantKeyStore,
payment_method_data as domain_payment_method_data, payment_method_data as domain_payment_method_data,
transformers::ForeignTryFrom,
type_encryption::{crypto_operation, CryptoOperation}, type_encryption::{crypto_operation, CryptoOperation},
}; };
@ -87,7 +89,6 @@ pub struct PaymentMethod {
pub network_token_locker_id: Option<String>, pub network_token_locker_id: Option<String>,
pub network_token_payment_method_data: OptionalEncryptableValue, pub network_token_payment_method_data: OptionalEncryptableValue,
} }
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
#[derive(Clone, Debug, router_derive::ToEncryption)] #[derive(Clone, Debug, router_derive::ToEncryption)]
pub struct PaymentMethod { pub struct PaymentMethod {
@ -915,6 +916,136 @@ pub struct PaymentMethodsSessionUpdateInternal {
pub tokenization_data: Option<pii::SecretSerdeValue>, pub tokenization_data: Option<pii::SecretSerdeValue>,
} }
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ConnectorCustomerDetails {
pub connector_customer_id: String,
pub merchant_connector_id: id_type::MerchantConnectorAccountId,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PaymentMethodCustomerMigrate {
pub customer: customers::CustomerRequest,
pub connector_customer_details: Option<Vec<ConnectorCustomerDetails>>,
}
#[cfg(feature = "v1")]
impl TryFrom<(payment_methods::PaymentMethodRecord, id_type::MerchantId)>
for PaymentMethodCustomerMigrate
{
type Error = error_stack::Report<ValidationError>;
fn try_from(
value: (payment_methods::PaymentMethodRecord, id_type::MerchantId),
) -> Result<Self, Self::Error> {
let (record, merchant_id) = value;
let connector_customer_details = record
.connector_customer_id
.and_then(|connector_customer_id| {
// Handle single merchant_connector_id
record
.merchant_connector_id
.as_ref()
.map(|merchant_connector_id| {
Ok(vec![ConnectorCustomerDetails {
connector_customer_id: connector_customer_id.clone(),
merchant_connector_id: merchant_connector_id.clone(),
}])
})
// Handle comma-separated merchant_connector_ids
.or_else(|| {
record
.merchant_connector_ids
.as_ref()
.map(|merchant_connector_ids_str| {
merchant_connector_ids_str
.split(',')
.map(|id| id.trim())
.filter(|id| !id.is_empty())
.map(|merchant_connector_id| {
id_type::MerchantConnectorAccountId::wrap(
merchant_connector_id.to_string(),
)
.map_err(|_| {
error_stack::report!(ValidationError::InvalidValue {
message: format!(
"Invalid merchant_connector_account_id: {}",
merchant_connector_id
),
})
})
.map(
|merchant_connector_id| ConnectorCustomerDetails {
connector_customer_id: connector_customer_id
.clone(),
merchant_connector_id,
},
)
})
.collect::<Result<Vec<_>, _>>()
})
})
})
.transpose()?;
Ok(Self {
customer: customers::CustomerRequest {
customer_id: Some(record.customer_id),
merchant_id,
name: record.name,
email: record.email,
phone: record.phone,
description: None,
phone_country_code: record.phone_country_code,
address: Some(payments::AddressDetails {
city: record.billing_address_city,
country: record.billing_address_country,
line1: record.billing_address_line1,
line2: record.billing_address_line2,
state: record.billing_address_state,
line3: record.billing_address_line3,
zip: record.billing_address_zip,
first_name: record.billing_address_first_name,
last_name: record.billing_address_last_name,
}),
metadata: None,
},
connector_customer_details,
})
}
}
#[cfg(feature = "v1")]
impl ForeignTryFrom<(&[payment_methods::PaymentMethodRecord], id_type::MerchantId)>
for Vec<PaymentMethodCustomerMigrate>
{
type Error = error_stack::Report<ValidationError>;
fn foreign_try_from(
(records, merchant_id): (&[payment_methods::PaymentMethodRecord], id_type::MerchantId),
) -> Result<Self, Self::Error> {
let (customers_migration, migration_errors): (Self, Vec<_>) = records
.iter()
.map(|record| {
PaymentMethodCustomerMigrate::try_from((record.clone(), merchant_id.clone()))
})
.fold((Self::new(), Vec::new()), |mut acc, result| {
match result {
Ok(customer) => acc.0.push(customer),
Err(e) => acc.1.push(e.to_string()),
}
acc
});
migration_errors
.is_empty()
.then_some(customers_migration)
.ok_or_else(|| {
error_stack::report!(ValidationError::InvalidValue {
message: migration_errors.join(", "),
})
})
}
}
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -0,0 +1,9 @@
pub trait ForeignFrom<F> {
fn foreign_from(from: F) -> Self;
}
pub trait ForeignTryFrom<F>: Sized {
type Error;
fn foreign_try_from(from: F) -> Result<Self, Self::Error>;
}

View File

@ -24,20 +24,22 @@ pub async fn migrate_payment_methods(
payment_methods: Vec<pm_api::PaymentMethodRecord>, payment_methods: Vec<pm_api::PaymentMethodRecord>,
merchant_id: &common_utils::id_type::MerchantId, merchant_id: &common_utils::id_type::MerchantId,
merchant_context: &merchant_context::MerchantContext, merchant_context: &merchant_context::MerchantContext,
mca_id: Option<common_utils::id_type::MerchantConnectorAccountId>, mca_ids: Option<Vec<common_utils::id_type::MerchantConnectorAccountId>>,
controller: &dyn pm::PaymentMethodsController, controller: &dyn pm::PaymentMethodsController,
) -> PmMigrationResult<Vec<pm_api::PaymentMethodMigrationResponse>> { ) -> PmMigrationResult<Vec<pm_api::PaymentMethodMigrationResponse>> {
let mut result = Vec::new(); let mut result = Vec::with_capacity(payment_methods.len());
for record in payment_methods { for record in payment_methods {
let req = pm_api::PaymentMethodMigrate::try_from(( let req = pm_api::PaymentMethodMigrate::try_from((
record.clone(), &record,
merchant_id.clone(), merchant_id.clone(),
mca_id.clone(), mca_ids.as_ref(),
)) ))
.map_err(|err| errors::ApiErrorResponse::InvalidRequestData { .map_err(|err| errors::ApiErrorResponse::InvalidRequestData {
message: format!("error: {:?}", err), message: format!("error: {:?}", err),
}) })
.attach_printable("record deserialization failed"); .attach_printable("record deserialization failed");
let res = match req { let res = match req {
Ok(migrate_request) => { Ok(migrate_request) => {
let res = migrate_payment_method( let res = migrate_payment_method(
@ -56,6 +58,7 @@ pub async fn migrate_payment_methods(
} }
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
}; };
result.push(pm_api::PaymentMethodMigrationResponse::from((res, record))); result.push(pm_api::PaymentMethodMigrationResponse::from((res, record)));
} }
Ok(api::ApplicationResponse::Json(result)) Ok(api::ApplicationResponse::Json(result))
@ -69,7 +72,138 @@ pub struct PaymentMethodsMigrateForm {
pub merchant_id: text::Text<common_utils::id_type::MerchantId>, pub merchant_id: text::Text<common_utils::id_type::MerchantId>,
pub merchant_connector_id: pub merchant_connector_id:
text::Text<Option<common_utils::id_type::MerchantConnectorAccountId>>, Option<text::Text<common_utils::id_type::MerchantConnectorAccountId>>,
pub merchant_connector_ids: Option<text::Text<String>>,
}
struct MerchantConnectorValidator;
impl MerchantConnectorValidator {
fn parse_comma_separated_ids(
ids_string: &str,
) -> Result<Vec<common_utils::id_type::MerchantConnectorAccountId>, errors::ApiErrorResponse>
{
// Estimate capacity based on comma count
let capacity = ids_string.matches(',').count() + 1;
let mut result = Vec::with_capacity(capacity);
for id in ids_string.split(',') {
let trimmed_id = id.trim();
if !trimmed_id.is_empty() {
let mca_id =
common_utils::id_type::MerchantConnectorAccountId::wrap(trimmed_id.to_string())
.map_err(|_| errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"Invalid merchant_connector_account_id: {}",
trimmed_id
),
})?;
result.push(mca_id);
}
}
Ok(result)
}
fn validate_form_csv_conflicts(
records: &[pm_api::PaymentMethodRecord],
form_has_single_id: bool,
form_has_multiple_ids: bool,
) -> Result<(), errors::ApiErrorResponse> {
if form_has_single_id {
// If form has merchant_connector_id, CSV records should not have merchant_connector_ids
for (index, record) in records.iter().enumerate() {
if record.merchant_connector_ids.is_some() {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"Record at line {} has merchant_connector_ids but form has merchant_connector_id. Only one should be provided",
index + 1
),
});
}
}
}
if form_has_multiple_ids {
// If form has merchant_connector_ids, CSV records should not have merchant_connector_id
for (index, record) in records.iter().enumerate() {
if record.merchant_connector_id.is_some() {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"Record at line {} has merchant_connector_id but form has merchant_connector_ids. Only one should be provided",
index + 1
),
});
}
}
}
Ok(())
}
}
type MigrationValidationResult = Result<
(
common_utils::id_type::MerchantId,
Vec<pm_api::PaymentMethodRecord>,
Option<Vec<common_utils::id_type::MerchantConnectorAccountId>>,
),
errors::ApiErrorResponse,
>;
impl PaymentMethodsMigrateForm {
pub fn validate_and_get_payment_method_records(self) -> MigrationValidationResult {
// Step 1: Validate form-level conflicts
let form_has_single_id = self.merchant_connector_id.is_some();
let form_has_multiple_ids = self.merchant_connector_ids.is_some();
if form_has_single_id && form_has_multiple_ids {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: "Both merchant_connector_id and merchant_connector_ids cannot be provided"
.to_string(),
});
}
// Ensure at least one is provided
if !form_has_single_id && !form_has_multiple_ids {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: "Either merchant_connector_id or merchant_connector_ids must be provided"
.to_string(),
});
}
// Step 2: Parse CSV
let records = parse_csv(self.file.data.to_bytes()).map_err(|e| {
errors::ApiErrorResponse::PreconditionFailed {
message: e.to_string(),
}
})?;
// Step 3: Validate CSV vs Form conflicts
MerchantConnectorValidator::validate_form_csv_conflicts(
&records,
form_has_single_id,
form_has_multiple_ids,
)?;
// Step 4: Prepare the merchant connector account IDs for return
let mca_ids = if let Some(ref single_id) = self.merchant_connector_id {
Some(vec![(**single_id).clone()])
} else if let Some(ref ids_string) = self.merchant_connector_ids {
let parsed_ids = MerchantConnectorValidator::parse_comma_separated_ids(ids_string)?;
if parsed_ids.is_empty() {
None
} else {
Some(parsed_ids)
}
} else {
None
};
// Step 5: Return the updated structure
Ok((self.merchant_id.clone(), records, mca_ids))
}
} }
fn parse_csv(data: &[u8]) -> csv::Result<Vec<pm_api::PaymentMethodRecord>> { fn parse_csv(data: &[u8]) -> csv::Result<Vec<pm_api::PaymentMethodRecord>> {
@ -84,27 +218,6 @@ fn parse_csv(data: &[u8]) -> csv::Result<Vec<pm_api::PaymentMethodRecord>> {
} }
Ok(records) Ok(records)
} }
pub fn get_payment_method_records(
form: PaymentMethodsMigrateForm,
) -> Result<
(
common_utils::id_type::MerchantId,
Vec<pm_api::PaymentMethodRecord>,
Option<common_utils::id_type::MerchantConnectorAccountId>,
),
errors::ApiErrorResponse,
> {
match parse_csv(form.file.data.to_bytes()) {
Ok(records) => {
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(),
}),
}
}
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn validate_card_expiry( pub fn validate_card_expiry(

View File

@ -9,6 +9,7 @@ use common_utils::{
}, },
}; };
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
use hyperswitch_domain_models::payment_methods as payment_methods_domain;
use masking::{ExposeInterface, Secret, SwitchStrategy}; use masking::{ExposeInterface, Secret, SwitchStrategy};
use payment_methods::controller::PaymentMethodsController; use payment_methods::controller::PaymentMethodsController;
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
@ -27,7 +28,7 @@ use crate::{
routes::{metrics, SessionState}, routes::{metrics, SessionState},
services, services,
types::{ types::{
api::{customers, payment_methods as payment_methods_api}, api::customers,
domain::{self, types}, domain::{self, types},
storage::{self, enums}, storage::{self, enums},
transformers::ForeignFrom, transformers::ForeignFrom,
@ -41,7 +42,7 @@ pub async fn create_customer(
state: SessionState, state: SessionState,
merchant_context: domain::MerchantContext, merchant_context: domain::MerchantContext,
customer_data: customers::CustomerRequest, customer_data: customers::CustomerRequest,
connector_customer_details: Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: Option<Vec<payment_methods_domain::ConnectorCustomerDetails>>,
) -> errors::CustomerResponse<customers::CustomerResponse> { ) -> errors::CustomerResponse<customers::CustomerResponse> {
let db: &dyn StorageInterface = state.store.as_ref(); let db: &dyn StorageInterface = state.store.as_ref();
let key_manager_state = &(&state).into(); let key_manager_state = &(&state).into();
@ -96,7 +97,9 @@ pub async fn create_customer(
trait CustomerCreateBridge { trait CustomerCreateBridge {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
db: &'a dyn StorageInterface, db: &'a dyn StorageInterface,
merchant_reference_id: &'a Option<id_type::CustomerId>, merchant_reference_id: &'a Option<id_type::CustomerId>,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
@ -115,7 +118,9 @@ trait CustomerCreateBridge {
impl CustomerCreateBridge for customers::CustomerRequest { impl CustomerCreateBridge for customers::CustomerRequest {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
db: &'a dyn StorageInterface, db: &'a dyn StorageInterface,
merchant_reference_id: &'a Option<id_type::CustomerId>, merchant_reference_id: &'a Option<id_type::CustomerId>,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
@ -175,13 +180,15 @@ impl CustomerCreateBridge for customers::CustomerRequest {
domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data) domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data)
.change_context(errors::CustomersErrorResponse::InternalServerError)?; .change_context(errors::CustomersErrorResponse::InternalServerError)?;
let connector_customer = connector_customer_details.as_ref().map(|details| { let connector_customer = connector_customer_details.as_ref().map(|details_vec| {
let merchant_connector_id = details.merchant_connector_id.get_string_repr().to_string(); let mut map = serde_json::Map::new();
let connector_customer_id = details.connector_customer_id.to_string(); for details in details_vec {
let object = serde_json::json!({ let merchant_connector_id =
merchant_connector_id: connector_customer_id details.merchant_connector_id.get_string_repr().to_string();
}); let connector_customer_id = details.connector_customer_id.clone();
pii::SecretSerdeValue::new(object) map.insert(merchant_connector_id, connector_customer_id.into());
}
pii::SecretSerdeValue::new(serde_json::Value::Object(map))
}); });
Ok(domain::Customer { Ok(domain::Customer {
@ -227,7 +234,9 @@ impl CustomerCreateBridge for customers::CustomerRequest {
impl CustomerCreateBridge for customers::CustomerRequest { impl CustomerCreateBridge for customers::CustomerRequest {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
_db: &'a dyn StorageInterface, _db: &'a dyn StorageInterface,
merchant_reference_id: &'a Option<id_type::CustomerId>, merchant_reference_id: &'a Option<id_type::CustomerId>,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
@ -297,12 +306,16 @@ impl CustomerCreateBridge for customers::CustomerRequest {
domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data) domain::FromRequestEncryptableCustomer::from_encryptable(encrypted_data)
.change_context(errors::CustomersErrorResponse::InternalServerError)?; .change_context(errors::CustomersErrorResponse::InternalServerError)?;
let connector_customer = connector_customer_details.as_ref().map(|details| { let connector_customer = connector_customer_details.as_ref().map(|details_vec| {
let mut map = std::collections::HashMap::new(); let map: std::collections::HashMap<_, _> = details_vec
map.insert( .iter()
details.merchant_connector_id.clone(), .map(|details| {
details.connector_customer_id.to_string(), (
); details.merchant_connector_id.clone(),
details.connector_customer_id.to_string(),
)
})
.collect();
common_types::customers::ConnectorCustomerMap::new(map) common_types::customers::ConnectorCustomerMap::new(map)
}); });
@ -1060,7 +1073,9 @@ pub async fn update_customer(
trait CustomerUpdateBridge { trait CustomerUpdateBridge {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
db: &'a dyn StorageInterface, db: &'a dyn StorageInterface,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
key_manager_state: &'a KeyManagerState, key_manager_state: &'a KeyManagerState,
@ -1232,7 +1247,9 @@ impl VerifyIdForUpdateCustomer<'_> {
impl CustomerUpdateBridge for customers::CustomerUpdateRequest { impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
_connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, _connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
db: &'a dyn StorageInterface, db: &'a dyn StorageInterface,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
key_manager_state: &'a KeyManagerState, key_manager_state: &'a KeyManagerState,
@ -1337,7 +1354,9 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
impl CustomerUpdateBridge for customers::CustomerUpdateRequest { impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
async fn create_domain_model_from_request<'a>( async fn create_domain_model_from_request<'a>(
&'a self, &'a self,
connector_customer_details: &'a Option<payment_methods_api::ConnectorCustomerDetails>, connector_customer_details: &'a Option<
Vec<payment_methods_domain::ConnectorCustomerDetails>,
>,
db: &'a dyn StorageInterface, db: &'a dyn StorageInterface,
merchant_context: &'a domain::MerchantContext, merchant_context: &'a domain::MerchantContext,
key_manager_state: &'a KeyManagerState, key_manager_state: &'a KeyManagerState,
@ -1454,7 +1473,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
pub async fn migrate_customers( pub async fn migrate_customers(
state: SessionState, state: SessionState,
customers_migration: Vec<payment_methods_api::PaymentMethodCustomerMigrate>, customers_migration: Vec<payment_methods_domain::PaymentMethodCustomerMigrate>,
merchant_context: domain::MerchantContext, merchant_context: domain::MerchantContext,
) -> errors::CustomerResponse<()> { ) -> errors::CustomerResponse<()> {
for customer_migration in customers_migration { for customer_migration in customers_migration {

View File

@ -10,6 +10,7 @@ use diesel_models::enums::IntentStatus;
use error_stack::ResultExt; use error_stack::ResultExt;
use hyperswitch_domain_models::{ use hyperswitch_domain_models::{
bulk_tokenization::CardNetworkTokenizeRequest, merchant_key_store::MerchantKeyStore, bulk_tokenization::CardNetworkTokenizeRequest, merchant_key_store::MerchantKeyStore,
payment_methods::PaymentMethodCustomerMigrate, transformers::ForeignTryFrom,
}; };
use router_env::{instrument, logger, tracing, Flow}; use router_env::{instrument, logger, tracing, Flow};
@ -331,10 +332,10 @@ pub async fn migrate_payment_methods(
MultipartForm(form): MultipartForm<migration::PaymentMethodsMigrateForm>, MultipartForm(form): MultipartForm<migration::PaymentMethodsMigrateForm>,
) -> HttpResponse { ) -> HttpResponse {
let flow = Flow::PaymentMethodsMigrate; let flow = Flow::PaymentMethodsMigrate;
let (merchant_id, records, merchant_connector_id) = let (merchant_id, records, merchant_connector_ids) =
match migration::get_payment_method_records(form) { match form.validate_and_get_payment_method_records() {
Ok((merchant_id, records, merchant_connector_id)) => { Ok((merchant_id, records, merchant_connector_ids)) => {
(merchant_id, records, merchant_connector_id) (merchant_id, records, merchant_connector_ids)
} }
Err(e) => return api::log_and_return_error_response(e.into()), Err(e) => return api::log_and_return_error_response(e.into()),
}; };
@ -345,7 +346,7 @@ pub async fn migrate_payment_methods(
records, records,
|state, _, req, _| { |state, _, req, _| {
let merchant_id = merchant_id.clone(); let merchant_id = merchant_id.clone();
let merchant_connector_id = merchant_connector_id.clone(); let merchant_connector_ids = merchant_connector_ids.clone();
async move { async move {
let (key_store, merchant_account) = let (key_store, merchant_account) =
get_merchant_account(&state, &merchant_id).await?; get_merchant_account(&state, &merchant_id).await?;
@ -354,20 +355,43 @@ pub async fn migrate_payment_methods(
domain::Context(merchant_account.clone(), key_store.clone()), domain::Context(merchant_account.clone(), key_store.clone()),
)); ));
customers::migrate_customers( let mut mca_cache = std::collections::HashMap::new();
state.clone(), let customers = Vec::<PaymentMethodCustomerMigrate>::foreign_try_from((
req.iter() &req,
.map(|e| { merchant_id.clone(),
payment_methods::PaymentMethodCustomerMigrate::from(( ))
e.clone(), .map_err(|e| errors::ApiErrorResponse::InvalidRequestData {
merchant_id.clone(), message: e.to_string(),
)) })?;
})
.collect(), for record in &customers {
merchant_context.clone(), if let Some(connector_customer_details) = &record.connector_customer_details {
) for connector_customer in connector_customer_details {
.await if !mca_cache.contains_key(&connector_customer.merchant_connector_id) {
.change_context(errors::ApiErrorResponse::InternalServerError)?; let mca = state
.store
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
&(&state).into(),
&merchant_id,
&connector_customer.merchant_connector_id,
merchant_context.get_merchant_key_store(),
)
.await
.to_not_found_response(
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: connector_customer.merchant_connector_id.get_string_repr().to_string(),
},
)?;
mca_cache
.insert(connector_customer.merchant_connector_id.clone(), mca);
}
}
}
}
customers::migrate_customers(state.clone(), customers, merchant_context.clone())
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let controller = cards::PmCards { let controller = cards::PmCards {
state: &state, state: &state,
merchant_context: &merchant_context, merchant_context: &merchant_context,
@ -377,7 +401,7 @@ pub async fn migrate_payment_methods(
req, req,
&merchant_id, &merchant_id,
&merchant_context, &merchant_context,
merchant_connector_id, merchant_connector_ids,
&controller, &controller,
)) ))
.await .await

View File

@ -1,32 +1,30 @@
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
pub use api_models::payment_methods::{ pub use api_models::payment_methods::{
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest,
CardNetworkTokenizeResponse, CardType, ConnectorCustomerDetails, CardNetworkTokenizeResponse, CardType, CustomerPaymentMethodResponseItem,
CustomerPaymentMethodResponseItem, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse,
GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, ListCountriesCurrenciesRequest, MigrateCardDetail, NetworkTokenDetailsPaymentMethod,
NetworkTokenDetailsPaymentMethod, NetworkTokenDetailsResponse, NetworkTokenResponse, NetworkTokenDetailsResponse, NetworkTokenResponse, PaymentMethodCollectLinkRenderRequest,
PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData,
PaymentMethodCreateData, PaymentMethodCustomerMigrate, PaymentMethodDeleteResponse, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm,
PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest,
PaymentMethodListRequest, PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodMigrateResponse,
PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData,
PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenDataResponse, PaymentMethodsData, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted,
TokenDetailsResponse, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1,
TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, TokenizedWalletValue2, TotalPaymentMethodCountResponse,
TotalPaymentMethodCountResponse,
}; };
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
pub use api_models::payment_methods::{ pub use api_models::payment_methods::{
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest, CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest,
CardNetworkTokenizeResponse, ConnectorCustomerDetails, CustomerPaymentMethod, CardNetworkTokenizeResponse, CustomerPaymentMethod, CustomerPaymentMethodsListResponse,
CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest,
GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail,
MigrateCardDetail, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate,
PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodCustomerMigrate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId,
PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate,
PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData,
PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizeCardRequest, TokenizeCardRequest, TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest,
TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest,
TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1,
TokenizedWalletValue2, TokenizedWalletValue2,
}; };

View File

@ -31,6 +31,7 @@
openssl openssl
pkg-config pkg-config
postgresql # for libpq postgresql # for libpq
protobuf
]; ];
# Minimal packages for running hyperswitch # Minimal packages for running hyperswitch
@ -40,6 +41,7 @@
# Development packages # Development packages
devPackages = base ++ (with pkgs; [ devPackages = base ++ (with pkgs; [
cargo-watch
nixd nixd
rust-bin.stable.${rustDevVersion}.default rust-bin.stable.${rustDevVersion}.default
swagger-cli swagger-cli