diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 77ddc657fd..3ba7097d3a 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5204,13 +5204,14 @@ "CustomerDeleteResponse": { "type": "object", "required": [ - "customer_id", + "merchant_reference_id", "customer_deleted", "address_deleted", - "payment_methods_deleted" + "payment_methods_deleted", + "id" ], "properties": { - "customer_id": { + "merchant_reference_id": { "type": "string", "description": "The identifier for the customer object", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", @@ -5230,6 +5231,10 @@ "type": "boolean", "description": "Whether payment methods deleted or not", "example": false + }, + "id": { + "type": "string", + "description": "Global id" } } }, diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index e689e66489..eb196a7207 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -352,6 +352,7 @@ impl CustomerId { } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct CustomerDeleteResponse { /// The identifier for the customer object @@ -368,6 +369,26 @@ pub struct CustomerDeleteResponse { pub payment_methods_deleted: bool, } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct CustomerDeleteResponse { + /// The identifier for the customer object + #[schema(value_type = String, max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub merchant_reference_id: Option, + /// Whether customer was deleted or not + #[schema(example = false)] + pub customer_deleted: bool, + /// Whether address was deleted or not + #[schema(example = false)] + pub address_deleted: bool, + /// Whether payment methods deleted or not + #[schema(example = false)] + pub payment_methods_deleted: bool, + /// Global id + pub id: String, +} + +/// The identifier for the customer object. If not provided the customer ID will be autogenerated. #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] pub struct CustomerUpdateRequest { diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs index b6fcb9b047..d2b30bcf01 100644 --- a/crates/api_models/src/events/customer.rs +++ b/crates/api_models/src/events/customer.rs @@ -1,10 +1,12 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::customers::CustomerId; #[cfg(all(feature = "v2", feature = "customer_v2"))] use crate::customers::GlobalId; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use crate::customers::{CustomerDeleteResponse, CustomerId}; -use crate::customers::{CustomerRequest, CustomerResponse, CustomerUpdateRequest}; +use crate::customers::{ + CustomerDeleteResponse, CustomerRequest, CustomerResponse, CustomerUpdateRequest, +}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl ApiEventMetric for CustomerDeleteResponse { @@ -15,6 +17,15 @@ impl ApiEventMetric for CustomerDeleteResponse { } } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +impl ApiEventMetric for CustomerDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + id: self.id.clone(), + }) + } +} + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl ApiEventMetric for CustomerRequest { fn get_api_event_type(&self) -> Option { diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 2de401934c..56d86d34bc 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -10,11 +10,11 @@ pub mod diesel_exports { 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, - DbRefundStatus as RefundStatus, + DbDeleteStatus as DeleteStatus, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbFraudCheckStatus as FraudCheckStatus, + DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, + DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, + DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, }; @@ -3135,3 +3135,23 @@ pub enum OrderFulfillmentTimeOrigin { Create, Confirm, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + 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 DeleteStatus { + Active, + Redacted, +} diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index c0d6639d1a..46b5059b42 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -1,10 +1,10 @@ -// #[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(feature = "v2", feature = "customer_v2"))] +use crate::enums::DeleteStatus; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::schema::customers; #[cfg(all(feature = "v2", feature = "customer_v2"))] @@ -32,21 +32,6 @@ pub struct CustomerNew { 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 { - #[allow(clippy::todo)] - 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) { @@ -77,8 +62,6 @@ impl From for Customer { } } -// V2 customer - #[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive( Clone, @@ -107,7 +90,7 @@ pub struct CustomerNew { pub merchant_reference_id: Option, pub default_billing_address: Option, pub default_shipping_address: Option, - // pub status: Option, + pub status: DeleteStatus, pub id: String, } @@ -138,8 +121,8 @@ impl From for Customer { 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, + status: customer_new.status, } } } @@ -189,7 +172,7 @@ pub struct Customer { pub merchant_reference_id: Option, pub default_billing_address: Option, pub default_shipping_address: Option, - // pub status: Option, + pub status: DeleteStatus, pub id: String, } @@ -264,6 +247,7 @@ pub struct CustomerUpdateInternal { pub updated_by: Option, pub default_billing_address: Option, pub default_shipping_address: Option, + pub status: Option, } #[cfg(all(feature = "v2", feature = "customer_v2"))] @@ -277,10 +261,10 @@ impl CustomerUpdateInternal { phone_country_code, metadata, connector_customer, - // address_id, default_payment_method_id, default_billing_address, default_shipping_address, + status, .. } = self; @@ -293,7 +277,6 @@ impl CustomerUpdateInternal { 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), @@ -301,6 +284,7 @@ impl CustomerUpdateInternal { .map_or(source.default_billing_address, Some), default_shipping_address: default_shipping_address .map_or(source.default_shipping_address, Some), + status: status.unwrap_or(source.status), ..source } } diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index e4ac55532c..8f9469c028 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -6,9 +6,9 @@ pub mod diesel_exports { 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, + DbDashboardMetadata as DashboardMetadata, DbDeleteStatus as DeleteStatus, + DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, + DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbGenericLinkType as GenericLinkType, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, diff --git a/crates/diesel_models/src/query/mandate.rs b/crates/diesel_models/src/query/mandate.rs index 26ba676501..9f5a402c17 100644 --- a/crates/diesel_models/src/query/mandate.rs +++ b/crates/diesel_models/src/query/mandate.rs @@ -61,6 +61,12 @@ impl Mandate { .await } + //Fix this function once V2 mandate is schema is being built + #[cfg(all(feature = "v2", feature = "customer_v2"))] + pub async fn find_by_global_id(_conn: &PgPooledConn, _id: &str) -> StorageResult> { + todo!() + } + pub async fn update_by_merchant_id_mandate_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index 1684356480..37c41c19dd 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -126,6 +126,16 @@ impl PaymentMethod { .await } + // Need to fix this function once we start moving to v2 for payment method + #[cfg(all(feature = "v2", feature = "customer_v2"))] + pub async fn find_by_global_id( + _conn: &PgPooledConn, + _id: &str, + _limit: Option, + ) -> StorageResult> { + todo!() + } + pub async fn get_count_by_customer_id_merchant_id_status( conn: &PgPooledConn, customer_id: &common_utils::id_type::CustomerId, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index e2bda4a170..cda3157767 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -317,6 +317,7 @@ diesel::table! { merchant_reference_id -> Nullable, default_billing_address -> Nullable, default_shipping_address -> Nullable, + status -> DeleteStatus, #[max_length = 64] id -> Varchar, } diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index a01f994b56..af901a8055 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -1,6 +1,6 @@ use api_models::customers::CustomerRequestWithEncryption; -// #[cfg(all(feature = "v2", feature = "customer_v2"))] -// use common_enums::SoftDeleteStatus; +#[cfg(all(feature = "v2", feature = "customer_v2"))] +use common_enums::DeleteStatus; use common_utils::{ crypto, date_time, encryption::Encryption, @@ -56,9 +56,9 @@ pub struct Customer { pub merchant_reference_id: Option, pub default_billing_address: Option, pub default_shipping_address: Option, - // pub status: Option, pub id: String, pub version: common_enums::ApiVersion, + pub status: DeleteStatus, } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] @@ -182,7 +182,7 @@ impl super::behaviour::Conversion for Customer { default_billing_address: self.default_billing_address.map(Encryption::from), default_shipping_address: self.default_shipping_address.map(Encryption::from), version: self.version, - // status: self.status, + status: self.status, }) } @@ -236,7 +236,7 @@ impl super::behaviour::Conversion for Customer { default_billing_address: item.default_billing_address, default_shipping_address: item.default_shipping_address, version: item.version, - // status: item.status, + status: item.status, }) } @@ -259,8 +259,8 @@ impl super::behaviour::Conversion for Customer { updated_by: self.updated_by, default_billing_address: self.default_billing_address, default_shipping_address: self.default_shipping_address, - // status: self.status, version: crate::consts::API_VERSION, + status: self.status, }) } } @@ -270,7 +270,7 @@ impl super::behaviour::Conversion for Customer { pub enum CustomerUpdate { Update { name: crypto::OptionalEncryptableName, - email: crypto::OptionalEncryptableEmail, + email: Box, phone: Box, description: Option, phone_country_code: Option, @@ -279,6 +279,7 @@ pub enum CustomerUpdate { default_billing_address: Option, default_shipping_address: Option, default_payment_method_id: Option>, + status: Option, }, ConnectorCustomer { connector_customer: Option, @@ -303,6 +304,7 @@ impl From for CustomerUpdateInternal { default_billing_address, default_shipping_address, default_payment_method_id, + status, } => Self { name: name.map(Encryption::from), email: email.map(Encryption::from), @@ -316,6 +318,7 @@ impl From for CustomerUpdateInternal { default_shipping_address, default_payment_method_id, updated_by: None, + status, }, CustomerUpdate::ConnectorCustomer { connector_customer } => Self { connector_customer, @@ -330,6 +333,7 @@ impl From for CustomerUpdateInternal { updated_by: None, default_billing_address: None, default_shipping_address: None, + status: None, }, CustomerUpdate::UpdateDefaultPaymentMethod { default_payment_method_id, @@ -346,6 +350,7 @@ impl From for CustomerUpdateInternal { updated_by: None, default_billing_address: None, default_shipping_address: None, + status: None, }, } } diff --git a/crates/router/src/compatibility/stripe/customers/types.rs b/crates/router/src/compatibility/stripe/customers/types.rs index 2b62d8f5da..83649f9823 100644 --- a/crates/router/src/compatibility/stripe/customers/types.rs +++ b/crates/router/src/compatibility/stripe/customers/types.rs @@ -182,6 +182,7 @@ impl From for CreateCustomerResponse { } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl From for CustomerDeleteResponse { fn from(cust: api::CustomerDeleteResponse) -> Self { Self { diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 8de8a42763..ea4becd935 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -1,36 +1,38 @@ use api_models::customers::CustomerRequestWithEmail; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use common_utils::{crypto::Encryptable, types::Description}; use common_utils::{ + crypto::Encryptable, errors::ReportSwitchExt, ext_traits::{AsyncExt, OptionExt}, id_type, type_name, - types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, + types::{ + keymanager::{Identifier, KeyManagerState, ToEncryptable}, + Description, + }, }; use error_stack::{report, ResultExt}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] 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; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +use crate::utils::CustomerAddress; use crate::{ - core::errors::{self, StorageErrorExt}, + core::{ + errors::{self, StorageErrorExt}, + payment_methods::cards, + }, db::StorageInterface, pii::PeekInterface, - routes::SessionState, + routes::{metrics, SessionState}, services, types::{ api::customers, domain::{self, types}, - storage::{self}, + storage::{self, enums}, transformers::ForeignFrom, }, }; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use crate::{ - core::payment_methods::cards, routes::metrics, types::storage::enums, utils::CustomerAddress, -}; pub const REDACTED: &str = "Redacted"; @@ -264,8 +266,8 @@ impl CustomerCreateBridge for customers::CustomerRequest { updated_by: None, 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: hyperswitch_domain_models::consts::API_VERSION, + status: common_enums::DeleteStatus::Active, }) } @@ -532,6 +534,178 @@ pub async fn list_customers( Ok(services::ApplicationResponse::Json(customers)) } +#[cfg(all( + feature = "v2", + feature = "customer_v2", + feature = "payment_methods_v2" +))] +#[instrument(skip_all)] +pub async fn delete_customer( + state: SessionState, + merchant_account: domain::MerchantAccount, + req: customers::GlobalId, + key_store: domain::MerchantKeyStore, +) -> errors::CustomerResponse { + let db = &*state.store; + let key_manager_state = &(&state).into(); + req.fetch_domain_model_and_update_and_generate_delete_customer_response( + db, + &key_store, + &merchant_account, + key_manager_state, + &state, + ) + .await +} + +#[cfg(all( + feature = "v2", + feature = "customer_v2", + feature = "payment_methods_v2" +))] +#[async_trait::async_trait] +impl CustomerDeleteBridge for customers::GlobalId { + async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + &'a self, + db: &'a dyn StorageInterface, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + key_manager_state: &'a KeyManagerState, + state: &'a SessionState, + ) -> errors::CustomerResponse { + let customer_orig = db + .find_customer_by_global_id( + key_manager_state, + &self.id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .switch()?; + + let merchant_reference_id = customer_orig.merchant_reference_id.clone(); + + let customer_mandates = db.find_mandate_by_global_id(&self.id).await.switch()?; + + for mandate in customer_mandates.into_iter() { + if mandate.mandate_status == enums::MandateStatus::Active { + Err(errors::CustomersErrorResponse::MandateActive)? + } + } + + match db + .find_payment_method_list_by_global_id(key_manager_state, key_store, &self.id, None) + .await + { + // check this in review + Ok(customer_payment_methods) => { + for pm in customer_payment_methods.into_iter() { + if pm.payment_method == Some(enums::PaymentMethod::Card) { + cards::delete_card_by_locker_id( + &state, + &self.id, + merchant_account.get_id(), + ) + .await + .switch()?; + } + // No solution as of now, need to discuss this further with payment_method_v2 + + // db.delete_payment_method( + // key_manager_state, + // key_store, + // pm, + // ) + // .await + // .switch()?; + } + } + Err(error) => { + if error.current_context().is_db_not_found() { + Ok(()) + } else { + Err(error) + .change_context(errors::CustomersErrorResponse::InternalServerError) + .attach_printable( + "failed find_payment_method_by_customer_id_merchant_id_list", + ) + }? + } + }; + + let key = key_store.key.get_inner().peek(); + + let identifier = Identifier::Merchant(key_store.merchant_id.clone()); + let redacted_encrypted_value: Encryptable> = types::crypto_operation( + key_manager_state, + type_name!(storage::Address), + types::CryptoOperation::Encrypt(REDACTED.to_string().into()), + identifier.clone(), + key, + ) + .await + .and_then(|val| val.try_into_operation()) + .switch()?; + + let redacted_encrypted_email = Encryptable::new( + redacted_encrypted_value + .clone() + .into_inner() + .switch_strategy(), + redacted_encrypted_value.clone().into_encrypted(), + ); + + let updated_customer = storage::CustomerUpdate::Update { + name: Some(redacted_encrypted_value.clone()), + email: Box::new(Some(redacted_encrypted_email)), + phone: Box::new(Some(redacted_encrypted_value.clone())), + description: Some(Description::new(REDACTED.to_string())), + phone_country_code: Some(REDACTED.to_string()), + metadata: None, + connector_customer: None, + default_billing_address: None, + default_shipping_address: None, + default_payment_method_id: None, + status: Some(common_enums::DeleteStatus::Redacted), + }; + + db.update_customer_by_global_id( + key_manager_state, + self.id.clone(), + customer_orig, + merchant_account.get_id(), + updated_customer, + &key_store, + merchant_account.storage_scheme, + ) + .await + .switch()?; + + let response = customers::CustomerDeleteResponse { + merchant_reference_id, + customer_deleted: true, + address_deleted: true, + payment_methods_deleted: true, + id: self.id.clone(), + }; + metrics::CUSTOMER_REDACTED.add(&metrics::CONTEXT, 1, &[]); + Ok(services::ApplicationResponse::Json(response)) + } +} + +#[async_trait::async_trait] +trait CustomerDeleteBridge { + async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + &'a self, + db: &'a dyn StorageInterface, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + key_manager_state: &'a KeyManagerState, + state: &'a SessionState, + ) -> errors::CustomerResponse; +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "customer_v2"), @@ -544,174 +718,203 @@ pub async fn delete_customer( req: customers::CustomerId, key_store: domain::MerchantKeyStore, ) -> errors::CustomerResponse { - let db = &state.store; + let db = &*state.store; let key_manager_state = &(&state).into(); - let customer_orig = db - .find_customer_by_customer_id_merchant_id( + req.fetch_domain_model_and_update_and_generate_delete_customer_response( + db, + &key_store, + &merchant_account, + key_manager_state, + &state, + ) + .await +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "customer_v2"), + not(feature = "payment_methods_v2") +))] +#[async_trait::async_trait] +impl CustomerDeleteBridge for customers::CustomerId { + async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + &'a self, + db: &'a dyn StorageInterface, + key_store: &'a domain::MerchantKeyStore, + merchant_account: &'a domain::MerchantAccount, + key_manager_state: &'a KeyManagerState, + state: &'a SessionState, + ) -> errors::CustomerResponse { + let customer_orig = db + .find_customer_by_customer_id_merchant_id( + key_manager_state, + &self.customer_id, + merchant_account.get_id(), + key_store, + merchant_account.storage_scheme, + ) + .await + .switch()?; + + let customer_mandates = db + .find_mandate_by_merchant_id_customer_id(merchant_account.get_id(), &self.customer_id) + .await + .switch()?; + + for mandate in customer_mandates.into_iter() { + if mandate.mandate_status == enums::MandateStatus::Active { + Err(errors::CustomersErrorResponse::MandateActive)? + } + } + + match db + .find_payment_method_by_customer_id_merchant_id_list( + key_manager_state, + key_store, + &self.customer_id, + merchant_account.get_id(), + None, + ) + .await + { + // check this in review + Ok(customer_payment_methods) => { + for pm in customer_payment_methods.into_iter() { + if pm.payment_method == Some(enums::PaymentMethod::Card) { + cards::delete_card_from_locker( + state, + &self.customer_id, + merchant_account.get_id(), + pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), + ) + .await + .switch()?; + } + db.delete_payment_method_by_merchant_id_payment_method_id( + key_manager_state, + key_store, + merchant_account.get_id(), + &pm.payment_method_id, + ) + .await + .switch()?; + } + } + Err(error) => { + if error.current_context().is_db_not_found() { + Ok(()) + } else { + Err(error) + .change_context(errors::CustomersErrorResponse::InternalServerError) + .attach_printable( + "failed find_payment_method_by_customer_id_merchant_id_list", + ) + }? + } + }; + + let key = key_store.key.get_inner().peek(); + let identifier = Identifier::Merchant(key_store.merchant_id.clone()); + let redacted_encrypted_value: Encryptable> = types::crypto_operation( key_manager_state, - &req.customer_id, - merchant_account.get_id(), - &key_store, + type_name!(storage::Address), + types::CryptoOperation::Encrypt(REDACTED.to_string().into()), + identifier.clone(), + key, + ) + .await + .and_then(|val| val.try_into_operation()) + .switch()?; + + let redacted_encrypted_email = Encryptable::new( + redacted_encrypted_value + .clone() + .into_inner() + .switch_strategy(), + redacted_encrypted_value.clone().into_encrypted(), + ); + + let update_address = storage::AddressUpdate::Update { + city: Some(REDACTED.to_string()), + country: None, + line1: Some(redacted_encrypted_value.clone()), + line2: Some(redacted_encrypted_value.clone()), + line3: Some(redacted_encrypted_value.clone()), + state: Some(redacted_encrypted_value.clone()), + zip: Some(redacted_encrypted_value.clone()), + first_name: Some(redacted_encrypted_value.clone()), + last_name: Some(redacted_encrypted_value.clone()), + phone_number: Some(redacted_encrypted_value.clone()), + country_code: Some(REDACTED.to_string()), + updated_by: merchant_account.storage_scheme.to_string(), + email: Some(redacted_encrypted_email), + }; + + match db + .update_address_by_merchant_id_customer_id( + key_manager_state, + &self.customer_id, + merchant_account.get_id(), + update_address, + key_store, + ) + .await + { + Ok(_) => Ok(()), + Err(error) => { + if error.current_context().is_db_not_found() { + Ok(()) + } else { + Err(error) + .change_context(errors::CustomersErrorResponse::InternalServerError) + .attach_printable("failed update_address_by_merchant_id_customer_id") + } + } + }?; + + let updated_customer = storage::CustomerUpdate::Update { + name: Some(redacted_encrypted_value.clone()), + email: Some( + types::crypto_operation( + key_manager_state, + type_name!(storage::Customer), + types::CryptoOperation::Encrypt(REDACTED.to_string().into()), + identifier, + key, + ) + .await + .and_then(|val| val.try_into_operation()) + .switch()?, + ), + phone: Box::new(Some(redacted_encrypted_value.clone())), + description: Some(Description::new(REDACTED.to_string())), + phone_country_code: Some(REDACTED.to_string()), + metadata: None, + connector_customer: None, + address_id: None, + }; + + db.update_customer_by_customer_id_merchant_id( + key_manager_state, + self.customer_id.clone(), + merchant_account.get_id().to_owned(), + customer_orig, + updated_customer, + key_store, merchant_account.storage_scheme, ) .await .switch()?; - let customer_mandates = db - .find_mandate_by_merchant_id_customer_id(merchant_account.get_id(), &req.customer_id) - .await - .switch()?; - - for mandate in customer_mandates.into_iter() { - if mandate.mandate_status == enums::MandateStatus::Active { - Err(errors::CustomersErrorResponse::MandateActive)? - } + let response = customers::CustomerDeleteResponse { + customer_id: self.customer_id.clone(), + customer_deleted: true, + address_deleted: true, + payment_methods_deleted: true, + }; + metrics::CUSTOMER_REDACTED.add(&metrics::CONTEXT, 1, &[]); + Ok(services::ApplicationResponse::Json(response)) } - - match db - .find_payment_method_by_customer_id_merchant_id_list( - key_manager_state, - &key_store, - &req.customer_id, - merchant_account.get_id(), - None, - ) - .await - { - // check this in review - Ok(customer_payment_methods) => { - for pm in customer_payment_methods.into_iter() { - if pm.payment_method == Some(enums::PaymentMethod::Card) { - cards::delete_card_from_locker( - &state, - &req.customer_id, - merchant_account.get_id(), - pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), - ) - .await - .switch()?; - } - db.delete_payment_method_by_merchant_id_payment_method_id( - key_manager_state, - &key_store, - merchant_account.get_id(), - &pm.payment_method_id, - ) - .await - .switch()?; - } - } - Err(error) => { - if error.current_context().is_db_not_found() { - Ok(()) - } else { - Err(error) - .change_context(errors::CustomersErrorResponse::InternalServerError) - .attach_printable("failed find_payment_method_by_customer_id_merchant_id_list") - }? - } - }; - - let key = key_store.key.get_inner().peek(); - let identifier = Identifier::Merchant(key_store.merchant_id.clone()); - let redacted_encrypted_value: Encryptable> = types::crypto_operation( - key_manager_state, - type_name!(storage::Address), - types::CryptoOperation::Encrypt(REDACTED.to_string().into()), - identifier.clone(), - key, - ) - .await - .and_then(|val| val.try_into_operation()) - .switch()?; - - let redacted_encrypted_email = Encryptable::new( - redacted_encrypted_value - .clone() - .into_inner() - .switch_strategy(), - redacted_encrypted_value.clone().into_encrypted(), - ); - - let update_address = storage::AddressUpdate::Update { - city: Some(REDACTED.to_string()), - country: None, - line1: Some(redacted_encrypted_value.clone()), - line2: Some(redacted_encrypted_value.clone()), - line3: Some(redacted_encrypted_value.clone()), - state: Some(redacted_encrypted_value.clone()), - zip: Some(redacted_encrypted_value.clone()), - first_name: Some(redacted_encrypted_value.clone()), - last_name: Some(redacted_encrypted_value.clone()), - phone_number: Some(redacted_encrypted_value.clone()), - country_code: Some(REDACTED.to_string()), - updated_by: merchant_account.storage_scheme.to_string(), - email: Some(redacted_encrypted_email), - }; - - match db - .update_address_by_merchant_id_customer_id( - key_manager_state, - &req.customer_id, - merchant_account.get_id(), - update_address, - &key_store, - ) - .await - { - Ok(_) => Ok(()), - Err(error) => { - if error.current_context().is_db_not_found() { - Ok(()) - } else { - Err(error) - .change_context(errors::CustomersErrorResponse::InternalServerError) - .attach_printable("failed update_address_by_merchant_id_customer_id") - } - } - }?; - - let updated_customer = storage::CustomerUpdate::Update { - name: Some(redacted_encrypted_value.clone()), - email: Some( - types::crypto_operation( - key_manager_state, - type_name!(storage::Customer), - types::CryptoOperation::Encrypt(REDACTED.to_string().into()), - identifier, - key, - ) - .await - .and_then(|val| val.try_into_operation()) - .switch()?, - ), - phone: Box::new(Some(redacted_encrypted_value.clone())), - description: Some(Description::new(REDACTED.to_string())), - phone_country_code: Some(REDACTED.to_string()), - metadata: None, - connector_customer: None, - address_id: None, - }; - db.update_customer_by_customer_id_merchant_id( - key_manager_state, - req.customer_id.clone(), - merchant_account.get_id().to_owned(), - customer_orig, - updated_customer, - &key_store, - merchant_account.storage_scheme, - ) - .await - .switch()?; - - let response = customers::CustomerDeleteResponse { - customer_id: req.customer_id, - customer_deleted: true, - address_deleted: true, - payment_methods_deleted: true, - }; - metrics::CUSTOMER_REDACTED.add(&metrics::CONTEXT, 1, &[]); - Ok(services::ApplicationResponse::Json(response)) } #[instrument(skip(state))] @@ -1072,7 +1275,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest { merchant_account.get_id(), storage::CustomerUpdate::Update { name: encryptable_customer.name, - email: encryptable_customer.email, + email: Box::new(encryptable_customer.email), phone: Box::new(encryptable_customer.phone), phone_country_code: self.phone_country_code.clone(), metadata: self.metadata.clone(), @@ -1081,6 +1284,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest { default_billing_address: encrypted_customer_billing_address.map(Into::into), default_shipping_address: encrypted_customer_shipping_address.map(Into::into), default_payment_method_id: Some(self.default_payment_method_id.clone()), + status: None, }, key_store, merchant_account.storage_scheme, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4fd3fc0008..cee4e9d1dd 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2008,6 +2008,15 @@ pub async fn delete_card_from_locker( .await } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +pub async fn delete_card_by_locker_id( + state: &routes::SessionState, + id: &String, + merchant_id: &id_type::MerchantId, +) -> errors::RouterResult { + todo!() +} + #[instrument(skip_all)] pub async fn add_card_hs( state: &routes::SessionState, @@ -2399,6 +2408,18 @@ pub async fn delete_card_from_hs_locker<'a>( } } +// Need to fix this function while completing v2 +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[instrument(skip_all)] +pub async fn delete_card_from_hs_locker_by_global_id<'a>( + state: &routes::SessionState, + id: &String, + merchant_id: &id_type::MerchantId, + card_reference: &'a str, +) -> errors::RouterResult { + todo!() +} + ///Mock api for local testing pub async fn mock_call_to_locker_hs( db: &dyn db::StorageInterface, diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index da97fedf2c..f594feb595 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -85,6 +85,14 @@ pub struct CardReqBody { pub card_reference: String, } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive(Debug, Deserialize, Serialize)] +pub struct CardReqBodyV2 { + pub merchant_id: id_type::MerchantId, + pub merchant_customer_id: String, // Not changing this as it might lead to api contract failure + pub card_reference: String, +} + #[derive(Debug, Deserialize, Serialize)] pub struct RetrieveCardResp { pub status: String, @@ -523,6 +531,42 @@ pub async fn mk_delete_card_request_hs( Ok(request) } +// Need to fix this once we start moving to v2 completion +#[cfg(all(feature = "v2", feature = "customer_v2"))] +pub async fn mk_delete_card_request_hs_by_id( + jwekey: &settings::Jwekey, + locker: &settings::Locker, + id: &String, + merchant_id: &id_type::MerchantId, + card_reference: &str, +) -> CustomResult { + let merchant_customer_id = id.to_owned(); + let card_req_body = CardReqBodyV2 { + merchant_id: merchant_id.to_owned(), + merchant_customer_id, + card_reference: card_reference.to_owned(), + }; + let payload = card_req_body + .encode_to_vec() + .change_context(errors::VaultError::RequestEncodingFailed)?; + + let private_key = jwekey.vault_private_key.peek().as_bytes(); + + let jws = encryption::jws_sign_payload(&payload, &locker.locker_signing_key_id, private_key) + .await + .change_context(errors::VaultError::RequestEncodingFailed)?; + + let jwe_payload = + mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::HyperswitchCardVault).await?; + + let mut url = locker.host.to_owned(); + url.push_str("/cards/delete"); + let mut request = services::Request::new(services::Method::Post, &url); + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); + Ok(request) +} + pub fn mk_delete_card_response( response: DeleteCardResponse, ) -> errors::RouterResult { diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index 3d8483f14f..01bfc7ce0a 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -1359,7 +1359,7 @@ impl CustomerInterface for MockDb { let customer = customers .iter() .find(|customer| { - customer.get_customer_id() == *customer_id && &customer.merchant_id == merchant_id + customer.customer_id == *customer_id && &customer.merchant_id == merchant_id }) .cloned(); customer diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 577ccfebab..31ed4ad93e 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -847,6 +847,14 @@ impl MandateInterface for KafkaStore { .await } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_mandate_by_global_id( + &self, + id: &String, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.find_mandate_by_global_id(id).await + } + async fn find_mandate_by_merchant_id_customer_id( &self, merchant_id: &id_type::MerchantId, @@ -1726,6 +1734,19 @@ impl PaymentMethodInterface for KafkaStore { .await } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method_list_by_global_id( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + id: &String, + limit: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_payment_method_list_by_global_id(state, key_store, id, limit) + .await + } + async fn find_payment_method_by_customer_id_merchant_id_status( &self, state: &KeyManagerState, diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index c85bb4cb17..f59ac2a80b 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -28,6 +28,13 @@ pub trait MandateInterface { customer_id: &id_type::CustomerId, ) -> CustomResult, errors::StorageError>; + // Fix this function once we move to mandate v2 + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_mandate_by_global_id( + &self, + id: &String, + ) -> CustomResult, errors::StorageError>; + async fn update_mandate_by_merchant_id_mandate_id( &self, merchant_id: &id_type::MerchantId, @@ -186,6 +193,18 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn find_mandate_by_global_id( + &self, + id: &String, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage_types::Mandate::find_by_global_id(&conn, id) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + #[instrument(skip_all)] async fn update_mandate_by_merchant_id_mandate_id( &self, @@ -429,6 +448,19 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } + // Need to fix this once we start moving to mandate v2 + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn find_mandate_by_global_id( + &self, + customer_id: &String, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage_types::Mandate::find_by_global_id(&conn, customer_id) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + #[instrument(skip_all)] async fn update_mandate_by_merchant_id_mandate_id( &self, @@ -530,6 +562,15 @@ impl MandateInterface for MockDb { .collect()); } + // Need to fix this once we move to v2 mandate + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_mandate_by_global_id( + &self, + id: &String, + ) -> CustomResult, errors::StorageError> { + todo!() + } + async fn update_mandate_by_merchant_id_mandate_id( &self, merchant_id: &id_type::MerchantId, diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index 62c34e7103..695ed68b79 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -47,6 +47,16 @@ pub trait PaymentMethodInterface { limit: Option, ) -> CustomResult, errors::StorageError>; + // Need to fix this once we start moving to v2 for payment method + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method_list_by_global_id( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + id: &String, + limit: Option, + ) -> CustomResult, errors::StorageError>; + #[allow(clippy::too_many_arguments)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -689,6 +699,22 @@ mod storage { Ok(domain_payment_methods) } + // Need to fix this once we start moving to v2 for payment method + #[cfg(all( + feature = "v2", + feature = "customer_v2", + feature = "payment_methods_v2" + ))] + async fn find_payment_method_list_by_global_id( + &self, + _state: &KeyManagerState, + _key_store: &domain::MerchantKeyStore, + _id: &String, + _limit: Option, + ) -> CustomResult, errors::StorageError> { + todo!() + } + #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -1090,6 +1116,22 @@ mod storage { Ok(domain_payment_methods) } + // Need to fix this once we move to payment method for customer + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn find_payment_method_list_by_global_id( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + id: &String, + limit: Option, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage_types::PaymentMethod::find_by_global_id(&conn, id, limit) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -1355,6 +1397,18 @@ impl PaymentMethodInterface for MockDb { } } + // Need to fix this once we complete v2 payment method + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method_list_by_global_id( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + _id: &String, + _limit: Option, + ) -> CustomResult, errors::StorageError> { + todo!() + } + async fn find_payment_method_by_customer_id_merchant_id_status( &self, state: &KeyManagerState, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f57ca47612..1e97c7e3f6 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -907,7 +907,8 @@ impl Customers { .service( web::resource("/{id}") .route(web::post().to(customers_update)) - .route(web::post().to(customers_retrieve)), + .route(web::post().to(customers_retrieve)) + .route(web::delete().to(customers_delete)), ) } #[cfg(all(feature = "olap", feature = "v2", feature = "customer_v2"))] diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 4c5294d98e..2e3ce35a97 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -1,6 +1,4 @@ -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use actix_web::Responder; -use actix_web::{web, HttpRequest, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use common_utils::id_type; use router_env::{instrument, tracing, Flow}; @@ -214,6 +212,32 @@ pub async fn customers_update( .await } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::CustomersDelete))] +pub async fn customers_delete( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let flow = Flow::CustomersDelete; + let payload = web::Json(customers::GlobalId::new(path.into_inner())).into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req, _| delete_customer(state, auth.merchant_account, req, auth.key_store), + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::CustomersDelete))] pub async fn customers_delete( @@ -221,7 +245,7 @@ pub async fn customers_delete( req: HttpRequest, path: web::Path, ) -> impl Responder { - let flow = Flow::CustomersCreate; + let flow = Flow::CustomersDelete; let payload = web::Json(customers::CustomerId { customer_id: path.into_inner(), }) diff --git a/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/down.sql b/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/down.sql new file mode 100644 index 0000000000..376519a324 --- /dev/null +++ b/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TYPE "DeleteStatus"; \ No newline at end of file diff --git a/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/up.sql b/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/up.sql new file mode 100644 index 0000000000..29e7e8f57f --- /dev/null +++ b/v2_migrations/2024-07-29-155137_add_v2_soft_delete_db_enum/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +CREATE TYPE "DeleteStatus" AS ENUM ('active', 'redacted'); \ No newline at end of file diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql index 63d514e4b5..cf261b322b 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql @@ -2,7 +2,8 @@ -- These migrations can be run as long as there's no v2 application running. ALTER TABLE customers DROP COLUMN IF EXISTS merchant_reference_id, DROP COLUMN IF EXISTS default_billing_address, - DROP COLUMN IF EXISTS default_shipping_address; + DROP COLUMN IF EXISTS default_shipping_address, + DROP COLUMN IF EXISTS status; ALTER TABLE business_profile DROP COLUMN routing_algorithm_id, DROP COLUMN order_fulfillment_time, diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql index 1866e41299..194d347f4c 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql @@ -3,8 +3,8 @@ ALTER TABLE customers ADD COLUMN IF NOT EXISTS merchant_reference_id VARCHAR(64), ADD COLUMN IF NOT EXISTS default_billing_address BYTEA DEFAULT NULL, - ADD COLUMN IF NOT EXISTS default_shipping_address BYTEA DEFAULT NULL; - + ADD COLUMN IF NOT EXISTS default_shipping_address BYTEA DEFAULT NULL, + ADD COLUMN IF NOT EXISTS status "DeleteStatus" NOT NULL DEFAULT 'active'; CREATE TYPE "OrderFulfillmentTimeOrigin" AS ENUM ('create', 'confirm'); ALTER TABLE business_profile