From 66e86205856a7458ee8394d71c366ba38c4a1cae Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:57:56 +0530 Subject: [PATCH] fix(mandates): handle the connector_mandate creation once and only if the payment is charged (#6358) --- crates/common_enums/src/enums.rs | 13 + crates/diesel_models/src/payment_attempt.rs | 42 ++- crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/schema_v2.rs | 1 + crates/diesel_models/src/user/sample_data.rs | 4 +- .../src/payments/payment_attempt.rs | 14 +- crates/router/src/core/payments/helpers.rs | 1 + .../operations/payment_complete_authorize.rs | 4 +- .../payments/operations/payment_confirm.rs | 5 +- .../payments/operations/payment_create.rs | 5 +- .../payments/operations/payment_response.rs | 269 +++++++++++++----- .../payments/operations/payment_update.rs | 4 +- crates/router/src/core/payments/retry.rs | 2 + .../router/src/core/payments/tokenization.rs | 161 +++-------- .../router/src/core/payments/transformers.rs | 29 +- crates/router/src/core/webhooks/incoming.rs | 4 +- crates/router/src/db/address.rs | 12 +- crates/router/src/db/customers.rs | 20 +- crates/router/src/db/mandate.rs | 16 +- crates/router/src/db/payment_method.rs | 20 +- crates/router/src/db/refund.rs | 28 +- crates/router/src/db/reverse_lookup.rs | 8 +- .../src/types/storage/payment_attempt.rs | 3 + .../src/types/storage/payment_method.rs | 1 + crates/router/src/utils/user/sample_data.rs | 1 + crates/storage_impl/src/lookup.rs | 8 +- .../src/mock_db/payment_attempt.rs | 1 + .../src/payments/payment_attempt.rs | 59 ++-- .../src/payments/payment_intent.rs | 12 +- .../src/payouts/payout_attempt.rs | 16 +- crates/storage_impl/src/payouts/payouts.rs | 16 +- crates/storage_impl/src/redis/kv_store.rs | 8 +- .../down.sql | 3 + .../up.sql | 5 + 34 files changed, 484 insertions(+), 312 deletions(-) create mode 100644 migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/down.sql create mode 100644 migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/up.sql diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 528f38ac61ae..d667dfb90872 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3297,3 +3297,16 @@ pub enum SurchargeCalculationOverride { /// Calculate surcharge Calculate, } + +/// Connector Mandate Status +#[derive( + Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, strum::Display, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorMandateStatus { + /// Indicates that the connector mandate is active and can be used for payments. + Active, + /// Indicates that the connector mandate is not active and hence cannot be used for payments. + Inactive, +} diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 75a9aeda2b86..b1d6fceefbf2 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -9,6 +9,16 @@ use crate::schema::payment_attempt; #[cfg(feature = "v2")] use crate::schema_v2::payment_attempt; +common_utils::impl_to_sql_from_sql_json!(ConnectorMandateReferenceId); +#[derive( + Clone, Debug, serde::Deserialize, serde::Serialize, Eq, PartialEq, diesel::AsExpression, +)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct ConnectorMandateReferenceId { + pub connector_mandate_id: Option, + pub payment_method_id: Option, + pub mandate_metadata: Option, +} #[cfg(feature = "v2")] #[derive( Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Selectable, @@ -72,6 +82,7 @@ pub struct PaymentAttempt { pub id: String, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -148,6 +159,7 @@ pub struct PaymentAttempt { pub card_network: Option, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -224,6 +236,7 @@ pub struct PaymentAttemptNew { pub card_network: Option, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -296,6 +309,7 @@ pub struct PaymentAttemptNew { pub card_network: Option, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -451,6 +465,7 @@ pub enum PaymentAttemptUpdate { unified_message: Option>, payment_method_data: Option, charge_id: Option, + connector_mandate_detail: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -761,6 +776,7 @@ pub struct PaymentAttemptUpdateInternal { client_version: Option, customer_acceptance: Option, card_network: Option, + connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -816,6 +832,7 @@ pub struct PaymentAttemptUpdateInternal { pub card_network: Option, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v2")] @@ -1004,6 +1021,7 @@ impl PaymentAttemptUpdate { card_network, shipping_cost, order_tax_amount, + connector_mandate_detail, } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); PaymentAttempt { amount: amount.unwrap_or(source.amount), @@ -1059,6 +1077,7 @@ impl PaymentAttemptUpdate { card_network: card_network.or(source.card_network), shipping_cost: shipping_cost.or(source.shipping_cost), order_tax_amount: order_tax_amount.or(source.order_tax_amount), + connector_mandate_detail: connector_mandate_detail.or(source.connector_mandate_detail), ..source } } @@ -2053,6 +2072,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::AuthenticationTypeUpdate { authentication_type, @@ -2107,6 +2127,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::ConfirmUpdate { amount, @@ -2191,6 +2212,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost, order_tax_amount, + connector_mandate_detail: None, }, PaymentAttemptUpdate::VoidUpdate { status, @@ -2246,6 +2268,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::RejectUpdate { status, @@ -2302,6 +2325,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::BlocklistUpdate { status, @@ -2358,6 +2382,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::PaymentMethodDetailsUpdate { payment_method_id, @@ -2412,6 +2437,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::ResponseUpdate { status, @@ -2434,6 +2460,7 @@ impl From for PaymentAttemptUpdateInternal { unified_message, payment_method_data, charge_id, + connector_mandate_detail, } => Self { status: Some(status), connector: connector.map(Some), @@ -2484,6 +2511,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail, }, PaymentAttemptUpdate::ErrorUpdate { connector, @@ -2548,6 +2576,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { status: Some(status), @@ -2599,6 +2628,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::UpdateTrackers { payment_token, @@ -2659,6 +2689,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -2720,6 +2751,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::PreprocessingUpdate { status, @@ -2779,6 +2811,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, @@ -2834,6 +2867,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::AmountToCaptureUpdate { status, @@ -2889,6 +2923,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::ConnectorResponse { authentication_data, @@ -2947,6 +2982,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { amount, @@ -3001,6 +3037,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::AuthenticationUpdate { status, @@ -3058,6 +3095,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, PaymentAttemptUpdate::ManualUpdate { status, @@ -3118,6 +3156,7 @@ impl From for PaymentAttemptUpdateInternal { card_network: None, shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }, } } @@ -3203,8 +3242,7 @@ mod tests { "user_agent": "amet irure esse" } }, - "mandate_type": { - "single_use": { + "mandate_type": {"single_use": { "amount": 6540, "currency": "USD" } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index a9e0b54260c1..f97b798a6ef5 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -846,6 +846,7 @@ diesel::table! { card_network -> Nullable, shipping_cost -> Nullable, order_tax_amount -> Nullable, + connector_mandate_detail -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 53f568f18c46..96e536e138d3 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -817,6 +817,7 @@ diesel::table! { id -> Varchar, shipping_cost -> Nullable, order_tax_amount -> Nullable, + connector_mandate_detail -> Nullable, } } diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index a354e4a02abe..8ce6077af510 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -12,7 +12,7 @@ use crate::schema::payment_attempt; use crate::schema_v2::payment_attempt; use crate::{ enums::{MandateDataType, MandateDetails}, - PaymentAttemptNew, + ConnectorMandateReferenceId, PaymentAttemptNew, }; #[cfg(feature = "v2")] @@ -203,6 +203,7 @@ pub struct PaymentAttemptBatchNew { pub organization_id: common_utils::id_type::OrganizationId, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v1")] @@ -281,6 +282,7 @@ impl PaymentAttemptBatchNew { organization_id: self.organization_id, shipping_cost: self.shipping_cost, order_tax_amount: self.order_tax_amount, + connector_mandate_detail: self.connector_mandate_detail, } } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index c71e08c79e8e..727c59a03883 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -10,7 +10,8 @@ use common_utils::{ }, }; use diesel_models::{ - PaymentAttempt as DieselPaymentAttempt, PaymentAttemptNew as DieselPaymentAttemptNew, + ConnectorMandateReferenceId, PaymentAttempt as DieselPaymentAttempt, + PaymentAttemptNew as DieselPaymentAttemptNew, }; use error_stack::ResultExt; use masking::Secret; @@ -222,6 +223,7 @@ pub struct PaymentAttempt { pub shipping_cost: Option, pub order_tax_amount: Option, pub id: String, + pub connector_mandate_detail: Option, } impl PaymentAttempt { @@ -336,6 +338,7 @@ pub struct PaymentAttempt { pub organization_id: id_type::OrganizationId, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v2")] @@ -492,6 +495,7 @@ pub struct PaymentAttemptNew { pub organization_id: id_type::OrganizationId, pub shipping_cost: Option, pub order_tax_amount: Option, + pub connector_mandate_detail: Option, } #[cfg(feature = "v2")] @@ -636,6 +640,7 @@ pub enum PaymentAttemptUpdate { unified_message: Option>, payment_method_data: Option, charge_id: Option, + connector_mandate_detail: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -815,6 +820,7 @@ impl behaviour::Conversion for PaymentAttempt { card_network, order_tax_amount: self.order_tax_amount, shipping_cost: self.shipping_cost, + connector_mandate_detail: self.connector_mandate_detail, }) } @@ -892,6 +898,7 @@ impl behaviour::Conversion for PaymentAttempt { organization_id: storage_model.organization_id, order_tax_amount: storage_model.order_tax_amount, shipping_cost: storage_model.shipping_cost, + connector_mandate_detail: storage_model.connector_mandate_detail, }) } .await @@ -973,6 +980,7 @@ impl behaviour::Conversion for PaymentAttempt { card_network, order_tax_amount: self.order_tax_amount, shipping_cost: self.shipping_cost, + connector_mandate_detail: self.connector_mandate_detail, }) } } @@ -1048,6 +1056,7 @@ impl behaviour::Conversion for PaymentAttempt { shipping_cost, order_tax_amount, connector, + connector_mandate_detail, } = self; Ok(DieselPaymentAttempt { @@ -1105,6 +1114,7 @@ impl behaviour::Conversion for PaymentAttempt { authentication_applied, external_reference_id, connector, + connector_mandate_detail, }) } @@ -1173,6 +1183,7 @@ impl behaviour::Conversion for PaymentAttempt { authentication_applied: storage_model.authentication_applied, external_reference_id: storage_model.external_reference_id, connector: storage_model.connector, + connector_mandate_detail: storage_model.connector_mandate_detail, }) } .await @@ -1240,6 +1251,7 @@ impl behaviour::Conversion for PaymentAttempt { order_tax_amount: self.order_tax_amount, shipping_cost: self.shipping_cost, amount_to_capture: self.amount_to_capture, + connector_mandate_detail: self.connector_mandate_detail, }) } } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 3782026835b7..bf2092989dff 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3963,6 +3963,7 @@ impl AttemptType { profile_id: old_payment_attempt.profile_id, shipping_cost: old_payment_attempt.shipping_cost, order_tax_amount: None, + connector_mandate_detail: None, } } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 99d8df2dfe98..92e462c78bcb 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -125,7 +125,7 @@ impl GetTracker, api::PaymentsRequest> for Co recurring_mandate_payment_data, mandate_connector, payment_method_info, - } = helpers::get_token_pm_type_mandate_details( + } = Box::pin(helpers::get_token_pm_type_mandate_details( state, request, mandate_type.to_owned(), @@ -133,7 +133,7 @@ impl GetTracker, api::PaymentsRequest> for Co key_store, payment_attempt.payment_method_id.clone(), payment_intent.customer_id.as_ref(), - ) + )) .await?; let customer_acceptance: Option = request .customer_acceptance diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 658a02f7d080..92bf397c87b1 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -540,7 +540,7 @@ impl GetTracker, api::PaymentsRequest> for Pa let mandate_details_fut = tokio::spawn( async move { - helpers::get_token_pm_type_mandate_details( + Box::pin(helpers::get_token_pm_type_mandate_details( &m_state, &m_request, m_mandate_type, @@ -548,7 +548,7 @@ impl GetTracker, api::PaymentsRequest> for Pa &m_key_store, None, payment_intent_customer_id.as_ref(), - ) + )) .await } .in_current_span(), @@ -612,7 +612,6 @@ impl GetTracker, api::PaymentsRequest> for Pa } else { (None, payment_method_info) }; - // The operation merges mandate data from both request and payment_attempt let setup_mandate = mandate_data.map(|mut sm| { sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 0921e38317a6..ff7b545be3a9 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -168,7 +168,7 @@ impl GetTracker, api::PaymentsRequest> for Pa recurring_mandate_payment_data, mandate_connector, payment_method_info, - } = helpers::get_token_pm_type_mandate_details( + } = Box::pin(helpers::get_token_pm_type_mandate_details( state, request, mandate_type, @@ -176,7 +176,7 @@ impl GetTracker, api::PaymentsRequest> for Pa merchant_key_store, None, None, - ) + )) .await?; let customer_details = helpers::get_customer_details_from_request(request); @@ -1212,6 +1212,7 @@ impl PaymentCreate { profile_id, shipping_cost: request.shipping_cost, order_tax_amount: None, + connector_mandate_detail: None, }, additional_pm_data, )) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 6a5299db8c41..92c1a2c0c380 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; +use api_models::payments::{ConnectorMandateReferenceId, MandateReferenceId}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use api_models::routing::RoutableConnectorChoice; use async_trait::async_trait; use common_enums::AuthorizationStatus; use common_utils::{ - ext_traits::{AsyncExt, Encode}, + ext_traits::{AsyncExt, Encode, ValueExt}, types::{keymanager::KeyManagerState, MinorUnit}, }; use error_stack::{report, ResultExt}; @@ -183,14 +184,11 @@ impl PostUpdateTracker, types::PaymentsAuthor let save_payment_call_future = Box::pin(tokenization::save_payment_method( state, connector_name.clone(), - merchant_connector_id.clone(), save_payment_data, customer_id.clone(), merchant_account, resp.request.payment_method_type, key_store, - Some(resp.request.amount), - Some(resp.request.currency), billing_name.clone(), payment_method_billing_address, business_profile, @@ -207,7 +205,7 @@ impl PostUpdateTracker, types::PaymentsAuthor resp.request.setup_future_usage, Some(enums::FutureUsage::OffSession) ); - + let storage_scheme = merchant_account.storage_scheme; if is_legacy_mandate { // Mandate is created on the application side and at the connector. let tokenization::SavePaymentMethodDataResponse { @@ -232,15 +230,46 @@ impl PostUpdateTracker, types::PaymentsAuthor // The mandate is created on connector's end. let tokenization::SavePaymentMethodDataResponse { payment_method_id, - mandate_reference_id, + connector_mandate_reference_id, .. } = save_payment_call_future.await?; - + payment_data.payment_method_info = if let Some(payment_method_id) = &payment_method_id { + match state + .store + .find_payment_method( + &(state.into()), + key_store, + payment_method_id, + storage_scheme, + ) + .await + { + Ok(payment_method) => Some(payment_method), + Err(error) => { + if error.current_context().is_db_not_found() { + logger::info!("Payment Method not found in db {:?}", error); + None + } else { + Err(error) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error retrieving payment method from db") + .map_err(|err| logger::error!(payment_method_retrieve=?err)) + .ok() + } + } + } + } else { + None + }; payment_data.payment_attempt.payment_method_id = payment_method_id; - + payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id + .clone() + .map(ForeignFrom::foreign_from); payment_data.set_mandate_id(api_models::payments::MandateIds { mandate_id: None, - mandate_reference_id, + mandate_reference_id: connector_mandate_reference_id.map(|connector_mandate_id| { + MandateReferenceId::ConnectorMandateId(connector_mandate_id) + }), }); Ok(()) } else if should_avoid_saving { @@ -255,16 +284,10 @@ impl PostUpdateTracker, types::PaymentsAuthor let key_store = key_store.clone(); let state = state.clone(); let customer_id = payment_data.payment_intent.customer_id.clone(); - - let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); let payment_attempt = payment_data.payment_attempt.clone(); let business_profile = business_profile.clone(); - - let amount = resp.request.amount; - let currency = resp.request.currency; let payment_method_type = resp.request.payment_method_type; - let storage_scheme = merchant_account.clone().storage_scheme; let payment_method_billing_address = payment_method_billing_address.cloned(); logger::info!("Call to save_payment_method in locker"); @@ -275,14 +298,11 @@ impl PostUpdateTracker, types::PaymentsAuthor let result = Box::pin(tokenization::save_payment_method( &state, connector_name, - merchant_connector_id, save_payment_data, customer_id, &merchant_account, payment_method_type, &key_store, - Some(amount), - Some(currency), billing_name, payment_method_billing_address.as_ref(), &business_profile, @@ -546,6 +566,33 @@ impl PostUpdateTracker, types::PaymentsSyncData> for where F: 'b + Clone + Send + Sync, { + let (connector_mandate_id, mandate_metadata) = resp + .response + .clone() + .ok() + .and_then(|resp| { + if let types::PaymentsResponseData::TransactionResponse { + mandate_reference, .. + } = resp + { + mandate_reference.map(|mandate_ref| { + ( + mandate_ref.connector_mandate_id.clone(), + mandate_ref.mandate_metadata.clone(), + ) + }) + } else { + None + } + }) + .unwrap_or((None, None)); + + update_connector_mandate_details_for_the_flow( + connector_mandate_id, + mandate_metadata, + payment_data, + )?; + update_payment_method_status_and_ntid( state, key_store, @@ -922,19 +969,16 @@ impl PostUpdateTracker, types::SetupMandateRequestDa let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); let tokenization::SavePaymentMethodDataResponse { payment_method_id, - mandate_reference_id, + connector_mandate_reference_id, .. } = Box::pin(tokenization::save_payment_method( state, connector_name, - merchant_connector_id.clone(), save_payment_data, customer_id.clone(), merchant_account, resp.request.payment_method_type, key_store, - resp.request.amount, - Some(resp.request.currency), billing_name, None, business_profile, @@ -953,9 +997,14 @@ impl PostUpdateTracker, types::SetupMandateRequestDa .await?; payment_data.payment_attempt.payment_method_id = payment_method_id; payment_data.payment_attempt.mandate_id = mandate_id; + payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id + .clone() + .map(ForeignFrom::foreign_from); payment_data.set_mandate_id(api_models::payments::MandateIds { mandate_id: None, - mandate_reference_id, + mandate_reference_id: connector_mandate_reference_id.map(|connector_mandate_id| { + MandateReferenceId::ConnectorMandateId(connector_mandate_id) + }), }); Ok(()) } @@ -1011,6 +1060,32 @@ impl PostUpdateTracker, types::CompleteAuthorizeData where F: 'b + Clone + Send + Sync, { + let (connector_mandate_id, mandate_metadata) = resp + .response + .clone() + .ok() + .and_then(|resp| { + if let types::PaymentsResponseData::TransactionResponse { + mandate_reference, .. + } = resp + { + mandate_reference.map(|mandate_ref| { + ( + mandate_ref.connector_mandate_id.clone(), + mandate_ref.mandate_metadata.clone(), + ) + }) + } else { + None + } + }) + .unwrap_or((None, None)); + update_connector_mandate_details_for_the_flow( + connector_mandate_id, + mandate_metadata, + payment_data, + )?; + update_payment_method_status_and_ntid( state, key_store, @@ -1367,6 +1442,74 @@ async fn payment_response_update_tracker( .payment_intent .fingerprint_id .clone_from(&payment_data.payment_attempt.fingerprint_id); + + if let Some(payment_method) = + payment_data.payment_method_info.clone() + { + // Parse value to check for mandates' existence + let mandate_details = payment_method + .connector_mandate_details + .clone() + .map(|val| { + val.parse_value::( + "PaymentsMandateReference", + ) + }) + .transpose() + .change_context( + errors::ApiErrorResponse::InternalServerError, + ) + .attach_printable( + "Failed to deserialize to Payment Mandate Reference ", + )?; + + if let Some(mca_id) = + payment_data.payment_attempt.merchant_connector_id.clone() + { + // check if the mandate has not already been set to active + if !mandate_details + .as_ref() + .map(|payment_mandate_reference| { + + payment_mandate_reference.0.get(&mca_id) + .map(|payment_mandate_reference_record| payment_mandate_reference_record.connector_mandate_status == Some(common_enums::ConnectorMandateStatus::Active)) + .unwrap_or(false) + }) + .unwrap_or(false) + { + let (connector_mandate_id, mandate_metadata) = payment_data.payment_attempt.connector_mandate_detail.clone() + .map(|cmr| (cmr.connector_mandate_id, cmr.mandate_metadata)) + .unwrap_or((None, None)); + + // Update the connector mandate details with the payment attempt connector mandate id + let connector_mandate_details = + tokenization::update_connector_mandate_details( + mandate_details, + payment_data.payment_attempt.payment_method_type, + Some( + payment_data.payment_attempt.amount.get_amount_as_i64() + ), + payment_data.payment_attempt.currency, + payment_data.payment_attempt.merchant_connector_id.clone(), + connector_mandate_id, + mandate_metadata, + )?; + // Update the payment method table with the active mandate record + payment_methods::cards::update_payment_method_connector_mandate_details( + state, + key_store, + &*state.store, + payment_method, + connector_mandate_details, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + } + } + } + metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); } @@ -1430,6 +1573,10 @@ async fn payment_response_update_tracker( encoded_data, payment_method_data: additional_payment_method_data, charge_id, + connector_mandate_detail: payment_data + .payment_attempt + .connector_mandate_detail + .clone(), }), ), }; @@ -1629,53 +1776,6 @@ async fn payment_response_update_tracker( .in_current_span(), ); - // When connector requires redirection for mandate creation it can update the connector mandate_id in payment_methods during Psync and CompleteAuthorize - - let flow_name = core_utils::get_flow_name::()?; - if flow_name == "PSync" || flow_name == "CompleteAuthorize" { - let (connector_mandate_id, mandate_metadata) = match router_data.response.clone() { - Ok(resp) => match resp { - types::PaymentsResponseData::TransactionResponse { - ref mandate_reference, - .. - } => { - if let Some(mandate_ref) = mandate_reference { - ( - mandate_ref.connector_mandate_id.clone(), - mandate_ref.mandate_metadata.clone(), - ) - } else { - (None, None) - } - } - _ => (None, None), - }, - Err(_) => (None, None), - }; - if let Some(payment_method) = payment_data.payment_method_info.clone() { - let connector_mandate_details = - tokenization::update_connector_mandate_details_in_payment_method( - payment_method.clone(), - payment_method.payment_method_type, - Some(payment_data.payment_attempt.amount.get_amount_as_i64()), - payment_data.payment_attempt.currency, - payment_data.payment_attempt.merchant_connector_id.clone(), - connector_mandate_id, - mandate_metadata, - )?; - payment_methods::cards::update_payment_method_connector_mandate_details( - state, - key_store, - &*state.store, - payment_method.clone(), - connector_mandate_details, - storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update payment method in db")? - } - } // When connector requires redirection for mandate creation it can update the connector mandate_id during Psync and CompleteAuthorize let m_db = state.clone().store; let m_router_data_merchant_id = router_data.merchant_id.clone(); @@ -1888,6 +1988,35 @@ async fn update_payment_method_status_and_ntid( Ok(()) } +fn update_connector_mandate_details_for_the_flow( + connector_mandate_id: Option, + mandate_metadata: Option, + payment_data: &mut PaymentData, +) -> RouterResult<()> { + let connector_mandate_reference_id = if connector_mandate_id.is_some() { + Some(ConnectorMandateReferenceId { + connector_mandate_id: connector_mandate_id.clone(), + payment_method_id: None, + update_history: None, + mandate_metadata: mandate_metadata.clone(), + }) + } else { + None + }; + + payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id + .clone() + .map(ForeignFrom::foreign_from); + + payment_data.set_mandate_id(api_models::payments::MandateIds { + mandate_id: None, + mandate_reference_id: connector_mandate_reference_id.map(|connector_mandate_id| { + MandateReferenceId::ConnectorMandateId(connector_mandate_id) + }), + }); + Ok(()) +} + fn response_to_capture_update( multiple_capture_data: &MultipleCaptureData, response_list: HashMap, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index c69595d1893e..a89ae4f1e817 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -150,7 +150,7 @@ impl GetTracker, api::PaymentsRequest> for Pa recurring_mandate_payment_data, mandate_connector, payment_method_info, - } = helpers::get_token_pm_type_mandate_details( + } = Box::pin(helpers::get_token_pm_type_mandate_details( state, request, mandate_type.to_owned(), @@ -158,7 +158,7 @@ impl GetTracker, api::PaymentsRequest> for Pa key_store, None, payment_intent.customer_id.as_ref(), - ) + )) .await?; helpers::validate_amount_to_capture_and_capture_method(Some(&payment_attempt), request)?; diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index c3c4a6d01ab4..9750094392e0 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -456,6 +456,7 @@ where unified_message: None, payment_method_data: additional_payment_method_data, charge_id, + connector_mandate_detail: None, }; #[cfg(feature = "v1")] @@ -647,6 +648,7 @@ pub fn make_new_payment_attempt( charge_id: Default::default(), customer_acceptance: Default::default(), order_tax_amount: Default::default(), + connector_mandate_detail: Default::default(), } } diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 6815d2dfc4fe..dd252fb68e5d 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -5,8 +5,8 @@ use std::collections::HashMap; not(feature = "payment_methods_v2") ))] use api_models::payment_methods::PaymentMethodsData; -use api_models::payments::{ConnectorMandateReferenceId, MandateReferenceId}; -use common_enums::PaymentMethod; +use api_models::payments::ConnectorMandateReferenceId; +use common_enums::{ConnectorMandateStatus, PaymentMethod}; use common_utils::{ crypto::Encryptable, ext_traits::{AsyncExt, Encode, ValueExt}, @@ -61,7 +61,7 @@ impl From<&types::RouterData pub struct SavePaymentMethodDataResponse { pub payment_method_id: Option, pub payment_method_status: Option, - pub mandate_reference_id: Option, + pub connector_mandate_reference_id: Option, } #[cfg(all( any(feature = "v1", feature = "v2"), @@ -72,14 +72,11 @@ pub struct SavePaymentMethodDataResponse { pub async fn save_payment_method( state: &SessionState, connector_name: String, - merchant_connector_id: Option, save_payment_method_data: SavePaymentMethodData, customer_id: Option, merchant_account: &domain::MerchantAccount, payment_method_type: Option, key_store: &domain::MerchantKeyStore, - amount: Option, - currency: Option, billing_name: Option>, payment_method_billing_address: Option<&api::Address>, business_profile: &domain::Profile, @@ -169,32 +166,6 @@ where } _ => (None, None), }; - let check_for_mit_mandates = save_payment_method_data - .request - .get_setup_mandate_details() - .is_none() - && save_payment_method_data - .request - .get_setup_future_usage() - .map(|future_usage| future_usage == storage_enums::FutureUsage::OffSession) - .unwrap_or(false); - // insert in PaymentMethods if its a off-session mit payment - let connector_mandate_details = if check_for_mit_mandates { - add_connector_mandate_details_in_payment_method( - payment_method_type, - amount, - currency, - merchant_connector_id.clone(), - connector_mandate_id.clone(), - mandate_metadata.clone(), - ) - } else { - None - } - .map(|connector_mandate_data| connector_mandate_data.encode_to_value()) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize customer acceptance to value")?; let pm_id = if customer_acceptance.is_some() { let payment_method_create_request = @@ -373,24 +344,6 @@ where .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; - if check_for_mit_mandates { - let connector_mandate_details = - update_connector_mandate_details_in_payment_method( - pm.clone(), - payment_method_type, - amount, - currency, - merchant_connector_id.clone(), - connector_mandate_id.clone(), - mandate_metadata.clone(), - )?; - - payment_methods::cards::update_payment_method_connector_mandate_details(state, - key_store,db, pm, connector_mandate_details, merchant_account.storage_scheme).await.change_context( - errors::ApiErrorResponse::InternalServerError, - ) - .attach_printable("Failed to update payment method in db")?; - } } Err(err) => { if err.current_context().is_db_not_found() { @@ -407,7 +360,7 @@ where customer_acceptance, pm_data_encrypted.map(Into::into), key_store, - connector_mandate_details, + None, pm_status, network_transaction_id, merchant_account.storage_scheme, @@ -478,28 +431,7 @@ where resp.payment_method_id = payment_method_id; let existing_pm = match payment_method { - Ok(pm) => { - // update if its a off-session mit payment - if check_for_mit_mandates { - let connector_mandate_details = - update_connector_mandate_details_in_payment_method( - pm.clone(), - payment_method_type, - amount, - currency, - merchant_connector_id.clone(), - connector_mandate_id.clone(), - mandate_metadata.clone(), - )?; - - payment_methods::cards::update_payment_method_connector_mandate_details( state, - key_store,db, pm.clone(), connector_mandate_details, merchant_account.storage_scheme).await.change_context( - errors::ApiErrorResponse::InternalServerError, - ) - .attach_printable("Failed to update payment method in db")?; - } - Ok(pm) - } + Ok(pm) => Ok(pm), Err(err) => { if err.current_context().is_db_not_found() { payment_methods::cards::create_payment_method( @@ -513,7 +445,7 @@ where customer_acceptance, pm_data_encrypted.map(Into::into), key_store, - connector_mandate_details, + None, pm_status, network_transaction_id, merchant_account.storage_scheme, @@ -720,7 +652,7 @@ where customer_acceptance, pm_data_encrypted.map(Into::into), key_store, - connector_mandate_details, + None, pm_status, network_transaction_id, merchant_account.storage_scheme, @@ -742,35 +674,28 @@ where } else { None }; - let cmid_config = db - .find_config_by_key_unwrap_or( - format!("{}_should_show_connector_mandate_id_in_payments_response", merchant_account.get_id().get_string_repr().to_owned()).as_str(), - Some("false".to_string()), - ) - .await.map_err(|err| services::logger::error!(message="Failed to fetch the config", connector_mandate_details_population=?err)).ok(); - - let mandate_reference_id = match cmid_config { - Some(config) if config.config == "true" => Some( - MandateReferenceId::ConnectorMandateId(ConnectorMandateReferenceId { - connector_mandate_id: connector_mandate_id.clone(), - payment_method_id: pm_id.clone(), - update_history: None, - mandate_metadata: mandate_metadata.clone(), - }), - ), - _ => None, + // check if there needs to be a config if yes then remove it to a different place + let connector_mandate_reference_id = if connector_mandate_id.is_some() { + Some(ConnectorMandateReferenceId { + connector_mandate_id: connector_mandate_id.clone(), + payment_method_id: None, + update_history: None, + mandate_metadata: mandate_metadata.clone(), + }) + } else { + None }; Ok(SavePaymentMethodDataResponse { payment_method_id: pm_id, payment_method_status: pm_status, - mandate_reference_id, + connector_mandate_reference_id, }) } Err(_) => Ok(SavePaymentMethodDataResponse { payment_method_id: None, payment_method_status: None, - mandate_reference_id: None, + connector_mandate_reference_id: None, }), } } @@ -782,14 +707,11 @@ where pub async fn save_payment_method( _state: &SessionState, _connector_name: String, - _merchant_connector_id: Option, _save_payment_method_data: SavePaymentMethodData, _customer_id: Option, _merchant_account: &domain::MerchantAccount, _payment_method_type: Option, _key_store: &domain::MerchantKeyStore, - _amount: Option, - _currency: Option, _billing_name: Option>, _payment_method_billing_address: Option<&api::Address>, _business_profile: &domain::Profile, @@ -1213,6 +1135,7 @@ pub fn add_connector_mandate_details_in_payment_method( original_payment_authorized_amount: authorized_amount, original_payment_authorized_currency: authorized_currency, mandate_metadata, + connector_mandate_status: Some(ConnectorMandateStatus::Active), }, ); Some(storage::PaymentsMandateReference(mandate_details)) @@ -1221,8 +1144,8 @@ pub fn add_connector_mandate_details_in_payment_method( } } -pub fn update_connector_mandate_details_in_payment_method( - payment_method: domain::PaymentMethod, +pub fn update_connector_mandate_details( + mandate_details: Option, payment_method_type: Option, authorized_amount: Option, authorized_currency: Option, @@ -1230,17 +1153,8 @@ pub fn update_connector_mandate_details_in_payment_method( connector_mandate_id: Option, mandate_metadata: Option, ) -> RouterResult> { - let mandate_reference = match payment_method.connector_mandate_details { - Some(_) => { - let mandate_details = payment_method - .connector_mandate_details - .map(|val| { - val.parse_value::("PaymentsMandateReference") - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; - + let mandate_reference = match mandate_details { + Some(mut payment_mandate_reference) => { if let Some((mca_id, connector_mandate_id)) = merchant_connector_id.clone().zip(connector_mandate_id) { @@ -1250,20 +1164,21 @@ pub fn update_connector_mandate_details_in_payment_method( original_payment_authorized_amount: authorized_amount, original_payment_authorized_currency: authorized_currency, mandate_metadata: mandate_metadata.clone(), + connector_mandate_status: Some(ConnectorMandateStatus::Active), }; - mandate_details.map(|mut payment_mandate_reference| { - payment_mandate_reference - .entry(mca_id) - .and_modify(|pm| *pm = updated_record) - .or_insert(storage::PaymentsMandateReferenceRecord { - connector_mandate_id, - payment_method_type, - original_payment_authorized_amount: authorized_amount, - original_payment_authorized_currency: authorized_currency, - mandate_metadata: mandate_metadata.clone(), - }); - payment_mandate_reference - }) + + payment_mandate_reference + .entry(mca_id) + .and_modify(|pm| *pm = updated_record) + .or_insert(storage::PaymentsMandateReferenceRecord { + connector_mandate_id, + payment_method_type, + original_payment_authorized_amount: authorized_amount, + original_payment_authorized_currency: authorized_currency, + mandate_metadata: mandate_metadata.clone(), + connector_mandate_status: Some(ConnectorMandateStatus::Active), + }); + Some(payment_mandate_reference) } else { None } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f96ead676a73..5341016d2292 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,8 +1,8 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; use api_models::payments::{ - Address, CustomerDetails, CustomerDetailsResponse, FrmMessage, PaymentChargeRequest, - PaymentChargeResponse, RequestSurchargeDetails, + Address, ConnectorMandateReferenceId, CustomerDetails, CustomerDetailsResponse, FrmMessage, + PaymentChargeRequest, PaymentChargeResponse, RequestSurchargeDetails, }; use common_enums::{Currency, RequestIncrementalAuthorization}; use common_utils::{ @@ -11,7 +11,10 @@ use common_utils::{ pii::Email, types::{AmountConvertor, MinorUnit, StringMajorUnitForConnector}, }; -use diesel_models::ephemeral_key; +use diesel_models::{ + ephemeral_key, + payment_attempt::ConnectorMandateReferenceId as DieselConnectorMandateReferenceId, +}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; use masking::{ExposeInterface, Maskable, PeekInterface, Secret}; @@ -2431,3 +2434,23 @@ impl ForeignFrom for router_request_types::CustomerDetails { } } } + +impl ForeignFrom for ConnectorMandateReferenceId { + fn foreign_from(value: DieselConnectorMandateReferenceId) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_id: value.payment_method_id, + update_history: None, + mandate_metadata: value.mandate_metadata, + } + } +} +impl ForeignFrom for DieselConnectorMandateReferenceId { + fn foreign_from(value: ConnectorMandateReferenceId) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_id: value.payment_method_id, + mandate_metadata: value.mandate_metadata, + } + } +} diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index af531984a19e..fe07cb95f0cb 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -259,13 +259,13 @@ async fn incoming_webhooks_core( let merchant_connector_account = match merchant_connector_account { Some(merchant_connector_account) => merchant_connector_account, None => { - helper_utils::get_mca_from_object_reference_id( + Box::pin(helper_utils::get_mca_from_object_reference_id( &state, object_ref_id.clone(), &merchant_account, &connector_name, &key_store, - ) + )) .await? } }; diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 335110248ab3..3a8750feff93 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -391,11 +391,11 @@ mod storage { let field = format!("add_{}", address_id); Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() }, @@ -502,14 +502,14 @@ mod storage { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::Hset::( (&field, redis_value), redis_entry, ), key, - ) + )) .await .change_context(errors::StorageError::KVError)? .try_into_hset() @@ -601,7 +601,7 @@ mod storage { }, }; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::HSetNx::( &field, @@ -609,7 +609,7 @@ mod storage { redis_entry, ), key, - ) + )) .await .change_context(errors::StorageError::KVError)? .try_into_hsetnx() diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index 9cd044f69eb1..5020c238e4a6 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -219,11 +219,11 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( // check for ValueNotFound async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() .map(Some) @@ -293,11 +293,11 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( // check for ValueNotFound async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() .map(Some) @@ -453,14 +453,14 @@ mod storage { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::Hset::( (&field, redis_value), redis_entry, ), key, - ) + )) .await .change_context(errors::StorageError::KVError)? .try_into_hset() @@ -584,11 +584,11 @@ mod storage { let field = format!("cust_{}", customer_id.get_string_repr()); Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() }, @@ -773,7 +773,7 @@ mod storage { }; let storage_customer = new_customer.into(); - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::HSetNx::( &field, @@ -781,7 +781,7 @@ mod storage { redis_entry, ), key, - ) + )) .await .change_context(errors::StorageError::KVError)? .try_into_hsetnx() diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index 40e5f88614d5..95733805b436 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -114,11 +114,11 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() }, @@ -172,11 +172,11 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -281,14 +281,14 @@ mod storage { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::::Hset( (&field, redis_value), redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -369,7 +369,7 @@ mod storage { .await?; } - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -377,7 +377,7 @@ mod storage { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index 6e052076799a..e75fb940a35b 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -210,13 +210,13 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet( &lookup.sk_id, ), key, - ) + )) .await? .try_into_hget() }, @@ -348,13 +348,13 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet( &lookup.sk_id, ), key, - ) + )) .await? .try_into_hget() }, @@ -500,7 +500,7 @@ mod storage { }, }; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -508,7 +508,7 @@ mod storage { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -597,14 +597,14 @@ mod storage { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::::Hset( (&field, redis_value), redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -743,11 +743,11 @@ mod storage { let pattern = "payment_method_id_*"; let redis_fut = async { - let kv_result = kv_wrapper::( + let kv_result = Box::pin(kv_wrapper::( self, KvOperation::::Scan(pattern), key, - ) + )) .await? .try_into_scan(); kv_result.map(|payment_methods| { diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index cf092ad410e5..2baa25ce5194 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -359,11 +359,11 @@ mod storage { }; Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -491,7 +491,7 @@ mod storage { futures::future::try_join_all(rev_look).await?; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -499,7 +499,7 @@ mod storage { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -560,11 +560,11 @@ mod storage { Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::Scan(&pattern), key, - ) + )) .await? .try_into_scan() }, @@ -619,14 +619,14 @@ mod storage { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::Hset::( (&field, redis_value), redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -672,11 +672,11 @@ mod storage { }; Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -730,11 +730,11 @@ mod storage { }; Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -777,11 +777,11 @@ mod storage { }; Box::pin(db_utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::Scan("pa_*_ref_*"), key, - ) + )) .await? .try_into_scan() }, diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index d51ed5ed385e..06bd84675b11 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -120,7 +120,7 @@ mod storage { }, }; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::SetNx(&created_rev_lookup, redis_entry), PartitionKey::CombinationKey { @@ -129,7 +129,7 @@ mod storage { &created_rev_lookup.lookup_id ), }, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() @@ -168,13 +168,13 @@ mod storage { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { let redis_fut = async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::Get, PartitionKey::CombinationKey { combination: &format!("reverse_lookup_{id}"), }, - ) + )) .await? .try_into_get() }; diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index f6caa64fe6e1..e6cd552b15cd 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -220,6 +220,7 @@ mod tests { organization_id: Default::default(), shipping_cost: Default::default(), order_tax_amount: Default::default(), + connector_mandate_detail: Default::default(), }; let store = state @@ -308,6 +309,7 @@ mod tests { organization_id: Default::default(), shipping_cost: Default::default(), order_tax_amount: Default::default(), + connector_mandate_detail: Default::default(), }; let store = state .stores @@ -409,6 +411,7 @@ mod tests { organization_id: Default::default(), shipping_cost: Default::default(), order_tax_amount: Default::default(), + connector_mandate_detail: Default::default(), }; let store = state .stores diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index bb1b801edb6f..20d0ee74b011 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -125,6 +125,7 @@ pub struct PaymentsMandateReferenceRecord { pub original_payment_authorized_amount: Option, pub original_payment_authorized_currency: Option, pub mandate_metadata: Option, + pub connector_mandate_status: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 1f7d75fba83d..b98a1328519d 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -333,6 +333,7 @@ pub async fn generate_sample_data( organization_id: org_id.clone(), shipping_cost: None, order_tax_amount: None, + connector_mandate_detail: None, }; let refund = if refunds_count < number_of_refunds && !is_failed_payment { diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index 943ef1f36f73..54377e452673 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -97,13 +97,13 @@ impl ReverseLookupInterface for KVRouterStore { }, }; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::SetNx(&created_rev_lookup, redis_entry), PartitionKey::CombinationKey { combination: &format!("reverse_lookup_{}", &created_rev_lookup.lookup_id), }, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() @@ -140,13 +140,13 @@ impl ReverseLookupInterface for KVRouterStore { storage_enums::MerchantStorageScheme::PostgresOnly => database_call().await, storage_enums::MerchantStorageScheme::RedisKv => { let redis_fut = async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::Get, PartitionKey::CombinationKey { combination: &format!("reverse_lookup_{id}"), }, - ) + )) .await? .try_into_get() }; diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index c37f9f6d3dac..52bfe828c407 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -187,6 +187,7 @@ impl PaymentAttemptInterface for MockDb { profile_id: payment_attempt.profile_id, shipping_cost: payment_attempt.shipping_cost, order_tax_amount: payment_attempt.order_tax_amount, + connector_mandate_detail: payment_attempt.connector_mandate_detail, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 63f5a0ff9f82..5c5c1fa7c67b 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -536,6 +536,7 @@ impl PaymentAttemptInterface for KVRouterStore { profile_id: payment_attempt.profile_id.clone(), shipping_cost: payment_attempt.shipping_cost, order_tax_amount: payment_attempt.order_tax_amount, + connector_mandate_detail: payment_attempt.connector_mandate_detail.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -563,7 +564,7 @@ impl PaymentAttemptInterface for KVRouterStore { self.insert_reverse_lookup(reverse_lookup, storage_scheme) .await?; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::HSetNx( &field, @@ -571,7 +572,7 @@ impl PaymentAttemptInterface for KVRouterStore { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -717,11 +718,11 @@ impl PaymentAttemptInterface for KVRouterStore { (_, _) => {} } - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::Hset::((&field, redis_value), redis_entry), key, - ) + )) .await .change_context(errors::StorageError::KVError)? .try_into_hset() @@ -805,7 +806,7 @@ impl PaymentAttemptInterface for KVRouterStore { Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper(self, KvOperation::::HGet(&lookup.sk_id), key).await?.try_into_hget() + Box::pin(kv_wrapper(self, KvOperation::::HGet(&lookup.sk_id), key)).await?.try_into_hget() }, || async {self.router_store.find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(connector_transaction_id, payment_id, merchant_id, storage_scheme).await}, )) @@ -846,11 +847,11 @@ impl PaymentAttemptInterface for KVRouterStore { let pattern = "pa_*"; let redis_fut = async { - let kv_result = kv_wrapper::( + let kv_result = Box::pin(kv_wrapper::( self, KvOperation::::Scan(pattern), key, - ) + )) .await? .try_into_scan(); kv_result.and_then(|mut payment_attempts| { @@ -905,11 +906,11 @@ impl PaymentAttemptInterface for KVRouterStore { let pattern = "pa_*"; let redis_fut = async { - let kv_result = kv_wrapper::( + let kv_result = Box::pin(kv_wrapper::( self, KvOperation::::Scan(pattern), key, - ) + )) .await? .try_into_scan(); kv_result.and_then(|mut payment_attempts| { @@ -981,11 +982,11 @@ impl PaymentAttemptInterface for KVRouterStore { }; Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -1038,9 +1039,13 @@ impl PaymentAttemptInterface for KVRouterStore { let field = format!("pa_{attempt_id}"); Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper(self, KvOperation::::HGet(&field), key) - .await? - .try_into_hget() + Box::pin(kv_wrapper( + self, + KvOperation::::HGet(&field), + key, + )) + .await? + .try_into_hget() }, || async { self.router_store @@ -1101,11 +1106,11 @@ impl PaymentAttemptInterface for KVRouterStore { }; Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -1190,11 +1195,11 @@ impl PaymentAttemptInterface for KVRouterStore { Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -1244,9 +1249,13 @@ impl PaymentAttemptInterface for KVRouterStore { }; Box::pin(try_redis_get_else_try_database_get( async { - kv_wrapper(self, KvOperation::::Scan("pa_*"), key) - .await? - .try_into_scan() + Box::pin(kv_wrapper( + self, + KvOperation::::Scan("pa_*"), + key, + )) + .await? + .try_into_scan() }, || async { self.router_store @@ -1446,6 +1455,7 @@ impl DataModelExt for PaymentAttempt { profile_id: self.profile_id, shipping_cost: self.shipping_cost, order_tax_amount: self.order_tax_amount, + connector_mandate_detail: self.connector_mandate_detail, } } @@ -1517,6 +1527,7 @@ impl DataModelExt for PaymentAttempt { profile_id: storage_model.profile_id, shipping_cost: storage_model.shipping_cost, order_tax_amount: storage_model.order_tax_amount, + connector_mandate_detail: storage_model.connector_mandate_detail, } } } @@ -1599,6 +1610,7 @@ impl DataModelExt for PaymentAttemptNew { profile_id: self.profile_id, shipping_cost: self.shipping_cost, order_tax_amount: self.order_tax_amount, + connector_mandate_detail: self.connector_mandate_detail, } } @@ -1669,6 +1681,7 @@ impl DataModelExt for PaymentAttemptNew { profile_id: storage_model.profile_id, shipping_cost: storage_model.shipping_cost, order_tax_amount: storage_model.order_tax_amount, + connector_mandate_detail: storage_model.connector_mandate_detail, } } } @@ -1857,6 +1870,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_message, payment_method_data, charge_id, + connector_mandate_detail, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1878,6 +1892,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_message, payment_method_data, charge_id, + connector_mandate_detail, }, Self::UnresolvedResponseUpdate { status, @@ -2213,6 +2228,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_message, payment_method_data, charge_id, + connector_mandate_detail, } => Self::ResponseUpdate { status, connector, @@ -2234,6 +2250,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_message, payment_method_data, charge_id, + connector_mandate_detail, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 4c7232f6f3f5..c0b37e071eb7 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -113,7 +113,7 @@ impl PaymentIntentInterface for KVRouterStore { .await .change_context(StorageError::EncryptionError)?; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -121,7 +121,7 @@ impl PaymentIntentInterface for KVRouterStore { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -228,11 +228,11 @@ impl PaymentIntentInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::::Hset((&field, redis_value), redis_entry), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -316,11 +316,11 @@ impl PaymentIntentInterface for KVRouterStore { let field = payment_id.get_hash_key_for_kv_store(); Box::pin(utils::try_redis_get_else_try_database_get( async { - kv_wrapper::( + Box::pin(kv_wrapper::( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() }, diff --git a/crates/storage_impl/src/payouts/payout_attempt.rs b/crates/storage_impl/src/payouts/payout_attempt.rs index 33d73f99e5f7..81d06a1cbd5f 100644 --- a/crates/storage_impl/src/payouts/payout_attempt.rs +++ b/crates/storage_impl/src/payouts/payout_attempt.rs @@ -115,7 +115,7 @@ impl PayoutAttemptInterface for KVRouterStore { self.insert_reverse_lookup(reverse_lookup, storage_scheme) .await?; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -123,7 +123,7 @@ impl PayoutAttemptInterface for KVRouterStore { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -227,11 +227,11 @@ impl PayoutAttemptInterface for KVRouterStore { _ => {} } - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::::Hset((&field, redis_value), redis_entry), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -284,11 +284,11 @@ impl PayoutAttemptInterface for KVRouterStore { }; Box::pin(utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, @@ -345,11 +345,11 @@ impl PayoutAttemptInterface for KVRouterStore { }; Box::pin(utils::try_redis_get_else_try_database_get( async { - kv_wrapper( + Box::pin(kv_wrapper( self, KvOperation::::HGet(&lookup.sk_id), key, - ) + )) .await? .try_into_hget() }, diff --git a/crates/storage_impl/src/payouts/payouts.rs b/crates/storage_impl/src/payouts/payouts.rs index 2a49b01458f7..9a94743da3f1 100644 --- a/crates/storage_impl/src/payouts/payouts.rs +++ b/crates/storage_impl/src/payouts/payouts.rs @@ -134,7 +134,7 @@ impl PayoutsInterface for KVRouterStore { }, }; - match kv_wrapper::( + match Box::pin(kv_wrapper::( self, KvOperation::::HSetNx( &field, @@ -142,7 +142,7 @@ impl PayoutsInterface for KVRouterStore { redis_entry, ), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hsetnx() @@ -208,11 +208,11 @@ impl PayoutsInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( + Box::pin(kv_wrapper::<(), _, _>( self, KvOperation::::Hset((&field, redis_value), redis_entry), key, - ) + )) .await .map_err(|err| err.to_redis_failed_response(&key_str))? .try_into_hset() @@ -255,11 +255,11 @@ impl PayoutsInterface for KVRouterStore { let field = format!("po_{payout_id}"); Box::pin(utils::try_redis_get_else_try_database_get( async { - kv_wrapper::( + Box::pin(kv_wrapper::( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() }, @@ -312,11 +312,11 @@ impl PayoutsInterface for KVRouterStore { let field = format!("po_{payout_id}"); Box::pin(utils::try_redis_get_else_try_database_get( async { - kv_wrapper::( + Box::pin(kv_wrapper::( self, KvOperation::::HGet(&field), key, - ) + )) .await? .try_into_hget() .map(Some) diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 74b1526fe8e8..2fde935b670d 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -312,8 +312,12 @@ where Op::Find => MerchantStorageScheme::RedisKv, Op::Update(_, _, Some("postgres_only")) => MerchantStorageScheme::PostgresOnly, Op::Update(partition_key, field, Some(_updated_by)) => { - match kv_wrapper::(store, KvOperation::::HGet(field), partition_key) - .await + match Box::pin(kv_wrapper::( + store, + KvOperation::::HGet(field), + partition_key, + )) + .await { Ok(_) => { metrics::KV_SOFT_KILL_ACTIVE_UPDATE.add(&metrics::CONTEXT, 1, &[]); diff --git a/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/down.sql b/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/down.sql new file mode 100644 index 000000000000..f4e6f2e2e262 --- /dev/null +++ b/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE + payment_attempt DROP COLUMN connector_mandate_detail; \ No newline at end of file diff --git a/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/up.sql b/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/up.sql new file mode 100644 index 000000000000..bb592f623d43 --- /dev/null +++ b/migrations/2024-10-13-182546_add_connector_mandate_id_in_payment_attempt/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE + payment_attempt +ADD + COLUMN connector_mandate_detail JSONB DEFAULT NULL; \ No newline at end of file