Skip to content

Commit

Permalink
refactor(payments_v2): use batch encryption for intent create and con…
Browse files Browse the repository at this point in the history
…firm intent (#6589)

Co-authored-by: Sanchith Hegde <[email protected]>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 26, 2024
1 parent 03423a1 commit 108b160
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 85 deletions.
159 changes: 159 additions & 0 deletions crates/hyperswitch_domain_models/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use masking::{PeekInterface, Secret};

#[derive(Default, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Address {
pub address: Option<AddressDetails>,
pub phone: Option<PhoneDetails>,
pub email: Option<common_utils::pii::Email>,
}

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<String>,
pub country: Option<common_enums::CountryAlpha2>,
pub line1: Option<Secret<String>>,
pub line2: Option<Secret<String>>,
pub line3: Option<Secret<String>>,
pub zip: Option<Secret<String>>,
pub state: Option<Secret<String>>,
pub first_name: Option<Secret<String>>,
pub last_name: Option<Secret<String>>,
}

impl AddressDetails {
pub fn get_optional_full_name(&self) -> Option<Secret<String>> {
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<Secret<String>>,
pub country_code: Option<String>,
}

impl From<api_models::payments::Address> 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<api_models::payments::AddressDetails> 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<api_models::payments::PhoneDetails> for PhoneDetails {
fn from(phone: api_models::payments::PhoneDetails) -> Self {
Self {
number: phone.number,
country_code: phone.country_code,
}
}
}

impl From<Address> 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<AddressDetails> 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<PhoneDetails> for api_models::payments::PhoneDetails {
fn from(phone: PhoneDetails) -> Self {
Self {
number: phone.number,
country_code: phone.country_code,
}
}
}
1 change: 1 addition & 0 deletions crates/hyperswitch_domain_models/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod address;
pub mod api;
pub mod behaviour;
pub mod business_profile;
Expand Down
39 changes: 29 additions & 10 deletions crates/hyperswitch_domain_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -349,10 +351,10 @@ pub struct PaymentIntent {
pub merchant_reference_id: Option<id_type::PaymentReferenceId>,
/// The billing address for the order in a denormalized form.
#[encrypt(ty = Value)]
pub billing_address: Option<Encryptable<Secret<Address>>>,
pub billing_address: Option<Encryptable<Address>>,
/// The shipping address for the order in a denormalized form.
#[encrypt(ty = Value)]
pub shipping_address: Option<Encryptable<Secret<Address>>>,
pub shipping_address: Option<Encryptable<Address>>,
/// Capture method for the payment
pub capture_method: storage_enums::CaptureMethod,
/// Authentication type that is requested by the merchant for this payment.
Expand Down Expand Up @@ -416,8 +418,7 @@ impl PaymentIntent {
merchant_account: &merchant_account::MerchantAccount,
profile: &business_profile::Profile,
request: api_models::payments::PaymentsCreateIntentRequest,
billing_address: Option<Encryptable<Secret<Address>>>,
shipping_address: Option<Encryptable<Secret<Address>>>,
decrypted_payment_intent: DecryptedPaymentIntent,
) -> CustomResult<Self, errors::api_error_response::ApiErrorResponse> {
let connector_metadata = request
.get_connector_metadata_as_value()
Expand Down Expand Up @@ -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,
Expand Down
70 changes: 45 additions & 25 deletions crates/hyperswitch_domain_models/src/payments/payment_attempt.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -259,12 +268,11 @@ pub struct PaymentAttempt {
pub connector_metadata: Option<pii::SecretSerdeValue>,
pub payment_experience: Option<storage_enums::PaymentExperience>,
/// 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<pii::SecretSerdeValue>,
/// 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<serde_json::Value>,
pub routing_result: Option<Value>,
pub preprocessing_step_id: Option<String>,
/// Number of captures that have happened for the payment attempt
pub multiple_capture_count: Option<i16>,
Expand Down Expand Up @@ -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<String>,
/// 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<Encryptable<Address>>,
/// The global identifier for the payment attempt
pub id: id_type::GlobalAttemptId,
/// The connector mandate details which are stored temporarily
Expand Down Expand Up @@ -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<Self, errors::api_error_response::ApiErrorResponse> {
let id = id_type::GlobalAttemptId::generate(&cell_id);
let intent_amount_details = payment_intent.amount_details.clone();
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
})
}
Expand Down
Loading

0 comments on commit 108b160

Please sign in to comment.