Skip to content
68 changes: 45 additions & 23 deletions crates/api_models/src/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand Down Expand Up @@ -2678,29 +2689,40 @@ 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_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: Some(record.billing_address_city),
country: record.billing_address_country,
line1: Some(record.billing_address_line1),
line2: record.billing_address_line2,
state: Some(record.billing_address_state),
line3: record.billing_address_line3,
zip: Some(record.billing_address_zip),
first_name: Some(record.billing_address_first_name),
last_name: Some(record.billing_address_last_name),
}),
metadata: None,
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: Some(record.billing_address_city),
country: record.billing_address_country,
line1: Some(record.billing_address_line1),
line2: record.billing_address_line2,
state: Some(record.billing_address_state),
line3: record.billing_address_line3,
zip: Some(record.billing_address_zip),
first_name: Some(record.billing_address_first_name),
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,
},
),
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions crates/common_types/src/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/compatibility/stripe/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 40 additions & 6 deletions crates/router/src/core/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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(),
Expand All @@ -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,
Comment on lines +230 to 231
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

connector_customer_details parameter is accepted but silently ignored

The customer_v2 implementation of CustomerCreateBridge::create_domain_model_from_request now receives connector_customer_details, yet the body never references it. This will:

  1. trigger an “unused variable” warning (hidden only by the feature gate),
  2. break functional parity with the V1 path, leaving connector_customer always None for v2 customers.

Either wire the data into the domain::Customer (preferred) or prefix the argument with _ to silence warnings until support is added.

🤖 Prompt for AI Agents
In crates/router/src/core/customers.rs around lines 230 to 231, the parameter
connector_customer_details is accepted but not used, causing unused variable
warnings and breaking functional parity with the V1 path. To fix this, either
integrate connector_customer_details into the domain::Customer model within the
create_domain_model_from_request function or rename the parameter to
_connector_customer_details to suppress the warning until full support is
implemented.

merchant_reference_id: &'a Option<id_type::CustomerId>,
merchant_context: &'a domain::MerchantContext,
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 => (),
Expand Down
4 changes: 2 additions & 2 deletions crates/router/src/routes/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 8 additions & 6 deletions crates/router/src/routes/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))]
Expand Down Expand Up @@ -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(),
)
Expand Down
42 changes: 22 additions & 20 deletions crates/router/src/types/api/payment_methods.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down
Loading