refactor(payments_v2): create customer at connector end and populate connector customer ID (#7246)

This commit is contained in:
Sanchith Hegde
2025-02-14 14:58:54 +05:30
committed by GitHub
parent 3c7cb9e59d
commit 17f9e6ee9e
11 changed files with 358 additions and 116 deletions

View File

@ -679,19 +679,20 @@ impl CustomerDeleteBridge for id_type::GlobalCustomerId {
redacted_encrypted_value.clone().into_encrypted(),
);
let updated_customer = storage::CustomerUpdate::Update {
name: Some(redacted_encrypted_value.clone()),
email: Box::new(Some(redacted_encrypted_email)),
phone: Box::new(Some(redacted_encrypted_value.clone())),
description: Some(Description::from_str_unchecked(REDACTED)),
phone_country_code: Some(REDACTED.to_string()),
metadata: None,
connector_customer: Box::new(None),
default_billing_address: None,
default_shipping_address: None,
default_payment_method_id: None,
status: Some(common_enums::DeleteStatus::Redacted),
};
let updated_customer =
storage::CustomerUpdate::Update(Box::new(storage::CustomerGeneralUpdate {
name: Some(redacted_encrypted_value.clone()),
email: Box::new(Some(redacted_encrypted_email)),
phone: Box::new(Some(redacted_encrypted_value.clone())),
description: Some(Description::from_str_unchecked(REDACTED)),
phone_country_code: Some(REDACTED.to_string()),
metadata: None,
connector_customer: Box::new(None),
default_billing_address: None,
default_shipping_address: None,
default_payment_method_id: None,
status: Some(common_enums::DeleteStatus::Redacted),
}));
db.update_customer_by_global_id(
key_manager_state,
@ -1338,7 +1339,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
&domain_customer.id,
domain_customer.to_owned(),
merchant_account.get_id(),
storage::CustomerUpdate::Update {
storage::CustomerUpdate::Update(Box::new(storage::CustomerGeneralUpdate {
name: encryptable_customer.name,
email: Box::new(encryptable_customer.email.map(|email| {
let encryptable: Encryptable<Secret<String, pii::EmailStrategy>> =
@ -1357,7 +1358,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest {
default_shipping_address: encrypted_customer_shipping_address.map(Into::into),
default_payment_method_id: Some(self.default_payment_method_id.clone()),
status: None,
},
})),
key_store,
merchant_account.storage_scheme,
)

View File

@ -3016,6 +3016,16 @@ where
id: merchant_connector_id.get_string_repr().to_owned(),
})?;
let updated_customer = call_create_connector_customer_if_required(
state,
customer,
merchant_account,
key_store,
&merchant_connector_account,
payment_data,
)
.await?;
let mut router_data = payment_data
.construct_router_data(
state,
@ -3068,8 +3078,7 @@ where
payment_data.clone(),
customer.clone(),
merchant_account.storage_scheme,
// TODO: update the customer with connector customer id
None,
updated_customer,
key_store,
frm_suggestion,
header_payload.clone(),
@ -3894,7 +3903,6 @@ where
merchant_connector_account.get_mca_id(),
)?;
#[cfg(feature = "v1")]
let label = {
let connector_label = core_utils::get_connector_label(
payment_data.get_payment_intent().business_country,
@ -3925,15 +3933,6 @@ where
}
};
#[cfg(feature = "v2")]
let label = {
merchant_connector_account
.get_mca_id()
.get_required_value("merchant_connector_account_id")?
.get_string_repr()
.to_owned()
};
let (should_call_connector, existing_connector_customer_id) =
customers::should_call_connector_create_customer(
state, &connector, customer, &label,
@ -3961,7 +3960,90 @@ where
let customer_update = customers::update_connector_customer_in_customers(
&label,
customer.as_ref(),
&connector_customer_id,
connector_customer_id.clone(),
)
.await;
payment_data.set_connector_customer_id(connector_customer_id);
Ok(customer_update)
} else {
// Customer already created in previous calls use the same value, no need to update
payment_data.set_connector_customer_id(
existing_connector_customer_id.map(ToOwned::to_owned),
);
Ok(None)
}
}
None => Ok(None),
}
}
#[cfg(feature = "v2")]
pub async fn call_create_connector_customer_if_required<F, Req, D>(
state: &SessionState,
customer: &Option<domain::Customer>,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
merchant_connector_account: &domain::MerchantConnectorAccount,
payment_data: &mut D,
) -> RouterResult<Option<storage::CustomerUpdate>>
where
F: Send + Clone + Sync,
Req: Send + Sync,
// To create connector flow specific interface data
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
D: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
// To construct connector flow specific api
dyn api::Connector:
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
{
let connector_name = payment_data.get_payment_attempt().connector.clone();
match connector_name {
Some(connector_name) => {
let connector = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
Some(merchant_connector_account.get_id()),
)?;
let merchant_connector_id = merchant_connector_account.get_id();
let (should_call_connector, existing_connector_customer_id) =
customers::should_call_connector_create_customer(
state,
&connector,
customer,
&merchant_connector_id,
);
if should_call_connector {
// Create customer at connector and update the customer table to store this data
let router_data = payment_data
.construct_router_data(
state,
connector.connector.id(),
merchant_account,
key_store,
customer,
merchant_connector_account,
None,
None,
)
.await?;
let connector_customer_id = router_data
.create_connector_customer(state, &connector)
.await?;
let customer_update = customers::update_connector_customer_in_customers(
merchant_connector_id,
customer.as_ref(),
connector_customer_id.clone(),
)
.await;

View File

@ -1,5 +1,5 @@
use common_utils::pii;
use masking::{ExposeOptionInterface, PeekInterface};
use masking::ExposeOptionInterface;
use router_env::{instrument, tracing};
use crate::{
@ -73,17 +73,7 @@ pub async fn create_connector_customer<F: Clone, T: Clone>(
Ok(connector_customer_id)
}
pub fn get_connector_customer_details_if_present<'a>(
customer: &'a domain::Customer,
connector_name: &str,
) -> Option<&'a str> {
customer
.connector_customer
.as_ref()
.and_then(|connector_customer_value| connector_customer_value.peek().get(connector_name))
.and_then(|connector_customer| connector_customer.as_str())
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
pub fn should_call_connector_create_customer<'a>(
state: &SessionState,
connector: &api::ConnectorData,
@ -98,9 +88,9 @@ pub fn should_call_connector_create_customer<'a>(
.contains(&connector.connector_name);
if connector_needs_customer {
let connector_customer_details = customer.as_ref().and_then(|customer| {
get_connector_customer_details_if_present(customer, connector_label)
});
let connector_customer_details = customer
.as_ref()
.and_then(|customer| customer.get_connector_customer_id(connector_label));
let should_call_connector = connector_customer_details.is_none();
(should_call_connector, connector_customer_details)
} else {
@ -108,25 +98,48 @@ pub fn should_call_connector_create_customer<'a>(
}
}
#[cfg(all(feature = "v2", feature = "customer_v2"))]
pub fn should_call_connector_create_customer<'a>(
state: &SessionState,
connector: &api::ConnectorData,
customer: &'a Option<domain::Customer>,
merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId,
) -> (bool, Option<&'a str>) {
// Check if create customer is required for the connector
let connector_needs_customer = state
.conf
.connector_customer
.connector_list
.contains(&connector.connector_name);
if connector_needs_customer {
let connector_customer_details = customer
.as_ref()
.and_then(|customer| customer.get_connector_customer_id(merchant_connector_id));
let should_call_connector = connector_customer_details.is_none();
(should_call_connector, connector_customer_details)
} else {
(false, None)
}
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
#[instrument]
pub async fn update_connector_customer_in_customers(
connector_label: &str,
customer: Option<&domain::Customer>,
connector_customer_id: &Option<String>,
connector_customer_id: Option<String>,
) -> Option<storage::CustomerUpdate> {
let connector_customer_map = customer
let mut connector_customer_map = customer
.and_then(|customer| customer.connector_customer.clone().expose_option())
.and_then(|connector_customer| connector_customer.as_object().cloned())
.unwrap_or_default();
let updated_connector_customer_map =
connector_customer_id.as_ref().map(|connector_customer_id| {
let mut connector_customer_map = connector_customer_map;
let connector_customer_value =
serde_json::Value::String(connector_customer_id.to_string());
connector_customer_map.insert(connector_label.to_string(), connector_customer_value);
connector_customer_map
});
let updated_connector_customer_map = connector_customer_id.map(|connector_customer_id| {
let connector_customer_value = serde_json::Value::String(connector_customer_id);
connector_customer_map.insert(connector_label.to_string(), connector_customer_value);
connector_customer_map
});
updated_connector_customer_map
.map(serde_json::Value::Object)
@ -136,3 +149,22 @@ pub async fn update_connector_customer_in_customers(
},
)
}
#[cfg(all(feature = "v2", feature = "customer_v2"))]
#[instrument]
pub async fn update_connector_customer_in_customers(
merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
customer: Option<&domain::Customer>,
connector_customer_id: Option<String>,
) -> Option<storage::CustomerUpdate> {
connector_customer_id.map(|connector_customer_id| {
let mut connector_customer_map = customer
.and_then(|customer| customer.connector_customer.clone())
.unwrap_or_default();
connector_customer_map.insert(merchant_connector_id, connector_customer_id);
storage::CustomerUpdate::ConnectorCustomer {
connector_customer: Some(connector_customer_map),
}
})
}

View File

@ -462,6 +462,25 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentConfirmData<F>, PaymentsConfirmInt
payment_data.payment_attempt = updated_payment_attempt;
if let Some((customer, updated_customer)) = customer.zip(updated_customer) {
let customer_id = customer.get_id().clone();
let customer_merchant_id = customer.merchant_id.clone();
let _updated_customer = db
.update_customer_by_global_id(
key_manager_state,
&customer_id,
customer,
&customer_merchant_id,
updated_customer,
key_store,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update customer during `update_trackers`")?;
}
Ok((Box::new(self), payment_data))
}
}

