Skip to content

Commit dc87f22

Browse files
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> (cherry picked from commit ce2b90b)
1 parent d0e1bf3 commit dc87f22

File tree

9 files changed

+437
-174
lines changed

9 files changed

+437
-174
lines changed

crates/api_models/src/payment_methods.rs

Lines changed: 47 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use utoipa::{schema, ToSchema};
1919
#[cfg(feature = "payouts")]
2020
use crate::payouts;
2121
use crate::{
22-
admin, customers, enums as api_enums,
22+
admin, enums as api_enums,
2323
payments::{self, BankCodeResponse},
2424
};
2525

@@ -2500,6 +2500,7 @@ pub struct PaymentMethodRecord {
25002500
pub billing_address_line3: Option<masking::Secret<String>>,
25012501
pub raw_card_number: Option<masking::Secret<String>>,
25022502
pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
2503+
pub merchant_connector_ids: Option<String>,
25032504
pub original_transaction_amount: Option<i64>,
25042505
pub original_transaction_currency: Option<common_enums::Currency>,
25052506
pub line_number: Option<i64>,
@@ -2509,18 +2510,6 @@ pub struct PaymentMethodRecord {
25092510
pub network_token_requestor_ref_id: Option<String>,
25102511
}
25112512

2512-
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2513-
pub struct ConnectorCustomerDetails {
2514-
pub connector_customer_id: String,
2515-
pub merchant_connector_id: id_type::MerchantConnectorAccountId,
2516-
}
2517-
2518-
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2519-
pub struct PaymentMethodCustomerMigrate {
2520-
pub customer: customers::CustomerRequest,
2521-
pub connector_customer_details: Option<ConnectorCustomerDetails>,
2522-
}
2523-
25242513
#[derive(Debug, Default, serde::Serialize)]
25252514
pub struct PaymentMethodMigrationResponse {
25262515
pub line_number: Option<i64>,
@@ -2637,47 +2626,56 @@ impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse
26372626

26382627
impl
26392628
TryFrom<(
2640-
PaymentMethodRecord,
2629+
&PaymentMethodRecord,
26412630
id_type::MerchantId,
2642-
Option<id_type::MerchantConnectorAccountId>,
2631+
Option<&Vec<id_type::MerchantConnectorAccountId>>,
26432632
)> for PaymentMethodMigrate
26442633
{
26452634
type Error = error_stack::Report<errors::ValidationError>;
2635+
26462636
fn try_from(
26472637
item: (
2648-
PaymentMethodRecord,
2638+
&PaymentMethodRecord,
26492639
id_type::MerchantId,
2650-
Option<id_type::MerchantConnectorAccountId>,
2640+
Option<&Vec<id_type::MerchantConnectorAccountId>>,
26512641
),
26522642
) -> Result<Self, Self::Error> {
2653-
let (record, merchant_id, mca_id) = item;
2643+
let (record, merchant_id, mca_ids) = item;
26542644
let billing = record.create_billing();
2655-
2656-
// if payment instrument id is present then only construct this
2657-
let connector_mandate_details = if record.payment_instrument_id.is_some() {
2658-
Some(PaymentsMandateReference(HashMap::from([(
2659-
mca_id.get_required_value("merchant_connector_id")?,
2660-
PaymentsMandateReferenceRecord {
2661-
connector_mandate_id: record
2662-
.payment_instrument_id
2663-
.get_required_value("payment_instrument_id")?
2664-
.peek()
2665-
.to_string(),
2666-
payment_method_type: record.payment_method_type,
2667-
original_payment_authorized_amount: record.original_transaction_amount,
2668-
original_payment_authorized_currency: record.original_transaction_currency,
2669-
},
2670-
)])))
2645+
let connector_mandate_details = if let Some(payment_instrument_id) =
2646+
&record.payment_instrument_id
2647+
{
2648+
let ids = mca_ids.get_required_value("mca_ids")?;
2649+
let mandate_map: HashMap<_, _> = ids
2650+
.iter()
2651+
.map(|mca_id| {
2652+
(
2653+
mca_id.clone(),
2654+
PaymentsMandateReferenceRecord {
2655+
connector_mandate_id: payment_instrument_id.peek().to_string(),
2656+
payment_method_type: record.payment_method_type,
2657+
original_payment_authorized_amount: record.original_transaction_amount,
2658+
original_payment_authorized_currency: record
2659+
.original_transaction_currency,
2660+
},
2661+
)
2662+
})
2663+
.collect();
2664+
Some(PaymentsMandateReference(mandate_map))
26712665
} else {
26722666
None
26732667
};
2668+
26742669
Ok(Self {
26752670
merchant_id,
2676-
customer_id: Some(record.customer_id),
2671+
customer_id: Some(record.customer_id.clone()),
26772672
card: Some(MigrateCardDetail {
2678-
card_number: record.raw_card_number.unwrap_or(record.card_number_masked),
2679-
card_exp_month: record.card_expiry_month,
2680-
card_exp_year: record.card_expiry_year,
2673+
card_number: record
2674+
.raw_card_number
2675+
.clone()
2676+
.unwrap_or_else(|| record.card_number_masked.clone()),
2677+
card_exp_month: record.card_expiry_month.clone(),
2678+
card_exp_year: record.card_expiry_year.clone(),
26812679
card_holder_name: record.name.clone(),
26822680
card_network: None,
26832681
card_type: None,
@@ -2687,10 +2685,16 @@ impl
26872685
}),
26882686
network_token: Some(MigrateNetworkTokenDetail {
26892687
network_token_data: MigrateNetworkTokenData {
2690-
network_token_number: record.network_token_number.unwrap_or_default(),
2691-
network_token_exp_month: record.network_token_expiry_month.unwrap_or_default(),
2692-
network_token_exp_year: record.network_token_expiry_year.unwrap_or_default(),
2693-
card_holder_name: record.name,
2688+
network_token_number: record.network_token_number.clone().unwrap_or_default(),
2689+
network_token_exp_month: record
2690+
.network_token_expiry_month
2691+
.clone()
2692+
.unwrap_or_default(),
2693+
network_token_exp_year: record
2694+
.network_token_expiry_year
2695+
.clone()
2696+
.unwrap_or_default(),
2697+
card_holder_name: record.name.clone(),
26942698
nick_name: record.nick_name.clone(),
26952699
card_issuing_country: None,
26962700
card_network: None,
@@ -2699,6 +2703,7 @@ impl
26992703
},
27002704
network_token_requestor_ref_id: record
27012705
.network_token_requestor_ref_id
2706+
.clone()
27022707
.unwrap_or_default(),
27032708
}),
27042709
payment_method: record.payment_method,
@@ -2723,45 +2728,6 @@ impl
27232728
}
27242729
}
27252730

2726-
#[cfg(feature = "v1")]
2727-
impl From<(PaymentMethodRecord, id_type::MerchantId)> for PaymentMethodCustomerMigrate {
2728-
fn from(value: (PaymentMethodRecord, id_type::MerchantId)) -> Self {
2729-
let (record, merchant_id) = value;
2730-
Self {
2731-
customer: customers::CustomerRequest {
2732-
customer_id: Some(record.customer_id),
2733-
merchant_id,
2734-
name: record.name,
2735-
email: record.email,
2736-
phone: record.phone,
2737-
description: None,
2738-
phone_country_code: record.phone_country_code,
2739-
address: Some(payments::AddressDetails {
2740-
city: record.billing_address_city,
2741-
country: record.billing_address_country,
2742-
line1: record.billing_address_line1,
2743-
line2: record.billing_address_line2,
2744-
state: record.billing_address_state,
2745-
line3: record.billing_address_line3,
2746-
zip: record.billing_address_zip,
2747-
first_name: record.billing_address_first_name,
2748-
last_name: record.billing_address_last_name,
2749-
}),
2750-
metadata: None,
2751-
},
2752-
connector_customer_details: record
2753-
.connector_customer_id
2754-
.zip(record.merchant_connector_id)
2755-
.map(
2756-
|(connector_customer_id, merchant_connector_id)| ConnectorCustomerDetails {
2757-
connector_customer_id,
2758-
merchant_connector_id,
2759-
},
2760-
),
2761-
}
2762-
}
2763-
}
2764-
27652731
#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)]
27662732
pub struct CardNetworkTokenizeRequest {
27672733
/// Merchant ID associated with the tokenization request

crates/hyperswitch_domain_models/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub mod router_response_types;
3636
pub mod routing;
3737
#[cfg(feature = "tokenization_v2")]
3838
pub mod tokenization;
39+
pub mod transformers;
3940
pub mod type_encryption;
4041
pub mod types;
4142
pub mod vault;

crates/hyperswitch_domain_models/src/payment_methods.rs

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#[cfg(feature = "v2")]
22
use api_models::payment_methods::PaymentMethodsData;
3+
use api_models::{customers, payment_methods, payments};
34
// specific imports because of using the macro
45
use common_enums::enums::MerchantStorageScheme;
56
#[cfg(feature = "v1")]
@@ -27,11 +28,12 @@ use time::PrimitiveDateTime;
2728
#[cfg(feature = "v2")]
2829
use crate::address::Address;
2930
#[cfg(feature = "v1")]
30-
use crate::{mandates, type_encryption::AsyncLift};
31+
use crate::type_encryption::AsyncLift;
3132
use crate::{
32-
mandates::CommonMandateReference,
33+
mandates::{self, CommonMandateReference},
3334
merchant_key_store::MerchantKeyStore,
3435
payment_method_data as domain_payment_method_data,
36+
transformers::ForeignTryFrom,
3537
type_encryption::{crypto_operation, CryptoOperation},
3638
};
3739

@@ -87,7 +89,6 @@ pub struct PaymentMethod {
8789
pub network_token_locker_id: Option<String>,
8890
pub network_token_payment_method_data: OptionalEncryptableValue,
8991
}
90-
9192
#[cfg(feature = "v2")]
9293
#[derive(Clone, Debug, router_derive::ToEncryption)]
9394
pub struct PaymentMethod {
@@ -915,6 +916,136 @@ pub struct PaymentMethodsSessionUpdateInternal {
915916
pub tokenization_data: Option<pii::SecretSerdeValue>,
916917
}
917918

919+
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
920+
pub struct ConnectorCustomerDetails {
921+
pub connector_customer_id: String,
922+
pub merchant_connector_id: id_type::MerchantConnectorAccountId,
923+
}
924+
925+
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
926+
pub struct PaymentMethodCustomerMigrate {
927+
pub customer: customers::CustomerRequest,
928+
pub connector_customer_details: Option<Vec<ConnectorCustomerDetails>>,
929+
}
930+
931+
#[cfg(feature = "v1")]
932+
impl TryFrom<(payment_methods::PaymentMethodRecord, id_type::MerchantId)>
933+
for PaymentMethodCustomerMigrate
934+
{
935+
type Error = error_stack::Report<ValidationError>;
936+
fn try_from(
937+
value: (payment_methods::PaymentMethodRecord, id_type::MerchantId),
938+
) -> Result<Self, Self::Error> {
939+
let (record, merchant_id) = value;
940+
let connector_customer_details = record
941+
.connector_customer_id
942+
.and_then(|connector_customer_id| {
943+
// Handle single merchant_connector_id
944+
record
945+
.merchant_connector_id
946+
.as_ref()
947+
.map(|merchant_connector_id| {
948+
Ok(vec![ConnectorCustomerDetails {
949+
connector_customer_id: connector_customer_id.clone(),
950+
merchant_connector_id: merchant_connector_id.clone(),
951+
}])
952+
})
953+
// Handle comma-separated merchant_connector_ids
954+
.or_else(|| {
955+
record
956+
.merchant_connector_ids
957+
.as_ref()
958+
.map(|merchant_connector_ids_str| {
959+
merchant_connector_ids_str
960+
.split(',')
961+
.map(|id| id.trim())
962+
.filter(|id| !id.is_empty())
963+
.map(|merchant_connector_id| {
964+
id_type::MerchantConnectorAccountId::wrap(
965+
merchant_connector_id.to_string(),
966+
)
967+
.map_err(|_| {
968+
error_stack::report!(ValidationError::InvalidValue {
969+
message: format!(
970+
"Invalid merchant_connector_account_id: {}",
971+
merchant_connector_id
972+
),
973+
})
974+
})
975+
.map(
976+
|merchant_connector_id| ConnectorCustomerDetails {
977+
connector_customer_id: connector_customer_id
978+
.clone(),
979+
merchant_connector_id,
980+
},
981+
)
982+
})
983+
.collect::<Result<Vec<_>, _>>()
984+
})
985+
})
986+
})
987+
.transpose()?;
988+
989+
Ok(Self {
990+
customer: customers::CustomerRequest {
991+
customer_id: Some(record.customer_id),
992+
merchant_id,
993+
name: record.name,
994+
email: record.email,
995+
phone: record.phone,
996+
description: None,
997+
phone_country_code: record.phone_country_code,
998+
address: Some(payments::AddressDetails {
999+
city: record.billing_address_city,
1000+
country: record.billing_address_country,
1001+
line1: record.billing_address_line1,
1002+
line2: record.billing_address_line2,
1003+
state: record.billing_address_state,
1004+
line3: record.billing_address_line3,
1005+
zip: record.billing_address_zip,
1006+
first_name: record.billing_address_first_name,
1007+
last_name: record.billing_address_last_name,
1008+
}),
1009+
metadata: None,
1010+
},
1011+
connector_customer_details,
1012+
})
1013+
}
1014+
}
1015+
1016+
#[cfg(feature = "v1")]
1017+
impl ForeignTryFrom<(&[payment_methods::PaymentMethodRecord], id_type::MerchantId)>
1018+
for Vec<PaymentMethodCustomerMigrate>
1019+
{
1020+
type Error = error_stack::Report<ValidationError>;
1021+
1022+
fn foreign_try_from(
1023+
(records, merchant_id): (&[payment_methods::PaymentMethodRecord], id_type::MerchantId),
1024+
) -> Result<Self, Self::Error> {
1025+
let (customers_migration, migration_errors): (Self, Vec<_>) = records
1026+
.iter()
1027+
.map(|record| {
1028+
PaymentMethodCustomerMigrate::try_from((record.clone(), merchant_id.clone()))
1029+
})
1030+
.fold((Self::new(), Vec::new()), |mut acc, result| {
1031+
match result {
1032+
Ok(customer) => acc.0.push(customer),
1033+
Err(e) => acc.1.push(e.to_string()),
1034+
}
1035+
acc
1036+
});
1037+
1038+
migration_errors
1039+
.is_empty()
1040+
.then_some(customers_migration)
1041+
.ok_or_else(|| {
1042+
error_stack::report!(ValidationError::InvalidValue {
1043+
message: migration_errors.join(", "),
1044+
})
1045+
})
1046+
}
1047+
}
1048+
9181049
#[cfg(feature = "v1")]
9191050
#[cfg(test)]
9201051
mod tests {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pub trait ForeignFrom<F> {
2+
fn foreign_from(from: F) -> Self;
3+
}
4+
5+
pub trait ForeignTryFrom<F>: Sized {
6+
type Error;
7+
8+
fn foreign_try_from(from: F) -> Result<Self, Self::Error>;
9+
}

0 commit comments

Comments
 (0)