From 7b337ac39d72f90dd0ebe58133218896ff279313 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:12:30 +0530 Subject: [PATCH] feat(mandates): allow off-session payments using `payment_method_id` (#4132) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/mandates.rs | 7 + crates/api_models/src/payments.rs | 21 +- crates/openapi/src/openapi.rs | 1 + crates/router/src/core/mandate/helpers.rs | 19 +- crates/router/src/core/payments.rs | 148 ++++++------ crates/router/src/core/payments/helpers.rs | 219 ++++++++++++------ .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 20 +- .../payments/operations/payment_confirm.rs | 18 +- .../payments/operations/payment_create.rs | 28 ++- .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 33 ++- .../payments_incremental_authorization.rs | 1 + crates/router/src/types/api/payments.rs | 5 +- openapi/openapi_spec.json | 75 ++++++ 20 files changed, 429 insertions(+), 173 deletions(-) diff --git a/crates/api_models/src/mandates.rs b/crates/api_models/src/mandates.rs index bd5c5b5a1a02..4adda0d9af45 100644 --- a/crates/api_models/src/mandates.rs +++ b/crates/api_models/src/mandates.rs @@ -113,3 +113,10 @@ pub struct MandateListConstraints { #[serde(rename = "created_time.gte")] pub created_time_gte: Option, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum RecurringDetails { + MandateId(String), + PaymentMethodId(String), +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index ce2b1c3d68a3..72592ec3cad7 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -19,10 +19,8 @@ use url::Url; use utoipa::ToSchema; use crate::{ - admin, disputes, - enums::{self as api_enums}, - ephemeral_key::EphemeralKeyCreateResponse, - refunds, + admin, disputes, enums as api_enums, ephemeral_key::EphemeralKeyCreateResponse, + mandates::RecurringDetails, refunds, }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -459,6 +457,9 @@ pub struct PaymentsRequest { /// Whether to perform external authentication (if applicable) #[schema(example = true)] pub request_external_three_ds_authentication: Option, + + /// Details required for recurring payment + pub recurring_details: Option, } impl PaymentsRequest { @@ -3360,7 +3361,7 @@ pub struct PaymentsRedirectionResponse { } pub struct MandateValidationFields { - pub mandate_id: Option, + pub recurring_details: Option, pub confirm: Option, pub customer_id: Option, pub mandate_data: Option, @@ -3370,8 +3371,14 @@ pub struct MandateValidationFields { impl From<&PaymentsRequest> for MandateValidationFields { fn from(req: &PaymentsRequest) -> Self { + let recurring_details = req + .mandate_id + .clone() + .map(RecurringDetails::MandateId) + .or(req.recurring_details.clone()); + Self { - mandate_id: req.mandate_id.clone(), + recurring_details, confirm: req.confirm, customer_id: req .customer @@ -3389,7 +3396,7 @@ impl From<&PaymentsRequest> for MandateValidationFields { impl From<&VerifyRequest> for MandateValidationFields { fn from(req: &VerifyRequest) -> Self { Self { - mandate_id: None, + recurring_details: None, confirm: Some(true), customer_id: req.customer_id.clone(), mandate_data: req.mandate_data.clone(), diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index c8e89f2ee70a..4a61a20d08d0 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -410,6 +410,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, api_models::mandates::MandateCardDetails, + api_models::mandates::RecurringDetails, api_models::ephemeral_key::EphemeralKeyCreateResponse, api_models::payments::CustomerDetails, api_models::payments::GiftCardData, diff --git a/crates/router/src/core/mandate/helpers.rs b/crates/router/src/core/mandate/helpers.rs index 150130ed9e5d..704b7ae99f55 100644 --- a/crates/router/src/core/mandate/helpers.rs +++ b/crates/router/src/core/mandate/helpers.rs @@ -1,8 +1,14 @@ +use common_enums::enums; use common_utils::errors::CustomResult; +use data_models::mandates::MandateData; use diesel_models::Mandate; use error_stack::ResultExt; -use crate::{core::errors, routes::AppState, types::domain}; +use crate::{ + core::{errors, payments}, + routes::AppState, + types::domain, +}; pub async fn get_profile_id_for_mandate( state: &AppState, @@ -33,3 +39,14 @@ pub async fn get_profile_id_for_mandate( }?; Ok(profile_id) } + +#[derive(Clone)] +pub struct MandateGenericData { + pub token: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub mandate_data: Option, + pub recurring_mandate_payment_data: Option, + pub mandate_connector: Option, + pub payment_method_info: Option, +} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 0a3a93923b73..e11a77496afd 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -15,6 +15,7 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ self, enums, + mandates::RecurringDetails, payments::{self as payments_api, HeaderPayload}, }; use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; @@ -2263,6 +2264,7 @@ where pub authorizations: Vec, pub authentication: Option, pub frm_metadata: Option, + pub recurring_details: Option, } #[derive(Debug, Default, Clone)] @@ -2907,7 +2909,7 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Invalid connector name received")?; - return decide_connector_for_token_based_mit_flow( + return decide_multiplex_connector_for_normal_or_recurring_payment( payment_data, routing_data, connector_data, @@ -2961,7 +2963,7 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Invalid connector name received")?; - return decide_connector_for_token_based_mit_flow( + return decide_multiplex_connector_for_normal_or_recurring_payment( payment_data, routing_data, connector_data, @@ -2980,24 +2982,26 @@ where .await } -pub fn decide_connector_for_token_based_mit_flow( +pub fn decide_multiplex_connector_for_normal_or_recurring_payment( payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, connectors: Vec, ) -> RouterResult { - if let Some((storage_enums::FutureUsage::OffSession, _)) = payment_data - .payment_intent - .setup_future_usage - .zip(payment_data.token_data.as_ref()) - { - logger::debug!("performing routing for token-based MIT flow"); + match ( + payment_data.payment_intent.setup_future_usage, + payment_data.token_data.as_ref(), + payment_data.recurring_details.as_ref(), + ) { + (Some(storage_enums::FutureUsage::OffSession), Some(_), None) + | (None, None, Some(RecurringDetails::PaymentMethodId(_))) => { + logger::debug!("performing routing for token-based MIT flow"); - let payment_method_info = payment_data - .payment_method_info - .as_ref() - .get_required_value("payment_method_info")?; + let payment_method_info = payment_data + .payment_method_info + .as_ref() + .get_required_value("payment_method_info")?; - let connector_mandate_details = payment_method_info + let connector_mandate_details = payment_method_info .connector_mandate_details .clone() .map(|details| { @@ -3010,67 +3014,69 @@ pub fn decide_connector_for_token_based_mit_flow( .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) .attach_printable("no eligible connector found for token-based MIT flow since there were no connector mandate details")?; - let mut connector_choice = None; - for connector_data in connectors { - if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() { - if let Some(mandate_reference_record) = - connector_mandate_details.get(merchant_connector_id) - { - connector_choice = Some((connector_data, mandate_reference_record.clone())); - break; + let mut connector_choice = None; + for connector_data in connectors { + if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() { + if let Some(mandate_reference_record) = + connector_mandate_details.get(merchant_connector_id) + { + connector_choice = Some((connector_data, mandate_reference_record.clone())); + break; + } } } - } - let (chosen_connector_data, mandate_reference_record) = connector_choice - .get_required_value("connector_choice") - .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .attach_printable("no eligible connector found for token-based MIT payment")?; + let (chosen_connector_data, mandate_reference_record) = connector_choice + .get_required_value("connector_choice") + .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable("no eligible connector found for token-based MIT payment")?; - routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string()); - #[cfg(feature = "connector_choice_mca_id")] - { - routing_data.merchant_connector_id = - chosen_connector_data.merchant_connector_id.clone(); - } + routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = + chosen_connector_data.merchant_connector_id.clone(); + } - payment_data.mandate_id = Some(payments_api::MandateIds { - mandate_id: None, - mandate_reference_id: Some(payments_api::MandateReferenceId::ConnectorMandateId( - payments_api::ConnectorMandateReferenceId { - connector_mandate_id: Some( - mandate_reference_record.connector_mandate_id.clone(), - ), - payment_method_id: Some(payment_method_info.payment_method_id.clone()), - update_history: None, - }, - )), - }); - - payment_data.recurring_mandate_payment_data = Some(RecurringMandatePaymentData { - payment_method_type: mandate_reference_record.payment_method_type, - original_payment_authorized_amount: mandate_reference_record - .original_payment_authorized_amount, - original_payment_authorized_currency: mandate_reference_record - .original_payment_authorized_currency, - }); - - Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data)) - } else { - let first_choice = connectors - .first() - .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .into_report() - .attach_printable("no eligible connector found for payment")? - .clone(); - - routing_data.routed_through = Some(first_choice.connector_name.to_string()); - #[cfg(feature = "connector_choice_mca_id")] - { - routing_data.merchant_connector_id = first_choice.merchant_connector_id; + payment_data.mandate_id = Some(payments_api::MandateIds { + mandate_id: None, + mandate_reference_id: Some(payments_api::MandateReferenceId::ConnectorMandateId( + payments_api::ConnectorMandateReferenceId { + connector_mandate_id: Some( + mandate_reference_record.connector_mandate_id.clone(), + ), + payment_method_id: Some(payment_method_info.payment_method_id.clone()), + update_history: None, + }, + )), + }); + + payment_data.recurring_mandate_payment_data = Some(RecurringMandatePaymentData { + payment_method_type: mandate_reference_record.payment_method_type, + original_payment_authorized_amount: mandate_reference_record + .original_payment_authorized_amount, + original_payment_authorized_currency: mandate_reference_record + .original_payment_authorized_currency, + }); + + Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data)) } + _ => { + let first_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("no eligible connector found for payment")? + .clone(); + + routing_data.routed_through = Some(first_choice.connector_name.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_choice.merchant_connector_id; + } - Ok(api::ConnectorCallType::Retryable(connectors)) + Ok(api::ConnectorCallType::Retryable(connectors)) + } } } @@ -3291,7 +3297,11 @@ where match transaction_data { TransactionData::Payment(payment_data) => { - decide_connector_for_token_based_mit_flow(payment_data, routing_data, connector_data) + decide_multiplex_connector_for_normal_or_recurring_payment( + payment_data, + routing_data, + connector_data, + ) } #[cfg(feature = "payouts")] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 7eab4c0d26ac..e9cefbf8aed5 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,9 @@ use std::borrow::Cow; -use api_models::payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails}; +use api_models::{ + mandates::RecurringDetails, + payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails}, +}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt}, @@ -34,6 +37,7 @@ use crate::{ consts::{self, BASE64_ENGINE}, core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, + mandate::helpers::MandateGenericData, payment_methods::{cards, vault, PaymentMethodRetrieve}, payments, pm_auth::retrieve_payment_method_from_auth_service, @@ -414,59 +418,117 @@ pub async fn get_token_pm_type_mandate_details( mandate_type: Option, merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, -) -> RouterResult<( - Option, - Option, - Option, - Option, - Option, - Option, -)> { +) -> RouterResult { let mandate_data = request.mandate_data.clone().map(MandateData::foreign_from); - match mandate_type { - Some(api::MandateTransactionType::NewMandateTransaction) => { - let setup_mandate = mandate_data.clone().get_required_value("mandate_data")?; - Ok(( - request.payment_token.to_owned(), - request.payment_method, - request.payment_method_type, - Some(setup_mandate), - None, - None, - )) - } + let ( + payment_token, + payment_method, + payment_method_type, + mandate_data, + recurring_payment_data, + mandate_connector_details, + payment_method_info, + ) = match mandate_type { + Some(api::MandateTransactionType::NewMandateTransaction) => ( + request.payment_token.to_owned(), + request.payment_method, + request.payment_method_type, + Some(mandate_data.clone().get_required_value("mandate_data")?), + None, + None, + None, + ), Some(api::MandateTransactionType::RecurringMandateTransaction) => { - let ( - token_, - payment_method_, - recurring_mandate_payment_data, - payment_method_type_, - mandate_connector, - ) = get_token_for_recurring_mandate( - state, - request, - merchant_account, - merchant_key_store, - ) - .await?; - Ok(( - token_, - payment_method_, - payment_method_type_.or(request.payment_method_type), - None, - recurring_mandate_payment_data, - mandate_connector, - )) + match &request.recurring_details { + Some(recurring_details) => match recurring_details { + RecurringDetails::MandateId(mandate_id) => { + let mandate_generic_data = get_token_for_recurring_mandate( + state, + request, + merchant_account, + merchant_key_store, + mandate_id.to_owned(), + ) + .await?; + + ( + mandate_generic_data.token, + mandate_generic_data.payment_method, + mandate_generic_data + .payment_method_type + .or(request.payment_method_type), + None, + mandate_generic_data.recurring_mandate_payment_data, + mandate_generic_data.mandate_connector, + None, + ) + } + RecurringDetails::PaymentMethodId(payment_method_id) => { + let payment_method_info = state + .store + .find_payment_method(payment_method_id) + .await + .to_not_found_response( + errors::ApiErrorResponse::PaymentMethodNotFound, + )?; + + ( + None, + Some(payment_method_info.payment_method), + payment_method_info.payment_method_type, + None, + None, + None, + Some(payment_method_info), + ) + } + }, + None => { + let mandate_id = request + .mandate_id + .clone() + .get_required_value("mandate_id")?; + let mandate_generic_data = get_token_for_recurring_mandate( + state, + request, + merchant_account, + merchant_key_store, + mandate_id, + ) + .await?; + ( + mandate_generic_data.token, + mandate_generic_data.payment_method, + mandate_generic_data + .payment_method_type + .or(request.payment_method_type), + None, + mandate_generic_data.recurring_mandate_payment_data, + mandate_generic_data.mandate_connector, + None, + ) + } + } } - None => Ok(( + None => ( request.payment_token.to_owned(), request.payment_method, request.payment_method_type, mandate_data, None, None, - )), - } + None, + ), + }; + Ok(MandateGenericData { + token: payment_token, + payment_method, + payment_method_type, + mandate_data, + recurring_mandate_payment_data: recurring_payment_data, + mandate_connector: mandate_connector_details, + payment_method_info, + }) } pub async fn get_token_for_recurring_mandate( @@ -474,15 +536,9 @@ pub async fn get_token_for_recurring_mandate( req: &api::PaymentsRequest, merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, -) -> RouterResult<( - Option, - Option, - Option, - Option, - Option, -)> { + mandate_id: String, +) -> RouterResult { let db = &*state.store; - let mandate_id = req.mandate_id.clone().get_required_value("mandate_id")?; let mandate = db .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, mandate_id.as_str()) @@ -566,29 +622,33 @@ pub async fn get_token_for_recurring_mandate( } }; - Ok(( - Some(token), - Some(payment_method.payment_method), - Some(payments::RecurringMandatePaymentData { + Ok(MandateGenericData { + token: Some(token), + payment_method: Some(payment_method.payment_method), + recurring_mandate_payment_data: Some(payments::RecurringMandatePaymentData { payment_method_type, original_payment_authorized_amount, original_payment_authorized_currency, }), - payment_method.payment_method_type, - Some(mandate_connector_details), - )) + payment_method_type: payment_method.payment_method_type, + mandate_connector: Some(mandate_connector_details), + mandate_data: None, + payment_method_info: None, + }) } else { - Ok(( - None, - Some(payment_method.payment_method), - Some(payments::RecurringMandatePaymentData { + Ok(MandateGenericData { + token: None, + payment_method: Some(payment_method.payment_method), + recurring_mandate_payment_data: Some(payments::RecurringMandatePaymentData { payment_method_type, original_payment_authorized_amount, original_payment_authorized_currency, }), - payment_method.payment_method_type, - Some(mandate_connector_details), - )) + payment_method_type: payment_method.payment_method_type, + mandate_connector: Some(mandate_connector_details), + mandate_data: None, + payment_method_info: None, + }) } } @@ -792,7 +852,7 @@ pub fn validate_mandate( let req: api::MandateValidationFields = req.into(); match req.validate_and_get_mandate_type().change_context( errors::ApiErrorResponse::MandateValidationFailed { - reason: "Expected one out of mandate_id and mandate_data but got both".into(), + reason: "Expected one out of recurring_details and mandate_data but got both".into(), }, )? { Some(api::MandateTransactionType::NewMandateTransaction) => { @@ -809,6 +869,23 @@ pub fn validate_mandate( } } +pub fn validate_recurring_details_and_token( + recurring_details: &Option, + payment_token: &Option, +) -> CustomResult<(), errors::ApiErrorResponse> { + utils::when( + recurring_details.is_some() && payment_token.is_some(), + || { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "Expected one out of recurring_details and payment_token but got both" + .into() + })) + }, + )?; + + Ok(()) +} + fn validate_new_mandate_request( req: api::MandateValidationFields, is_confirm_operation: bool, @@ -940,7 +1017,8 @@ pub fn create_complete_authorize_url( } fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> { - req.mandate_id.check_value_present("mandate_id")?; + req.recurring_details + .check_value_present("recurring_details")?; req.customer_id.check_value_present("customer_id")?; @@ -1950,7 +2028,8 @@ pub(crate) fn validate_payment_method_fields_present( utils::when( req.payment_method.is_some() && req.payment_method_data.is_none() - && req.payment_token.is_none(), + && req.payment_token.is_none() + && req.recurring_details.is_none(), || { Err(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data", diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 65b856364cc5..a2132bed8c1e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -178,6 +178,7 @@ impl authorizations: vec![], frm_metadata: None, authentication: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index b25c0e2b9090..ab5feb5d9b56 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -187,6 +187,7 @@ impl authorizations: vec![], frm_metadata: None, authentication: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 47d339f15be3..4445589f42f8 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -231,6 +231,7 @@ impl authorizations: vec![], frm_metadata: None, authentication: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { 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 8a773904ea32..b788ebc95f72 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -10,6 +10,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, + mandate::helpers::MandateGenericData, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, utils as core_utils, @@ -71,17 +72,18 @@ impl "confirm", )?; - let ( + let MandateGenericData { token, payment_method, payment_method_type, - setup_mandate, + mandate_data, recurring_mandate_payment_data, mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( + payment_method_info, + } = helpers::get_token_pm_type_mandate_details( state, request, - mandate_type.clone(), + mandate_type.to_owned(), merchant_account, key_store, ) @@ -225,7 +227,7 @@ impl payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(Into::into); + let setup_mandate = mandate_data.map(Into::into); let mandate_details_present = payment_attempt.mandate_details.is_some() || request.mandate_data.is_some(); @@ -270,7 +272,7 @@ impl .payment_method_data .as_ref() .map(|pmd| pmd.payment_method_data.clone()), - payment_method_info: None, + payment_method_info, force_sync: None, refunds: vec![], disputes: vec![], @@ -291,6 +293,7 @@ impl authorizations: vec![], authentication: None, frm_metadata: None, + recurring_details: request.recurring_details.clone(), }; let customer_details = Some(CustomerDetails { @@ -455,6 +458,11 @@ impl ValidateRequest .setup_future_usage .or(payment_intent.setup_future_usage); - let ( + let MandateGenericData { token, payment_method, payment_method_type, - mut setup_mandate, + mandate_data, recurring_mandate_payment_data, mandate_connector, - ) = mandate_details; + payment_method_info, + } = mandate_details; let browser_info = request .browser_info @@ -406,7 +408,7 @@ impl (Some(token_data), payment_method_info) } else { - (None, None) + (None, payment_method_info) }; payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); @@ -478,7 +480,7 @@ impl .or(payment_attempt.business_sub_label); // The operation merges mandate data from both request and payment_attempt - setup_mandate = setup_mandate.map(|mut sm| { + let setup_mandate = mandate_data.map(|mut sm| { sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm.update_mandate_id = payment_attempt .mandate_data @@ -619,6 +621,7 @@ impl authorizations: vec![], frm_metadata: request.frm_metadata.clone(), authentication, + recurring_details: request.recurring_details.clone(), }; let get_trackers_response = operations::GetTrackerResponse { @@ -1199,6 +1202,11 @@ impl ValidateRequest })? }; - let ( + let MandateGenericData { token, payment_method, payment_method_type, - setup_mandate, + mandate_data, recurring_mandate_payment_data, mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( + payment_method_info, + } = helpers::get_token_pm_type_mandate_details( state, request, mandate_type, @@ -291,6 +293,14 @@ impl let mandate_id = request .mandate_id .as_ref() + .or_else(|| { + request.recurring_details + .as_ref() + .and_then(|recurring_details| match recurring_details { + RecurringDetails::MandateId(id) => Some(id), + _ => None, + }) + }) .async_and_then(|mandate_id| async { let mandate = db .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id) @@ -359,7 +369,7 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(MandateData::from); + let setup_mandate = mandate_data.map(MandateData::from); let customer_acceptance = request.customer_acceptance.clone().map(From::from); @@ -398,7 +408,7 @@ impl token_data: None, confirm: request.confirm, payment_method_data: payment_method_data_after_card_bin_call, - payment_method_info: None, + payment_method_info, refunds: vec![], disputes: vec![], attempts: None, @@ -419,6 +429,7 @@ impl authorizations: vec![], authentication: None, frm_metadata: request.frm_metadata.clone(), + recurring_details: request.recurring_details.clone(), }; let get_trackers_response = operations::GetTrackerResponse { @@ -676,6 +687,11 @@ impl ValidateRequest authorizations: vec![], authentication: None, frm_metadata: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 805b5fb36306..25b419e3222f 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -199,6 +199,7 @@ impl authorizations: vec![], authentication: None, frm_metadata: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 520336be0e1a..465a2e5818dc 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -186,6 +186,7 @@ impl authorizations: vec![], authentication: None, frm_metadata: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 101d997db217..4d6161469089 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -462,6 +462,7 @@ async fn get_tracker_for_sync< authorizations, authentication, frm_metadata: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 2a0b99bc54b5..684f7c4b6983 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -1,6 +1,8 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payments::RequestSurchargeDetails}; +use api_models::{ + enums::FrmSuggestion, mandates::RecurringDetails, payments::RequestSurchargeDetails, +}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use error_stack::{report, IntoReport, ResultExt}; @@ -11,6 +13,7 @@ use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, Valida use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, + mandate::helpers::MandateGenericData, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, utils as core_utils, @@ -94,17 +97,19 @@ impl )?; helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; - let ( + + let MandateGenericData { token, payment_method, payment_method_type, - setup_mandate, + mandate_data, recurring_mandate_payment_data, mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( + payment_method_info, + } = helpers::get_token_pm_type_mandate_details( state, request, - mandate_type.clone(), + mandate_type.to_owned(), merchant_account, key_store, ) @@ -263,6 +268,14 @@ impl let mandate_id = request .mandate_id .as_ref() + .or_else(|| { + request.recurring_details + .as_ref() + .and_then(|recurring_details| match recurring_details { + RecurringDetails::MandateId(id) => Some(id), + _ => None, + }) + }) .async_and_then(|mandate_id| async { let mandate = db .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id) @@ -357,7 +370,7 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(Into::into); + let setup_mandate = mandate_data.map(Into::into); let mandate_details_present = payment_attempt.mandate_details.is_some() || request.mandate_data.is_some(); helpers::validate_mandate_data_and_future_usage( @@ -405,7 +418,7 @@ impl .payment_method_data .as_ref() .map(|pmd| pmd.payment_method_data.clone()), - payment_method_info: None, + payment_method_info, force_sync: None, refunds: vec![], disputes: vec![], @@ -426,6 +439,7 @@ impl authorizations: vec![], authentication: None, frm_metadata: request.frm_metadata.clone(), + recurring_details: request.recurring_details.clone(), }; let get_trackers_response = operations::GetTrackerResponse { @@ -737,6 +751,11 @@ impl ValidateRequest authorizations: vec![], authentication: None, frm_metadata: None, + recurring_details: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 8e13f6c99353..d465300dd61b 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -115,10 +115,11 @@ impl MandateValidationFieldsExt for MandateValidationFields { fn validate_and_get_mandate_type( &self, ) -> errors::CustomResult, errors::ValidationError> { - match (&self.mandate_data, &self.mandate_id) { + match (&self.mandate_data, &self.recurring_details) { (None, None) => Ok(None), (Some(_), Some(_)) => Err(errors::ValidationError::InvalidValue { - message: "Expected one out of mandate_id and mandate_data but got both".to_string(), + message: "Expected one out of recurring_details and mandate_data but got both" + .to_string(), }) .into_report(), (_, Some(_)) => Ok(Some(MandateTransactionType::RecurringMandateTransaction)), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 2005f9c11e01..fa9c7435632d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -13334,6 +13334,14 @@ "description": "Whether to perform external authentication (if applicable)", "example": true, "nullable": true + }, + "recurring_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RecurringDetails" + } + ], + "nullable": true } } }, @@ -13726,6 +13734,14 @@ "description": "Whether to perform external authentication (if applicable)", "example": true, "nullable": true + }, + "recurring_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RecurringDetails" + } + ], + "nullable": true } } }, @@ -14220,6 +14236,14 @@ "description": "Whether to perform external authentication (if applicable)", "example": true, "nullable": true + }, + "recurring_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RecurringDetails" + } + ], + "nullable": true } } }, @@ -15220,6 +15244,14 @@ "description": "Whether to perform external authentication (if applicable)", "example": true, "nullable": true + }, + "recurring_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RecurringDetails" + } + ], + "nullable": true } } }, @@ -16129,6 +16161,49 @@ "disabled" ] }, + "RecurringDetails": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "mandate_id" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "payment_method_id" + ] + }, + "data": { + "type": "string" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "RedirectResponse": { "type": "object", "properties": {