diff --git a/crates/hyperswitch_domain_models/src/address.rs b/crates/hyperswitch_domain_models/src/address.rs new file mode 100644 index 0000000000..85595c1ad9 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/address.rs @@ -0,0 +1,159 @@ +use masking::{PeekInterface, Secret}; + +#[derive(Default, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Address { + pub address: Option, + pub phone: Option, + pub email: Option, +} + +impl masking::SerializableSecret for Address {} + +impl Address { + /// Unify the address, giving priority to `self` when details are present in both + pub fn unify_address(self, other: Option<&Self>) -> Self { + let other_address_details = other.and_then(|address| address.address.as_ref()); + Self { + address: self + .address + .map(|address| address.unify_address_details(other_address_details)) + .or(other_address_details.cloned()), + email: self.email.or(other.and_then(|other| other.email.clone())), + phone: self.phone.or(other.and_then(|other| other.phone.clone())), + } + } +} + +#[derive(Clone, Default, Debug, Eq, serde::Deserialize, serde::Serialize, PartialEq)] +pub struct AddressDetails { + pub city: Option, + pub country: Option, + pub line1: Option>, + pub line2: Option>, + pub line3: Option>, + pub zip: Option>, + pub state: Option>, + pub first_name: Option>, + pub last_name: Option>, +} + +impl AddressDetails { + pub fn get_optional_full_name(&self) -> Option> { + match (self.first_name.as_ref(), self.last_name.as_ref()) { + (Some(first_name), Some(last_name)) => Some(Secret::new(format!( + "{} {}", + first_name.peek(), + last_name.peek() + ))), + (Some(name), None) | (None, Some(name)) => Some(name.to_owned()), + _ => None, + } + } + + /// Unify the address details, giving priority to `self` when details are present in both + pub fn unify_address_details(self, other: Option<&Self>) -> Self { + if let Some(other) = other { + let (first_name, last_name) = if self + .first_name + .as_ref() + .is_some_and(|first_name| !first_name.peek().trim().is_empty()) + { + (self.first_name, self.last_name) + } else { + (other.first_name.clone(), other.last_name.clone()) + }; + + Self { + first_name, + last_name, + city: self.city.or(other.city.clone()), + country: self.country.or(other.country), + line1: self.line1.or(other.line1.clone()), + line2: self.line2.or(other.line2.clone()), + line3: self.line3.or(other.line3.clone()), + zip: self.zip.or(other.zip.clone()), + state: self.state.or(other.state.clone()), + } + } else { + self + } + } +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct PhoneDetails { + pub number: Option>, + pub country_code: Option, +} + +impl From for Address { + fn from(address: api_models::payments::Address) -> Self { + Self { + address: address.address.map(AddressDetails::from), + phone: address.phone.map(PhoneDetails::from), + email: address.email, + } + } +} + +impl From for AddressDetails { + fn from(address: api_models::payments::AddressDetails) -> Self { + Self { + city: address.city, + country: address.country, + line1: address.line1, + line2: address.line2, + line3: address.line3, + zip: address.zip, + state: address.state, + first_name: address.first_name, + last_name: address.last_name, + } + } +} + +impl From for PhoneDetails { + fn from(phone: api_models::payments::PhoneDetails) -> Self { + Self { + number: phone.number, + country_code: phone.country_code, + } + } +} + +impl From
for api_models::payments::Address { + fn from(address: Address) -> Self { + Self { + address: address + .address + .map(api_models::payments::AddressDetails::from), + phone: address.phone.map(api_models::payments::PhoneDetails::from), + email: address.email, + } + } +} + +impl From for api_models::payments::AddressDetails { + fn from(address: AddressDetails) -> Self { + Self { + city: address.city, + country: address.country, + line1: address.line1, + line2: address.line2, + line3: address.line3, + zip: address.zip, + state: address.state, + first_name: address.first_name, + last_name: address.last_name, + } + } +} + +impl From for api_models::payments::PhoneDetails { + fn from(phone: PhoneDetails) -> Self { + Self { + number: phone.number, + country_code: phone.country_code, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 386e0f01f3..64c6c97a0f 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -1,3 +1,4 @@ +pub mod address; pub mod api; pub mod behaviour; pub mod business_profile; diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 276035214a..4cb9340321 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] -use api_models::payments::Address; +use common_utils::ext_traits::ValueExt; use common_utils::{ self, crypto::Encryptable, @@ -28,11 +28,13 @@ use common_enums as storage_enums; use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; use self::payment_attempt::PaymentAttempt; +#[cfg(feature = "v1")] use crate::RemoteStorageObject; #[cfg(feature = "v2")] -use crate::{business_profile, merchant_account}; -#[cfg(feature = "v2")] -use crate::{errors, payment_method_data, ApiModelToDieselModelConvertor}; +use crate::{ + address::Address, business_profile, errors, merchant_account, payment_method_data, + ApiModelToDieselModelConvertor, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToEncryption)] @@ -349,10 +351,10 @@ pub struct PaymentIntent { pub merchant_reference_id: Option, /// The billing address for the order in a denormalized form. #[encrypt(ty = Value)] - pub billing_address: Option>>, + pub billing_address: Option>, /// The shipping address for the order in a denormalized form. #[encrypt(ty = Value)] - pub shipping_address: Option>>, + pub shipping_address: Option>, /// Capture method for the payment pub capture_method: storage_enums::CaptureMethod, /// Authentication type that is requested by the merchant for this payment. @@ -416,8 +418,7 @@ impl PaymentIntent { merchant_account: &merchant_account::MerchantAccount, profile: &business_profile::Profile, request: api_models::payments::PaymentsCreateIntentRequest, - billing_address: Option>>, - shipping_address: Option>>, + decrypted_payment_intent: DecryptedPaymentIntent, ) -> CustomResult { let connector_metadata = request .get_connector_metadata_as_value() @@ -480,8 +481,26 @@ impl PaymentIntent { frm_metadata: request.frm_metadata, customer_details: None, merchant_reference_id: request.merchant_reference_id, - billing_address, - shipping_address, + billing_address: decrypted_payment_intent + .billing_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode billing address")?, + shipping_address: decrypted_payment_intent + .shipping_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode shipping address")?, capture_method: request.capture_method.unwrap_or_default(), authentication_type: request.authentication_type.unwrap_or_default(), prerouting_algorithm: None, diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index ec1463d1b7..4ca6084c95 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -1,6 +1,11 @@ #[cfg(all(feature = "v1", feature = "olap"))] use api_models::enums::Connector; use common_enums as storage_enums; +#[cfg(feature = "v2")] +use common_utils::{ + crypto::Encryptable, encryption::Encryption, ext_traits::ValueExt, + types::keymanager::ToEncryptable, +}; use common_utils::{ errors::{CustomResult, ValidationError}, id_type, pii, @@ -18,15 +23,19 @@ use error_stack::ResultExt; #[cfg(feature = "v2")] use masking::PeekInterface; use masking::Secret; +#[cfg(feature = "v2")] +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +#[cfg(feature = "v2")] +use serde_json::Value; use time::PrimitiveDateTime; #[cfg(all(feature = "v1", feature = "olap"))] use super::PaymentIntent; #[cfg(feature = "v2")] -use crate::merchant_key_store::MerchantKeyStore; +use crate::type_encryption::{crypto_operation, CryptoOperation}; #[cfg(feature = "v2")] -use crate::router_response_types; +use crate::{address::Address, merchant_key_store::MerchantKeyStore, router_response_types}; use crate::{ behaviour, errors, mandates::{MandateDataType, MandateDetails}, @@ -222,7 +231,7 @@ pub struct ErrorDetails { /// Few fields which are related are grouped together for better readability and understandability. /// These fields will be flattened and stored in the database in individual columns #[cfg(feature = "v2")] -#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, router_derive::ToEncryption)] pub struct PaymentAttempt { /// Payment id for the payment attempt pub payment_id: id_type::GlobalPaymentId, @@ -259,12 +268,11 @@ pub struct PaymentAttempt { pub connector_metadata: Option, pub payment_experience: Option, /// The insensitive data of the payment method data is stored here - // TODO: evaluate what details should be stored here. Use a domain type instead of serde_json::Value pub payment_method_data: Option, /// The result of the routing algorithm. /// This will store the list of connectors and other related information that was used to route the payment. // TODO: change this to type instead of serde_json::Value - pub routing_result: Option, + pub routing_result: Option, pub preprocessing_step_id: Option, /// Number of captures that have happened for the payment attempt pub multiple_capture_count: Option, @@ -306,8 +314,8 @@ pub struct PaymentAttempt { /// A reference to the payment at connector side. This is returned by the connector pub external_reference_id: Option, /// The billing address for the payment method - // TODO: use a type here instead of value - pub payment_method_billing_address: common_utils::crypto::OptionalEncryptableValue, + #[encrypt(ty = Value)] + pub payment_method_billing_address: Option>, /// The global identifier for the payment attempt pub id: id_type::GlobalAttemptId, /// The connector mandate details which are stored temporarily @@ -364,6 +372,7 @@ impl PaymentAttempt { cell_id: id_type::CellId, storage_scheme: storage_enums::MerchantStorageScheme, request: &api_models::payments::PaymentsConfirmIntentRequest, + encrypted_data: DecryptedPaymentAttempt, ) -> CustomResult { let id = id_type::GlobalAttemptId::generate(&cell_id); let intent_amount_details = payment_intent.amount_details.clone(); @@ -1755,13 +1764,39 @@ impl behaviour::Conversion for PaymentAttempt { where Self: Sized, { - use crate::type_encryption; - async { let connector_payment_id = storage_model .get_optional_connector_transaction_id() .cloned(); + let decrypted_data = crypto_operation( + state, + common_utils::type_name!(Self::DstType), + CryptoOperation::BatchDecrypt(EncryptedPaymentAttempt::to_encryptable( + EncryptedPaymentAttempt { + payment_method_billing_address: storage_model + .payment_method_billing_address, + }, + )), + key_manager_identifier, + key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation())?; + + let decrypted_data = EncryptedPaymentAttempt::from_encryptable(decrypted_data) + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Invalid batch operation data")?; + + let payment_method_billing_address = decrypted_data + .payment_method_billing_address + .map(|billing| { + billing.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing Address")?; + let amount_details = AttemptAmountDetails { net_amount: storage_model.net_amount, tax_on_surcharge: storage_model.tax_on_surcharge, @@ -1772,18 +1807,6 @@ impl behaviour::Conversion for PaymentAttempt { amount_to_capture: storage_model.amount_to_capture, }; - let inner_decrypt = |inner| async { - type_encryption::crypto_operation( - state, - common_utils::type_name!(Self::DstType), - type_encryption::CryptoOperation::DecryptOptional(inner), - key_manager_identifier.clone(), - key.peek(), - ) - .await - .and_then(|val| val.try_into_optionaloperation()) - }; - let error = storage_model .error_code .zip(storage_model.error_message) @@ -1838,10 +1861,7 @@ impl behaviour::Conversion for PaymentAttempt { authentication_applied: storage_model.authentication_applied, external_reference_id: storage_model.external_reference_id, connector: storage_model.connector, - payment_method_billing_address: inner_decrypt( - storage_model.payment_method_billing_address, - ) - .await?, + payment_method_billing_address, connector_mandate_detail: storage_model.connector_mandate_detail, }) } diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 0a03fc741f..5965bdc885 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -4,8 +4,10 @@ use api_models::{ payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsConfirmIntentRequest}, }; use async_trait::async_trait; +use common_utils::{ext_traits::Encode, types::keymanager::ToEncryptable}; use error_stack::ResultExt; use hyperswitch_domain_models::payments::PaymentConfirmData; +use masking::PeekInterface; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -26,7 +28,7 @@ use crate::{ types::{ self, api::{self, ConnectorCallType, PaymentIdTypeExt}, - domain::{self}, + domain::{self, types as domain_types}, storage::{self, enums as storage_enums}, }, utils::{self, OptionExt}, @@ -176,12 +178,36 @@ impl GetTracker, PaymentsConfirmIntent let cell_id = state.conf.cell_information.id.clone(); + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::to_encryptable( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt { + payment_method_billing_address: request.payment_method_data.billing.as_ref().map(|address| address.clone().encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode payment_method_billing address")?.map(masking::Secret::new), + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + let payment_attempt_domain_model = hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::create_domain_model( &payment_intent, cell_id, storage_scheme, - request + request, + encrypted_data ) .await?; diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index b46992a6ae..bf5b4fb80c 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -4,9 +4,11 @@ use api_models::{enums::FrmSuggestion, payments::PaymentsCreateIntentRequest}; use async_trait::async_trait; use common_utils::{ errors::CustomResult, - ext_traits::{AsyncExt, ValueExt}, + ext_traits::{AsyncExt, Encode, ValueExt}, + types::keymanager::ToEncryptable, }; use error_stack::ResultExt; +use masking::PeekInterface; use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; @@ -18,7 +20,8 @@ use crate::{ routes::{app::ReqState, SessionState}, services, types::{ - api, domain, + api, + domain::{self, types as domain_types}, storage::{self, enums}, }, }; @@ -100,51 +103,39 @@ impl GetTracker, PaymentsCrea let key_manager_state = &state.into(); let storage_scheme = merchant_account.storage_scheme; - // Derivation of directly supplied Billing Address data in our Payment Create Request - // Encrypting our Billing Address Details to be stored in Payment Intent - let billing_address = request - .billing - .clone() - .async_map(|billing_details| { - create_encrypted_data(key_manager_state, key_store, billing_details) - }) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt billing details")? - .map(|encrypted_value| { - encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize decrypted value to Address")?; - // Derivation of directly supplied Shipping Address data in our Payment Create Request - // Encrypting our Shipping Address Details to be stored in Payment Intent - let shipping_address = request - .shipping - .clone() - .async_map(|shipping_details| { - create_encrypted_data(key_manager_state, key_store, shipping_details) - }) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt shipping details")? - .map(|encrypted_value| { - encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize decrypted value to Address")?; + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::PaymentIntent), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::to_encryptable( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent { + shipping_address: request.shipping.clone().map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode shipping address")?.map(masking::Secret::new), + billing_address: request.billing.clone().map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode billing address")?.map(masking::Secret::new), + customer_details: None, + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + let payment_intent_domain = hyperswitch_domain_models::payments::PaymentIntent::create_domain_model_from_request( payment_id, merchant_account, profile, request.clone(), - billing_address, - shipping_address, + encrypted_data, ) .await?; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d9bd374ef1..3269750810 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -947,11 +947,13 @@ where billing: payment_intent .billing_address .clone() - .map(|billing| billing.into_inner().expose()), + .map(|billing| billing.into_inner()) + .map(From::from), shipping: payment_intent .shipping_address .clone() - .map(|shipping| shipping.into_inner().expose()), + .map(|shipping| shipping.into_inner()) + .map(From::from), customer_id: payment_intent.customer_id.clone(), customer_present: payment_intent.customer_present.clone(), description: payment_intent.description.clone(), diff --git a/crates/router_derive/src/macros/to_encryptable.rs b/crates/router_derive/src/macros/to_encryptable.rs index dfcfb72169..561c3a7237 100644 --- a/crates/router_derive/src/macros/to_encryptable.rs +++ b/crates/router_derive/src/macros/to_encryptable.rs @@ -242,13 +242,17 @@ fn generate_to_encryptable( let inner_types = get_field_and_inner_types(&fields); - let inner_type = inner_types.first().map(|(_, ty)| ty).ok_or_else(|| { + let inner_type = inner_types.first().ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), "Please use the macro with attribute #[encrypt] on the fields you want to encrypt", ) })?; + let provided_ty = get_encryption_ty_meta(&inner_type.0) + .map(|ty| ty.value.clone()) + .unwrap_or(inner_type.1.clone()); + let structs = struct_types.iter().map(|(prefix, struct_type)| { let name = format_ident!("{}{}", prefix, struct_name); let temp_fields = struct_type.generate_struct_fields(&inner_types); @@ -275,15 +279,15 @@ fn generate_to_encryptable( let decrypted_name = format_ident!("Decrypted{}", struct_name); ( quote! { #decrypted_name }, - quote! { Secret<#inner_type> }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, + quote! { Secret<#provided_ty> }, ) } StructType::Encrypted => { let decrypted_name = format_ident!("Decrypted{}", struct_name); ( quote! { #decrypted_name }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, quote! { Encryption }, ) } @@ -291,8 +295,8 @@ fn generate_to_encryptable( let decrypted_update_name = format_ident!("DecryptedUpdate{}", struct_name); ( quote! { #decrypted_update_name }, - quote! { Secret<#inner_type> }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, + quote! { Secret<#provided_ty> }, ) } //Unreachable statement