Skip to content

Commit

Permalink
feat(mandates): allow off-session payments using payment_method_id (#…
Browse files Browse the repository at this point in the history
…4132)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
Chethan-rao and hyperswitch-bot[bot] authored Mar 28, 2024
1 parent ffb3f4b commit 7b337ac
Show file tree
Hide file tree
Showing 20 changed files with 429 additions and 173 deletions.
7 changes: 7 additions & 0 deletions crates/api_models/src/mandates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,10 @@ pub struct MandateListConstraints {
#[serde(rename = "created_time.gte")]
pub created_time_gte: Option<PrimitiveDateTime>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum RecurringDetails {
MandateId(String),
PaymentMethodId(String),
}
21 changes: 14 additions & 7 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -459,6 +457,9 @@ pub struct PaymentsRequest {
/// Whether to perform external authentication (if applicable)
#[schema(example = true)]
pub request_external_three_ds_authentication: Option<bool>,

/// Details required for recurring payment
pub recurring_details: Option<RecurringDetails>,
}

impl PaymentsRequest {
Expand Down Expand Up @@ -3360,7 +3361,7 @@ pub struct PaymentsRedirectionResponse {
}

pub struct MandateValidationFields {
pub mandate_id: Option<String>,
pub recurring_details: Option<RecurringDetails>,
pub confirm: Option<bool>,
pub customer_id: Option<String>,
pub mandate_data: Option<MandateData>,
Expand All @@ -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
Expand All @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/openapi/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion crates/router/src/core/mandate/helpers.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -33,3 +39,14 @@ pub async fn get_profile_id_for_mandate(
}?;
Ok(profile_id)
}

#[derive(Clone)]
pub struct MandateGenericData {
pub token: Option<String>,
pub payment_method: Option<enums::PaymentMethod>,
pub payment_method_type: Option<enums::PaymentMethodType>,
pub mandate_data: Option<MandateData>,
pub recurring_mandate_payment_data: Option<payments::RecurringMandatePaymentData>,
pub mandate_connector: Option<payments::MandateConnectorDetails>,
pub payment_method_info: Option<diesel_models::PaymentMethod>,
}
148 changes: 79 additions & 69 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -2263,6 +2264,7 @@ where
pub authorizations: Vec<diesel_models::authorization::Authorization>,
pub authentication: Option<storage::Authentication>,
pub frm_metadata: Option<serde_json::Value>,
pub recurring_details: Option<RecurringDetails>,
}

#[derive(Debug, Default, Clone)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -2980,24 +2982,26 @@ where
.await
}

pub fn decide_connector_for_token_based_mit_flow<F: Clone>(
pub fn decide_multiplex_connector_for_normal_or_recurring_payment<F: Clone>(
payment_data: &mut PaymentData<F>,
routing_data: &mut storage::RoutingData,
connectors: Vec<api::ConnectorData>,
) -> RouterResult<ConnectorCallType> {
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| {
Expand All @@ -3010,67 +3014,69 @@ pub fn decide_connector_for_token_based_mit_flow<F: Clone>(
.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))
}
}
}

Expand Down Expand Up @@ -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")]
Expand Down
Loading

0 comments on commit 7b337ac

Please sign in to comment.