feat(core): add config for update_mandate_flow (#3542)

This commit is contained in:
Amisha Prabhat
2024-02-07 23:23:14 +05:30
committed by GitHub
parent ef302dd398
commit 14c0a2b03f
13 changed files with 231 additions and 93 deletions

View File

@ -391,6 +391,9 @@ bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} # Mandate supp
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" } wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" }
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"} # Update Mandate supported payment method type and connector for card
card.debit = {connector_list ="cybersource"} # Update Mandate supported payment method type and connector for card
# Required fields info used while listing the payment_method_data # Required fields info used while listing the payment_method_data
[required_fields.pay_later] # payment_method = "pay_later" [required_fields.pay_later] # payment_method = "pay_later"

View File

@ -120,6 +120,10 @@ wallet.paypal.connector_list = "adyen"
bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"}
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"}
card.debit = {connector_list ="cybersource"}
[multiple_api_version_supported_connectors] [multiple_api_version_supported_connectors]
supported_connectors = "braintree" supported_connectors = "braintree"

View File

@ -120,6 +120,10 @@ wallet.paypal.connector_list = "adyen"
bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"}
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"}
card.debit = {connector_list ="cybersource"}
[multiple_api_version_supported_connectors] [multiple_api_version_supported_connectors]
supported_connectors = "braintree" supported_connectors = "braintree"

View File

@ -120,6 +120,10 @@ wallet.paypal.connector_list = "adyen"
bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"}
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"}
card.debit = {connector_list ="cybersource"}
[multiple_api_version_supported_connectors] [multiple_api_version_supported_connectors]
supported_connectors = "braintree" supported_connectors = "braintree"

View File

@ -487,6 +487,10 @@ bank_debit.sepa = { connector_list = "gocardless"}
bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"}
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"}
card.debit = {connector_list ="cybersource"}
[connector_request_reference_id_config] [connector_request_reference_id_config]
merchant_ids_send_payment_id_as_connector_request_id = [] merchant_ids_send_payment_id_as_connector_request_id = []

View File

@ -1011,6 +1011,23 @@ impl PaymentMethodData {
self.to_owned() self.to_owned()
} }
} }
pub fn get_payment_method(&self) -> Option<api_enums::PaymentMethod> {
match self {
Self::Card(_) => Some(api_enums::PaymentMethod::Card),
Self::CardRedirect(_) => Some(api_enums::PaymentMethod::CardRedirect),
Self::Wallet(_) => Some(api_enums::PaymentMethod::Wallet),
Self::PayLater(_) => Some(api_enums::PaymentMethod::PayLater),
Self::BankRedirect(_) => Some(api_enums::PaymentMethod::BankRedirect),
Self::BankDebit(_) => Some(api_enums::PaymentMethod::BankDebit),
Self::BankTransfer(_) => Some(api_enums::PaymentMethod::BankTransfer),
Self::Crypto(_) => Some(api_enums::PaymentMethod::Crypto),
Self::Reward => Some(api_enums::PaymentMethod::Reward),
Self::Upi(_) => Some(api_enums::PaymentMethod::Upi),
Self::Voucher(_) => Some(api_enums::PaymentMethod::Voucher),
Self::GiftCard(_) => Some(api_enums::PaymentMethod::GiftCard),
Self::CardToken(_) | Self::MandatePayment => None,
}
}
} }
pub trait GetPaymentMethodType { pub trait GetPaymentMethodType {

View File

@ -209,6 +209,7 @@ impl Default for Mandates {
])), ])),
), ),
])), ])),
update_mandate_supported: SupportedPaymentMethodsForMandate(HashMap::default()),
} }
} }
} }

View File

