feat(router): add routing support for token-based mit payments (#4012)

This commit is contained in:
Shanks
2024-03-11 15:26:07 +05:30
committed by GitHub
parent 5584f1131a
commit 43ebfbc47f
27 changed files with 429 additions and 180 deletions

View File

@ -740,7 +740,7 @@ pub enum MandateTransactionType {
#[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MandateIds {
pub mandate_id: String,
pub mandate_id: Option<String>,
pub mandate_reference_id: Option<MandateReferenceId>,
}
@ -767,7 +767,7 @@ pub struct UpdateHistory {
impl MandateIds {
pub fn new(mandate_id: String) -> Self {
Self {
mandate_id,
mandate_id: Some(mandate_id),
mandate_reference_id: None,
}
}

View File

@ -139,7 +139,7 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP
.request
.mandate_id
.as_ref()
.map(|ids| ids.mandate_id.clone()),
.and_then(|ids| ids.mandate_id.clone()),
// [#595[FEATURE] Pass Mandate history information in payment flows/request]
installments: item
.router_data

View File

@ -326,48 +326,52 @@ where
Err(_) => {}
Ok(_) => match resp.request.get_mandate_id() {
Some(mandate_id) => {
let mandate_id = &mandate_id.mandate_id;
let mandate = state
.store
.find_mandate_by_merchant_id_mandate_id(resp.merchant_id.as_ref(), mandate_id)
.await
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
let mandate = match mandate.mandate_type {
storage_enums::MandateType::SingleUse => state
if let Some(ref mandate_id) = mandate_id.mandate_id {
let mandate = state
.store
.update_mandate_by_merchant_id_mandate_id(
&resp.merchant_id,
.find_mandate_by_merchant_id_mandate_id(
resp.merchant_id.as_ref(),
mandate_id,
storage::MandateUpdate::StatusUpdate {
mandate_status: storage_enums::MandateStatus::Revoked,
},
)
.await
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
storage_enums::MandateType::MultiUse => state
.store
.update_mandate_by_merchant_id_mandate_id(
&resp.merchant_id,
mandate_id,
storage::MandateUpdate::CaptureAmountUpdate {
amount_captured: Some(
mandate.amount_captured.unwrap_or(0)
+ resp.request.get_amount(),
),
},
)
.await
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
}?;
metrics::SUBSEQUENT_MANDATE_PAYMENT.add(
&metrics::CONTEXT,
1,
&[metrics::request::add_attributes(
"connector",
mandate.connector,
)],
);
resp.payment_method_id = Some(mandate.payment_method_id);
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
let mandate = match mandate.mandate_type {
storage_enums::MandateType::SingleUse => state
.store
.update_mandate_by_merchant_id_mandate_id(
&resp.merchant_id,
mandate_id,
storage::MandateUpdate::StatusUpdate {
mandate_status: storage_enums::MandateStatus::Revoked,
},
)
.await
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
storage_enums::MandateType::MultiUse => state
.store
.update_mandate_by_merchant_id_mandate_id(
&resp.merchant_id,
mandate_id,
storage::MandateUpdate::CaptureAmountUpdate {
amount_captured: Some(
mandate.amount_captured.unwrap_or(0)
+ resp.request.get_amount(),
),
},
)
.await
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
}?;
metrics::SUBSEQUENT_MANDATE_PAYMENT.add(
&metrics::CONTEXT,
1,
&[metrics::request::add_attributes(
"connector",
mandate.connector,
)],
);
resp.payment_method_id = Some(mandate.payment_method_id);
}
}
None => {
if resp.request.get_setup_mandate_details().is_some() {
@ -409,7 +413,7 @@ where
logger::debug!("{:?}", new_mandate_data);
resp.request
.set_mandate_id(Some(api_models::payments::MandateIds {
mandate_id: new_mandate_data.mandate_id.clone(),
mandate_id: Some(new_mandate_data.mandate_id.clone()),
mandate_reference_id: new_mandate_data
.connector_mandate_ids
.clone()

View File

@ -13,7 +13,10 @@ pub mod types;
use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter};
use api_models::{self, enums, payments::HeaderPayload};
use api_models::{
self, enums,
payments::{self as payments_api, HeaderPayload},
};
use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge};
use data_models::mandates::{CustomerAcceptance, MandateData};
use diesel_models::{ephemeral_key, fraud_check::FraudCheck};
@ -2061,9 +2064,11 @@ where
pub customer_acceptance: Option<CustomerAcceptance>,
pub address: PaymentAddress,
pub token: Option<String>,
pub token_data: Option<storage::PaymentTokenData>,
pub confirm: Option<bool>,
pub force_sync: Option<bool>,
pub payment_method_data: Option<api::PaymentMethodData>,
pub payment_method_info: Option<storage::PaymentMethod>,
pub refunds: Vec<storage::Refund>,
pub disputes: Vec<storage::Dispute>,
pub attempts: Option<Vec<storage::PaymentAttempt>>,
@ -2652,7 +2657,10 @@ where
.as_ref()
.zip(payment_data.payment_attempt.payment_method_type.as_ref())
{
if let Some(choice) = pre_routing_results.get(storage_pm_type) {
if let (Some(choice), None) = (
pre_routing_results.get(storage_pm_type),
&payment_data.token_data,
) {
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&choice.connector.to_string(),
@ -2687,6 +2695,12 @@ where
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
#[cfg(feature = "business_profile_routing")]
let profile_id = payment_data.payment_intent.profile_id.clone();
#[cfg(not(feature = "business_profile_routing"))]
let _profile_id: Option<String> = None;
connectors = routing::perform_eligibility_analysis_with_fallback(
&state.clone(),
key_store,
@ -2695,20 +2709,13 @@ where
&TransactionData::Payment(payment_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
payment_data.payment_intent.profile_id.clone(),
profile_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.into_report()
.attach_printable("Empty connector list returned")?
.clone();
let connector_data = connectors
.into_iter()
.map(|conn| {
@ -2726,17 +2733,11 @@ where
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label.clone();
}
routing_data.routing_info.algorithm = Some(routing_algorithm);
return Ok(api::ConnectorCallType::Retryable(connector_data));
return decide_connector_for_token_based_mit_flow(
payment_data,
routing_data,
connector_data,
);
}
if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm {
@ -2748,6 +2749,12 @@ where
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
#[cfg(feature = "business_profile_routing")]
let profile_id = payment_data.payment_intent.profile_id.clone();
#[cfg(not(feature = "business_profile_routing"))]
let _profile_id: Option<String> = None;
connectors = routing::perform_eligibility_analysis_with_fallback(
&state,
key_store,
@ -2756,20 +2763,13 @@ where
&TransactionData::Payment(payment_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
payment_data.payment_intent.profile_id.clone(),
profile_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.into_report()
.attach_printable("Empty connector list returned")?
.clone();
let connector_data = connectors
.into_iter()
.map(|conn| {
@ -2787,16 +2787,11 @@ where
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label;
}
return Ok(api::ConnectorCallType::Retryable(connector_data));
return decide_connector_for_token_based_mit_flow(
payment_data,
routing_data,
connector_data,
);
}
route_connector_v1(
@ -2804,13 +2799,107 @@ where
merchant_account,
business_profile,
key_store,
&TransactionData::Payment(payment_data),
TransactionData::Payment(payment_data),
routing_data,
eligible_connectors,
)
.await
}
pub fn decide_connector_for_token_based_mit_flow<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");
let payment_method_info = payment_data
.payment_method_info
.as_ref()
.get_required_value("payment_method_info")?;
let connector_mandate_details = payment_method_info
.connector_mandate_details
.clone()
.map(|details| {
details.parse_value::<storage::PaymentsMandateReference>("connector_mandate_details")
})
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize connector mandate details")?
.get_required_value("connector_mandate_details")
.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 (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();
}
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;
}
Ok(api::ConnectorCallType::Retryable(connectors))
}
}
pub fn should_add_task_to_process_tracker<F: Clone>(payment_data: &PaymentData<F>) -> bool {
let connector = payment_data.payment_attempt.connector.as_deref();
@ -2940,7 +3029,7 @@ pub async fn route_connector_v1<F>(
merchant_account: &domain::MerchantAccount,
business_profile: &storage::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
transaction_data: &TransactionData<'_, F>,
transaction_data: TransactionData<'_, F>,
routing_data: &mut storage::RoutingData,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
) -> RouterResult<ConnectorCallType>
@ -2948,7 +3037,7 @@ where
F: Send + Clone,
{
#[allow(unused_variables)]
let (profile_id, routing_algorithm) = match transaction_data {
let (profile_id, routing_algorithm) = match &transaction_data {
TransactionData::Payment(payment_data) => {
if cfg!(feature = "business_profile_routing") {
(
@ -2983,7 +3072,7 @@ where
state,
&merchant_account.merchant_id,
algorithm_ref,
transaction_data,
&transaction_data,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
@ -2993,7 +3082,7 @@ where
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
transaction_data,
&transaction_data,
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
profile_id,
@ -3002,6 +3091,7 @@ where
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
#[cfg(feature = "payouts")]
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
@ -3009,17 +3099,6 @@ where
.attach_printable("Empty connector list returned")?
.clone();
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label;
}
let connector_data = connectors
.into_iter()
.map(|conn| {
@ -3037,7 +3116,27 @@ where
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
Ok(ConnectorCallType::Retryable(connector_data))
match transaction_data {
TransactionData::Payment(payment_data) => {
decide_connector_for_token_based_mit_flow(payment_data, routing_data, connector_data)
}
#[cfg(feature = "payouts")]
TransactionData::Payout(_) => {
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label;
}
Ok(ConnectorCallType::Retryable(connector_data))
}
}
}
#[instrument(skip_all)]

View File

@ -100,6 +100,8 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
merchant_account,
self.request.payment_method_type,
key_store,
Some(resp.request.amount),
Some(resp.request.currency),
))
.await?;
@ -134,6 +136,8 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
&merchant_account,
self.request.payment_method_type,
&key_store,
Some(resp.request.amount),
Some(resp.request.currency),
))
.await;

View File

@ -107,6 +107,8 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
merchant_account,
self.request.payment_method_type,
key_store,
resp.request.amount,
Some(resp.request.currency),
))
.await?;
@ -244,6 +246,8 @@ impl types::SetupMandateRouterData {
merchant_account,
payment_method_type,
key_store,
resp.request.amount,
Some(resp.request.currency),
))
.await?;
@ -330,6 +334,8 @@ impl types::SetupMandateRouterData {
merchant_account,
self.request.payment_method_type,
key_store,
resp.request.amount,
Some(resp.request.currency),
))
.await?
.0;

View File

@ -1602,6 +1602,90 @@ pub async fn retrieve_card_with_permanent_token(
Ok(api::PaymentMethodData::Card(api_card))
}
pub async fn retrieve_payment_method_from_db_with_token_data(
state: &AppState,
token_data: &storage::PaymentTokenData,
) -> RouterResult<Option<storage::PaymentMethod>> {
match token_data {
storage::PaymentTokenData::PermanentCard(data) => {
if let Some(ref payment_method_id) = data.payment_method_id {
state
.store
.find_payment_method(payment_method_id)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)
.attach_printable("error retrieving payment method from DB")
.map(Some)
} else {
Ok(None)
}
}
storage::PaymentTokenData::WalletToken(data) => state
.store
.find_payment_method(&data.payment_method_id)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)
.attach_printable("error retrieveing payment method from DB")
.map(Some),
storage::PaymentTokenData::Temporary(_)
| storage::PaymentTokenData::TemporaryGeneric(_)
| storage::PaymentTokenData::Permanent(_)
| storage::PaymentTokenData::AuthBankDebit(_) => Ok(None),
}
}
pub async fn retrieve_payment_token_data(
state: &AppState,
token: String,
payment_method: Option<storage_enums::PaymentMethod>,
) -> RouterResult<storage::PaymentTokenData> {
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let key = format!(
"pm_token_{}_{}_hyperswitch",
token,
payment_method.get_required_value("payment_method")?
);
let token_data_string = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?
.ok_or(error_stack::Report::new(
errors::ApiErrorResponse::UnprocessableEntity {
message: "Token is invalid or expired".to_owned(),
},
))?;
let token_data_result = token_data_string
.clone()
.parse_struct("PaymentTokenData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to deserialize hyperswitch token data");
let token_data = match token_data_result {
Ok(data) => data,
Err(e) => {
// The purpose of this logic is backwards compatibility to support tokens
// in redis that might be following the old format.
if token_data_string.starts_with('{') {
return Err(e);
} else {
storage::PaymentTokenData::temporary_generic(token_data_string)
}
}
};
Ok(token_data)
}
pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
operation: BoxedOperation<'a, F, R, Ctx>,
state: &'a AppState,
@ -1630,72 +1714,13 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
}
}
let token = payment_data.token.clone();
let hyperswitch_token = match payment_data.mandate_id {
Some(_) => token.map(storage::PaymentTokenData::temporary_generic),
None => {
if let Some(token) = token {
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let key = format!(
"pm_token_{}_{}_hyperswitch",
token,
payment_data
.payment_attempt
.payment_method
.to_owned()
.get_required_value("payment_method")?,
);
let token_data_string = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?
.ok_or(error_stack::Report::new(
errors::ApiErrorResponse::UnprocessableEntity {
message: "Token is invalid or expired".to_owned(),
},
))?;
let token_data_result = token_data_string
.clone()
.parse_struct("PaymentTokenData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to deserialize hyperswitch token data");
let token_data = match token_data_result {
Ok(data) => data,
Err(e) => {
// The purpose of this logic is backwards compatibility to support tokens
// in redis that might be following the old format.
if token_data_string.starts_with('{') {
return Err(e);
} else {
storage::PaymentTokenData::temporary_generic(token_data_string)
}
}
};
Some(token_data)
} else {
None
}
}
};
// TODO: Handle case where payment method and token both are present in request properly.
let (payment_method, pm_id) = match (request, hyperswitch_token) {
let (payment_method, pm_id) = match (request, payment_data.token_data.as_ref()) {
(_, Some(hyperswitch_token)) => {
let pm_data = Ctx::retrieve_payment_method_with_token(
state,
merchant_key_store,
&hyperswitch_token,
hyperswitch_token,
&payment_data.payment_intent,
card_token_data.as_ref(),
customer,

View File

@ -149,6 +149,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -158,6 +159,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -157,6 +157,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -166,6 +167,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -202,6 +202,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: payments::PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -211,6 +212,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
refunds: vec![],
disputes: vec![],
attempts: None,

View File

@ -131,6 +131,12 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
}
}
let token_data = if let Some(token) = token.clone() {
Some(helpers::retrieve_payment_token_data(state, token, payment_method).await?)
} else {
None
};
payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method);
payment_attempt.browser_info = browser_info;
payment_attempt.payment_method_type =
@ -247,6 +253,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate,
customer_acceptance: None,
token,
token_data,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -259,6 +266,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
.payment_method_data
.as_ref()
.map(|pmd| pmd.payment_method_data.clone()),
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -383,6 +383,23 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
&token,
)?;
let (token_data, payment_method_info) = if let Some(token) = token.clone() {
let token_data = helpers::retrieve_payment_token_data(
state,
token,
payment_method.or(payment_attempt.payment_method),
)
.await?;
let payment_method_info =
helpers::retrieve_payment_method_from_db_with_token_data(state, &token_data)
.await?;
(Some(token_data), payment_method_info)
} else {
(None, None)
};
payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method);
payment_attempt.browser_info = browser_info;
payment_attempt.payment_method_type =
@ -548,6 +565,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate,
customer_acceptance,
token,
token_data,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -557,6 +575,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: request.confirm,
payment_method_data: payment_method_data_after_card_bin_call,
payment_method_info,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -302,7 +302,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
mandate_obj.connector_mandate_ids,
) {
(Some(network_tx_id), _) => Ok(api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(mandate_obj.mandate_id),
mandate_reference_id: Some(
api_models::payments::MandateReferenceId::NetworkMandateId(
network_tx_id,
@ -314,7 +314,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
.change_context(errors::ApiErrorResponse::MandateNotFound)
.map(|connector_id: api_models::payments::ConnectorMandateReferenceId| {
api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(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,
@ -325,7 +325,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
}
}),
(_, _) => Ok(api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(mandate_obj.mandate_id),
mandate_reference_id: None,
}),
}
@ -390,6 +390,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate,
customer_acceptance,
token,
token_data: None,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -399,6 +400,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: request.confirm,
payment_method_data: payment_method_data_after_card_bin_call,
payment_method_info: None,
refunds: vec![],
disputes: vec![],
attempts: None,

View File

@ -145,6 +145,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -154,6 +155,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -654,7 +654,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
mandate_id: payment_data
.mandate_id
.clone()
.map(|mandate| mandate.mandate_id),
.and_then(|mandate| mandate.mandate_id),
connector_metadata,
payment_token: None,
error_code: error_status.clone(),
@ -844,7 +844,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
.or(payment_data
.mandate_id
.clone()
.map(|mandate_ids| mandate_ids.mandate_id));
.and_then(|mandate_ids| mandate_ids.mandate_id));
let m_router_data_response = router_data.response.clone();
let mandate_update_fut = tokio::spawn(
async move {

View File

@ -169,6 +169,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
mandate_connector: None,
customer_acceptance: None,
token: None,
token_data: None,
setup_mandate: None,
address: payments::PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
@ -179,6 +180,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -114,6 +114,15 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
)
.await?;
let token_data = if let Some(token) = payment_attempt.payment_token.clone() {
Some(
helpers::retrieve_payment_token_data(state, token, payment_attempt.payment_method)
.await?,
)
} else {
None
};
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
@ -147,6 +156,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: payment_attempt.payment_token.clone(),
token_data,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -157,6 +167,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
confirm: Some(payment_attempt.confirm),
payment_attempt,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -410,13 +410,14 @@ async fn get_tracker_for_sync<
.mandate_id
.clone()
.map(|id| api_models::payments::MandateIds {
mandate_id: id,
mandate_id: Some(id),
mandate_reference_id: None,
}),
mandate_connector: None,
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: PaymentAddress {
shipping: shipping_address.as_ref().map(|a| a.into()),
billing: billing_address.as_ref().map(|a| a.into()),
@ -426,6 +427,7 @@ async fn get_tracker_for_sync<
},
confirm: Some(request.force_sync),
payment_method_data: None,
payment_method_info: None,
force_sync: Some(
request.force_sync
&& (helpers::check_force_psync_precondition(&payment_attempt.status)

View File

@ -254,6 +254,12 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
)?;
}
let token_data = if let Some(token) = token.clone() {
Some(helpers::retrieve_payment_token_data(state, token, payment_method).await?)
} else {
None
};
let mandate_id = request
.mandate_id
.as_ref()
@ -268,7 +274,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
mandate_obj.connector_mandate_ids,
) {
(Some(network_tx_id), _) => Ok(api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(mandate_obj.mandate_id),
mandate_reference_id: Some(
api_models::payments::MandateReferenceId::NetworkMandateId(
network_tx_id,
@ -280,14 +286,14 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
.change_context(errors::ApiErrorResponse::MandateNotFound)
.map(|connector_id: api_models::payments::ConnectorMandateReferenceId| {
api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(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, update_history: None },
))
}
}),
(_, _) => Ok(api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_id: Some(mandate_obj.mandate_id),
mandate_reference_id: None,
}),
}
@ -383,6 +389,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
mandate_id,
mandate_connector,
token,
token_data,
setup_mandate,
customer_acceptance,
address: PaymentAddress {
@ -395,6 +402,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
.payment_method_data
.as_ref()
.map(|pmd| pmd.payment_method_data.clone()),
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -122,6 +122,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
setup_mandate: None,
customer_acceptance: None,
token: None,
token_data: None,
address: PaymentAddress {
billing: None,
shipping: None,
@ -129,6 +130,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
},
confirm: None,
payment_method_data: None,
payment_method_info: None,
force_sync: None,
refunds: vec![],
disputes: vec![],

View File

@ -377,7 +377,7 @@ where
mandate_id: payment_data
.mandate_id
.clone()
.map(|mandate| mandate.mandate_id),
.and_then(|mandate| mandate.mandate_id),
connector_metadata,
payment_token: None,
error_code: None,

View File

@ -39,6 +39,8 @@ pub async fn save_payment_method<F: Clone, FData>(
merchant_account: &domain::MerchantAccount,
payment_method_type: Option<storage_enums::PaymentMethodType>,
key_store: &domain::MerchantKeyStore,
amount: Option<i64>,
currency: Option<storage_enums::Currency>,
) -> RouterResult<(Option<String>, Option<common_enums::PaymentMethodStatus>)>
where
FData: mandate::MandateBehaviour,
@ -94,7 +96,13 @@ where
.map(|future_usage| future_usage == storage_enums::FutureUsage::OffSession)
.unwrap_or(false)
{
add_connector_mandate_details_in_payment_method(responses, connector)
add_connector_mandate_details_in_payment_method(
responses,
payment_method_type,
amount,
currency,
connector,
)
} else {
None
}
@ -648,6 +656,9 @@ pub async fn add_payment_method_token<F: Clone, T: types::Tokenizable + Clone>(
fn add_connector_mandate_details_in_payment_method(
resp: types::PaymentsResponseData,
payment_method_type: Option<storage_enums::PaymentMethodType>,
authorized_amount: Option<i64>,
authorized_currency: Option<storage_enums::Currency>,
connector: &api::ConnectorData,
) -> Option<storage::PaymentsMandateReference> {
let mut mandate_details = HashMap::new();
@ -665,8 +676,20 @@ fn add_connector_mandate_details_in_payment_method(
_ => None,
};
if let Some(mca_id) = connector.merchant_connector_id.clone() {
mandate_details.insert(mca_id, connector_mandate_id);
if let Some((mca_id, connector_mandate_id)) = connector
.merchant_connector_id
.clone()
.zip(connector_mandate_id)
{
mandate_details.insert(
mca_id,
storage::PaymentsMandateReferenceRecord {
connector_mandate_id,
payment_method_type,
original_payment_authorized_amount: authorized_amount,
original_payment_authorized_currency: authorized_currency,
},
);
Some(storage::PaymentsMandateReference(mandate_details))
} else {
None

View File

@ -327,7 +327,9 @@ where
phone: customer
.as_ref()
.and_then(|cus| cus.phone.as_ref().map(|s| s.to_owned())),
mandate_id: data.mandate_id.map(|mandate_ids| mandate_ids.mandate_id),
mandate_id: data
.mandate_id
.and_then(|mandate_ids| mandate_ids.mandate_id),
payment_method: data.payment_attempt.payment_method,
payment_method_data: payment_method_data_response,
payment_token: data.token,

View File

@ -600,7 +600,7 @@ pub async fn decide_payout_connector(
merchant_account,
&payout_data.business_profile,
key_store,
&TransactionData::<()>::Payout(payout_data),
TransactionData::<()>::Payout(payout_data),
routing_data,
eligible_connectors,
)

View File

@ -36,12 +36,11 @@ use crate::{core::errors, services::api as service_api, types::storage};
#[cfg(feature = "business_profile_routing")]
use crate::{errors, services::api as service_api};
#[derive(Clone)]
pub enum TransactionData<'a, F>
where
F: Clone,
{
Payment(&'a payments::PaymentData<F>),
Payment(&'a mut payments::PaymentData<F>),
#[cfg(feature = "payouts")]
Payout(&'a payouts::PayoutData),
}

View File

@ -1,4 +1,7 @@
use std::collections::HashMap;
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
};
use api_models::payment_methods;
use diesel_models::enums;
@ -40,7 +43,7 @@ pub struct WalletTokenData {
pub payment_method_id: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PaymentTokenData {
// The variants 'Temporary' and 'Permanent' are added for backwards compatibility
@ -76,4 +79,26 @@ impl PaymentTokenData {
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PaymentsMandateReference(pub HashMap<String, Option<String>>);
pub struct PaymentsMandateReferenceRecord {
pub connector_mandate_id: String,
pub payment_method_type: Option<common_enums::PaymentMethodType>,
pub original_payment_authorized_amount: Option<i64>,
pub original_payment_authorized_currency: Option<common_enums::Currency>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PaymentsMandateReference(pub HashMap<String, PaymentsMandateReferenceRecord>);
impl Deref for PaymentsMandateReference {
type Target = HashMap<String, PaymentsMandateReferenceRecord>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PaymentsMandateReference {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}