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 utoipa::{schema, ToSchema};
#[cfg(feature = "v1")]
use crate::customers;
#[cfg(feature = "payouts")]
use crate::payouts;
use crate::{
admin, enums as api_enums,
admin, customers, enums as api_enums,
payments::{self, BankCodeResponse},
};
@ -2485,6 +2483,7 @@ pub struct PaymentMethodRecord {
pub payment_method_type: Option<api_enums::PaymentMethodType>,
pub nick_name: 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_expiry_month: 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>,
}
#[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)]
pub struct PaymentMethodMigrationResponse {
pub line_number: Option<i64>,
@ -2678,10 +2689,11 @@ impl
}
#[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 {
let (record, merchant_id) = value;
Self {
customer: customers::CustomerRequest {
customer_id: Some(record.customer_id),
merchant_id,
name: record.name,
@ -2701,6 +2713,16 @@ impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerReq
last_name: Some(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,
},
),
}
}
}

View File

@ -9,6 +9,16 @@ pub struct ConnectorCustomerMap(
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")]
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(
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 {
is_connected_allowed: false,

View File

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

View File

@ -25,7 +25,7 @@ pub async fn customers_create(
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
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::V2ApiKeyAuth {
@ -58,7 +58,7 @@ pub async fn customers_create(
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
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::HeaderAuth(auth::ApiKeyAuth {

View File

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

View File

@ -1,30 +1,32 @@
#[cfg(feature = "v2")]
pub use api_models::payment_methods::{
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest,
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,
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,
};
#[cfg(feature = "v1")]
pub use api_models::payment_methods::{
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CardNetworkTokenizeRequest,
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,
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,
TokenizePaymentMethodRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1,
TokenizedWalletValue2,
};