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

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

View File

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

View File

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