@ -237,6 +237,7 @@ pub struct DummyConnector {
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Mandates { pub struct Mandates {
pub supported_payment_methods: SupportedPaymentMethodsForMandate, pub supported_payment_methods: SupportedPaymentMethodsForMandate,
pub update_mandate_supported: SupportedPaymentMethodsForMandate,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]

View File

@ -59,7 +59,6 @@ pub async fn revoke_mandate(
.find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &req.mandate_id) .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &req.mandate_id)
.await .await
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
let mandate_revoke_status = match mandate.mandate_status { let mandate_revoke_status = match mandate.mandate_status {
common_enums::MandateStatus::Active common_enums::MandateStatus::Active
| common_enums::MandateStatus::Inactive | common_enums::MandateStatus::Inactive

View File

@ -1219,6 +1219,7 @@ pub async fn list_payment_methods(
mca.connector_name.clone(), mca.connector_name.clone(),
pm_config_mapping, pm_config_mapping,
&state.conf.mandates.supported_payment_methods, &state.conf.mandates.supported_payment_methods,
&state.conf.mandates.update_mandate_supported,
) )
.await?; .await?;
} }
@ -1985,6 +1986,7 @@ pub async fn filter_payment_methods(
connector: String, connector: String,
config: &settings::ConnectorFilters, config: &settings::ConnectorFilters,
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate, supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,
supported_payment_methods_for_update_mandate: &settings::SupportedPaymentMethodsForMandate,
) -> errors::CustomResult<(), errors::ApiErrorResponse> { ) -> errors::CustomResult<(), errors::ApiErrorResponse> {
for payment_method in payment_methods.into_iter() { for payment_method in payment_methods.into_iter() {
let parse_result = serde_json::from_value::<PaymentMethodsEnabled>(payment_method); let parse_result = serde_json::from_value::<PaymentMethodsEnabled>(payment_method);
@ -2091,13 +2093,20 @@ pub async fn filter_payment_methods(
), ),
}; };
if mandate_type_present || update_mandate_id_present { if mandate_type_present {
filter_pm_based_on_supported_payments_for_mandate( filter_pm_based_on_supported_payments_for_mandate(
supported_payment_methods_for_mandate, supported_payment_methods_for_mandate,
&payment_method, &payment_method,
&payment_method_object.payment_method_type, &payment_method_object.payment_method_type,
connector_variant, connector_variant,
) )
} else if update_mandate_id_present {
filter_pm_based_on_update_mandate_support_for_connector(
supported_payment_methods_for_update_mandate,
&payment_method,
&payment_method_object.payment_method_type,
connector_variant,
)
} else { } else {
true true
} }
@ -2121,6 +2130,19 @@ pub async fn filter_payment_methods(
} }
Ok(()) Ok(())
} }
pub fn filter_pm_based_on_update_mandate_support_for_connector(
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,
payment_method: &api_enums::PaymentMethod,
payment_method_type: &api_enums::PaymentMethodType,
connector: api_enums::Connector,
) -> bool {
supported_payment_methods_for_mandate
.0
.get(payment_method)
.and_then(|payment_method_type_hm| payment_method_type_hm.0.get(payment_method_type))
.map(|supported_connectors| supported_connectors.connector_list.contains(&connector))
.unwrap_or(false)
}
fn filter_pm_based_on_supported_payments_for_mandate( fn filter_pm_based_on_supported_payments_for_mandate(
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate, supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,

View File

@ -3,9 +3,11 @@ use error_stack::{IntoReport, ResultExt};
use super::{ConstructFlowSpecificData, Feature}; use super::{ConstructFlowSpecificData, Feature};
use crate::{ use crate::{
configs::settings,
core::{ core::{
errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt}, errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt},
mandate, mandate,
payment_methods::cards,
payments::{ payments::{
self, access_token, customers, helpers, tokenization, transformers, PaymentData, self, access_token, customers, helpers, tokenization, transformers, PaymentData,
}, },
@ -60,106 +62,53 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
connector_request: Option<services::Request>, connector_request: Option<services::Request>,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> { ) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
> = connector.connector.get_connector_integration();
let resp = services::execute_connector_processing_step(
state,
connector_integration,
&self,
call_connector_action.clone(),
connector_request,
)
.await
.to_setup_mandate_failed_response()?;
let is_mandate = resp.request.setup_mandate_details.is_some();
let pm_id = Box::pin(tokenization::save_payment_method(
state,
connector,
resp.to_owned(),
maybe_customer,
merchant_account,
self.request.payment_method_type,
key_store,
is_mandate,
))
.await?;
if let Some(mandate_id) = self if let Some(mandate_id) = self
.request .request
.setup_mandate_details .setup_mandate_details
.as_ref() .as_ref()
.and_then(|mandate_data| mandate_data.update_mandate_id.clone()) .and_then(|mandate_data| mandate_data.update_mandate_id.clone())
{ {
let mandate = state self.update_mandate_flow(
.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, state,
merchant_account, merchant_account,
mandate.clone(), mandate_id,
connector,
key_store,
call_connector_action,
&state.conf.mandates.update_mandate_supported,
connector_request,
maybe_customer,
) )
.await?; .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 { } else {
let connector_integration: services::BoxedConnectorIntegration<
'_,
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
> = connector.connector.get_connector_integration();
let resp = services::execute_connector_processing_step(
state,
connector_integration,
&self,
call_connector_action.clone(),
connector_request,
)
.await
.to_setup_mandate_failed_response()?;
let is_mandate = resp.request.setup_mandate_details.is_some();
let pm_id = Box::pin(tokenization::save_payment_method(
state,
connector,
resp.to_owned(),
maybe_customer,
merchant_account,
self.request.payment_method_type,
key_store,
is_mandate,
))
.await?;
mandate::mandate_procedure( mandate::mandate_procedure(
state, state,
resp, resp,
@ -310,6 +259,130 @@ impl types::SetupMandateRouterData {
_ => Ok(self.clone()), _ => Ok(self.clone()),
} }
} }
async fn update_mandate_flow(
self,
state: &AppState,
merchant_account: &domain::MerchantAccount,
mandate_id: String,
connector: &api::ConnectorData,
key_store: &domain::MerchantKeyStore,
call_connector_action: payments::CallConnectorAction,
supported_connectors_for_update_mandate: &settings::SupportedPaymentMethodsForMandate,
connector_request: Option<services::Request>,
maybe_customer: &Option<domain::Customer>,
) -> RouterResult<Self> {
let payment_method_type = self.request.payment_method_type;
let payment_method = self.request.payment_method_data.get_payment_method();
let supported_connectors_config =
payment_method
.zip(payment_method_type)
.map_or(false, |(pm, pmt)| {
cards::filter_pm_based_on_update_mandate_support_for_connector(
supported_connectors_for_update_mandate,
&pm,
&pmt,
connector.connector_name,
)
});
if supported_connectors_config {
let connector_integration: services::BoxedConnectorIntegration<
'_,
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
> = connector.connector.get_connector_integration();
let resp = services::execute_connector_processing_step(
state,
connector_integration,
&self,
call_connector_action.clone(),
connector_request,
)
.await
.to_setup_mandate_failed_response()?;
let is_mandate = resp.request.setup_mandate_details.is_some();
let pm_id = Box::pin(tokenization::save_payment_method(
state,
connector,
resp.to_owned(),
maybe_customer,
merchant_account,
self.request.payment_method_type,
key_store,
is_mandate,
))
.await?;
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 {
Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Update Mandate Flow not implemented for the connector ")?
}
}
} }
impl mandate::MandateBehaviour for types::SetupMandateRequestData { impl mandate::MandateBehaviour for types::SetupMandateRequestData {

View File

@ -1342,7 +1342,6 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsRejectDa
}) })
} }
} }
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionData { impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionData {
type Error = error_stack::Report<errors::ApiErrorResponse>; type Error = error_stack::Report<errors::ApiErrorResponse>;

View File

@ -252,6 +252,13 @@ bank_debit.sepa = { connector_list = "gocardless"}
bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"}
bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"}
[mandates.update_mandate_supported]
card.credit ={connector_list ="cybersource"}
card.debit = {connector_list ="cybersource"}
[mandates.update_mandate_supported]
connector_list = "cybersource"
[analytics] [analytics]
source = "sqlx" source = "sqlx"