feat(payment_methods): populate connector_customer during customer creation step in payment methods migrate flow (#8319)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2025-06-17 14:56:29 +05:30
committed by GitHub
parent e0845f3c8c
commit 3899ddd529
7 changed files with 128 additions and 58 deletions

View File

@ -16,12 +16,10 @@ use masking::PeekInterface;
use serde::de; use serde::de;
use utoipa::{schema, ToSchema}; use utoipa::{schema, ToSchema};
#[cfg(feature = "v1")]
use crate::customers;
#[cfg(feature = "payouts")] #[cfg(feature = "payouts")]
use crate::payouts; use crate::payouts;
use crate::{ use crate::{
admin, enums as api_enums, admin, customers, enums as api_enums,
payments::{self, BankCodeResponse}, payments::{self, BankCodeResponse},
}; };
@ -2485,6 +2483,7 @@ pub struct PaymentMethodRecord {
pub payment_method_type: Option<api_enums::PaymentMethodType>, pub payment_method_type: Option<api_enums::PaymentMethodType>,
pub nick_name: masking::Secret<String>, pub nick_name: masking::Secret<String>,
pub payment_instrument_id: Option<masking::Secret<String>>, pub payment_instrument_id: Option<masking::Secret<String>>,
pub connector_customer_id: Option<String>,
pub card_number_masked: masking::Secret<String>, pub card_number_masked: masking::Secret<String>,
pub card_expiry_month: masking::Secret<String>, pub card_expiry_month: masking::Secret<String>,
pub card_expiry_year: masking::Secret<String>, pub card_expiry_year: masking::Secret<String>,
@ -2510,6 +2509,18 @@ 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>,
@ -2678,10 +2689,11 @@ impl
} }
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerRequest { impl From<(PaymentMethodRecord, id_type::MerchantId)> for PaymentMethodCustomerMigrate {
fn from(value: (PaymentMethodRecord, id_type::MerchantId)) -> Self { fn from(value: (PaymentMethodRecord, id_type::MerchantId)) -> Self {
let (record, merchant_id) = value; let (record, merchant_id) = value;
Self { Self {
customer: customers::CustomerRequest {
customer_id: Some(record.customer_id), customer_id: Some(record.customer_id),
merchant_id, merchant_id,
name: record.name, name: record.name,
@ -2701,6 +2713,16 @@ impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerReq
last_name: Some(record.billing_address_last_name), last_name: Some(record.billing_address_last_name),
}), }),
metadata: None, 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,
},
),
} }
} }
} }

View File

@ -9,6 +9,16 @@ pub struct ConnectorCustomerMap(
std::collections::HashMap<common_utils::id_type::MerchantConnectorAccountId, String>, std::collections::HashMap<common_utils::id_type::MerchantConnectorAccountId, String>,
); );
#[cfg(feature = "v2")]
impl ConnectorCustomerMap {
/// Creates a new `ConnectorCustomerMap` from a HashMap
pub fn new(
map: std::collections::HashMap<common_utils::id_type::MerchantConnectorAccountId, String>,
) -> Self {
Self(map)
}
}
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
common_utils::impl_to_sql_from_sql_json!(ConnectorCustomerMap); common_utils::impl_to_sql_from_sql_json!(ConnectorCustomerMap);

View File

