From bb9a97154c19eeadfdf17428c18d2facebe1dd3a Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Fri, 28 Jun 2024 15:06:19 +0530 Subject: [PATCH] feat(core): customer_details storage in payment_intent (#5007) Co-authored-by: Narayan Bhat Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference/openapi_spec.json | 4 +- crates/api_models/src/payments.rs | 6 +- crates/diesel_models/src/payment_intent.rs | 21 +++- crates/diesel_models/src/schema.rs | 1 + .../hyperswitch_domain_models/src/payments.rs | 6 +- .../src/payments/payment_attempt.rs | 111 ++++++++++-------- .../src/payments/payment_intent.rs | 43 +++++-- .../router/src/core/payment_methods/cards.rs | 34 +++--- crates/router/src/core/payments/helpers.rs | 66 ++++++++++- .../payments/operations/payment_confirm.rs | 2 + .../payments/operations/payment_create.rs | 53 ++++++++- .../payments/operations/payment_update.rs | 23 +++- .../router/src/core/payments/tokenization.rs | 10 +- .../router/src/core/payments/transformers.rs | 51 +++++++- crates/router/src/core/payouts/helpers.rs | 4 +- crates/router/src/core/pm_auth.rs | 2 + crates/router/src/types/transformers.rs | 48 ++++++-- crates/router/src/utils/user/sample_data.rs | 1 + .../down.sql | 2 + .../up.sql | 2 + 20 files changed, 380 insertions(+), 110 deletions(-) create mode 100644 migrations/2024-06-12-060604_add_customer_details_in_payment_intent/down.sql create mode 100644 migrations/2024-06-12-060604_add_customer_details_in_payment_intent/up.sql diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index e8d82cffdd..cfb57ca872 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8496,14 +8496,12 @@ }, "CustomerDetailsResponse": { "type": "object", - "required": [ - "id" - ], "properties": { "id": { "type": "string", "description": "The identifier for the customer.", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, "maxLength": 64, "minLength": 1 }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f18d1a67c3..2b06afc639 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -202,11 +202,11 @@ pub struct CustomerDetails { pub phone_country_code: Option, } -#[derive(Debug, serde::Serialize, Clone, ToSchema, PartialEq)] +#[derive(Debug, Default, serde::Serialize, Clone, ToSchema, PartialEq, Setter)] pub struct CustomerDetailsResponse { /// The identifier for the customer. - #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub id: id_type::CustomerId, + #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub id: Option, /// The customer's name #[schema(max_length = 255, value_type = Option, example = "John Doe")] diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 6326eeef64..fa37eecdbf 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -4,9 +4,9 @@ use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; -use crate::{enums as storage_enums, schema::payment_intent}; +use crate::{encryption::Encryption, enums as storage_enums, schema::payment_intent}; -#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] #[diesel(table_name = payment_intent, primary_key(payment_id, merchant_id))] pub struct PaymentIntent { pub payment_id: String, @@ -59,10 +59,11 @@ pub struct PaymentIntent { pub request_external_three_ds_authentication: Option, pub charges: Option, pub frm_metadata: Option, + pub customer_details: Option, } #[derive( - Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, + Clone, Debug, PartialEq, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, )] #[diesel(table_name = payment_intent)] pub struct PaymentIntentNew { @@ -114,6 +115,7 @@ pub struct PaymentIntentNew { pub request_external_three_ds_authentication: Option, pub charges: Option, pub frm_metadata: Option, + pub customer_details: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -130,12 +132,13 @@ pub enum PaymentIntentUpdate { metadata: pii::SecretSerdeValue, updated_by: String, }, - ReturnUrlUpdate { + PaymentCreateUpdate { return_url: Option, status: Option, customer_id: Option, shipping_address_id: Option, billing_address_id: Option, + customer_details: Option, updated_by: String, }, MerchantStatusUpdate { @@ -171,6 +174,7 @@ pub enum PaymentIntentUpdate { fingerprint_id: Option, request_external_three_ds_authentication: Option, frm_metadata: Option, + customer_details: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -246,6 +250,7 @@ pub struct PaymentIntentUpdateInternal { pub fingerprint_id: Option, pub request_external_three_ds_authentication: Option, pub frm_metadata: Option, + pub customer_details: Option, } impl PaymentIntentUpdate { @@ -281,6 +286,7 @@ impl PaymentIntentUpdate { fingerprint_id, request_external_three_ds_authentication, frm_metadata, + customer_details, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -318,6 +324,7 @@ impl PaymentIntentUpdate { request_external_three_ds_authentication: request_external_three_ds_authentication .or(source.request_external_three_ds_authentication), frm_metadata: frm_metadata.or(source.frm_metadata), + customer_details: customer_details.or(source.customer_details), ..source } } @@ -348,6 +355,7 @@ impl From for PaymentIntentUpdateInternal { fingerprint_id, request_external_three_ds_authentication, frm_metadata, + customer_details, } => Self { amount: Some(amount), currency: Some(currency), @@ -371,6 +379,7 @@ impl From for PaymentIntentUpdateInternal { fingerprint_id, request_external_three_ds_authentication, frm_metadata, + customer_details, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { @@ -382,12 +391,13 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::ReturnUrlUpdate { + PaymentIntentUpdate::PaymentCreateUpdate { return_url, status, customer_id, shipping_address_id, billing_address_id, + customer_details, updated_by, } => Self { return_url, @@ -395,6 +405,7 @@ impl From for PaymentIntentUpdateInternal { customer_id, shipping_address_id, billing_address_id, + customer_details, modified_at: Some(common_utils::date_time::now()), updated_by, ..Default::default() diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 162dac1377..6149af1f50 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -888,6 +888,7 @@ diesel::table! { request_external_three_ds_authentication -> Nullable, charges -> Nullable, frm_metadata -> Nullable, + customer_details -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index c09df82f87..f6e4f88879 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -1,4 +1,5 @@ -use common_utils::{self, id_type, pii, types::MinorUnit}; +use common_utils::{self, crypto::Encryptable, id_type, pii, types::MinorUnit}; +use masking::Secret; use time::PrimitiveDateTime; pub mod payment_attempt; @@ -9,7 +10,7 @@ use common_enums as storage_enums; use self::payment_attempt::PaymentAttempt; use crate::RemoteStorageObject; -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, serde::Serialize)] pub struct PaymentIntent { pub payment_id: String, pub merchant_id: String, @@ -61,4 +62,5 @@ pub struct PaymentIntent { pub request_external_three_ds_authentication: Option, pub charges: Option, pub frm_metadata: Option, + pub customer_details: Option>>, } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 4f50948b48..1dca50e648 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -4,6 +4,8 @@ use common_utils::{ errors::{CustomResult, ValidationError}, types::MinorUnit, }; +use error_stack::ResultExt; +use masking::PeekInterface; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; @@ -11,6 +13,7 @@ use super::PaymentIntent; use crate::{ behaviour, errors, mandates::{MandateDataType, MandateDetails}, + type_encryption::{decrypt, AsyncLift}, ForeignIDRef, RemoteStorageObject, }; @@ -472,7 +475,8 @@ impl ForeignIDRef for PaymentAttempt { } use diesel_models::{ - PaymentIntent as DieselPaymentIntent, PaymentIntentNew as DieselPaymentIntentNew, + encryption::Encryption, PaymentIntent as DieselPaymentIntent, + PaymentIntentNew as DieselPaymentIntentNew, }; #[async_trait::async_trait] @@ -525,61 +529,73 @@ impl behaviour::Conversion for PaymentIntent { request_external_three_ds_authentication: self.request_external_three_ds_authentication, charges: self.charges, frm_metadata: self.frm_metadata, + customer_details: self.customer_details.map(Encryption::from), }) } async fn convert_back( storage_model: Self::DstType, - _key: &masking::Secret>, + key: &masking::Secret>, ) -> CustomResult where Self: Sized, { - Ok(Self { - payment_id: storage_model.payment_id, - merchant_id: storage_model.merchant_id, - status: storage_model.status, - amount: storage_model.amount, - currency: storage_model.currency, - amount_captured: storage_model.amount_captured, - customer_id: storage_model.customer_id, - description: storage_model.description, - return_url: storage_model.return_url, - metadata: storage_model.metadata, - connector_id: storage_model.connector_id, - shipping_address_id: storage_model.shipping_address_id, - billing_address_id: storage_model.billing_address_id, - statement_descriptor_name: storage_model.statement_descriptor_name, - statement_descriptor_suffix: storage_model.statement_descriptor_suffix, - created_at: storage_model.created_at, - modified_at: storage_model.modified_at, - last_synced: storage_model.last_synced, - setup_future_usage: storage_model.setup_future_usage, - off_session: storage_model.off_session, - client_secret: storage_model.client_secret, - active_attempt: RemoteStorageObject::ForeignID(storage_model.active_attempt_id), - business_country: storage_model.business_country, - business_label: storage_model.business_label, - order_details: storage_model.order_details, - allowed_payment_method_types: storage_model.allowed_payment_method_types, - connector_metadata: storage_model.connector_metadata, - feature_metadata: storage_model.feature_metadata, - attempt_count: storage_model.attempt_count, - profile_id: storage_model.profile_id, - merchant_decision: storage_model.merchant_decision, - payment_link_id: storage_model.payment_link_id, - payment_confirm_source: storage_model.payment_confirm_source, - updated_by: storage_model.updated_by, - surcharge_applicable: storage_model.surcharge_applicable, - request_incremental_authorization: storage_model.request_incremental_authorization, - incremental_authorization_allowed: storage_model.incremental_authorization_allowed, - authorization_count: storage_model.authorization_count, - fingerprint_id: storage_model.fingerprint_id, - session_expiry: storage_model.session_expiry, - request_external_three_ds_authentication: storage_model - .request_external_three_ds_authentication, - charges: storage_model.charges, - frm_metadata: storage_model.frm_metadata, + async { + let inner_decrypt = |inner| decrypt(inner, key.peek()); + Ok::>(Self { + payment_id: storage_model.payment_id, + merchant_id: storage_model.merchant_id, + status: storage_model.status, + amount: storage_model.amount, + currency: storage_model.currency, + amount_captured: storage_model.amount_captured, + customer_id: storage_model.customer_id, + description: storage_model.description, + return_url: storage_model.return_url, + metadata: storage_model.metadata, + connector_id: storage_model.connector_id, + shipping_address_id: storage_model.shipping_address_id, + billing_address_id: storage_model.billing_address_id, + statement_descriptor_name: storage_model.statement_descriptor_name, + statement_descriptor_suffix: storage_model.statement_descriptor_suffix, + created_at: storage_model.created_at, + modified_at: storage_model.modified_at, + last_synced: storage_model.last_synced, + setup_future_usage: storage_model.setup_future_usage, + off_session: storage_model.off_session, + client_secret: storage_model.client_secret, + active_attempt: RemoteStorageObject::ForeignID(storage_model.active_attempt_id), + business_country: storage_model.business_country, + business_label: storage_model.business_label, + order_details: storage_model.order_details, + allowed_payment_method_types: storage_model.allowed_payment_method_types, + connector_metadata: storage_model.connector_metadata, + feature_metadata: storage_model.feature_metadata, + attempt_count: storage_model.attempt_count, + profile_id: storage_model.profile_id, + merchant_decision: storage_model.merchant_decision, + payment_link_id: storage_model.payment_link_id, + payment_confirm_source: storage_model.payment_confirm_source, + updated_by: storage_model.updated_by, + surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, + session_expiry: storage_model.session_expiry, + request_external_three_ds_authentication: storage_model + .request_external_three_ds_authentication, + charges: storage_model.charges, + frm_metadata: storage_model.frm_metadata, + customer_details: storage_model + .customer_details + .async_lift(inner_decrypt) + .await?, + }) + } + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting payment intent".to_string(), }) } @@ -628,6 +644,7 @@ impl behaviour::Conversion for PaymentIntent { request_external_three_ds_authentication: self.request_external_three_ds_authentication, charges: self.charges, frm_metadata: self.frm_metadata, + customer_details: self.customer_details.map(Encryption::from), }) } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 66ecf7ec57..30fd1061c7 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1,10 +1,13 @@ use common_enums as storage_enums; use common_utils::{ consts::{PAYMENTS_LIST_MAX_LIMIT_V1, PAYMENTS_LIST_MAX_LIMIT_V2}, - id_type, pii, + crypto::Encryptable, + id_type, + pii::{self, Email}, types::MinorUnit, }; -use serde::{Deserialize, Serialize}; +use masking::{Deserialize, Secret}; +use serde::Serialize; use time::PrimitiveDateTime; use super::{payment_attempt::PaymentAttempt, PaymentIntent}; @@ -76,7 +79,15 @@ pub trait PaymentIntentInterface { ) -> error_stack::Result, errors::StorageError>; } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, router_derive::DebugAsDisplay, Serialize, Deserialize)] +pub struct CustomerData { + pub name: Option>, + pub email: Option, + pub phone: Option>, + pub phone_country_code: Option, +} + +#[derive(Clone, Debug, PartialEq)] pub struct PaymentIntentNew { pub payment_id: String, pub merchant_id: String, @@ -122,9 +133,10 @@ pub struct PaymentIntentNew { pub session_expiry: Option, pub request_external_three_ds_authentication: Option, pub charges: Option, + pub customer_details: Option>>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub enum PaymentIntentUpdate { ResponseUpdate { status: storage_enums::IntentStatus, @@ -138,12 +150,13 @@ pub enum PaymentIntentUpdate { metadata: pii::SecretSerdeValue, updated_by: String, }, - ReturnUrlUpdate { + PaymentCreateUpdate { return_url: Option, status: Option, customer_id: Option, shipping_address_id: Option, billing_address_id: Option, + customer_details: Option>>, updated_by: String, }, MerchantStatusUpdate { @@ -179,6 +192,7 @@ pub enum PaymentIntentUpdate { fingerprint_id: Option, session_expiry: Option, request_external_three_ds_authentication: Option, + customer_details: Option>>, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -255,6 +269,7 @@ pub struct PaymentIntentUpdateInternal { pub session_expiry: Option, pub request_external_three_ds_authentication: Option, pub frm_metadata: Option, + pub customer_details: Option>>, } impl From for PaymentIntentUpdateInternal { @@ -282,6 +297,7 @@ impl From for PaymentIntentUpdateInternal { session_expiry, request_external_three_ds_authentication, frm_metadata, + customer_details, } => Self { amount: Some(amount), currency: Some(currency), @@ -305,6 +321,7 @@ impl From for PaymentIntentUpdateInternal { session_expiry, request_external_three_ds_authentication, frm_metadata, + customer_details, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { @@ -316,12 +333,13 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::ReturnUrlUpdate { + PaymentIntentUpdate::PaymentCreateUpdate { return_url, status, customer_id, shipping_address_id, billing_address_id, + customer_details, updated_by, } => Self { return_url, @@ -329,6 +347,7 @@ impl From for PaymentIntentUpdateInternal { customer_id, shipping_address_id, billing_address_id, + customer_details, modified_at: Some(common_utils::date_time::now()), updated_by, ..Default::default() @@ -456,7 +475,7 @@ impl From for PaymentIntentUpdateInternal { } } -use diesel_models::PaymentIntentUpdate as DieselPaymentIntentUpdate; +use diesel_models::{encryption::Encryption, PaymentIntentUpdate as DieselPaymentIntentUpdate}; impl From for DieselPaymentIntentUpdate { fn from(value: PaymentIntentUpdate) -> Self { @@ -483,19 +502,21 @@ impl From for DieselPaymentIntentUpdate { metadata, updated_by, }, - PaymentIntentUpdate::ReturnUrlUpdate { + PaymentIntentUpdate::PaymentCreateUpdate { return_url, status, customer_id, shipping_address_id, billing_address_id, + customer_details, updated_by, - } => Self::ReturnUrlUpdate { + } => Self::PaymentCreateUpdate { return_url, status, customer_id, shipping_address_id, billing_address_id, + customer_details: customer_details.map(Encryption::from), updated_by, }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -540,6 +561,7 @@ impl From for DieselPaymentIntentUpdate { session_expiry, request_external_three_ds_authentication, frm_metadata, + customer_details, } => Self::Update { amount, currency, @@ -562,6 +584,7 @@ impl From for DieselPaymentIntentUpdate { session_expiry, request_external_three_ds_authentication, frm_metadata, + customer_details: customer_details.map(Encryption::from), }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { active_attempt_id, @@ -663,6 +686,7 @@ impl From for diesel_models::PaymentIntentUpdateInt fingerprint_id, request_external_three_ds_authentication, frm_metadata, + customer_details, } = value; Self { @@ -696,6 +720,7 @@ impl From for diesel_models::PaymentIntentUpdateInt fingerprint_id, request_external_three_ds_authentication, frm_metadata, + customer_details: customer_details.map(Encryption::from), } } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 7d0ab75054..21f2612439 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -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( key_store: &domain::MerchantKeyStore, data: Option, -) -> Option +) -> Option>> where T: Debug + serde::Serialize, { let key = key_store.key.get_inner().peek(); - let encrypted_data: Option = 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( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index a60d75100d..a16f5ca44f 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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") + }) + .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()) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index bfd63111e2..9c1e753776 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1084,6 +1084,7 @@ impl UpdateTracker, 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 UpdateTracker, api::PaymentsRequest> for Paymen session_expiry, request_external_three_ds_authentication: None, frm_metadata: m_frm_metadata, + customer_details, }, &m_key_store, storage_scheme, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 870fd78abe..63ae07868a 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -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 GetTracker, 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 UpdateTracker, api::PaymentsRequest> for Paymen state: &'b SessionState, _req_state: ReqState, mut payment_data: PaymentData, - _customer: Option, + customer: Option, storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, key_store: &domain::MerchantKeyStore, @@ -628,16 +632,30 @@ impl UpdateTracker, 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::( + decrypt::( 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, @@ -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, }) } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index a299b70e89..8095660d4f 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -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 UpdateTracker, 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 UpdateTracker, 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 UpdateTracker, api::PaymentsRequest> for Paymen .payment_intent .request_external_three_ds_authentication, frm_metadata, + customer_details, }, key_store, storage_scheme, @@ -740,6 +747,18 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen } } +impl TryFrom for CustomerData { + type Error = errors::ApiErrorResponse; + fn try_from(value: domain::Customer) -> Result { + 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 ValidateRequest for PaymentUpdate { #[instrument(skip_all)] fn validate_request<'a, 'b>( diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 161a9a4353..d6ceeee65e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -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 { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 0dbbbbf674..9fcce65fba 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -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 = + 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::(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 diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 7a353ec157..ba00370e88 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -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 { diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index e31a4dd75c..e78c2153bb 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -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(); diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 57fc613af7..894f0ee95a 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -14,6 +14,7 @@ use common_utils::{ }; use diesel_models::enums as storage_enums; use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::payments::payment_intent::CustomerData; use masking::{ExposeInterface, PeekInterface}; use super::domain; @@ -1140,33 +1141,60 @@ impl ForeignTryFrom<&HeaderMap> for payments::HeaderPayload { } impl - ForeignFrom<( + ForeignTryFrom<( Option<&storage::PaymentAttempt>, + Option<&storage::PaymentIntent>, Option<&domain::Address>, Option<&domain::Address>, Option<&domain::Customer>, )> for payments::PaymentsRequest { - fn foreign_from( + type Error = error_stack::Report; + fn foreign_try_from( value: ( Option<&storage::PaymentAttempt>, + Option<&storage::PaymentIntent>, Option<&domain::Address>, Option<&domain::Address>, Option<&domain::Customer>, ), - ) -> Self { - let (payment_attempt, shipping, billing, customer) = value; - Self { + ) -> Result { + let (payment_attempt, payment_intent, shipping, billing, customer) = value; + // Populating the dynamic fields directly, for the cases where we have customer details stored in + // Payment Intent + let customer_details_from_pi = payment_intent + .and_then(|payment_intent| payment_intent.customer_details.clone()) + .map(|customer_details| { + customer_details + .into_inner() + .peek() + .clone() + .parse_value::("CustomerData") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "customer_details", + }) + .attach_printable("Failed to parse customer_details") + }) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "customer_details", + })?; + Ok(Self { currency: payment_attempt.map(|pa| pa.currency.unwrap_or_default()), shipping: shipping.map(api_types::Address::from), billing: billing.map(api_types::Address::from), amount: payment_attempt.map(|pa| api_types::Amount::from(pa.amount)), email: customer - .and_then(|cust| cust.email.as_ref().map(|em| pii::Email::from(em.clone()))), - phone: customer.and_then(|cust| cust.phone.as_ref().map(|p| p.clone().into_inner())), - name: customer.and_then(|cust| cust.name.as_ref().map(|n| n.clone().into_inner())), + .and_then(|cust| cust.email.as_ref().map(|em| pii::Email::from(em.clone()))) + .or(customer_details_from_pi.clone().and_then(|cd| cd.email)), + phone: customer + .and_then(|cust| cust.phone.as_ref().map(|p| p.clone().into_inner())) + .or(customer_details_from_pi.clone().and_then(|cd| cd.phone)), + name: customer + .and_then(|cust| cust.name.as_ref().map(|n| n.clone().into_inner())) + .or(customer_details_from_pi.clone().and_then(|cd| cd.name)), ..Self::default() - } + }) } } @@ -1266,7 +1294,7 @@ impl ForeignFrom for gsm_api_types::GsmResponse { impl ForeignFrom<&domain::Customer> for payments::CustomerDetailsResponse { fn foreign_from(customer: &domain::Customer) -> Self { Self { - id: customer.customer_id.clone(), + id: Some(customer.customer_id.clone()), name: customer .name .as_ref() diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index a2909e5f3c..97c063eb3a 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -225,6 +225,7 @@ pub async fn generate_sample_data( request_external_three_ds_authentication: None, charges: None, frm_metadata: Default::default(), + customer_details: None, }; let payment_attempt = PaymentAttemptBatchNew { attempt_id: attempt_id.clone(), diff --git a/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/down.sql b/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/down.sql new file mode 100644 index 0000000000..9b7bb65f08 --- /dev/null +++ b/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS customer_details; diff --git a/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/up.sql b/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/up.sql new file mode 100644 index 0000000000..8a92056017 --- /dev/null +++ b/migrations/2024-06-12-060604_add_customer_details_in_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS customer_details BYTEA DEFAULT NULL;