feat(core): customer_details storage in payment_intent (#5007)

Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2024-06-28 15:06:19 +05:30
committed by GitHub
parent a172cba5d3
commit bb9a97154c
20 changed files with 380 additions and 110 deletions

View File

@ -22,6 +22,7 @@ use api_models::{
use common_enums::enums::MerchantStorageScheme;
use common_utils::{
consts,
crypto::Encryptable,
ext_traits::{AsyncExt, Encode, StringExt, ValueExt},
generate_id, id_type,
types::MinorUnit,
@ -66,7 +67,7 @@ use crate::{
types::{decrypt, encrypt_optional, AsyncLift},
},
storage::{self, enums, PaymentMethodListContext, PaymentTokenData},
transformers::ForeignFrom,
transformers::{ForeignFrom, ForeignTryFrom},
},
utils::{self, ConnectorResponseExt, OptionExt},
};
@ -467,8 +468,9 @@ pub async fn add_payment_method_data(
};
let updated_pmd = Some(PaymentMethodsData::Card(updated_card));
let pm_data_encrypted =
create_encrypted_data(&key_store, updated_pmd).await;
let pm_data_encrypted = create_encrypted_data(&key_store, updated_pmd)
.await
.map(|details| details.into());
let pm_update = storage::PaymentMethodUpdate::AdditionalDataUpdate {
payment_method_data: pm_data_encrypted,
@ -689,7 +691,9 @@ pub async fn add_payment_method(
let updated_pmd = updated_card.as_ref().map(|card| {
PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()))
});
let pm_data_encrypted = create_encrypted_data(key_store, updated_pmd).await;
let pm_data_encrypted = create_encrypted_data(key_store, updated_pmd)
.await
.map(|details| details.into());
let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
payment_method_data: pm_data_encrypted,
@ -763,7 +767,10 @@ pub async fn insert_payment_method(
.card
.as_ref()
.map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
let pm_data_encrypted = create_encrypted_data(key_store, pm_card_details).await;
let pm_data_encrypted = create_encrypted_data(key_store, pm_card_details)
.await
.map(|details| details.into());
create_payment_method(
db,
&req,
@ -943,7 +950,9 @@ pub async fn update_customer_payment_method(
let updated_pmd = updated_card
.as_ref()
.map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
let pm_data_encrypted = create_encrypted_data(&key_store, updated_pmd).await;
let pm_data_encrypted = create_encrypted_data(&key_store, updated_pmd)
.await
.map(|details| details.into());
let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
payment_method_data: pm_data_encrypted,
@ -2227,12 +2236,13 @@ pub async fn list_payment_methods(
.then_some(billing_address.as_ref())
.flatten();
let req = api_models::payments::PaymentsRequest::foreign_from((
let req = api_models::payments::PaymentsRequest::foreign_try_from((
payment_attempt.as_ref(),
payment_intent.as_ref(),
shipping_address.as_ref(),
billing_address_for_calculating_required_fields,
customer.as_ref(),
));
))?;
let req_val = serde_json::to_value(req).ok();
logger::debug!(filtered_payment_methods=?response);
@ -4358,14 +4368,13 @@ pub async fn delete_payment_method(
pub async fn create_encrypted_data<T>(
key_store: &domain::MerchantKeyStore,
data: Option<T>,
) -> Option<Encryption>
) -> Option<Encryptable<Secret<serde_json::Value>>>
where
T: Debug + serde::Serialize,
{
let key = key_store.key.get_inner().peek();
let encrypted_data: Option<Encryption> = data
.as_ref()
data.as_ref()
.map(Encode::encode_to_value)
.transpose()
.change_context(errors::StorageError::SerializationFailed)
@ -4383,9 +4392,6 @@ where
logger::error!(err=?err);
None
})
.map(|details| details.into());
encrypted_data
}
pub async fn list_countries_currencies_for_connector_payment_method(

View File

@ -18,7 +18,7 @@ use error_stack::{report, ResultExt};
use futures::future::Either;
use hyperswitch_domain_models::{
mandates::MandateData,
payments::{payment_attempt::PaymentAttempt, PaymentIntent},
payments::{payment_attempt::PaymentAttempt, payment_intent::CustomerData, PaymentIntent},
router_data::KlarnaSdkResponse,
};
use josekit::jwe;
@ -44,7 +44,11 @@ use crate::{
authentication,
errors::{self, CustomResult, RouterResult, StorageErrorExt},
mandate::helpers::MandateGenericData,
payment_methods::{self, cards, vault},
payment_methods::{
self,
cards::{self, create_encrypted_data},
vault,
},
payments,
pm_auth::retrieve_payment_method_from_auth_service,
},
@ -1580,6 +1584,61 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R>(
.get_required_value("customer")
.change_context(errors::StorageError::ValueNotFound("customer".to_owned()))?;
let temp_customer_data = if request_customer_details.name.is_some()
|| request_customer_details.email.is_some()
|| request_customer_details.phone.is_some()
|| request_customer_details.phone_country_code.is_some()
{
Some(CustomerData {
name: request_customer_details.name.clone(),
email: request_customer_details.email.clone(),
phone: request_customer_details.phone.clone(),
phone_country_code: request_customer_details.phone_country_code.clone(),
})
} else {
None
};
// Updation of Customer Details for the cases where both customer_id and specific customer
// details are provided in Payment Update Request
let raw_customer_details = payment_data
.payment_intent
.customer_details
.clone()
.map(|customer_details_encrypted| {
customer_details_encrypted
.into_inner()
.expose()
.parse_value::<CustomerData>("CustomerData")
})
.transpose()
.change_context(errors::StorageError::DeserializationFailed)
.attach_printable("Failed to parse customer data from payment intent")?
.map(|parsed_customer_data| CustomerData {
name: request_customer_details
.name
.clone()
.or(parsed_customer_data.name.clone()),
email: request_customer_details
.email
.clone()
.or(parsed_customer_data.email.clone()),
phone: request_customer_details
.phone
.clone()
.or(parsed_customer_data.phone.clone()),
phone_country_code: request_customer_details
.phone_country_code
.clone()
.or(parsed_customer_data.phone_country_code.clone()),
})
.or(temp_customer_data);
payment_data.payment_intent.customer_details = raw_customer_details
.clone()
.async_and_then(|_| async { create_encrypted_data(key_store, raw_customer_details).await })
.await;
let customer_id = request_customer_details
.customer_id
.or(payment_data.payment_intent.customer_id.clone());
@ -3062,6 +3121,7 @@ mod tests {
request_external_three_ds_authentication: None,
charges: None,
frm_metadata: None,
customer_details: None,
};
let req_cs = Some("1".to_string());
assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_ok());
@ -3121,6 +3181,7 @@ mod tests {
request_external_three_ds_authentication: None,
charges: None,
frm_metadata: None,
customer_details: None,
};
let req_cs = Some("1".to_string());
assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent,).is_err())
@ -3179,6 +3240,7 @@ mod tests {
request_external_three_ds_authentication: None,
charges: None,
frm_metadata: None,
customer_details: None,
};
let req_cs = Some("1".to_string());
assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_err())

View File

@ -1084,6 +1084,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
None
};
let customer_details = payment_data.payment_intent.customer_details.clone();
let business_sub_label = payment_data.payment_attempt.business_sub_label.clone();
let authentication_type = payment_data.payment_attempt.authentication_type;
@ -1255,6 +1256,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
session_expiry,
request_external_three_ds_authentication: None,
frm_metadata: m_frm_metadata,
customer_details,
},
&m_key_store,
storage_scheme,

