diff --git a/.github/workflows/migration-check.yaml b/.github/workflows/migration-check.yaml index c453469b90..f1172614d3 100644 --- a/.github/workflows/migration-check.yaml +++ b/.github/workflows/migration-check.yaml @@ -26,7 +26,11 @@ jobs: runs-on: ubuntu-latest env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres + DATABASE_URL: postgres://postgres:postgres@localhost:5432/hyperswitch_db + PGUSER: postgres + PGPASSWORD: postgres + PGHOST: localhost + PGPORT: 5432 services: postgres: @@ -41,6 +45,7 @@ jobs: ports: - 5432:5432 + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -59,6 +64,10 @@ jobs: - uses: baptiste0928/cargo-install@v2.2.0 with: crate: just + + - name: Create database + shell: bash + run: just resurrect hyperswitch_db - name: Verify `diesel migration run for v1` shell: bash @@ -67,6 +76,10 @@ jobs: - name: Verify `diesel migration redo for v1` shell: bash run: just migrate redo --locked-schema --all + + - name: Drop `DB for v2 column ordering mismatch` + shell: bash + run: just resurrect hyperswitch_db - name: Verify `diesel migration run for v2` shell: bash @@ -75,3 +88,4 @@ jobs: - name: Verify `diesel migration redo for v2` shell: bash run: just migrate_v2 redo --locked-schema --all + diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index e774085da2..8f76a3f835 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -3,7 +3,7 @@ use common_utils::{ encryption::Encryption, id_type, pii::{self, EmailStrategy}, - types::keymanager::ToEncryptable, + types::{keymanager::ToEncryptable, Description}, }; use euclid::dssa::graph::euclid_graph_prelude::FxHashMap; use masking::{ExposeInterface, Secret, SwitchStrategy}; @@ -33,8 +33,8 @@ pub struct CustomerRequest { #[schema(value_type = Option, max_length = 255, example = "9123456789")] pub phone: Option>, /// An arbitrary string that you can attach to a customer object. - #[schema(max_length = 255, example = "First Customer")] - pub description: Option, + #[schema(max_length = 255, example = "First Customer", value_type = Option)] + pub description: Option, /// The country code for the customer phone number #[schema(max_length = 255, example = "+65")] pub phone_country_code: Option, @@ -82,8 +82,8 @@ pub struct CustomerRequest { #[schema(value_type = Option, max_length = 255, example = "9123456789")] pub phone: Option>, /// An arbitrary string that you can attach to a customer object. - #[schema(max_length = 255, example = "First Customer")] - pub description: Option, + #[schema(max_length = 255, example = "First Customer", value_type = Option)] + pub description: Option, /// The country code for the customer phone number #[schema(max_length = 255, example = "+65")] pub phone_country_code: Option, @@ -217,8 +217,8 @@ pub struct CustomerResponse { #[schema(max_length = 255, example = "+65")] pub phone_country_code: Option, /// An arbitrary string that you can attach to a customer object. - #[schema(max_length = 255, example = "First Customer")] - pub description: Option, + #[schema(max_length = 255, example = "First Customer", value_type = Option)] + pub description: Option, /// The address for the customer #[schema(value_type = Option)] pub address: Option, @@ -262,8 +262,8 @@ pub struct CustomerResponse { #[schema(max_length = 255, example = "+65")] pub phone_country_code: Option, /// An arbitrary string that you can attach to a customer object. - #[schema(max_length = 255, example = "First Customer")] - pub description: Option, + #[schema(max_length = 255, example = "First Customer", value_type = Option)] + pub description: Option, /// The default billing address for the customer #[schema(value_type = Option)] pub default_billing_address: Option, diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs index daa8748a24..8457d6b662 100644 --- a/crates/api_models/src/events/customer.rs +++ b/crates/api_models/src/events/customer.rs @@ -14,7 +14,7 @@ impl ApiEventMetric for CustomerRequest { fn get_api_event_type(&self) -> Option { self.get_merchant_reference_id() .clone() - .map(|customer_id| ApiEventsType::Customer { customer_id }) + .map(|cid| ApiEventsType::Customer { customer_id: cid }) } } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index f80dc9698d..66177fd198 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,11 +6,11 @@ use utoipa::ToSchema; #[doc(hidden)] pub mod diesel_exports { pub use super::{ - DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, - DbCaptureStatus as CaptureStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, - DbDisputeStatus as DisputeStatus, DbEventType as EventType, + DbApiVersion as ApiVersion, DbAttemptStatus as AttemptStatus, + DbAuthenticationType as AuthenticationType, DbBlocklistDataKind as BlocklistDataKind, + DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, @@ -3037,6 +3037,27 @@ pub enum Owner { serde::Serialize, strum::Display, strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ApiVersion { + V1, + V2, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, )] #[router_derive::diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 83d005dd63..f0ecb70859 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -705,3 +705,62 @@ pub struct ChargeRefunds { } crate::impl_to_sql_from_sql_json!(ChargeRefunds); + +/// Domain type for description +#[derive( + Debug, Clone, PartialEq, Eq, Queryable, serde::Deserialize, serde::Serialize, AsExpression, +)] +#[diesel(sql_type = sql_types::Text)] +pub struct Description(String); + +impl Description { + /// Create a new Description Domain type + pub fn new(value: String) -> Self { + Self(value) + } +} + +impl From for String { + fn from(description: Description) -> Self { + description.0 + } +} + +impl From for Description { + fn from(description: String) -> Self { + Self(description) + } +} + +impl Queryable for Description +where + DB: Backend, + Self: FromSql, +{ + type Row = Self; + + fn build(row: Self::Row) -> deserialize::Result { + Ok(row) + } +} + +impl FromSql for Description +where + DB: Backend, + String: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let val = String::from_sql(bytes)?; + Ok(Self::from(val)) + } +} + +impl ToSql for Description +where + DB: Backend, + String: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index 7219bb9052..35595c05d9 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -1,11 +1,18 @@ -use common_utils::{encryption::Encryption, pii}; +// #[cfg(all(feature = "v2", feature = "customer_v2"))] +// use crate::enums::SoftDeleteStatus; +use common_enums::ApiVersion; +use common_utils::{encryption::Encryption, pii, types::Description}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use time::PrimitiveDateTime; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::schema::customers; +#[cfg(all(feature = "v2", feature = "customer_v2"))] +use crate::schema_v2::customers; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive( - Clone, Debug, Insertable, router_derive::DebugAsDisplay, serde::Deserialize, serde::Serialize, + Clone, Debug, router_derive::DebugAsDisplay, serde::Deserialize, serde::Serialize, Insertable, )] #[diesel(table_name = customers)] pub struct CustomerNew { @@ -14,22 +21,39 @@ pub struct CustomerNew { pub name: Option, pub email: Option, pub phone: Option, - pub description: Option, + pub description: Option, pub phone_country_code: Option, pub metadata: Option, - pub connector_customer: Option, + pub connector_customer: Option, pub created_at: PrimitiveDateTime, pub modified_at: PrimitiveDateTime, pub address_id: Option, pub updated_by: Option, + pub version: ApiVersion, } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +impl Customer { + pub fn get_customer_id(&self) -> common_utils::id_type::CustomerId { + self.customer_id.clone() + } +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl Customer { + pub fn get_customer_id(&self) -> common_utils::id_type::CustomerId { + todo!() + } +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl CustomerNew { pub fn update_storage_scheme(&mut self, storage_scheme: common_enums::MerchantStorageScheme) { self.updated_by = Some(storage_scheme.to_string()); } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl From for Customer { fn from(customer_new: CustomerNew) -> Self { Self { @@ -47,10 +71,79 @@ impl From for Customer { address_id: customer_new.address_id, default_payment_method_id: None, updated_by: customer_new.updated_by, + version: customer_new.version, } } } +// V2 customer + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive( + Clone, + Debug, + PartialEq, + Insertable, + router_derive::DebugAsDisplay, + serde::Deserialize, + serde::Serialize, +)] +#[diesel(table_name = customers, primary_key(id))] +pub struct CustomerNew { + pub merchant_id: common_utils::id_type::MerchantId, + pub name: Option, + pub email: Option, + pub phone: Option, + pub phone_country_code: Option, + pub description: Option, + pub created_at: PrimitiveDateTime, + pub metadata: Option, + pub connector_customer: Option, + pub modified_at: PrimitiveDateTime, + pub default_payment_method_id: Option, + pub updated_by: Option, + pub version: ApiVersion, + pub merchant_reference_id: Option, + pub default_billing_address: Option, + pub default_shipping_address: Option, + // pub status: Option, + pub id: String, +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl CustomerNew { + pub fn update_storage_scheme(&mut self, storage_scheme: common_enums::MerchantStorageScheme) { + self.updated_by = Some(storage_scheme.to_string()); + } +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl From for Customer { + fn from(customer_new: CustomerNew) -> Self { + Self { + merchant_id: customer_new.merchant_id, + name: customer_new.name, + email: customer_new.email, + phone: customer_new.phone, + phone_country_code: customer_new.phone_country_code, + description: customer_new.description, + created_at: customer_new.created_at, + metadata: customer_new.metadata, + connector_customer: customer_new.connector_customer, + modified_at: customer_new.modified_at, + default_payment_method_id: None, + updated_by: customer_new.updated_by, + merchant_reference_id: customer_new.merchant_reference_id, + default_billing_address: customer_new.default_billing_address, + default_shipping_address: customer_new.default_shipping_address, + id: customer_new.id, + // status: customer_new.status, + version: customer_new.version, + } + } +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive( Clone, Debug, Identifiable, Queryable, Selectable, serde::Deserialize, serde::Serialize, )] @@ -62,40 +155,63 @@ pub struct Customer { pub email: Option, pub phone: Option, pub phone_country_code: Option, - pub description: Option, + pub description: Option, pub created_at: PrimitiveDateTime, pub metadata: Option, - pub connector_customer: Option, + pub connector_customer: Option, pub modified_at: PrimitiveDateTime, pub address_id: Option, pub default_payment_method_id: Option, pub updated_by: Option, + pub version: ApiVersion, } +#[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive( - Clone, - Debug, - Default, - AsChangeset, - router_derive::DebugAsDisplay, - serde::Deserialize, - serde::Serialize, + Clone, Debug, Identifiable, Queryable, Selectable, serde::Serialize, serde::Deserialize, +)] +#[diesel(table_name = customers, primary_key(id))] +pub struct Customer { + pub merchant_id: common_utils::id_type::MerchantId, + pub name: Option, + pub email: Option, + pub phone: Option, + pub phone_country_code: Option, + pub description: Option, + pub created_at: PrimitiveDateTime, + pub metadata: Option, + pub connector_customer: Option, + pub modified_at: PrimitiveDateTime, + pub default_payment_method_id: Option, + pub updated_by: Option, + pub version: ApiVersion, + pub merchant_reference_id: Option, + pub default_billing_address: Option, + pub default_shipping_address: Option, + // pub status: Option, + pub id: String, +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[derive( + Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, serde::Deserialize, serde::Serialize, )] #[diesel(table_name = customers)] pub struct CustomerUpdateInternal { pub name: Option, pub email: Option, pub phone: Option, - pub description: Option, + pub description: Option, pub phone_country_code: Option, pub metadata: Option, - pub modified_at: Option, - pub connector_customer: Option, + pub modified_at: PrimitiveDateTime, + pub connector_customer: Option, pub address_id: Option, pub default_payment_method_id: Option>, pub updated_by: Option, } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl CustomerUpdateInternal { pub fn apply_changeset(self, source: Customer) -> Customer { let Self { @@ -128,3 +244,55 @@ impl CustomerUpdateInternal { } } } + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive( + Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, serde::Deserialize, serde::Serialize, +)] +#[diesel(table_name = customers)] +pub struct CustomerUpdateInternal { + pub name: Option, + pub email: Option, + pub phone: Option, + pub description: Option, + pub phone_country_code: Option, + pub metadata: Option, + pub modified_at: PrimitiveDateTime, + pub connector_customer: Option, + pub default_payment_method_id: Option>, + pub updated_by: Option, +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl CustomerUpdateInternal { + pub fn apply_changeset(self, source: Customer) -> Customer { + let Self { + name, + email, + phone, + description, + phone_country_code, + metadata, + connector_customer, + // address_id, + default_payment_method_id, + .. + } = self; + + Customer { + name: name.map_or(source.name, Some), + email: email.map_or(source.email, Some), + phone: phone.map_or(source.phone, Some), + description: description.map_or(source.description, Some), + phone_country_code: phone_country_code.map_or(source.phone_country_code, Some), + metadata: metadata.map_or(source.metadata, Some), + modified_at: common_utils::date_time::now(), + connector_customer: connector_customer.map_or(source.connector_customer, Some), + // address_id: address_id.map_or(source.address_id, Some), + default_payment_method_id: default_payment_method_id + .flatten() + .map_or(source.default_payment_method_id, Some), + ..source + } + } +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 55bfa9f2f2..e4ac55532c 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -1,10 +1,11 @@ #[doc(hidden)] pub mod diesel_exports { pub use super::{ - DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, - DbCaptureStatus as CaptureStatus, DbConnectorStatus as ConnectorStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbApiVersion as ApiVersion, DbAttemptStatus as AttemptStatus, + DbAuthenticationType as AuthenticationType, DbBlocklistDataKind as BlocklistDataKind, + DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, + DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDashboardMetadata as DashboardMetadata, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index c4d7c993c1..84bc58f4e1 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -141,7 +141,7 @@ impl DBOperation { Updateable::CustomerUpdate(cust) => DBResult::Customer(Box::new( Customer::update_by_customer_id_merchant_id( conn, - cust.orig.customer_id.clone(), + cust.orig.get_customer_id().clone(), cust.orig.merchant_id.clone(), cust.update_data, ) diff --git a/crates/diesel_models/src/query/customers.rs b/crates/diesel_models/src/query/customers.rs index dd9353e72d..ef0fa7d7e2 100644 --- a/crates/diesel_models/src/query/customers.rs +++ b/crates/diesel_models/src/query/customers.rs @@ -1,10 +1,18 @@ -use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +#[cfg(all(feature = "v2", feature = "customer_v2"))] +use common_utils::id_type; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use diesel::BoolExpressionMethods; +use diesel::{associations::HasTable, ExpressionMethods}; use super::generics; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::errors; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::schema::customers::dsl; +#[cfg(all(feature = "v2", feature = "customer_v2"))] +use crate::schema_v2::customers::dsl; use crate::{ customers::{Customer, CustomerNew, CustomerUpdateInternal}, - errors, - schema::customers::dsl, PgPooledConn, StorageResult, }; @@ -14,6 +22,81 @@ impl CustomerNew { } } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl Customer { + pub async fn update_by_customer_id_merchant_id( + _conn: &PgPooledConn, + _customer_id: id_type::CustomerId, + _merchant_id: id_type::MerchantId, + _customer: CustomerUpdateInternal, + ) -> StorageResult { + // match generics::generic_update_by_id::<::Table, _, _, _>( + // conn, + // (customer_id.clone(), merchant_id.clone()), + // customer, + // ) + // .await + // { + // Err(error) => match error.current_context() { + // errors::DatabaseError::NoFieldsToUpdate => { + // generics::generic_find_by_id::<::Table, _, _>( + // conn, + // (customer_id, merchant_id), + // ) + // .await + // } + // _ => Err(error), + // }, + // result => result, + todo!() + // } + } + pub async fn find_by_id_merchant_id(conn: &PgPooledConn, id: &str) -> StorageResult { + generics::generic_find_by_id::<::Table, _, _>(conn, id.to_owned()).await + } + + pub async fn find_by_customer_id_merchant_id( + _conn: &PgPooledConn, + _customer_id: &id_type::CustomerId, + _merchant_id: &id_type::MerchantId, + ) -> StorageResult { + // generics::generic_find_by_id::<::Table, _, _>( + // conn, + // (customer_id.to_owned(), merchant_id.to_owned()), + // ) + // .await + todo!() + } + + pub async fn list_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &id_type::MerchantId, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + None, + None, + Some(dsl::created_at), + ) + .await + } + + pub async fn find_optional_by_customer_id_merchant_id( + _conn: &PgPooledConn, + _customer_id: &id_type::CustomerId, + _merchant_id: &id_type::MerchantId, + ) -> StorageResult> { + // generics::generic_find_by_id_optional::<::Table, _, _>( + // conn, + // (customer_id.to_owned(), merchant_id.to_owned()), + // ) + // .await + todo!() + } +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl Customer { pub async fn update_by_customer_id_merchant_id( conn: &PgPooledConn, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index d2c66fc37f..3aef9d271c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -303,6 +303,7 @@ diesel::table! { default_payment_method_id -> Nullable, #[max_length = 64] updated_by -> Nullable, + version -> ApiVersion, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 91c02b6903..b8bef4e2a6 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -288,10 +288,7 @@ diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; - customers (customer_id, merchant_id) { - id -> Int4, - #[max_length = 64] - customer_id -> Varchar, + customers (id) { #[max_length = 64] merchant_id -> Varchar, name -> Nullable, @@ -306,11 +303,16 @@ diesel::table! { connector_customer -> Nullable, modified_at -> Timestamp, #[max_length = 64] - address_id -> Nullable, - #[max_length = 64] default_payment_method_id -> Nullable, #[max_length = 64] updated_by -> Nullable, + version -> ApiVersion, + #[max_length = 64] + merchant_reference_id -> Nullable, + default_billing_address -> Nullable, + default_shipping_address -> Nullable, + #[max_length = 64] + id -> Varchar, } } diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index 504c1c2046..6b08e30d33 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -1,10 +1,16 @@ use api_models::customers::CustomerRequestWithEncryption; +// #[cfg(all(feature = "v2", feature = "customer_v2"))] +// use common_enums::SoftDeleteStatus; +use common_enums::ApiVersion; use common_utils::{ crypto, date_time, encryption::Encryption, errors::{CustomResult, ValidationError}, id_type, pii, - types::keymanager::{self, KeyManagerState, ToEncryptable}, + types::{ + keymanager::{self, KeyManagerState, ToEncryptable}, + Description, + }, }; use diesel_models::customers::CustomerUpdateInternal; use error_stack::ResultExt; @@ -12,14 +18,8 @@ use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; use crate::type_encryption as types; -// use api_models::payments::Address; -// use crate::type_encryption::AsyncLift; - -pub enum SoftDeleteStatus { - Active, - Redacted, -} +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive(Clone, Debug)] pub struct Customer { pub customer_id: id_type::CustomerId, @@ -28,20 +28,55 @@ pub struct Customer { pub email: crypto::OptionalEncryptableEmail, pub phone: crypto::OptionalEncryptablePhone, pub phone_country_code: Option, - pub description: Option, + pub description: Option, pub created_at: PrimitiveDateTime, pub metadata: Option, pub modified_at: PrimitiveDateTime, - pub connector_customer: Option, + pub connector_customer: Option, pub address_id: Option, pub default_payment_method_id: Option, pub updated_by: Option, - // pub default_billing_address: Option
, - // pub default_shipping_address: Option
, - // pub merchant_reference_id: Option, - // pub status: Option + pub version: ApiVersion, } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive(Clone, Debug)] +pub struct Customer { + pub merchant_id: id_type::MerchantId, + pub name: crypto::OptionalEncryptableName, + pub email: crypto::OptionalEncryptableEmail, + pub phone: crypto::OptionalEncryptablePhone, + pub phone_country_code: Option, + pub description: Option, + pub created_at: PrimitiveDateTime, + pub metadata: Option, + pub connector_customer: Option, + pub modified_at: PrimitiveDateTime, + pub default_payment_method_id: Option, + pub updated_by: Option, + pub version: ApiVersion, + pub merchant_reference_id: Option, + pub default_billing_address: Option, + pub default_shipping_address: Option, + // pub status: Option, + pub id: String, +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +impl Customer { + pub fn get_customer_id(&self) -> id_type::CustomerId { + self.customer_id.clone() + } +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl Customer { + pub fn get_customer_id(&self) -> id_type::CustomerId { + todo!() + } +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[async_trait::async_trait] impl super::behaviour::Conversion for Customer { type DstType = diesel_models::customers::Customer; @@ -50,8 +85,8 @@ impl super::behaviour::Conversion for Customer { Ok(diesel_models::customers::Customer { customer_id: self.customer_id, merchant_id: self.merchant_id, - name: self.name.map(|value| value.into()), - email: self.email.map(|value| value.into()), + name: self.name.map(Encryption::from), + email: self.email.map(Encryption::from), phone: self.phone.map(Encryption::from), phone_country_code: self.phone_country_code, description: self.description, @@ -62,6 +97,7 @@ impl super::behaviour::Conversion for Customer { address_id: self.address_id, default_payment_method_id: self.default_payment_method_id, updated_by: self.updated_by, + version: self.version, }) } @@ -69,7 +105,7 @@ impl super::behaviour::Conversion for Customer { state: &KeyManagerState, item: Self::DstType, key: &Secret>, - _key_manager_identifier: keymanager::Identifier, + _key_store_ref_id: keymanager::Identifier, ) -> CustomResult where Self: Sized, @@ -108,6 +144,7 @@ impl super::behaviour::Conversion for Customer { address_id: item.address_id, default_payment_method_id: item.default_payment_method_id, updated_by: item.updated_by, + version: item.version, }) } @@ -127,30 +164,210 @@ impl super::behaviour::Conversion for Customer { connector_customer: self.connector_customer, address_id: self.address_id, updated_by: self.updated_by, + version: self.version, }) } } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[async_trait::async_trait] +impl super::behaviour::Conversion for Customer { + type DstType = diesel_models::customers::Customer; + type NewDstType = diesel_models::customers::CustomerNew; + async fn convert(self) -> CustomResult { + Ok(diesel_models::customers::Customer { + id: self.id, + merchant_reference_id: self.merchant_reference_id, + merchant_id: self.merchant_id, + name: self.name.map(Encryption::from), + email: self.email.map(Encryption::from), + phone: self.phone.map(Encryption::from), + phone_country_code: self.phone_country_code, + description: self.description, + created_at: self.created_at, + metadata: self.metadata, + modified_at: self.modified_at, + connector_customer: self.connector_customer, + default_payment_method_id: self.default_payment_method_id, + updated_by: self.updated_by, + default_billing_address: self.default_billing_address.map(Encryption::from), + default_shipping_address: self.default_shipping_address.map(Encryption::from), + // status: self.status, + version: self.version, + }) + } + + async fn convert_back( + state: &KeyManagerState, + item: Self::DstType, + key: &Secret>, + _key_store_ref_id: keymanager::Identifier, + ) -> CustomResult + where + Self: Sized, + { + let decrypted = types::batch_decrypt( + state, + CustomerRequestWithEncryption::to_encryptable(CustomerRequestWithEncryption { + name: item.name.clone(), + phone: item.phone.clone(), + email: item.email.clone(), + }), + keymanager::Identifier::Merchant(item.merchant_id.clone()), + key.peek(), + ) + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting customer data".to_string(), + })?; + let encryptable_customer = CustomerRequestWithEncryption::from_encryptable(decrypted) + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting customer data".to_string(), + })?; + + Ok(Self { + id: item.id, + merchant_reference_id: item.merchant_reference_id, + merchant_id: item.merchant_id, + name: encryptable_customer.name, + email: encryptable_customer.email, + phone: encryptable_customer.phone, + phone_country_code: item.phone_country_code, + description: item.description, + created_at: item.created_at, + metadata: item.metadata, + modified_at: item.modified_at, + connector_customer: item.connector_customer, + default_payment_method_id: item.default_payment_method_id, + updated_by: item.updated_by, + default_billing_address: item.default_billing_address, + default_shipping_address: item.default_shipping_address, + // status: item.status, + version: item.version, + }) + } + + async fn construct_new(self) -> CustomResult { + let now = date_time::now(); + Ok(diesel_models::customers::CustomerNew { + id: self.id, + merchant_reference_id: self.merchant_reference_id, + merchant_id: self.merchant_id, + name: self.name.map(Encryption::from), + email: self.email.map(Encryption::from), + phone: self.phone.map(Encryption::from), + description: self.description, + phone_country_code: self.phone_country_code, + metadata: self.metadata, + default_payment_method_id: None, + created_at: now, + modified_at: now, + connector_customer: self.connector_customer, + updated_by: self.updated_by, + default_billing_address: self.default_billing_address, + default_shipping_address: self.default_shipping_address, + // status: self.status, + version: self.version, + }) + } +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive(Clone, Debug)] pub enum CustomerUpdate { Update { name: crypto::OptionalEncryptableName, email: crypto::OptionalEncryptableEmail, phone: Box, - description: Option, + description: Option, phone_country_code: Option, metadata: Option, - connector_customer: Option, - address_id: Option, + connector_customer: Option, }, ConnectorCustomer { - connector_customer: Option, + connector_customer: Option, }, UpdateDefaultPaymentMethod { default_payment_method_id: Option>, }, } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl From for CustomerUpdateInternal { + fn from(customer_update: CustomerUpdate) -> Self { + match customer_update { + CustomerUpdate::Update { + name, + email, + phone, + description, + phone_country_code, + metadata, + connector_customer, + } => Self { + name: name.map(Encryption::from), + email: email.map(Encryption::from), + phone: phone.map(Encryption::from), + description, + phone_country_code, + metadata, + connector_customer, + modified_at: date_time::now(), + default_payment_method_id: None, + updated_by: None, + }, + CustomerUpdate::ConnectorCustomer { connector_customer } => Self { + connector_customer, + modified_at: date_time::now(), + name: None, + email: None, + phone: None, + description: None, + phone_country_code: None, + metadata: None, + default_payment_method_id: None, + updated_by: None, + }, + CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id, + } => Self { + default_payment_method_id, + modified_at: date_time::now(), + name: None, + email: None, + phone: None, + description: None, + phone_country_code: None, + metadata: None, + connector_customer: None, + updated_by: None, + }, + } + } +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[derive(Clone, Debug)] +pub enum CustomerUpdate { + Update { + name: crypto::OptionalEncryptableName, + email: crypto::OptionalEncryptableEmail, + phone: Box, + description: Option, + phone_country_code: Option, + metadata: Option, + connector_customer: Option, + address_id: Option, + }, + ConnectorCustomer { + connector_customer: Option, + }, + UpdateDefaultPaymentMethod { + default_payment_method_id: Option>, + }, +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl From for CustomerUpdateInternal { fn from(customer_update: CustomerUpdate) -> Self { match customer_update { @@ -171,21 +388,38 @@ impl From for CustomerUpdateInternal { phone_country_code, metadata, connector_customer, - modified_at: Some(date_time::now()), + modified_at: date_time::now(), address_id, - ..Default::default() + default_payment_method_id: None, + updated_by: None, }, CustomerUpdate::ConnectorCustomer { connector_customer } => Self { connector_customer, - modified_at: Some(date_time::now()), - ..Default::default() + modified_at: date_time::now(), + name: None, + email: None, + phone: None, + description: None, + phone_country_code: None, + metadata: None, + default_payment_method_id: None, + updated_by: None, + address_id: None, }, CustomerUpdate::UpdateDefaultPaymentMethod { default_payment_method_id, } => Self { default_payment_method_id, - modified_at: Some(date_time::now()), - ..Default::default() + modified_at: date_time::now(), + name: None, + email: None, + phone: None, + description: None, + phone_country_code: None, + metadata: None, + connector_customer: None, + updated_by: None, + address_id: None, }, } } diff --git a/crates/router/src/compatibility/stripe/app.rs b/crates/router/src/compatibility/stripe/app.rs index 9ef29596cf..639e4ce29e 100644 --- a/crates/router/src/compatibility/stripe/app.rs +++ b/crates/router/src/compatibility/stripe/app.rs @@ -2,11 +2,14 @@ use actix_web::{web, Scope}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use super::customers::*; -use super::{payment_intents::*, refunds::*, setup_intents::*, webhooks::*}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use super::{payment_intents::*, setup_intents::*}; +use super::{refunds::*, webhooks::*}; use crate::routes::{self, mandates, webhooks}; pub struct PaymentIntents; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl PaymentIntents { pub fn server(state: routes::AppState) -> Scope { let mut route = web::scope("/payment_intents").app_data(web::Data::new(state)); @@ -42,6 +45,7 @@ impl PaymentIntents { pub struct SetupIntents; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl SetupIntents { pub fn server(state: routes::AppState) -> Scope { web::scope("/setup_intents") diff --git a/crates/router/src/compatibility/stripe/customers/types.rs b/crates/router/src/compatibility/stripe/customers/types.rs index 1dc4317534..82b787445c 100644 --- a/crates/router/src/compatibility/stripe/customers/types.rs +++ b/crates/router/src/compatibility/stripe/customers/types.rs @@ -6,6 +6,7 @@ use common_utils::{crypto::Encryptable, date_time}; use common_utils::{ id_type, pii::{self, Email}, + types::Description, }; use serde::{Deserialize, Serialize}; @@ -40,7 +41,7 @@ pub struct CreateCustomerRequest { pub phone: Option>, pub address: Option, pub metadata: Option, - pub description: Option, + pub description: Option, pub shipping: Option, pub payment_method: Option, // not used pub balance: Option, // not used @@ -59,7 +60,7 @@ pub struct CreateCustomerRequest { #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct CustomerUpdateRequest { - pub description: Option, + pub description: Option, pub email: Option, pub phone: Option>, pub name: Option>, @@ -85,7 +86,7 @@ pub struct CreateCustomerResponse { pub id: id_type::CustomerId, pub object: String, pub created: u64, - pub description: Option, + pub description: Option, pub email: Option, pub metadata: Option, pub name: Option>, diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index 4acace4b18..3017b8c89b 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -2,18 +2,25 @@ pub mod types; use actix_web::{web, HttpRequest, HttpResponse}; use api_models::payments as payment_types; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use error_stack::report; -use router_env::{instrument, tracing, Flow, Tag}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use router_env::Tag; +use router_env::{instrument, tracing, Flow}; use crate::{ compatibility::{stripe::errors, wrap}, - core::{api_locking::GetLockingInput, payments}, - logger, - routes::{self, payments::get_or_generate_payment_id}, + core::payments, + routes::{self}, services::{api, authentication as auth}, +}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::{ + core::api_locking::GetLockingInput, logger, routes::payments::get_or_generate_payment_id, types::api as api_types, }; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate, payment_id))] pub async fn payment_intents_create( state: web::Data, @@ -78,6 +85,8 @@ pub async fn payment_intents_create( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieveForceSync))] pub async fn payment_intents_retrieve( state: web::Data, @@ -139,6 +148,8 @@ pub async fn payment_intents_retrieve( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow))] pub async fn payment_intents_retrieve_with_gateway_creds( state: web::Data, @@ -210,6 +221,8 @@ pub async fn payment_intents_retrieve_with_gateway_creds( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate))] pub async fn payment_intents_update( state: web::Data, @@ -277,6 +290,8 @@ pub async fn payment_intents_update( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm, payment_id))] pub async fn payment_intents_confirm( state: web::Data, @@ -350,6 +365,8 @@ pub async fn payment_intents_confirm( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsCapture, payment_id))] pub async fn payment_intents_capture( state: web::Data, @@ -412,6 +429,8 @@ pub async fn payment_intents_capture( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsCancel, payment_id))] pub async fn payment_intents_cancel( state: web::Data, diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index cfe0958d7d..d9147b08b9 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -1,10 +1,14 @@ pub mod types; - +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use actix_web::{web, HttpRequest, HttpResponse}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use api_models::payments as payment_types; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use error_stack::report; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use router_env::{instrument, tracing, Flow}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ compatibility::{ stripe::{errors, payment_intents::types as stripe_payment_types}, @@ -16,6 +20,7 @@ use crate::{ types::api as api_types, }; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate))] pub async fn setup_intents_create( state: web::Data, @@ -79,6 +84,8 @@ pub async fn setup_intents_create( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieveForceSync))] pub async fn setup_intents_retrieve( state: web::Data, @@ -140,6 +147,8 @@ pub async fn setup_intents_retrieve( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate))] pub async fn setup_intents_update( state: web::Data, @@ -213,6 +222,8 @@ pub async fn setup_intents_update( )) .await } + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm))] pub async fn setup_intents_confirm( state: web::Data, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index bdf8db2828..383de01cd4 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -130,3 +130,9 @@ pub const CONNECTOR_CREDS_TOKEN_TTL: i64 = 900; //max_amount allowed is 999999999 in minor units pub const MAX_ALLOWED_AMOUNT: i64 = 999999999; + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V1; + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V2; diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index c14b7e1e25..73368cba0e 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -1,6 +1,6 @@ use api_models::customers::CustomerRequestWithEmail; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use common_utils::{crypto::Encryptable, ext_traits::OptionExt}; +use common_utils::{crypto::Encryptable, ext_traits::OptionExt, types::Description}; use common_utils::{ errors::ReportSwitchExt, ext_traits::AsyncExt, @@ -14,7 +14,10 @@ use hyperswitch_domain_models::type_encryption::encrypt; use masking::{Secret, SwitchStrategy}; use router_env::{instrument, tracing}; +#[cfg(all(feature = "v2", feature = "customer_v2"))] +use crate::core::payment_methods::cards::create_encrypted_data; use crate::{ + consts::API_VERSION, core::errors::{self, StorageErrorExt}, db::StorageInterface, pii::PeekInterface, @@ -176,6 +179,7 @@ impl CustomerCreateBridge for customers::CustomerRequest { modified_at: common_utils::date_time::now(), default_payment_method_id: None, updated_by: None, + version: API_VERSION, }) } @@ -202,10 +206,24 @@ impl CustomerCreateBridge for customers::CustomerRequest { merchant_reference_id: &'a Option, merchant_account: &'a domain::MerchantAccount, key_state: &'a KeyManagerState, - _state: &'a SessionState, + state: &'a SessionState, ) -> errors::CustomResult { - let _default_customer_billing_address = self.get_default_customer_billing_address(); - let _default_customer_shipping_address = self.get_default_customer_shipping_address(); + let default_customer_billing_address = self.get_default_customer_billing_address(); + let encrypted_customer_billing_address = default_customer_billing_address + .async_map(|billing_address| create_encrypted_data(state, key_store, billing_address)) + .await + .transpose() + .change_context(errors::CustomersErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt default customer billing address")?; + let default_customer_shipping_address = self.get_default_customer_shipping_address(); + + let encrypted_customer_shipping_address = default_customer_shipping_address + .async_map(|shipping_address| create_encrypted_data(state, key_store, shipping_address)) + .await + .transpose() + .change_context(errors::CustomersErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt default customer shipping address")?; + let merchant_id = merchant_account.get_id().clone(); let key = key_store.key.get_inner().peek(); @@ -227,9 +245,8 @@ impl CustomerCreateBridge for customers::CustomerRequest { .change_context(errors::CustomersErrorResponse::InternalServerError)?; Ok(domain::Customer { - customer_id: merchant_reference_id - .to_owned() - .ok_or(errors::CustomersErrorResponse::InternalServerError)?, // doing this to make it compile, will remove once we start moving to domain models + id: common_utils::generate_time_ordered_id("cus"), + merchant_reference_id: merchant_reference_id.to_owned(), merchant_id, name: encryptable_customer.name, email: encryptable_customer.email, @@ -238,15 +255,14 @@ impl CustomerCreateBridge for customers::CustomerRequest { phone_country_code: self.phone_country_code.clone(), metadata: self.metadata.clone(), connector_customer: None, - address_id: None, created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), default_payment_method_id: None, updated_by: None, - // default_billing_address: default_customer_billing_address, - // default_shipping_address: default_customer_shipping_address, - // merchant_reference_id, + default_billing_address: encrypted_customer_billing_address.map(Into::into), + default_shipping_address: encrypted_customer_shipping_address.map(Into::into), // status: Some(customer_domain::SoftDeleteStatus::Active) + version: API_VERSION, }) } @@ -560,7 +576,7 @@ pub async fn delete_customer( .switch()?, ), phone: Box::new(Some(redacted_encrypted_value.clone())), - description: Some(REDACTED.to_string()), + description: Some(Description::new(REDACTED.to_string())), phone_country_code: Some(REDACTED.to_string()), metadata: None, connector_customer: None, diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 4aae07382f..c8549ef374 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -50,7 +50,9 @@ impl ConstructFlowSpecificData = self.payment_attempt.get_browser_info().ok(); - let customer_id = customer.to_owned().map(|customer| customer.customer_id); + let customer_id = customer + .to_owned() + .map(|customer| customer.get_customer_id()); let router_data = RouterData { flow: std::marker::PhantomData, diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index 3933a37676..011805510e 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -46,7 +46,9 @@ impl ConstructFlowSpecificData CustomResult, errors::ApiErrorResponse> { + todo!() +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] pub async fn rust_locker_migration( state: SessionState, merchant_id: &id_type::MerchantId, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 988d6beeb8..48bf88bc44 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -289,7 +289,7 @@ pub async fn render_pm_collect_link( publishable_key: masking::Secret::new(merchant_account.publishable_key), client_secret: link_data.client_secret.clone(), pm_collect_link_id: pm_collect_link.link_id, - customer_id: customer.customer_id, + customer_id: customer.get_customer_id(), session_expiry: pm_collect_link.expiry, return_url: pm_collect_link.return_url, ui_config: ui_config_data, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 985036ec71..a7a240339e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2467,7 +2467,7 @@ pub async fn list_payment_methods( if wallet_pm_exists { match db .find_payment_method_by_customer_id_merchant_id_list( - &customer.customer_id, + &customer.get_customer_id(), merchant_account.get_id(), None, ) @@ -4790,7 +4790,7 @@ pub async fn set_default_payment_method( default_payment_method_id: Some(Some(payment_method_id.to_owned())), }; - let customer_id = customer.customer_id.clone(); + let customer_id = customer.get_customer_id().clone(); // update the db with the default payment method id let updated_customer_details = db diff --git a/crates/router/src/core/payments/customers.rs b/crates/router/src/core/payments/customers.rs index c7797cedd1..4b71c43aca 100644 --- a/crates/router/src/core/payments/customers.rs +++ b/crates/router/src/core/payments/customers.rs @@ -1,3 +1,5 @@ +use common_utils::pii; +use masking::{ExposeOptionInterface, PeekInterface}; use router_env::{instrument, metrics::add_attributes, tracing}; use crate::{ @@ -79,7 +81,7 @@ pub fn get_connector_customer_details_if_present<'a>( customer .connector_customer .as_ref() - .and_then(|connector_customer_value| connector_customer_value.get(connector_name)) + .and_then(|connector_customer_value| connector_customer_value.peek().get(connector_name)) .and_then(|connector_customer| connector_customer.as_str()) } @@ -114,9 +116,8 @@ pub async fn update_connector_customer_in_customers( connector_customer_id: &Option, ) -> Option { let connector_customer_map = customer - .and_then(|customer| customer.connector_customer.as_ref()) - .and_then(|connector_customer| connector_customer.as_object()) - .map(ToOwned::to_owned) + .and_then(|customer| customer.connector_customer.clone().expose_option()) + .and_then(|connector_customer| connector_customer.as_object().cloned()) .unwrap_or_default(); let updated_connector_customer_map = @@ -132,7 +133,7 @@ pub async fn update_connector_customer_in_customers( .map(serde_json::Value::Object) .map( |connector_customer_value| storage::CustomerUpdate::ConnectorCustomer { - connector_customer: Some(connector_customer_value), + connector_customer: Some(pii::SecretSerdeValue::new(connector_customer_value)), }, ) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 9f661a8b0c..6b39ec672e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,7 +1,8 @@ use std::{borrow::Cow, str::FromStr}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use api_models::customers::CustomerRequestWithEmail; use api_models::{ - customers::CustomerRequestWithEmail, mandates::RecurringDetails, payments::{AddressDetailsWithPhone, CardToken, GetPaymentMethodType, RequestSurchargeDetails}, }; @@ -20,9 +21,11 @@ use diesel_models::enums::{self}; // TODO : Evaluate all the helper functions () use error_stack::{report, ResultExt}; use futures::future::Either; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use hyperswitch_domain_models::payments::payment_intent::CustomerData; use hyperswitch_domain_models::{ mandates::MandateData, - payments::{payment_attempt::PaymentAttempt, payment_intent::CustomerData, PaymentIntent}, + payments::{payment_attempt::PaymentAttempt, PaymentIntent}, router_data::KlarnaSdkResponse, }; use hyperswitch_interfaces::integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject}; @@ -51,7 +54,7 @@ use crate::{ mandate::helpers::MandateGenericData, payment_methods::{ self, - cards::{self, create_encrypted_data}, + cards::{self}, vault, }, payments, @@ -66,9 +69,7 @@ use crate::{ self, types::{self, AsyncLift}, }, - storage::{ - self, enums as storage_enums, ephemeral_key, CardTokenData, CustomerUpdate::Update, - }, + storage::{self, enums as storage_enums, ephemeral_key, CardTokenData}, transformers::{ForeignFrom, ForeignTryFrom}, AdditionalMerchantData, AdditionalPaymentMethodConnectorResponse, ErrorResponse, MandateReference, MerchantAccountData, MerchantRecipientData, PaymentsResponseData, @@ -80,6 +81,10 @@ use crate::{ OptionExt, StringExt, }, }; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::{ + core::payment_methods::cards::create_encrypted_data, types::storage::CustomerUpdate::Update, +}; pub fn create_identity_from_certificate_and_key( encoded_certificate: masking::Secret, @@ -1534,6 +1539,22 @@ pub async fn get_connector_default( )) } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[instrument(skip_all)] +#[allow(clippy::type_complexity)] +pub async fn create_customer_if_not_exist<'a, F: Clone, R>( + _state: &SessionState, + _operation: BoxedOperation<'a, F, R>, + _payment_data: &mut PaymentData, + _req: Option, + _merchant_id: &id_type::MerchantId, + _key_store: &domain::MerchantKeyStore, + _storage_scheme: common_enums::enums::MerchantStorageScheme, +) -> CustomResult<(BoxedOperation<'a, F, R>, Option), errors::StorageError> { + todo!() +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all)] #[allow(clippy::type_complexity)] pub async fn create_customer_if_not_exist<'a, F: Clone, R>( @@ -1689,6 +1710,7 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R>( address_id: None, default_payment_method_id: None, updated_by: None, + version: common_enums::ApiVersion::V1, }; metrics::CUSTOMER_CREATED.add(&metrics::CONTEXT, 1, &[]); db.insert_customer(new_customer, key_manager_state, key_store, storage_scheme) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 52d76289d8..318a2e0373 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1129,7 +1129,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen .clone(), ); - let customer_id = customer.clone().map(|c| c.customer_id); + let customer_id = customer.clone().map(|c| c.get_customer_id()); let return_url = payment_data.payment_intent.return_url.take(); let setup_future_usage = payment_data.payment_intent.setup_future_usage; let business_label = payment_data.payment_intent.business_label.clone(); @@ -1323,7 +1323,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen let customer_fut = if let Some((updated_customer, customer)) = updated_customer.zip(customer) { - let m_customer_customer_id = customer.customer_id.to_owned(); + let m_customer_customer_id = customer.get_customer_id().to_owned(); let m_customer_merchant_id = customer.merchant_id.to_owned(); let m_key_store = key_store.clone(); let m_updated_customer = updated_customer.clone(); diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 3a5cd00b9c..773e75c0c2 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -677,7 +677,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let customer_id = customer.clone().map(|c| c.customer_id); + let customer_id = customer.clone().map(|c| c.get_customer_id()); let intent_status = { let current_intent_status = payment_data.payment_intent.status; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 35d72ac6ef..404eb4bae7 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -105,7 +105,9 @@ where customer_data: customer, }; - let customer_id = customer.to_owned().map(|customer| customer.customer_id); + let customer_id = customer + .to_owned() + .map(|customer| customer.get_customer_id()); let supported_connector = &state .conf @@ -344,7 +346,7 @@ where verify_id: Some(data.payment_intent.payment_id), merchant_id: Some(data.payment_intent.merchant_id), client_secret: data.payment_intent.client_secret.map(Secret::new), - customer_id: customer.as_ref().map(|x| x.customer_id.clone()), + customer_id: customer.as_ref().map(|x| x.get_customer_id().clone()), email: customer .as_ref() .and_then(|cus| cus.email.as_ref().map(|s| s.to_owned())), @@ -750,7 +752,7 @@ where .set_client_secret(payment_intent.client_secret.map(Secret::new)) .set_created(Some(payment_intent.created_at)) .set_currency(currency.to_string()) - .set_customer_id(customer.as_ref().map(|cus| cus.clone().customer_id)) + .set_customer_id(customer.as_ref().map(|cus| cus.clone().get_customer_id())) .set_email( customer .as_ref() @@ -1127,7 +1129,7 @@ impl ForeignFrom<(storage::Payouts, storage::PayoutAttempt, domain::Customer)> currency: payout.destination_currency, connector: payout_attempt.connector, payout_type: payout.payout_type, - customer_id: customer.customer_id, + customer_id: customer.get_customer_id(), auto_fulfill: payout.auto_fulfill, email: customer.email, name: customer.name, @@ -1372,7 +1374,7 @@ impl TryFrom> for types::PaymentsAuthoriz let customer_id = additional_data .customer_data .as_ref() - .map(|data| data.customer_id.clone()); + .map(|data| data.get_customer_id().clone()); let charges = match payment_data.payment_intent.charges { Some(charges) => charges diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 77b5c1463f..4966a0f544 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -174,7 +174,7 @@ pub async fn initiate_payout_link( client_secret: link_data.client_secret.clone(), payout_link_id: payout_link.link_id, payout_id: payout_link.primary_reference, - customer_id: customer.customer_id, + customer_id: customer.get_customer_id(), session_expiry: payout_link.expiry, return_url: payout_link .return_url diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 75756a9639..06dd72b1ed 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -1085,7 +1085,7 @@ pub async fn create_recipient( Ok(recipient_create_data) => { let db = &*state.store; if let Some(customer) = customer_details { - let customer_id = customer.customer_id.to_owned(); + let customer_id = customer.get_customer_id().to_owned(); let merchant_id = merchant_account.get_id().to_owned(); if let Some(updated_customer) = customers::update_connector_customer_in_customers( @@ -2175,7 +2175,7 @@ pub async fn payout_create_db_entries( field_name: "customer_id", }) })? - .customer_id; + .get_customer_id(); // Validate whether profile_id passed in request is valid and is linked to the merchant let business_profile = @@ -2393,7 +2393,7 @@ pub async fn make_payout_data( Some(payout_token) => { let customer_id = customer_details .as_ref() - .map(|cd| cd.customer_id.to_owned()) + .map(|cd| cd.get_customer_id().to_owned()) .get_required_value("customer")?; helpers::make_payout_method_data( state, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 66ade4ee53..c4010490c2 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,15 +1,20 @@ -use api_models::{customers::CustomerRequestWithEmail, enums, payment_methods::Card, payouts}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use api_models::customers::CustomerRequestWithEmail; +use api_models::{enums, payment_methods::Card, payouts}; use common_utils::{ encryption::Encryption, errors::CustomResult, ext_traits::{AsyncExt, StringExt}, - fp_utils, generate_customer_id_of_default_length, id_type, + fp_utils, id_type, types::{ - keymanager::{Identifier, KeyManagerState, ToEncryptable}, + keymanager::{Identifier, KeyManagerState}, MinorUnit, }, }; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use common_utils::{generate_customer_id_of_default_length, types::keymanager::ToEncryptable}; use error_stack::{report, ResultExt}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::type_encryption::batch_encrypt; use masking::{PeekInterface, Secret}; use router_env::logger; @@ -598,6 +603,17 @@ pub async fn save_payout_data_to_locker( Ok(()) } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +pub async fn get_or_create_customer_details( + _state: &SessionState, + _customer_details: &CustomerDetails, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, +) -> RouterResult> { + todo!() +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] pub async fn get_or_create_customer_details( state: &SessionState, customer_details: &CustomerDetails, @@ -659,6 +675,7 @@ pub async fn get_or_create_customer_details( address_id: None, default_payment_method_id: None, updated_by: None, + version: common_enums::ApiVersion::V1, }; Ok(Some( @@ -823,10 +840,10 @@ pub async fn get_default_payout_connector( } pub fn should_call_payout_connector_create_customer<'a>( - state: &SessionState, - connector: &api::ConnectorData, + state: &'a SessionState, + connector: &'a api::ConnectorData, customer: &'a Option, - connector_label: &str, + connector_label: &'a str, ) -> (bool, Option<&'a str>) { // Check if create customer is required for the connector match enums::PayoutConnectors::try_from(connector.connector_name) { diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 1500721cc7..7a648f1376 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -10,7 +10,7 @@ use common_utils::{errors::CustomResult, ext_traits::AsyncExt, types::MinorUnit} use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{payment_address::PaymentAddress, router_data::ErrorResponse}; #[cfg(feature = "payouts")] -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface}; use maud::{html, PreEscaped}; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -128,8 +128,14 @@ pub async fn construct_payout_router_data<'a, F>( let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) - .and_then(|cc| cc.get(connector_label)) - .and_then(|id| serde_json::from_value::(id.to_owned()).ok()); + .and_then(|connector_customer_value| { + connector_customer_value + .clone() + .expose() + .get(connector_label) + .cloned() + }) + .and_then(|id| serde_json::from_value::(id).ok()); let vendor_details: Option = match api_models::enums::PayoutConnectors::try_from(connector_name.to_owned()).map_err( @@ -151,7 +157,7 @@ pub async fn construct_payout_router_data<'a, F>( let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), - customer_id: customer_details.to_owned().map(|c| c.customer_id), + customer_id: customer_details.to_owned().map(|c| c.get_customer_id()), connector_customer: connector_customer_id, connector: connector_name.to_string(), payment_id: "".to_string(), @@ -182,7 +188,7 @@ pub async fn construct_payout_router_data<'a, F>( customer_details: customer_details .to_owned() .map(|c| payments::CustomerDetails { - customer_id: Some(c.customer_id), + customer_id: Some(c.get_customer_id()), name: c.name.map(Encryptable::into_inner), email: c.email.map(Email::from), phone: c.phone.map(Encryptable::into_inner), diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index 01c7d7737a..9de76567de 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -105,6 +105,7 @@ mod storage { utils::db_utils, }; + #[cfg(all(feature = "v2", feature = "customer_v2"))] #[async_trait::async_trait] impl CustomerInterface for Store { #[instrument(skip_all)] @@ -362,7 +363,345 @@ mod storage { key_store: &domain::MerchantKeyStore, storage_scheme: MerchantStorageScheme, ) -> CustomResult { - let customer_id = customer_data.customer_id.clone(); + let customer_id = customer_data.get_customer_id().clone(); + let merchant_id = customer_data.merchant_id.clone(); + let mut new_customer = customer_data + .construct_new() + .await + .change_context(errors::StorageError::EncryptionError)?; + let storage_scheme = decide_storage_scheme::<_, diesel_models::Customer>( + self, + storage_scheme, + Op::Insert, + ) + .await; + new_customer.update_storage_scheme(storage_scheme); + let create_customer = match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + let conn = connection::pg_connection_write(self).await?; + new_customer + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + MerchantStorageScheme::RedisKv => { + let key = PartitionKey::MerchantIdCustomerId { + merchant_id: &merchant_id, + customer_id: customer_id.get_string_repr(), + }; + let field = format!("cust_{}", customer_id.get_string_repr()); + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Insert { + insertable: kv::Insertable::Customer(new_customer.clone()), + }, + }; + let storage_customer = new_customer.into(); + + match kv_wrapper::( + self, + KvOperation::HSetNx::( + &field, + &storage_customer, + redis_entry, + ), + key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hsetnx() + { + Ok(redis_interface::HsetnxReply::KeyNotSet) => { + Err(report!(errors::StorageError::DuplicateValue { + entity: "customer", + key: Some(customer_id.get_string_repr().to_string()), + })) + } + Ok(redis_interface::HsetnxReply::KeySet) => Ok(storage_customer), + Err(er) => Err(er).change_context(errors::StorageError::KVError), + } + } + }?; + + create_customer + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn delete_customer_by_customer_id_merchant_id( + &self, + _customer_id: &id_type::CustomerId, + _merchant_id: &id_type::MerchantId, + ) -> CustomResult { + todo!() + } + } + + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] + #[async_trait::async_trait] + impl CustomerInterface for Store { + #[instrument(skip_all)] + // check customer not found in kv and fallback to db + async fn find_customer_optional_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + let database_call = || async { + storage_types::Customer::find_optional_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + ) + .await + .map_err(|err| report!(errors::StorageError::from(err))) + }; + let storage_scheme = + decide_storage_scheme::<_, diesel_models::Customer>(self, storage_scheme, Op::Find) + .await; + let maybe_customer = match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = PartitionKey::MerchantIdCustomerId { + merchant_id, + customer_id: customer_id.get_string_repr(), + }; + let field = format!("cust_{}", customer_id.get_string_repr()); + Box::pin(db_utils::try_redis_get_else_try_database_get( + // check for ValueNotFound + async { + kv_wrapper( + self, + KvOperation::::HGet(&field), + key, + ) + .await? + .try_into_hget() + .map(Some) + }, + database_call, + )) + .await + } + }?; + + let maybe_result = maybe_customer + .async_map(|c| async { + c.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + .transpose()?; + + maybe_result.map_or(Ok(None), |customer: domain::Customer| match customer.name { + Some(ref name) if name.peek() == REDACTED => { + Err(errors::StorageError::CustomerRedacted)? + } + _ => Ok(Some(customer)), + }) + } + + #[instrument(skip_all)] + async fn update_customer_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: id_type::CustomerId, + merchant_id: id_type::MerchantId, + customer: customer::Customer, + customer_update: storage_types::CustomerUpdate, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + let customer = Conversion::convert(customer) + .await + .change_context(errors::StorageError::EncryptionError)?; + let database_call = || async { + storage_types::Customer::update_by_customer_id_merchant_id( + &conn, + customer_id.clone(), + merchant_id.clone(), + customer_update.clone().into(), + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + }; + let key = PartitionKey::MerchantIdCustomerId { + merchant_id: &merchant_id, + customer_id: customer_id.get_string_repr(), + }; + let field = format!("cust_{}", customer_id.get_string_repr()); + let storage_scheme = decide_storage_scheme::<_, diesel_models::Customer>( + self, + storage_scheme, + Op::Update(key.clone(), &field, customer.updated_by.as_deref()), + ) + .await; + let updated_object = match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let updated_customer = + diesel_models::CustomerUpdateInternal::from(customer_update.clone()) + .apply_changeset(customer.clone()); + + let redis_value = serde_json::to_string(&updated_customer) + .change_context(errors::StorageError::KVError)?; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Update { + updatable: kv::Updateable::CustomerUpdate(kv::CustomerUpdateMems { + orig: customer, + update_data: customer_update.into(), + }), + }, + }; + + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::( + (&field, redis_value), + redis_entry, + ), + key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + + Ok(updated_customer) + } + }; + + updated_object? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn find_customer_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + let database_call = || async { + storage_types::Customer::find_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + }; + let storage_scheme = + decide_storage_scheme::<_, diesel_models::Customer>(self, storage_scheme, Op::Find) + .await; + let customer = match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = PartitionKey::MerchantIdCustomerId { + merchant_id, + customer_id: customer_id.get_string_repr(), + }; + let field = format!("cust_{}", customer_id.get_string_repr()); + Box::pin(db_utils::try_redis_get_else_try_database_get( + async { + kv_wrapper( + self, + KvOperation::::HGet(&field), + key, + ) + .await? + .try_into_hget() + }, + database_call, + )) + .await + } + }?; + + let result: customer::Customer = customer + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?; + //.await + + match result.name { + Some(ref name) if name.peek() == REDACTED => { + Err(errors::StorageError::CustomerRedacted)? + } + _ => Ok(result), + } + } + + #[instrument(skip_all)] + async fn list_customers_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + + let encrypted_customers = + storage_types::Customer::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let customers = try_join_all(encrypted_customers.into_iter().map( + |encrypted_customer| async { + encrypted_customer + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }, + )) + .await?; + + Ok(customers) + } + + #[instrument(skip_all)] + async fn insert_customer( + &self, + customer_data: customer::Customer, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let customer_id = customer_data.get_customer_id().clone(); let merchant_id = customer_data.merchant_id.clone(); let mut new_customer = customer_data .construct_new() @@ -476,6 +815,7 @@ mod storage { }, }; + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[async_trait::async_trait] impl CustomerInterface for Store { #[instrument(skip_all)] @@ -646,6 +986,178 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } } + + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[async_trait::async_trait] + impl CustomerInterface for Store { + #[instrument(skip_all)] + async fn find_customer_optional_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + let maybe_customer: Option = + storage_types::Customer::find_optional_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .async_map(|c| async { + c.convert(state, key_store.key.get_inner(), merchant_id.to_string()) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + .transpose()?; + maybe_customer.map_or(Ok(None), |customer| { + // in the future, once #![feature(is_some_and)] is stable, we can make this more concise: + // `if customer.name.is_some_and(|ref name| name == REDACTED) ...` + match customer.name { + Some(ref name) if name.peek() == REDACTED => { + Err(errors::StorageError::CustomerRedacted)? + } + _ => Ok(Some(customer)), + } + }) + } + + #[instrument(skip_all)] + async fn update_customer_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: id_type::CustomerId, + merchant_id: id_type::MerchantId, + _customer: customer::Customer, + customer_update: storage_types::CustomerUpdate, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage_types::Customer::update_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id.clone(), + customer_update.into(), + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + .async_and_then(|c| async { + c.convert(state, key_store.key.get_inner(), merchant_id) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + } + + #[instrument(skip_all)] + async fn find_customer_by_customer_id_merchant_id( + &self, + state: &KeyManagerState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + let customer: customer::Customer = + storage_types::Customer::find_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + .async_and_then(|c| async { + c.convert(state, key_store.key.get_inner(), merchant_id.to_string()) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await?; + match customer.name { + Some(ref name) if name.peek() == REDACTED => { + Err(errors::StorageError::CustomerRedacted)? + } + _ => Ok(customer), + } + } + + #[instrument(skip_all)] + async fn list_customers_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + + let encrypted_customers = + storage_types::Customer::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let customers = try_join_all(encrypted_customers.into_iter().map( + |encrypted_customer| async { + encrypted_customer + .convert(state, key_store.key.get_inner(), merchant_id.to_string()) + .await + .change_context(errors::StorageError::DecryptionError) + }, + )) + .await?; + + Ok(customers) + } + + #[instrument(skip_all)] + async fn insert_customer( + &self, + customer_data: customer::Customer, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + customer_data + .construct_new() + .await + .change_context(errors::StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + .async_and_then(|c| async { + c.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + } + + #[instrument(skip_all)] + async fn delete_customer_by_customer_id_merchant_id( + &self, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage_types::Customer::delete_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + } } #[async_trait::async_trait] @@ -663,7 +1175,7 @@ impl CustomerInterface for MockDb { let customer = customers .iter() .find(|customer| { - customer.customer_id == *customer_id && &customer.merchant_id == merchant_id + customer.get_customer_id() == *customer_id && customer.merchant_id == *merchant_id }) .cloned(); customer diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index b851b8d458..e3f86f670f 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -45,7 +45,7 @@ impl ForeignFrom<(domain::Customer, Option)> for Custo impl ForeignFrom for CustomerResponse { fn foreign_from(cust: domain::Customer) -> Self { customers::CustomerResponse { - merchant_reference_id: Some(cust.customer_id), + merchant_reference_id: Some(cust.get_customer_id()), name: cust.name, email: cust.email, phone: cust.phone, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 467867ef84..f59be7ed52 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1497,7 +1497,7 @@ impl ForeignFrom for gsm_api_types::GsmResponse { impl ForeignFrom<&domain::Customer> for payments::CustomerDetailsResponse { fn foreign_from(customer: &domain::Customer) -> Self { Self { - id: Some(customer.customer_id.clone()), + id: Some(customer.get_customer_id().clone()), name: customer .name .as_ref() diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 6fad1e2195..5b46dce139 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -28,6 +28,7 @@ pub use common_utils::{ crypto, ext_traits::{ByteSliceExt, BytesExt, Encode, StringExt, ValueExt}, fp_utils::when, + id_type, validation::validate_email, }; use error_stack::ResultExt; @@ -768,15 +769,15 @@ pub trait CustomerAddress { address_details: payments::AddressDetails, key: &[u8], storage_scheme: storage::enums::MerchantStorageScheme, - merchant_id: common_utils::id_type::MerchantId, + merchant_id: id_type::MerchantId, ) -> CustomResult; async fn get_domain_address( &self, state: &SessionState, address_details: payments::AddressDetails, - merchant_id: &common_utils::id_type::MerchantId, - customer_id: &common_utils::id_type::CustomerId, + merchant_id: &id_type::MerchantId, + customer_id: &id_type::CustomerId, key: &[u8], storage_scheme: storage::enums::MerchantStorageScheme, ) -> CustomResult; @@ -791,7 +792,7 @@ impl CustomerAddress for api_models::customers::CustomerRequest { address_details: payments::AddressDetails, key: &[u8], storage_scheme: storage::enums::MerchantStorageScheme, - merchant_id: common_utils::id_type::MerchantId, + merchant_id: id_type::MerchantId, ) -> CustomResult { let encrypted_data = batch_encrypt( &state.into(), @@ -827,8 +828,8 @@ impl CustomerAddress for api_models::customers::CustomerRequest { &self, state: &SessionState, address_details: payments::AddressDetails, - merchant_id: &common_utils::id_type::MerchantId, - customer_id: &common_utils::id_type::CustomerId, + merchant_id: &id_type::MerchantId, + customer_id: &id_type::CustomerId, key: &[u8], storage_scheme: storage::enums::MerchantStorageScheme, ) -> CustomResult { @@ -875,7 +876,7 @@ impl CustomerAddress for api_models::customers::CustomerRequest { pub fn add_apple_pay_flow_metrics( apple_pay_flow: &Option, connector: Option, - merchant_id: common_utils::id_type::MerchantId, + merchant_id: id_type::MerchantId, ) { if let Some(flow) = apple_pay_flow { match flow { @@ -909,7 +910,7 @@ pub fn add_apple_pay_payment_status_metrics( payment_attempt_status: enums::AttemptStatus, apple_pay_flow: Option, connector: Option, - merchant_id: common_utils::id_type::MerchantId, + merchant_id: id_type::MerchantId, ) { if payment_attempt_status == enums::AttemptStatus::Charged { if let Some(flow) = apple_pay_flow { diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 386f442db6..2bfd90bf34 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -15,6 +15,7 @@ payouts = ["hyperswitch_domain_models/payouts"] v1 = ["api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1"] v2 = ["api_models/v2", "diesel_models/v2", "hyperswitch_domain_models/v2"] payment_v2 = ["hyperswitch_domain_models/payment_v2", "diesel_models/payment_v2"] +customer_v2 =["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2"] [dependencies] # First Party dependencies diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 7bb0c700d7..6cac75c189 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -427,6 +427,7 @@ impl UniqueConstraints for diesel_models::Mandate { } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl UniqueConstraints for diesel_models::Customer { fn unique_constraints(&self) -> Vec { vec![format!( @@ -440,6 +441,16 @@ impl UniqueConstraints for diesel_models::Customer { } } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl UniqueConstraints for diesel_models::Customer { + fn unique_constraints(&self) -> Vec { + vec![format!("customer_{}", self.id.clone())] + } + fn table_name(&self) -> &str { + "Customer" + } +} + #[cfg(not(feature = "payouts"))] impl PayoutAttemptInterface for KVRouterStore {} #[cfg(not(feature = "payouts"))] diff --git a/crates/storage_impl/src/payouts/payouts.rs b/crates/storage_impl/src/payouts/payouts.rs index eabe8f5db8..c72965f010 100644 --- a/crates/storage_impl/src/payouts/payouts.rs +++ b/crates/storage_impl/src/payouts/payouts.rs @@ -1,14 +1,18 @@ #[cfg(feature = "olap")] use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; use common_utils::ext_traits::Encode; +#[cfg(all( + feature = "olap", + any(feature = "v1", feature = "v2"), + not(feature = "customer_v2") +))] +use diesel::JoinOnDsl; #[cfg(feature = "olap")] -use diesel::{associations::HasTable, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; #[cfg(feature = "olap")] use diesel_models::{ - customers::Customer as DieselCustomer, - payout_attempt::PayoutAttempt as DieselPayoutAttempt, - query::generics::db_metrics, - schema::{customers::dsl as cust_dsl, payout_attempt::dsl as poa_dsl, payouts::dsl as po_dsl}, + customers::Customer as DieselCustomer, query::generics::db_metrics, + schema::payouts::dsl as po_dsl, }; use diesel_models::{ enums::MerchantStorageScheme, @@ -18,6 +22,15 @@ use diesel_models::{ PayoutsUpdate as DieselPayoutsUpdate, }, }; +#[cfg(all( + feature = "olap", + any(feature = "v1", feature = "v2"), + not(feature = "customer_v2") +))] +use diesel_models::{ + payout_attempt::PayoutAttempt as DieselPayoutAttempt, + schema::{customers::dsl as cust_dsl, payout_attempt::dsl as poa_dsl}, +}; use error_stack::ResultExt; #[cfg(feature = "olap")] use hyperswitch_domain_models::payouts::PayoutFetchConstraints; @@ -517,7 +530,11 @@ impl PayoutsInterface for crate::RouterStore { }) } - #[cfg(feature = "olap")] + #[cfg(all( + any(feature = "v1", feature = "v2"), + feature = "olap", + not(feature = "customer_v2") + ))] #[instrument(skip_all)] async fn filter_payouts_and_attempts( &self, @@ -651,6 +668,18 @@ impl PayoutsInterface for crate::RouterStore { }) } + #[cfg(feature = "olap")] + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn filter_payouts_and_attempts( + &self, + _merchant_id: &common_utils::id_type::MerchantId, + _filters: &PayoutFetchConstraints, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result, StorageError> { + todo!() + } + #[cfg(feature = "olap")] #[instrument(skip_all)] async fn filter_payouts_by_time_range_constraints( diff --git a/justfile b/justfile index 452de7b301..c3cea28c67 100644 --- a/justfile +++ b/justfile @@ -178,9 +178,9 @@ migrate_v2 operation=default_operation *args='': exit $EXIT_CODE # Drop database if exists and then create a new 'hyperswitch_db' Database -resurrect: - psql -U postgres -c 'DROP DATABASE IF EXISTS hyperswitch_db'; - psql -U postgres -c 'CREATE DATABASE hyperswitch_db'; +resurrect database_name='hyperswitch_db': + psql -U postgres -c 'DROP DATABASE IF EXISTS {{ database_name }}'; + psql -U postgres -c 'CREATE DATABASE {{ database_name }}'; ci_hack: scripts/ci-checks.sh diff --git a/migrations/2024-07-21-120246_add_version_mapping/down.sql b/migrations/2024-07-21-120246_add_version_mapping/down.sql new file mode 100644 index 0000000000..4d52d75390 --- /dev/null +++ b/migrations/2024-07-21-120246_add_version_mapping/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE customers DROP COLUMN version; +DROP TYPE "ApiVersion"; \ No newline at end of file diff --git a/migrations/2024-07-21-120246_add_version_mapping/up.sql b/migrations/2024-07-21-120246_add_version_mapping/up.sql new file mode 100644 index 0000000000..f3c5749e07 --- /dev/null +++ b/migrations/2024-07-21-120246_add_version_mapping/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here + +CREATE TYPE "ApiVersion" AS ENUM ('v1', 'v2'); + +ALTER TABLE customers ADD COLUMN IF NOT EXISTS version "ApiVersion" NOT NULL DEFAULT 'v1'; \ No newline at end of file diff --git a/scripts/ci-checks-v2.sh b/scripts/ci-checks-v2.sh index 98697387ec..cbdae11402 100755 --- a/scripts/ci-checks-v2.sh +++ b/scripts/ci-checks-v2.sh @@ -32,7 +32,7 @@ if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]]; then # A package must be checked if it has been modified if grep --quiet --extended-regexp "^crates/${package_name}" <<< "${files_modified}"; then if [[ "${package_name}" == "storage_impl" ]]; then - all_commands+=("cargo hack clippy --features 'v2,payment_v2' -p storage_impl") + all_commands+=("cargo hack clippy --features 'v2,payment_v2,customer_v2' -p storage_impl") else all_commands+=("cargo hack clippy --feature-powerset --depth 2 --ignore-unknown-features --at-least-one-of 'v2 ' --include-features '${v2_feature_set}' --package '${package_name}'") fi @@ -47,7 +47,7 @@ if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]]; then else # If we are doing this locally or on merge queue, then check for all the V2 crates - all_commands+=("cargo hack clippy --features 'v2,payment_v2' -p storage_impl") + all_commands+=("cargo hack clippy --features 'v2,payment_v2,customer_v2' -p storage_impl") common_command="cargo hack clippy --feature-powerset --depth 2 --ignore-unknown-features --at-least-one-of 'v2 ' --include-features '${v2_feature_set}'" crates_to_include="" diff --git a/v2_migrations/2024-07-30-100323_customer_v2/down.sql b/v2_migrations/2024-07-30-100323_customer_v2/down.sql new file mode 100644 index 0000000000..15b1ea3f53 --- /dev/null +++ b/v2_migrations/2024-07-30-100323_customer_v2/down.sql @@ -0,0 +1,20 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE customers DROP COLUMN IF EXISTS merchant_reference_id; +ALTER TABLE customers DROP COLUMN IF EXISTS default_billing_address; +ALTER TABLE customers DROP COLUMN IF EXISTS default_shipping_address; + +-- Run this query only when V1 is deprecated +ALTER TABLE customers DROP CONSTRAINT customers_pkey; +ALTER TABLE customers DROP COLUMN IF EXISTS id; +ALTER TABLE customers ADD COLUMN IF NOT EXISTS id SERIAL; + +ALTER TABLE customers ADD COLUMN customer_id VARCHAR(64); + +-- Back filling before making it primary key +UPDATE customers +SET customer_id = id; + + +ALTER TABLE customers ADD PRIMARY KEY (merchant_id, customer_id); + +ALTER TABLE customers ADD COLUMN address_id VARCHAR(64); diff --git a/v2_migrations/2024-07-30-100323_customer_v2/up.sql b/v2_migrations/2024-07-30-100323_customer_v2/up.sql new file mode 100644 index 0000000000..e84aa67fb7 --- /dev/null +++ b/v2_migrations/2024-07-30-100323_customer_v2/up.sql @@ -0,0 +1,22 @@ +-- Your SQL goes here +ALTER TABLE customers ADD COLUMN merchant_reference_id VARCHAR(64); +ALTER TABLE customers ADD COLUMN IF NOT EXISTS default_billing_address BYTEA DEFAULT NULL; +ALTER TABLE customers ADD COLUMN IF NOT EXISTS default_shipping_address BYTEA DEFAULT NULL; + +-- Run this query only when V1 is deprecated +ALTER TABLE customers DROP CONSTRAINT IF EXISTS customers_pkey; + +ALTER TABLE customers DROP COLUMN IF EXISTS id; +ALTER TABLE customers ADD COLUMN IF NOT EXISTS id VARCHAR(64); + +-- Back filling before making it primary key +UPDATE customers +SET id = customer_id; + +ALTER TABLE customers ADD PRIMARY KEY (id); + +-- Run this query only when V1 is deprecated +ALTER TABLE customers DROP COLUMN customer_id; + +-- Run this query only when V1 is deprecated +ALTER TABLE customers DROP COLUMN address_id; \ No newline at end of file