View File

@ -211,6 +211,12 @@ pub async fn construct_payment_router_data_for_authorize<'a>(
"Invalid global customer generated, not able to convert to reference id",
)?;
let connector_customer_id = customer.as_ref().and_then(|customer| {
customer
.get_connector_customer_id(&merchant_connector_account.get_id())
.map(String::from)
});
let payment_method = payment_data.payment_attempt.payment_method_type;
let router_base_url = &state.base_url;
@ -352,7 +358,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>(
reference_id: None,
payment_method_status: None,
payment_method_token: None,
connector_customer: None,
connector_customer: connector_customer_id,
recurring_mandate_payment_data: None,
// TODO: This has to be generated as the reference id based on the connector configuration
// Some connectros might not accept accept the global id. This has to be done when generating the reference id
@ -867,6 +873,12 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>(
"Invalid global customer generated, not able to convert to reference id",
)?;
let connector_customer_id = customer.as_ref().and_then(|customer| {
customer
.get_connector_customer_id(&merchant_connector_account.get_id())
.map(String::from)
});
let payment_method = payment_data.payment_attempt.payment_method_type;
let router_base_url = &state.base_url;
@ -994,7 +1006,7 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>(
reference_id: None,
payment_method_status: None,
payment_method_token: None,
connector_customer: None,
connector_customer: connector_customer_id,
recurring_mandate_payment_data: None,
// TODO: This has to be generated as the reference id based on the connector configuration
// Some connectros might not accept accept the global id. This has to be done when generating the reference id

View File

@ -1203,6 +1203,7 @@ pub async fn complete_create_recipient(
Ok(())
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
pub async fn create_recipient(
state: &SessionState,
merchant_account: &domain::MerchantAccount,
@ -1262,7 +1263,7 @@ pub async fn create_recipient(
customers::update_connector_customer_in_customers(
&connector_label,
Some(&customer),
&recipient_create_data.connector_payout_id.clone(),
recipient_create_data.connector_payout_id.clone(),
)
.await
{
@ -1402,6 +1403,17 @@ pub async fn create_recipient(
Ok(())
}
#[cfg(all(feature = "v2", feature = "customer_v2"))]
pub async fn create_recipient(
state: &SessionState,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
connector_data: &api::ConnectorData,
payout_data: &mut PayoutData,
) -> RouterResult<()> {
todo!()
}
pub async fn complete_payout_eligibility(
state: &SessionState,
merchant_account: &domain::MerchantAccount,

View File

@ -29,10 +29,7 @@ use crate::{
transformers::{DataDuplicationCheck, StoreCardReq, StoreGenericReq, StoreLockerReq},
vault,
},
payments::{
customers::get_connector_customer_details_if_present, helpers as payment_helpers,
routing, CustomerDetails,
},
payments::{helpers as payment_helpers, routing, CustomerDetails},
routing::TransactionData,
utils as core_utils,
},
@ -965,6 +962,7 @@ pub async fn get_default_payout_connector(
))
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
pub fn should_call_payout_connector_create_customer<'a>(
state: &'a SessionState,
connector: &'a api::ConnectorData,
@ -981,9 +979,39 @@ pub fn should_call_payout_connector_create_customer<'a>(
.contains(&connector);
if connector_needs_customer {
let connector_customer_details = customer.as_ref().and_then(|customer| {
get_connector_customer_details_if_present(customer, connector_label)
});
let connector_customer_details = customer
.as_ref()
.and_then(|customer| customer.get_connector_customer_id(connector_label));
let should_call_connector = connector_customer_details.is_none();
(should_call_connector, connector_customer_details)
} else {
(false, None)
}
}
_ => (false, None),
}
}
#[cfg(all(feature = "v2", feature = "customer_v2"))]
pub fn should_call_payout_connector_create_customer<'a>(
state: &'a SessionState,
connector: &'a api::ConnectorData,
customer: &'a Option<domain::Customer>,
merchant_connector_id: &'a id_type::MerchantConnectorAccountId,
) -> (bool, Option<&'a str>) {
// Check if create customer is required for the connector
match enums::PayoutConnectors::try_from(connector.connector_name) {
Ok(connector) => {
let connector_needs_customer = state
.conf
.connector_customer
.payout_connector_list
.contains(&connector);
if connector_needs_customer {
let connector_customer_details = customer
.as_ref()
.and_then(|customer| customer.get_connector_customer_id(merchant_connector_id));
let should_call_connector = connector_customer_details.is_none();
(should_call_connector, connector_customer_details)
} else {

View File

@ -1,3 +1,5 @@
pub use diesel_models::customers::{Customer, CustomerNew, CustomerUpdateInternal};
#[cfg(all(feature = "v2", feature = "customer_v2"))]
pub use crate::types::domain::CustomerGeneralUpdate;
pub use crate::types::domain::CustomerUpdate;