View File

@ -12,7 +12,8 @@ use diesel_models::{ephemeral_key, PaymentMethod};
use error_stack::{self, ResultExt};
use hyperswitch_domain_models::{
mandates::{MandateData, MandateDetails},
payments::payment_attempt::PaymentAttempt,
payments::{payment_attempt::PaymentAttempt, payment_intent::CustomerData},
type_encryption::decrypt,
};
use masking::{ExposeInterface, PeekInterface, Secret};
use router_derive::PaymentOperation;
@ -26,6 +27,7 @@ use crate::{
errors::{self, CustomResult, RouterResult, StorageErrorExt},
mandate::helpers as m_helpers,
payment_link,
payment_methods::cards::create_encrypted_data,
payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
utils as core_utils,
},
@ -245,8 +247,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
};
let payment_intent_new = Self::make_payment_intent(
state,
&payment_id,
merchant_account,
merchant_key_store,
money,
request,
shipping_address
@ -560,7 +564,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
state: &'b SessionState,
_req_state: ReqState,
mut payment_data: PaymentData<F>,
_customer: Option<domain::Customer>,
customer: Option<domain::Customer>,
storage_scheme: enums::MerchantStorageScheme,
_updated_customer: Option<storage::CustomerUpdate>,
key_store: &domain::MerchantKeyStore,
@ -628,16 +632,30 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let customer_id = payment_data.payment_intent.customer_id.clone();
let raw_customer_details = customer
.map(|customer| CustomerData::try_from(customer.clone()))
.transpose()?;
// Updation of Customer Details for the cases where both customer_id and specific customer
// details are provided in Payment Create Request
let customer_details = raw_customer_details
.clone()
.async_and_then(|_| async {
create_encrypted_data(key_store, raw_customer_details).await
})
.await;
payment_data.payment_intent = state
.store
.update_payment_intent(
payment_data.payment_intent,
storage::PaymentIntentUpdate::ReturnUrlUpdate {
storage::PaymentIntentUpdate::PaymentCreateUpdate {
return_url: None,
status,
customer_id,
shipping_address_id: None,
billing_address_id: None,
customer_details,
updated_by: storage_scheme.to_string(),
},
key_store,
@ -815,7 +833,7 @@ impl PaymentCreate {
additional_pm_data = payment_method_info
.as_ref()
.async_map(|pm_info| async {
domain::types::decrypt::<serde_json::Value, masking::WithType>(
decrypt::<serde_json::Value, masking::WithType>(
pm_info.payment_method_data.clone(),
key_store.key.get_inner().peek(),
)
@ -956,8 +974,10 @@ impl PaymentCreate {
#[instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
async fn make_payment_intent(
_state: &SessionState,
payment_id: &str,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
money: (api::Amount, enums::Currency),
request: &api::PaymentsRequest,
shipping_address_id: Option<String>,
@ -1023,6 +1043,30 @@ impl PaymentCreate {
.change_context(errors::ApiErrorResponse::InternalServerError)?
.map(Secret::new);
// Derivation of directly supplied Customer data in our Payment Create Request
let raw_customer_details = if request.customer_id.is_none()
&& (request.name.is_some()
|| request.email.is_some()
|| request.phone.is_some()
|| request.phone_country_code.is_some())
{
Some(CustomerData {
name: request.name.clone(),
phone: request.phone.clone(),
email: request.email.clone(),
phone_country_code: request.phone_country_code.clone(),
})
} else {
None
};
// Encrypting our Customer Details to be stored in Payment Intent
let customer_details = if raw_customer_details.is_some() {
create_encrypted_data(key_store, raw_customer_details).await
} else {
None
};
Ok(storage::PaymentIntent {
payment_id: payment_id.to_string(),
merchant_id: merchant_account.merchant_id.to_string(),
@ -1070,6 +1114,7 @@ impl PaymentCreate {
.request_external_three_ds_authentication,
charges,
frm_metadata: request.frm_metadata.clone(),
customer_details,
})
}

View File

@ -4,8 +4,12 @@ use api_models::{
enums::FrmSuggestion, mandates::RecurringDetails, payments::RequestSurchargeDetails,
};
use async_trait::async_trait;
use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
use common_utils::{
ext_traits::{AsyncExt, Encode, ValueExt},
pii::Email,
};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::payments::payment_intent::CustomerData;
use router_derive::PaymentOperation;
use router_env::{instrument, tracing};
@ -661,7 +665,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let customer_id = customer.map(|c| c.customer_id);
let customer_id = customer.clone().map(|c| c.customer_id);
let intent_status = {
let current_intent_status = payment_data.payment_intent.status;
@ -681,6 +685,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
payment_data.payment_intent.billing_address_id.clone(),
);
let customer_details = payment_data.payment_intent.customer_details.clone();
let return_url = payment_data.payment_intent.return_url.clone();
let setup_future_usage = payment_data.payment_intent.setup_future_usage;
let business_label = payment_data.payment_intent.business_label.clone();
@ -726,6 +732,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.payment_intent
.request_external_three_ds_authentication,
frm_metadata,
customer_details,
},
key_store,
storage_scheme,
@ -740,6 +747,18 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
}
}
impl TryFrom<domain::Customer> for CustomerData {
type Error = errors::ApiErrorResponse;
fn try_from(value: domain::Customer) -> Result<Self, Self::Error> {
Ok(Self {
name: value.name.map(|name| name.into_inner()),
email: value.email.map(Email::from),
phone: value.phone.map(|ph| ph.into_inner()),
phone_country_code: value.phone_country_code,
})
}
}
impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentUpdate {
#[instrument(skip_all)]
fn validate_request<'a, 'b>(

View File

@ -210,14 +210,17 @@ where
});
let pm_data_encrypted =
payment_methods::cards::create_encrypted_data(key_store, pm_card_details).await;
payment_methods::cards::create_encrypted_data(key_store, pm_card_details)
.await
.map(|details| details.into());
let encrypted_payment_method_billing_address =
payment_methods::cards::create_encrypted_data(
key_store,
payment_method_billing_address,
)
.await;
.await
.map(|details| details.into());
let mut payment_method_id = resp.payment_method_id.clone();
let mut locker_id = None;
@ -509,7 +512,8 @@ where
key_store,
updated_pmd,
)
.await;
.await
.map(|details| details.into());
let pm_update =
storage::PaymentMethodUpdate::PaymentMethodDataUpdate {

View File

@ -1,16 +1,17 @@
use std::{fmt::Debug, marker::PhantomData, str::FromStr};
use api_models::payments::{
FrmMessage, GetAddressFromPaymentMethodData, PaymentChargeRequest, PaymentChargeResponse,
RequestSurchargeDetails,
CustomerDetailsResponse, FrmMessage, GetAddressFromPaymentMethodData, PaymentChargeRequest,
PaymentChargeResponse, RequestSurchargeDetails,
};
#[cfg(feature = "payouts")]
use api_models::payouts::PayoutAttemptResponse;
use common_enums::RequestIncrementalAuthorization;
use common_utils::{consts::X_HS_LATENCY, fp_utils, types::MinorUnit};
use common_utils::{consts::X_HS_LATENCY, fp_utils, pii::Email, types::MinorUnit};
use diesel_models::ephemeral_key;
use error_stack::{report, ResultExt};
use masking::{Maskable, PeekInterface, Secret};
use hyperswitch_domain_models::payments::payment_intent::CustomerData;
use masking::{ExposeInterface, Maskable, PeekInterface, Secret};
use router_env::{instrument, metrics::add_attributes, tracing};
use super::{flows::Feature, types::AuthenticationData, PaymentData};
@ -506,7 +507,47 @@ where
))
}
let customer_details_response = customer.as_ref().map(ForeignInto::foreign_into);
// For the case when we don't have Customer data directly stored in Payment intent
let customer_table_response: Option<CustomerDetailsResponse> =
customer.as_ref().map(ForeignInto::foreign_into);
// If we have customer data in Payment Intent, We are populating the Retrieve response from the
// same
let customer_details_response =
if let Some(customer_details_raw) = payment_intent.customer_details.clone() {
let customer_details_encrypted =
serde_json::from_value::<CustomerData>(customer_details_raw.into_inner().expose());
if let Ok(customer_details_encrypted_data) = customer_details_encrypted {
Some(CustomerDetailsResponse {
id: customer_table_response.and_then(|customer_data| customer_data.id),
name: customer_details_encrypted_data
.name
.or(customer.as_ref().and_then(|customer| {
customer.name.as_ref().map(|name| name.clone().into_inner())
})),
email: customer_details_encrypted_data.email.or(customer
.as_ref()
.and_then(|customer| customer.email.clone().map(Email::from))),
phone: customer_details_encrypted_data
.phone
.or(customer.as_ref().and_then(|customer| {
customer
.phone
.as_ref()
.map(|phone| phone.clone().into_inner())
})),
phone_country_code: customer_details_encrypted_data.phone_country_code.or(
customer
.as_ref()
.and_then(|customer| customer.phone_country_code.clone()),
),
})
} else {
customer_table_response
}
} else {
customer_table_response
};
headers.extend(
external_latency

View File

@ -449,7 +449,9 @@ pub async fn save_payout_data_to_locker(
)
});
(
cards::create_encrypted_data(key_store, Some(pm_data)).await,
cards::create_encrypted_data(key_store, Some(pm_data))
.await
.map(|details| details.into()),
payment_method,
)
} else {

View File

@ -421,6 +421,7 @@ async fn store_bank_details_in_payment_methods(
let encrypted_data =
cards::create_encrypted_data(&key_store, Some(payment_method_data))
.await
.map(|details| details.into())
.ok_or(ApiErrorResponse::InternalServerError)?;
let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
payment_method_data: Some(encrypted_data),
@ -432,6 +433,7 @@ async fn store_bank_details_in_payment_methods(
let encrypted_data =
cards::create_encrypted_data(&key_store, Some(payment_method_data))
.await
.map(|details| details.into())
.ok_or(ApiErrorResponse::InternalServerError)?;
let pm_id = generate_id(consts::ID_LENGTH, "pm");
let now = common_utils::date_time::now();