refactor(core): accept customer data in customer object (#1447)

Co-authored-by: Abhishek Marrivagu <68317979+Abhicodes-crypto@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2023-06-16 18:55:01 +05:30
committed by GitHub
parent 3ef1d2935e
commit cff1ce61f0
11 changed files with 195 additions and 66 deletions

View File

@ -45,6 +45,28 @@ pub struct BankCodeResponse {
pub eligible_connectors: Vec<String>, pub eligible_connectors: Vec<String>,
} }
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct CustomerDetails {
/// The identifier for the customer.
pub id: String,
/// The customer's name
#[schema(max_length = 255, value_type = Option<String>, example = "John Doe")]
pub name: Option<Secret<String>>,
/// The customer's email address
#[schema(max_length = 255, value_type = Option<String>, example = "johntest@test.com")]
pub email: Option<Email>,
/// The customer's phone number
#[schema(value_type = Option<String>, max_length = 10, example = "3141592653")]
pub phone: Option<Secret<String>>,
/// The country code for the customer's phone number
#[schema(max_length = 2, example = "+1")]
pub phone_country_code: Option<String>,
}
#[derive( #[derive(
Default, Default,
Debug, Debug,
@ -114,23 +136,33 @@ pub struct PaymentsRequest {
#[schema(default = false, example = true)] #[schema(default = false, example = true)]
pub confirm: Option<bool>, pub confirm: Option<bool>,
/// The identifier for the customer object. If not provided the customer ID will be autogenerated. /// The details of a customer for this payment
/// This will create the customer if `customer.id` does not exist
/// If customer id already exists, it will update the details of the customer
pub customer: Option<CustomerDetails>,
/// The identifier for the customer object.
/// This field will be deprecated soon, use the customer object instead
#[schema(max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] #[schema(max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")]
pub customer_id: Option<String>, pub customer_id: Option<String>,
/// description: The customer's email address /// The customer's email address
/// This field will be deprecated soon, use the customer object instead
#[schema(max_length = 255, value_type = Option<String>, example = "johntest@test.com")] #[schema(max_length = 255, value_type = Option<String>, example = "johntest@test.com")]
pub email: Option<Email>, pub email: Option<Email>,
/// description: The customer's name /// description: The customer's name
/// This field will be deprecated soon, use the customer object instead
#[schema(value_type = Option<String>, max_length = 255, example = "John Test")] #[schema(value_type = Option<String>, max_length = 255, example = "John Test")]
pub name: Option<Secret<String>>, pub name: Option<Secret<String>>,
/// The customer's phone number /// The customer's phone number
/// This field will be deprecated soon, use the customer object instead
#[schema(value_type = Option<String>, max_length = 255, example = "3141592653")] #[schema(value_type = Option<String>, max_length = 255, example = "3141592653")]
pub phone: Option<Secret<String>>, pub phone: Option<Secret<String>>,
/// The country code for the customer phone number /// The country code for the customer phone number
/// This field will be deprecated soon, use the customer object instead
#[schema(max_length = 255, example = "+1")] #[schema(max_length = 255, example = "+1")]
pub phone_country_code: Option<String>, pub phone_country_code: Option<String>,
@ -303,27 +335,6 @@ pub struct VerifyRequest {
pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>, pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>,
} }
impl From<PaymentsRequest> for VerifyRequest {
fn from(item: PaymentsRequest) -> Self {
Self {
client_secret: item.client_secret,
merchant_id: item.merchant_id,
customer_id: item.customer_id,
email: item.email,
name: item.name,
phone: item.phone,
phone_country_code: item.phone_country_code,
payment_method: item.payment_method,
payment_method_data: item.payment_method_data,
payment_token: item.payment_token,
mandate_data: item.mandate_data,
setup_future_usage: item.setup_future_usage,
off_session: item.off_session,
merchant_connector_details: item.merchant_connector_details,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum MandateTxnType { pub enum MandateTxnType {
@ -1478,7 +1489,12 @@ impl From<&PaymentsRequest> for MandateValidationFields {
Self { Self {
mandate_id: req.mandate_id.clone(), mandate_id: req.mandate_id.clone(),
confirm: req.confirm, confirm: req.confirm,
customer_id: req.customer_id.clone(), customer_id: req
.customer
.as_ref()
.map(|customer_details| &customer_details.id)
.or(req.customer_id.as_ref())
.map(ToOwned::to_owned),
mandate_data: req.mandate_data.clone(), mandate_data: req.mandate_data.clone(),
setup_future_usage: req.setup_future_usage, setup_future_usage: req.setup_future_usage,
off_session: req.off_session, off_session: req.off_session,

View File

@ -96,7 +96,7 @@ pub async fn get_address_for_payment_request(
req_address: Option<&api::Address>, req_address: Option<&api::Address>,
address_id: Option<&str>, address_id: Option<&str>,
merchant_id: &str, merchant_id: &str,
customer_id: &Option<String>, customer_id: Option<&String>,
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> { ) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
let key = types::get_merchant_enc_key(db, merchant_id.to_string()) let key = types::get_merchant_enc_key(db, merchant_id.to_string())
.await .await
@ -179,7 +179,7 @@ pub async fn get_address_for_payment_request(
} }
None => { None => {
// generate a new address here // generate a new address here
let customer_id = customer_id.as_deref().get_required_value("customer_id")?; let customer_id = customer_id.get_required_value("customer_id")?;
let address_details = address.address.clone().unwrap_or_default(); let address_details = address.address.clone().unwrap_or_default();
Some( Some(
@ -810,6 +810,109 @@ pub async fn get_customer_from_details<F: Clone>(
} }
} }
// Checks if the inner values of two options are not equal and throws appropriate error
fn validate_options_for_inequality<T: PartialEq>(
first_option: Option<&T>,
second_option: Option<&T>,
field_name: &str,
) -> Result<(), errors::ApiErrorResponse> {
fp_utils::when(
first_option
.zip(second_option)
.map(|(value1, value2)| value1 != value2)
.unwrap_or(false),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: format!("The field name `{field_name}` sent in both places is ambiguous"),
})
},
)
}
// Checks if the customer details are passed in both places
// If so, raise an error
pub fn validate_customer_details_in_request(
request: &api_models::payments::PaymentsRequest,
) -> Result<(), errors::ApiErrorResponse> {
if let Some(customer_details) = request.customer.as_ref() {
validate_options_for_inequality(
request.customer_id.as_ref(),
Some(&customer_details.id),
"customer_id",
)?;
validate_options_for_inequality(
request.email.as_ref(),
customer_details.email.as_ref(),
"email",
)?;
validate_options_for_inequality(
request.name.as_ref(),
customer_details.name.as_ref(),
"name",
)?;
validate_options_for_inequality(
request.phone.as_ref(),
customer_details.phone.as_ref(),
"phone",
)?;
validate_options_for_inequality(
request.phone_country_code.as_ref(),
customer_details.phone_country_code.as_ref(),
"phone_country_code",
)?;
}
Ok(())
}
/// Get the customer details from customer field if present
/// or from the individual fields in `PaymentsRequest`
pub fn get_customer_details_from_request(
request: &api_models::payments::PaymentsRequest,
) -> CustomerDetails {
let customer_id = request
.customer
.as_ref()
.map(|customer_details| customer_details.id.clone())
.or(request.customer_id.clone());
let customer_name = request
.customer
.as_ref()
.and_then(|customer_details| customer_details.name.clone())
.or(request.name.clone());
let customer_email = request
.customer
.as_ref()
.and_then(|customer_details| customer_details.email.clone())
.or(request.email.clone());
let customer_phone = request
.customer
.as_ref()
.and_then(|customer_details| customer_details.phone.clone())
.or(request.phone.clone());
let customer_phone_code = request
.customer
.as_ref()
.and_then(|customer_details| customer_details.phone_country_code.clone())
.or(request.phone_country_code.clone());
CustomerDetails {
customer_id,
name: customer_name,
email: customer_email,
phone: customer_phone,
phone_country_code: customer_phone_code,
}
}
pub async fn get_connector_default( pub async fn get_connector_default(
_state: &AppState, _state: &AppState,
request_connector: Option<serde_json::Value>, request_connector: Option<serde_json::Value>,

View File

@ -80,7 +80,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsCancelRequest>
None, None,
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;
let billing_address = helpers::get_address_for_payment_request( let billing_address = helpers::get_address_for_payment_request(
@ -88,7 +88,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsCancelRequest>
None, None,
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;

View File

@ -99,7 +99,7 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
None, None,
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;
@ -108,7 +108,7 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
None, None,
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;

View File

@ -135,7 +135,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
request.shipping.as_ref(), request.shipping.as_ref(),
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;
let billing_address = helpers::get_address_for_payment_request( let billing_address = helpers::get_address_for_payment_request(
@ -143,7 +143,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
request.billing.as_ref(), request.billing.as_ref(),
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;

View File

@ -122,6 +122,8 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
field_name: "browser_info", field_name: "browser_info",
})?; })?;
let customer_details = helpers::get_customer_details_from_request(request);
let token = token.or_else(|| payment_attempt.payment_token.clone()); let token = token.or_else(|| payment_attempt.payment_token.clone());
helpers::validate_pm_or_token_given( helpers::validate_pm_or_token_given(
@ -159,7 +161,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
&payment_intent &payment_intent
.customer_id .customer_id
.clone() .clone()
.or_else(|| request.customer_id.clone()), .or_else(|| customer_details.customer_id.clone()),
)?; )?;
let shipping_address = helpers::get_address_for_payment_request( let shipping_address = helpers::get_address_for_payment_request(
@ -167,7 +169,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request.shipping.as_ref(), request.shipping.as_ref(),
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent
.customer_id
.as_ref()
.or(customer_details.customer_id.as_ref()),
) )
.await?; .await?;
let billing_address = helpers::get_address_for_payment_request( let billing_address = helpers::get_address_for_payment_request(
@ -175,7 +180,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request.billing.as_ref(), request.billing.as_ref(),
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent
.customer_id
.as_ref()
.or(customer_details.customer_id.as_ref()),
) )
.await?; .await?;
@ -255,13 +263,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
ephemeral_key: None, ephemeral_key: None,
redirect_response: None, redirect_response: None,
}, },
Some(CustomerDetails { Some(customer_details),
customer_id: request.customer_id.clone(),
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
phone_country_code: request.phone_country_code.clone(),
}),
)) ))
} }
} }
@ -472,6 +474,9 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentConfir
{ {
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })? Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
} }
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id { let given_payment_id = match &request.payment_id {
Some(id_type) => Some( Some(id_type) => Some(
id_type id_type

View File

@ -74,12 +74,14 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
) )
.await?; .await?;
let customer_details = helpers::get_customer_details_from_request(request);
let shipping_address = helpers::get_address_for_payment_request( let shipping_address = helpers::get_address_for_payment_request(
db, db,
request.shipping.as_ref(), request.shipping.as_ref(),
None, None,
merchant_id, merchant_id,
&request.customer_id, customer_details.customer_id.as_ref(),
) )
.await?; .await?;
@ -88,7 +90,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request.billing.as_ref(), request.billing.as_ref(),
None, None,
merchant_id, merchant_id,
&request.customer_id, customer_details.customer_id.as_ref(),
) )
.await?; .await?;
@ -256,13 +258,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
ephemeral_key, ephemeral_key,
redirect_response: None, redirect_response: None,
}, },
Some(CustomerDetails { Some(customer_details),
customer_id: request.customer_id.clone(),
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
phone_country_code: request.phone_country_code.clone(),
}),
)) ))
} }
} }
@ -423,6 +419,9 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentCreate
{ {
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })? Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
} }
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id { let given_payment_id = match &request.payment_id {
Some(id_type) => Some( Some(id_type) => Some(
id_type id_type

View File

@ -87,7 +87,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
None, None,
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;
@ -96,7 +96,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
None, None,
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;

View File

@ -81,7 +81,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
None, None,
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;
let billing_address = helpers::get_address_for_payment_request( let billing_address = helpers::get_address_for_payment_request(
@ -89,7 +89,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
None, None,
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent.customer_id.as_ref(),
) )
.await?; .await?;

View File

@ -107,6 +107,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
}; };
payment_attempt.payment_method = payment_method_type.or(payment_attempt.payment_method); payment_attempt.payment_method = payment_method_type.or(payment_attempt.payment_method);
let customer_details = helpers::get_customer_details_from_request(request);
let amount = request let amount = request
.amount .amount
@ -120,7 +121,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
&payment_intent &payment_intent
.customer_id .customer_id
.clone() .clone()
.or_else(|| request.customer_id.clone()), .or_else(|| customer_details.customer_id.clone()),
)?; )?;
} }
@ -129,7 +130,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request.shipping.as_ref(), request.shipping.as_ref(),
payment_intent.shipping_address_id.as_deref(), payment_intent.shipping_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent
.customer_id
.as_ref()
.or(customer_details.customer_id.as_ref()),
) )
.await?; .await?;
let billing_address = helpers::get_address_for_payment_request( let billing_address = helpers::get_address_for_payment_request(
@ -137,7 +141,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request.billing.as_ref(), request.billing.as_ref(),
payment_intent.billing_address_id.as_deref(), payment_intent.billing_address_id.as_deref(),
merchant_id, merchant_id,
&payment_intent.customer_id, payment_intent
.customer_id
.as_ref()
.or(customer_details.customer_id.as_ref()),
) )
.await?; .await?;
@ -281,6 +288,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.clone() .clone()
.map(ForeignInto::foreign_into)), .map(ForeignInto::foreign_into)),
}); });
Ok(( Ok((
next_operation, next_operation,
PaymentData { PaymentData {
@ -312,13 +320,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
ephemeral_key: None, ephemeral_key: None,
redirect_response: None, redirect_response: None,
}, },
Some(CustomerDetails { Some(customer_details),
customer_id: request.customer_id.clone(),
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
phone_country_code: request.phone_country_code.clone(),
}),
)) ))
} }
} }
@ -526,6 +528,9 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentUpdate
{ {
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })? Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
} }
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id { let given_payment_id = match &request.payment_id {
Some(id_type) => Some( Some(id_type) => Some(
id_type id_type

View File

@ -241,6 +241,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::mandates::MandateResponse, api_models::mandates::MandateResponse,
api_models::mandates::MandateCardDetails, api_models::mandates::MandateCardDetails,
api_models::ephemeral_key::EphemeralKeyCreateResponse, api_models::ephemeral_key::EphemeralKeyCreateResponse,
api_models::payments::CustomerDetails,
crate::types::api::admin::MerchantAccountResponse, crate::types::api::admin::MerchantAccountResponse,
crate::types::api::admin::MerchantConnectorId, crate::types::api::admin::MerchantConnectorId,
crate::types::api::admin::MerchantDetails, crate::types::api::admin::MerchantDetails,