From 02074dfc23f1a126e76935ba5311c6aed6590ca5 Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:35:42 +0530 Subject: [PATCH] feat(core): update card_details for an existing mandate (#3452) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 12 ++- crates/data_models/src/mandates.rs | 17 +++ .../src/payments/payment_attempt.rs | 8 +- crates/diesel_models/src/enums.rs | 46 ++++++++ crates/diesel_models/src/mandate.rs | 31 +++++- crates/diesel_models/src/payment_attempt.rs | 4 +- crates/diesel_models/src/user/sample_data.rs | 4 +- .../stripe/payment_intents/types.rs | 1 + crates/router/src/core/mandate.rs | 102 +++++++++++++----- crates/router/src/core/mandate/helpers.rs | 35 ++++++ .../router/src/core/payment_methods/cards.rs | 17 ++- .../core/payments/flows/setup_mandate_flow.rs | 93 ++++++++++++++-- crates/router/src/core/payments/helpers.rs | 2 +- .../payments/operations/payment_confirm.rs | 24 ++++- .../payments/operations/payment_create.rs | 50 +++++++-- .../payments/operations/payment_update.rs | 5 +- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/db/mandate.rs | 12 +++ crates/router/src/services/api.rs | 2 +- crates/router/src/types/transformers.rs | 1 + .../src/payments/payment_attempt.rs | 51 ++++++++- openapi/openapi_spec.json | 5 + 22 files changed, 451 insertions(+), 72 deletions(-) create mode 100644 crates/router/src/core/mandate/helpers.rs diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 57c66dc5a7..8c27f498d7 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -617,10 +617,18 @@ pub enum MandateReferenceId { NetworkMandateId(String), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] pub struct ConnectorMandateReferenceId { pub connector_mandate_id: Option, pub payment_method_id: Option, + pub update_history: Option>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct UpdateHistory { + pub connector_mandate_id: Option, + pub payment_method_id: String, + pub original_payment_id: Option, } impl MandateIds { @@ -637,6 +645,8 @@ impl MandateIds { #[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used diff --git a/crates/data_models/src/mandates.rs b/crates/data_models/src/mandates.rs index afdcda3a40..319a78cf66 100644 --- a/crates/data_models/src/mandates.rs +++ b/crates/data_models/src/mandates.rs @@ -9,6 +9,13 @@ use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MandateDataType { @@ -16,6 +23,13 @@ pub enum MandateDataType { MultiUse(Option), } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, @@ -29,6 +43,8 @@ pub struct MandateAmountData { // information about creating mandates #[derive(Default, Eq, PartialEq, Debug, Clone)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used @@ -90,6 +106,7 @@ impl From for MandateData { Self { customer_acceptance: value.customer_acceptance.map(|d| d.into()), mandate_type: value.mandate_type.map(|d| d.into()), + update_mandate_id: value.update_mandate_id, } } } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 3e6ba9e37f..e6b9950b45 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use super::PaymentIntent; -use crate::{errors, mandates::MandateDataType, ForeignIDRef}; +use crate::{errors, mandates::MandateTypeDetails, ForeignIDRef}; #[async_trait::async_trait] pub trait PaymentAttemptInterface { @@ -143,7 +143,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -184,7 +184,7 @@ pub struct PaymentAttemptNew { pub attempt_id: String, pub status: storage_enums::AttemptStatus, pub amount: i64, - /// amount + surcharge_amount + tax_amount + /// amount + surcharge_amount + tax_amount /// This field will always be derived before updating in the Database pub net_amount: i64, pub currency: Option, @@ -221,7 +221,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index a06937c99a..babffdbc4a 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -166,6 +166,17 @@ use diesel::{ expression::AsExpression, sql_types::Jsonb, }; + +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, )] @@ -185,6 +196,7 @@ where Ok(serde_json::from_value(value)?) } } + impl ToSql for MandateDataType where serde_json::Value: ToSql, @@ -199,6 +211,40 @@ where } } +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} + +impl FromSql for MandateTypeDetails +where + serde_json::Value: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value(value)?) + } +} +impl ToSql for MandateTypeDetails +where + serde_json::Value: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + + // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends + // please refer to the diesel migration blog: + // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations + >::to_sql(&value, &mut out.reborrow()) + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, diff --git a/crates/diesel_models/src/mandate.rs b/crates/diesel_models/src/mandate.rs index cc3474914c..31c6ef62f2 100644 --- a/crates/diesel_models/src/mandate.rs +++ b/crates/diesel_models/src/mandate.rs @@ -75,6 +75,12 @@ pub enum MandateUpdate { ConnectorReferenceUpdate { connector_mandate_ids: Option, }, + ConnectorMandateIdUpdate { + connector_mandate_id: Option, + connector_mandate_ids: Option, + payment_method_id: String, + original_payment_id: Option, + }, } #[derive(Clone, Eq, PartialEq, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] @@ -89,6 +95,9 @@ pub struct MandateUpdateInternal { mandate_status: Option, amount_captured: Option, connector_mandate_ids: Option, + connector_mandate_id: Option, + payment_method_id: Option, + original_payment_id: Option, } impl From for MandateUpdateInternal { @@ -98,16 +107,34 @@ impl From for MandateUpdateInternal { mandate_status: Some(mandate_status), connector_mandate_ids: None, amount_captured: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::CaptureAmountUpdate { amount_captured } => Self { mandate_status: None, amount_captured, connector_mandate_ids: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::ConnectorReferenceUpdate { - connector_mandate_ids: connector_mandate_id, + connector_mandate_ids, } => Self { - connector_mandate_ids: connector_mandate_id, + connector_mandate_ids, + ..Default::default() + }, + MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, + } => Self { + connector_mandate_id, + connector_mandate_ids, + payment_method_id: Some(payment_method_id), + original_payment_id, ..Default::default() }, } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index d08c146b0b..4a7603384c 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -51,7 +51,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -126,7 +126,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index 5a1ea696c5..5a2226f067 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -5,7 +5,7 @@ use common_enums::{ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; -use crate::{enums::MandateDataType, schema::payment_attempt, PaymentAttemptNew}; +use crate::{enums::MandateTypeDetails, schema::payment_attempt, PaymentAttemptNew}; #[derive( Clone, Debug, Default, diesel::Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, @@ -50,7 +50,7 @@ pub struct PaymentAttemptBatchNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub connector_transaction_id: Option, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 810e0ed1d2..aac150b507 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -753,6 +753,7 @@ impl ForeignTryFrom<(Option, Option)> for Option { - let profile_id = if let Some(ref payment_id) = mandate.original_payment_id { - let pi = db - .find_payment_intent_by_payment_id_merchant_id( - payment_id, - &merchant_account.merchant_id, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - let profile_id = pi.profile_id.clone().ok_or( - errors::ApiErrorResponse::BusinessProfileNotFound { - id: pi - .profile_id - .unwrap_or_else(|| "Profile id is Null".to_string()), - }, - )?; - Ok(profile_id) - } else { - Err(errors::ApiErrorResponse::PaymentNotFound) - }?; + let profile_id = + helpers::get_profile_id_for_mandate(&state, &merchant_account, mandate.clone()) + .await?; - let merchant_connector_account = helpers::get_merchant_connector_account( + let merchant_connector_account = payment_helper::get_merchant_connector_account( &state, &merchant_account.merchant_id, None, &key_store, &profile_id, - &mandate.connector, + &mandate.connector.clone(), mandate.merchant_connector_id.as_ref(), ) .await?; @@ -243,7 +226,72 @@ where _ => Some(router_data.request.get_payment_method_data()), } } +pub async fn update_mandate_procedure( + state: &AppState, + resp: types::RouterData, + mandate: Mandate, + merchant_id: &str, + pm_id: Option, +) -> errors::RouterResult> +where + FData: MandateBehaviour, +{ + let mandate_details = match &resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { + mandate_reference, .. + }) => mandate_reference, + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => return Ok(resp), + }; + let old_record = payments::UpdateHistory { + connector_mandate_id: mandate.connector_mandate_id, + payment_method_id: mandate.payment_method_id, + original_payment_id: mandate.original_payment_id, + }; + + let mandate_ref = mandate + .connector_mandate_ids + .parse_value::("Connector Reference Id") + .change_context(errors::ApiErrorResponse::MandateDeserializationFailed)?; + + let mut update_history = mandate_ref.update_history.unwrap_or_default(); + update_history.push(old_record); + + let updated_mandate_ref = payments::ConnectorMandateReferenceId { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|mandate_ref| mandate_ref.connector_mandate_id.clone()), + payment_method_id: pm_id.clone(), + update_history: Some(update_history), + }; + + let connector_mandate_ids = + Encode::::encode_to_value(&updated_mandate_ref) + .change_context(errors::ApiErrorResponse::InternalServerError) + .map(masking::Secret::new)?; + + let _update_mandate_details = state + .store + .update_mandate_by_merchant_id_mandate_id( + merchant_id, + &mandate.mandate_id, + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|man_ref| man_ref.connector_mandate_id.clone()), + connector_mandate_ids: Some(connector_mandate_ids), + payment_method_id: pm_id + .unwrap_or("Error retrieving the payment_method_id".to_string()), + original_payment_id: Some(resp.payment_id.clone()), + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateUpdateFailed)?; + Ok(resp) +} pub async fn mandate_procedure( state: &AppState, mut resp: types::RouterData, @@ -324,7 +372,7 @@ where }) .transpose()?; - if let Some(new_mandate_data) = helpers::generate_mandate( + if let Some(new_mandate_data) = payment_helper::generate_mandate( resp.merchant_id.clone(), resp.payment_id.clone(), resp.connector.clone(), @@ -363,6 +411,8 @@ where api_models::payments::ConnectorMandateReferenceId { connector_mandate_id: connector_id.connector_mandate_id, payment_method_id: connector_id.payment_method_id, + update_history:None, + } ))) })); diff --git a/crates/router/src/core/mandate/helpers.rs b/crates/router/src/core/mandate/helpers.rs new file mode 100644 index 0000000000..150130ed9e --- /dev/null +++ b/crates/router/src/core/mandate/helpers.rs @@ -0,0 +1,35 @@ +use common_utils::errors::CustomResult; +use diesel_models::Mandate; +use error_stack::ResultExt; + +use crate::{core::errors, routes::AppState, types::domain}; + +pub async fn get_profile_id_for_mandate( + state: &AppState, + merchant_account: &domain::MerchantAccount, + mandate: Mandate, +) -> CustomResult { + let profile_id = if let Some(ref payment_id) = mandate.original_payment_id { + let pi = state + .store + .find_payment_intent_by_payment_id_merchant_id( + payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let profile_id = + pi.profile_id + .clone() + .ok_or(errors::ApiErrorResponse::BusinessProfileNotFound { + id: pi + .profile_id + .unwrap_or_else(|| "Profile id is Null".to_string()), + })?; + Ok(profile_id) + } else { + Err(errors::ApiErrorResponse::PaymentNotFound) + }?; + Ok(profile_id) +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 3dbd6fcb21..e08dbc61f5 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1823,14 +1823,24 @@ pub async fn list_payment_methods( } else { api_surcharge_decision_configs::MerchantSurchargeConfigs::default() }; + print!("PAMT{:?}", payment_attempt); Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, merchant_name: merchant_account.merchant_name, payment_type, payment_methods: payment_method_responses, - mandate_payment: payment_attempt.and_then(|inner| inner.mandate_details).map( - |d| match d { + mandate_payment: payment_attempt + .and_then(|inner| inner.mandate_details) + .and_then(|man_type_details| match man_type_details { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .map(|d| match d { data_models::mandates::MandateDataType::SingleUse(i) => { api::MandateType::SingleUse(api::MandateAmountData { amount: i.amount, @@ -1852,8 +1862,7 @@ pub async fn list_payment_methods( data_models::mandates::MandateDataType::MultiUse(None) => { api::MandateType::MultiUse(None) } - }, - ), + }), show_surcharge_breakup_screen: merchant_surcharge_configs .show_surcharge_breakup_screen .unwrap_or_default(), diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index d6343ed871..8b0b54158f 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; +use error_stack::{IntoReport, ResultExt}; use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ - errors::{self, ConnectorErrorExt, RouterResult}, + errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt}, mandate, payments::{ self, access_token, customers, helpers, tokenization, transformers, PaymentData, @@ -65,16 +66,16 @@ impl Feature for types::Setup types::SetupMandateRequestData, types::PaymentsResponseData, > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( state, connector_integration, &self, - call_connector_action, + call_connector_action.clone(), connector_request, ) .await .to_setup_mandate_failed_response()?; - let pm_id = Box::pin(tokenization::save_payment_method( state, connector, @@ -86,14 +87,84 @@ impl Feature for types::Setup )) .await?; - mandate::mandate_procedure( - state, - resp, - maybe_customer, - pm_id, - connector.merchant_connector_id.clone(), - ) - .await + if let Some(mandate_id) = self + .request + .setup_mandate_details + .as_ref() + .and_then(|mandate_data| mandate_data.update_mandate_id.clone()) + { + let mandate = state + .store + .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &mandate_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + + let profile_id = mandate::helpers::get_profile_id_for_mandate( + state, + merchant_account, + mandate.clone(), + ) + .await?; + match resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { .. }) => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + types::api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > = connector.connector.get_connector_integration(); + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + &merchant_account.merchant_id, + None, + key_store, + &profile_id, + &mandate.connector, + mandate.merchant_connector_id.as_ref(), + ) + .await?; + + let router_data = mandate::utils::construct_mandate_revoke_router_data( + merchant_connector_account, + merchant_account, + mandate.clone(), + ) + .await?; + + let _response = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + call_connector_action, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + // TODO:Add the revoke mandate task to process tracker + mandate::update_mandate_procedure( + state, + resp, + mandate, + &merchant_account.merchant_id, + pm_id, + ) + .await + } + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => Ok(resp), + } + } else { + mandate::mandate_procedure( + state, + resp, + maybe_customer, + pm_id, + connector.merchant_connector_id.clone(), + ) + .await + } } async fn add_access_token<'a>( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0cbed25534..520582eb22 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -840,7 +840,7 @@ fn validate_new_mandate_request( let mandate_details = match mandate_data.mandate_type { Some(api_models::payments::MandateType::SingleUse(details)) => Some(details), Some(api_models::payments::MandateType::MultiUse(details)) => details, - None => None, + _ => None, }; mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)| utils::when (start_date >= end_date, || { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index c81145c5de..14fc28d672 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -431,7 +431,29 @@ impl // The operation merges mandate data from both request and payment_attempt setup_mandate = setup_mandate.map(|mut sm| { - sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); + sm.mandate_type = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .or(sm.mandate_type); + sm.update_mandate_id = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(_) => None, + data_models::mandates::MandateTypeDetails::MandateDetails(update_id) => { + Some(update_id.update_mandate_id) + } + }) + .flatten() + .or(sm.update_mandate_id); sm }); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2b25a74deb..d02ad15fbd 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -3,7 +3,10 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; -use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; +use data_models::{ + mandates::{MandateData, MandateDetails, MandateTypeDetails}, + payments::payment_attempt::PaymentAttempt, +}; use diesel_models::ephemeral_key; use error_stack::{self, ResultExt}; use masking::PeekInterface; @@ -255,7 +258,7 @@ impl .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { payment_id: payment_id.clone(), })?; - + // connector mandate reference update history let mandate_id = request .mandate_id .as_ref() @@ -284,10 +287,11 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId{ + connector_mandate_id: connector_id.connector_mandate_id, + payment_method_id: connector_id.payment_method_id, + update_history: None + } )) } }), @@ -701,6 +705,35 @@ impl PaymentCreate { .surcharge_details .and_then(|surcharge_details| surcharge_details.tax_amount); + if request.mandate_data.as_ref().map_or(false, |mandate_data| { + mandate_data.update_mandate_id.is_some() && mandate_data.mandate_type.is_some() + }) { + Err(errors::ApiErrorResponse::InvalidRequestData {message:"Only one field out of 'mandate_type' and 'update_mandate_id' was expected, found both".to_string()})? + } + + let mandate_dets = if let Some(update_id) = request + .mandate_data + .as_ref() + .and_then(|inner| inner.update_mandate_id.clone()) + { + let mandate_data = MandateDetails { + update_mandate_id: Some(update_id), + mandate_type: None, + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + } else { + // let mandate_type: data_models::mandates::MandateDataType = + + let mandate_data = MandateDetails { + update_mandate_id: None, + mandate_type: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + }; + Ok(( storage::PaymentAttemptNew { payment_id: payment_id.to_string(), @@ -727,10 +760,7 @@ impl PaymentCreate { business_sub_label: request.business_sub_label.clone(), surcharge_amount, tax_amount, - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + mandate_details: mandate_dets, ..storage::PaymentAttemptNew::default() }, additional_pm_data, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e002b92d18..015ef5cea6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -255,10 +255,7 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId {connector_mandate_id:connector_id.connector_mandate_id,payment_method_id:connector_id.payment_method_id, update_history: None }, )) } }), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5f0c702a29..61917fdcd2 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -636,6 +636,7 @@ where api::MandateType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, }), auth_flow == services::AuthFlow::Merchant, ) diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index fcd7171965..0cf5cabf2e 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -202,6 +202,18 @@ impl MandateInterface for MockDb { } => { mandate.connector_mandate_ids = connector_mandate_ids; } + + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, + } => { + mandate.connector_mandate_ids = connector_mandate_ids; + mandate.connector_mandate_id = connector_mandate_id; + mandate.payment_method_id = payment_method_id; + mandate.original_payment_id = original_payment_id + } } Ok(mandate.clone()) } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 307dff5507..bc8ab2e05e 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1674,7 +1674,7 @@ pub fn build_redirection_form( // Initialize the ThreeDSService const threeDS = gateway.get3DSecure(); - + const options = {{ customerVaultId: '{customer_vault_id}', currency: '{currency}', diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 786a8c5518..41aefc9026 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -323,6 +323,7 @@ impl ForeignFrom for data_models::mandates::M data_models::mandates::MandateDataType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, } } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index f8f752c6bc..b8d71cb32b 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -2,7 +2,7 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMet use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, - mandates::{MandateAmountData, MandateDataType}, + mandates::{MandateAmountData, MandateDataType, MandateDetails, MandateTypeDetails}, payments::{ payment_attempt::{ PaymentAttempt, PaymentAttemptInterface, PaymentAttemptNew, PaymentAttemptUpdate, @@ -14,6 +14,7 @@ use data_models::{ use diesel_models::{ enums::{ MandateAmountData as DieselMandateAmountData, MandateDataType as DieselMandateType, + MandateDetails as DieselMandateDetails, MandateTypeDetails as DieselMandateTypeOrDetails, MerchantStorageScheme, }, kv, @@ -999,6 +1000,50 @@ impl DataModelExt for MandateAmountData { } } } +impl DataModelExt for MandateDetails { + type StorageModel = DieselMandateDetails; + fn to_storage_model(self) -> Self::StorageModel { + DieselMandateDetails { + update_mandate_id: self.update_mandate_id, + mandate_type: self + .mandate_type + .map(|mand_type| mand_type.to_storage_model()), + } + } + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + update_mandate_id: storage_model.update_mandate_id, + mandate_type: storage_model + .mandate_type + .map(MandateDataType::from_storage_model), + } + } +} +impl DataModelExt for MandateTypeDetails { + type StorageModel = DieselMandateTypeOrDetails; + + fn to_storage_model(self) -> Self::StorageModel { + match self { + Self::MandateType(mandate_type) => { + DieselMandateTypeOrDetails::MandateType(mandate_type.to_storage_model()) + } + Self::MandateDetails(mandate_details) => { + DieselMandateTypeOrDetails::MandateDetails(mandate_details.to_storage_model()) + } + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + match storage_model { + DieselMandateTypeOrDetails::MandateType(data) => { + Self::MandateType(MandateDataType::from_storage_model(data)) + } + DieselMandateTypeOrDetails::MandateDetails(data) => { + Self::MandateDetails(MandateDetails::from_storage_model(data)) + } + } + } +} impl DataModelExt for MandateDataType { type StorageModel = DieselMandateType; @@ -1123,7 +1168,7 @@ impl DataModelExt for PaymentAttempt { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, multiple_capture_count: storage_model.multiple_capture_count, connector_response_reference_id: storage_model.connector_response_reference_id, @@ -1231,7 +1276,7 @@ impl DataModelExt for PaymentAttemptNew { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, connector_response_reference_id: storage_model.connector_response_reference_id, multiple_capture_count: storage_model.multiple_capture_count, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7858029961..09cb5fe140 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9026,6 +9026,11 @@ "MandateData": { "type": "object", "properties": { + "update_mandate_id": { + "type": "string", + "description": "A way to update the mandate's payment method details", + "nullable": true + }, "customer_acceptance": { "allOf": [ {