@ -57,7 +57,7 @@ pub async fn customer_create(
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new( let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store), domain::Context(auth.merchant_account, auth.key_store),
)); ));
customers::create_customer(state, merchant_context, req) customers::create_customer(state, merchant_context, req, None)
}, },
&auth::HeaderAuth(auth::ApiKeyAuth { &auth::HeaderAuth(auth::ApiKeyAuth {
is_connected_allowed: false, is_connected_allowed: false,

View File

@ -27,7 +27,7 @@ use crate::{
routes::{metrics, SessionState}, routes::{metrics, SessionState},
services, services,
types::{ types::{
api::customers, api::{customers, payment_methods as payment_methods_api},
domain::{self, types}, domain::{self, types},
storage::{self, enums}, storage::{self, enums},
transformers::ForeignFrom, transformers::ForeignFrom,
@ -41,6 +41,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>,
) -> 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();
@ -69,6 +70,7 @@ pub async fn create_customer(
let domain_customer = customer_data let domain_customer = customer_data
.create_domain_model_from_request( .create_domain_model_from_request(
&connector_customer_details,
db, db,
&merchant_reference_id, &merchant_reference_id,
&merchant_context, &merchant_context,
@ -94,6 +96,7 @@ 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>,
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,
@ -112,6 +115,7 @@ 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>,
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,
@ -171,6 +175,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 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)
});
Ok(domain::Customer { Ok(domain::Customer {
customer_id: merchant_reference_id customer_id: merchant_reference_id
.to_owned() .to_owned()
@ -188,7 +201,7 @@ impl CustomerCreateBridge for customers::CustomerRequest {
description: self.description.clone(), description: self.description.clone(),
phone_country_code: self.phone_country_code.clone(), phone_country_code: self.phone_country_code.clone(),
metadata: self.metadata.clone(), metadata: self.metadata.clone(),
connector_customer: None, connector_customer,
address_id: address_from_db.clone().map(|addr| addr.address_id), address_id: address_from_db.clone().map(|addr| addr.address_id),
created_at: common_utils::date_time::now(), created_at: common_utils::date_time::now(),
modified_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(),
@ -214,6 +227,7 @@ 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>,
_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,
@ -283,6 +297,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 mut map = std::collections::HashMap::new();
map.insert(
details.merchant_connector_id.clone(),
details.connector_customer_id.to_string(),
);
common_types::customers::ConnectorCustomerMap::new(map)
});
Ok(domain::Customer { Ok(domain::Customer {
id: id_type::GlobalCustomerId::generate(&state.conf.cell_information.id), id: id_type::GlobalCustomerId::generate(&state.conf.cell_information.id),
merchant_reference_id: merchant_reference_id.to_owned(), merchant_reference_id: merchant_reference_id.to_owned(),
@ -299,7 +322,7 @@ impl CustomerCreateBridge for customers::CustomerRequest {
description: self.description.clone(), description: self.description.clone(),
phone_country_code: self.phone_country_code.clone(), phone_country_code: self.phone_country_code.clone(),
metadata: self.metadata.clone(), metadata: self.metadata.clone(),
connector_customer: None, connector_customer,
created_at: common_utils::date_time::now(), created_at: common_utils::date_time::now(),
modified_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(),
default_payment_method_id: None, default_payment_method_id: None,
@ -1024,6 +1047,7 @@ pub async fn update_customer(
let updated_customer = update_customer let updated_customer = update_customer
.request .request
.create_domain_model_from_request( .create_domain_model_from_request(
&None,
db, db,
&merchant_context, &merchant_context,
key_manager_state, key_manager_state,
@ -1039,6 +1063,7 @@ 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>,
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,
@ -1211,6 +1236,7 @@ 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>,
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,
@ -1315,6 +1341,7 @@ 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>,
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,
@ -1432,11 +1459,18 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
pub async fn migrate_customers( pub async fn migrate_customers(
state: SessionState, state: SessionState,
customers: Vec<customers::CustomerRequest>, customers_migration: Vec<payment_methods_api::PaymentMethodCustomerMigrate>,
merchant_context: domain::MerchantContext, merchant_context: domain::MerchantContext,
) -> errors::CustomerResponse<()> { ) -> errors::CustomerResponse<()> {
for customer in customers { for customer_migration in customers_migration {
match create_customer(state.clone(), merchant_context.clone(), customer).await { match create_customer(
state.clone(),
merchant_context.clone(),
customer_migration.customer,
customer_migration.connector_customer_details,
)
.await
{
Ok(_) => (), Ok(_) => (),
Err(e) => match e.current_context() { Err(e) => match e.current_context() {
errors::CustomersErrorResponse::CustomerAlreadyExists => (), errors::CustomersErrorResponse::CustomerAlreadyExists => (),

View File

@ -25,7 +25,7 @@ pub async fn customers_create(
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new( let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store), domain::Context(auth.merchant_account, auth.key_store),
)); ));
create_customer(state, merchant_context, req) create_customer(state, merchant_context, req, None)
}, },
auth::auth_type( auth::auth_type(
&auth::V2ApiKeyAuth { &auth::V2ApiKeyAuth {
@ -58,7 +58,7 @@ pub async fn customers_create(
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new( let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store), domain::Context(auth.merchant_account, auth.key_store),
)); ));
create_customer(state, merchant_context, req) create_customer(state, merchant_context, req, None)
}, },
auth::auth_type( auth::auth_type(
&auth::HeaderAuth(auth::ApiKeyAuth { &auth::HeaderAuth(auth::ApiKeyAuth {

View File

@ -14,6 +14,8 @@ use hyperswitch_domain_models::{
use router_env::{instrument, logger, tracing, Flow}; use router_env::{instrument, logger, tracing, Flow};
use super::app::{AppState, SessionState}; use super::app::{AppState, SessionState};
#[cfg(all(feature = "v1", any(feature = "olap", feature = "oltp")))]
use crate::core::{customers, payment_methods::tokenize};
use crate::{ use crate::{
core::{ core::{
api_locking, api_locking,
@ -27,11 +29,6 @@ use crate::{
storage::payment_method::PaymentTokenData, storage::payment_method::PaymentTokenData,
}, },
}; };
#[cfg(all(feature = "v1", any(feature = "olap", feature = "oltp")))]
use crate::{
core::{customers, payment_methods::tokenize},
types::api::customers::CustomerRequest,
};
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))]
@ -360,7 +357,12 @@ pub async fn migrate_payment_methods(
customers::migrate_customers( customers::migrate_customers(
state.clone(), state.clone(),
req.iter() req.iter()
.map(|e| CustomerRequest::from((e.clone(), merchant_id.clone()))) .map(|e| {
payment_methods::PaymentMethodCustomerMigrate::from((
e.clone(),
merchant_id.clone(),
))
})
.collect(), .collect(),
merchant_context.clone(), merchant_context.clone(),
) )

View File

@ -1,30 +1,32 @@
#[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, CustomerPaymentMethodResponseItem, CardNetworkTokenizeResponse, CardType, ConnectorCustomerDetails,
DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse, CustomerPaymentMethodResponseItem, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest,
ListCountriesCurrenciesRequest, MigrateCardDetail, NetworkTokenDetailsPaymentMethod, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail,
NetworkTokenDetailsResponse, NetworkTokenResponse, PaymentMethodCollectLinkRenderRequest, NetworkTokenDetailsPaymentMethod, NetworkTokenDetailsResponse, NetworkTokenResponse,
PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate,
PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodCreateData, PaymentMethodCustomerMigrate, PaymentMethodDeleteResponse,
PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest, PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData,
PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodListRequest, PaymentMethodListResponseForSession, PaymentMethodMigrate,
PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData,
PaymentMethodsData, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenDataResponse,
TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenDetailsResponse, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1,
TokenizedWalletValue2, TotalPaymentMethodCountResponse, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2,
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, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, CardNetworkTokenizeResponse, ConnectorCustomerDetails, CustomerPaymentMethod,
DefaultPaymentMethod, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest,
GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail, GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest,
PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, MigrateCardDetail, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest,
PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodCustomerMigrate,
PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest,
PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodMigrateResponse,
TokenizeCardRequest, TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizeCardRequest,
TokenizeDataRequest, TokenizePayloadEncrypted, TokenizePayloadRequest,
TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1,
TokenizedWalletValue2, TokenizedWalletValue2,
}; };