mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 09:38:33 +08:00
feat: Filter payment methods based on payment (#331)
This commit is contained in:
@ -71,7 +71,7 @@ pub struct ListPaymentMethodRequest {
|
|||||||
pub client_secret: Option<String>,
|
pub client_secret: Option<String>,
|
||||||
pub accepted_countries: Option<Vec<String>>,
|
pub accepted_countries: Option<Vec<String>>,
|
||||||
pub accepted_currencies: Option<Vec<api_enums::Currency>>,
|
pub accepted_currencies: Option<Vec<api_enums::Currency>>,
|
||||||
pub amount: Option<i32>,
|
pub amount: Option<i64>,
|
||||||
pub recurring_enabled: Option<bool>,
|
pub recurring_enabled: Option<bool>,
|
||||||
pub installment_payment_enabled: Option<bool>,
|
pub installment_payment_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
@ -174,8 +174,8 @@ pub struct ListPaymentMethodResponse {
|
|||||||
pub payment_schemes: Option<Vec<String>>,
|
pub payment_schemes: Option<Vec<String>>,
|
||||||
pub accepted_countries: Option<Vec<String>>,
|
pub accepted_countries: Option<Vec<String>>,
|
||||||
pub accepted_currencies: Option<Vec<api_enums::Currency>>,
|
pub accepted_currencies: Option<Vec<api_enums::Currency>>,
|
||||||
pub minimum_amount: Option<i32>,
|
pub minimum_amount: Option<i64>,
|
||||||
pub maximum_amount: Option<i32>,
|
pub maximum_amount: Option<i64>,
|
||||||
pub recurring_enabled: bool,
|
pub recurring_enabled: bool,
|
||||||
pub installment_payment_enabled: bool,
|
pub installment_payment_enabled: bool,
|
||||||
pub payment_experience: Option<Vec<PaymentExperience>>,
|
pub payment_experience: Option<Vec<PaymentExperience>>,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::collections;
|
use std::collections;
|
||||||
|
|
||||||
use common_utils::{consts, generate_id};
|
use common_utils::{consts, ext_traits::AsyncExt, generate_id};
|
||||||
use error_stack::{report, ResultExt};
|
use error_stack::{report, ResultExt};
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
@ -355,13 +355,35 @@ pub async fn list_payment_methods(
|
|||||||
merchant_account: storage::MerchantAccount,
|
merchant_account: storage::MerchantAccount,
|
||||||
mut req: api::ListPaymentMethodRequest,
|
mut req: api::ListPaymentMethodRequest,
|
||||||
) -> errors::RouterResponse<Vec<api::ListPaymentMethodResponse>> {
|
) -> errors::RouterResponse<Vec<api::ListPaymentMethodResponse>> {
|
||||||
helpers::verify_client_secret(
|
let payment_intent = helpers::verify_client_secret(
|
||||||
db,
|
db,
|
||||||
merchant_account.storage_scheme,
|
merchant_account.storage_scheme,
|
||||||
req.client_secret.clone(),
|
req.client_secret.clone(),
|
||||||
&merchant_account.merchant_id,
|
&merchant_account.merchant_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let address = payment_intent
|
||||||
|
.as_ref()
|
||||||
|
.async_map(|pi| async {
|
||||||
|
helpers::get_address_by_id(db, pi.billing_address_id.clone()).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.transpose()?
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let payment_attempt = payment_intent
|
||||||
|
.as_ref()
|
||||||
|
.async_map(|pi| async {
|
||||||
|
db.find_payment_attempt_by_payment_id_merchant_id(
|
||||||
|
&pi.payment_id,
|
||||||
|
&pi.merchant_id,
|
||||||
|
merchant_account.storage_scheme,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::PaymentNotFound)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let all_mcas = db
|
let all_mcas = db
|
||||||
.find_merchant_connector_account_by_merchant_id_list(&merchant_account.merchant_id)
|
.find_merchant_connector_account_by_merchant_id_list(&merchant_account.merchant_id)
|
||||||
@ -378,19 +400,31 @@ pub async fn list_payment_methods(
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
filter_payment_methods(payment_methods, &mut req, &mut response);
|
filter_payment_methods(
|
||||||
|
payment_methods,
|
||||||
|
&mut req,
|
||||||
|
&mut response,
|
||||||
|
payment_intent.as_ref(),
|
||||||
|
payment_attempt.as_ref(),
|
||||||
|
address.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
.is_empty()
|
.is_empty()
|
||||||
.then(|| Err(report!(errors::ApiErrorResponse::PaymentMethodNotFound)))
|
.then(|| Err(report!(errors::ApiErrorResponse::PaymentMethodNotFound)))
|
||||||
.unwrap_or(Ok(services::ApplicationResponse::Json(response)))
|
.unwrap_or(Ok(services::ApplicationResponse::Json(response)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_payment_methods(
|
async fn filter_payment_methods(
|
||||||
payment_methods: Vec<serde_json::Value>,
|
payment_methods: Vec<serde_json::Value>,
|
||||||
req: &mut api::ListPaymentMethodRequest,
|
req: &mut api::ListPaymentMethodRequest,
|
||||||
resp: &mut Vec<api::ListPaymentMethodResponse>,
|
resp: &mut Vec<api::ListPaymentMethodResponse>,
|
||||||
) {
|
payment_intent: Option<&storage::PaymentIntent>,
|
||||||
|
payment_attempt: Option<&storage::PaymentAttempt>,
|
||||||
|
address: Option<&storage::Address>,
|
||||||
|
) -> errors::CustomResult<(), errors::ApiErrorResponse> {
|
||||||
for payment_method in payment_methods.into_iter() {
|
for payment_method in payment_methods.into_iter() {
|
||||||
if let Ok(payment_method_object) =
|
if let Ok(payment_method_object) =
|
||||||
serde_json::from_value::<api::ListPaymentMethodResponse>(payment_method)
|
serde_json::from_value::<api::ListPaymentMethodResponse>(payment_method)
|
||||||
@ -420,13 +454,23 @@ fn filter_payment_methods(
|
|||||||
&payment_method_object.accepted_currencies,
|
&payment_method_object.accepted_currencies,
|
||||||
&req.accepted_currencies,
|
&req.accepted_currencies,
|
||||||
);
|
);
|
||||||
|
let filter3 = if let Some(payment_intent) = payment_intent {
|
||||||
|
filter_payment_country_based(&payment_method_object, address).await?
|
||||||
|
&& filter_payment_currency_based(payment_intent, &payment_method_object)
|
||||||
|
&& filter_payment_amount_based(payment_intent, &payment_method_object)
|
||||||
|
&& filter_payment_mandate_based(payment_attempt, &payment_method_object)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
if filter && filter2 {
|
if filter && filter2 && filter3 {
|
||||||
resp.push(payment_method_object);
|
resp.push(payment_method_object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_accepted_enum_based<T: Eq + std::hash::Hash + Clone>(
|
fn filter_accepted_enum_based<T: Eq + std::hash::Hash + Clone>(
|
||||||
@ -449,7 +493,7 @@ fn filter_accepted_enum_based<T: Eq + std::hash::Hash + Clone>(
|
|||||||
|
|
||||||
fn filter_amount_based(
|
fn filter_amount_based(
|
||||||
payment_method: &api::ListPaymentMethodResponse,
|
payment_method: &api::ListPaymentMethodResponse,
|
||||||
amount: Option<i32>,
|
amount: Option<i64>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let min_check = amount
|
let min_check = amount
|
||||||
.and_then(|amt| payment_method.minimum_amount.map(|min_amt| amt >= min_amt))
|
.and_then(|amt| payment_method.minimum_amount.map(|min_amt| amt >= min_amt))
|
||||||
@ -484,6 +528,51 @@ fn filter_installment_based(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn filter_payment_country_based(
|
||||||
|
pm: &api::ListPaymentMethodResponse,
|
||||||
|
address: Option<&storage::Address>,
|
||||||
|
) -> errors::CustomResult<bool, errors::ApiErrorResponse> {
|
||||||
|
Ok(address.map_or(true, |address| {
|
||||||
|
address.country.as_ref().map_or(true, |country| {
|
||||||
|
pm.accepted_countries
|
||||||
|
.clone()
|
||||||
|
.map_or(true, |ac| ac.contains(country))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_payment_currency_based(
|
||||||
|
payment_intent: &storage::PaymentIntent,
|
||||||
|
pm: &api::ListPaymentMethodResponse,
|
||||||
|
) -> bool {
|
||||||
|
payment_intent.currency.map_or(true, |currency| {
|
||||||
|
pm.accepted_currencies
|
||||||
|
.clone()
|
||||||
|
.map_or(true, |ac| ac.contains(¤cy.foreign_into()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_payment_amount_based(
|
||||||
|
payment_intent: &storage::PaymentIntent,
|
||||||
|
pm: &api::ListPaymentMethodResponse,
|
||||||
|
) -> bool {
|
||||||
|
let amount = payment_intent.amount;
|
||||||
|
pm.maximum_amount.map_or(true, |amt| amount < amt)
|
||||||
|
&& pm.minimum_amount.map_or(true, |amt| amount > amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn filter_payment_mandate_based(
|
||||||
|
payment_attempt: Option<&storage::PaymentAttempt>,
|
||||||
|
pm: &api::ListPaymentMethodResponse,
|
||||||
|
) -> errors::CustomResult<bool, errors::ApiErrorResponse> {
|
||||||
|
let recurring_filter = if !pm.recurring_enabled {
|
||||||
|
payment_attempt.map_or(true, |pa| pa.mandate_id.is_none())
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
Ok(recurring_filter)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_customer_payment_method(
|
pub async fn list_customer_payment_method(
|
||||||
state: &routes::AppState,
|
state: &routes::AppState,
|
||||||
merchant_account: storage::MerchantAccount,
|
merchant_account: storage::MerchantAccount,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use common_utils::ext_traits::AsyncExt;
|
||||||
// TODO : Evaluate all the helper functions ()
|
// TODO : Evaluate all the helper functions ()
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use masking::{ExposeOptionInterface, PeekInterface};
|
use masking::{ExposeOptionInterface, PeekInterface};
|
||||||
@ -1353,11 +1354,10 @@ pub(crate) async fn verify_client_secret(
|
|||||||
storage_scheme: storage_enums::MerchantStorageScheme,
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
||||||
client_secret: Option<String>,
|
client_secret: Option<String>,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
) -> error_stack::Result<(), errors::ApiErrorResponse> {
|
) -> error_stack::Result<Option<storage::PaymentIntent>, errors::ApiErrorResponse> {
|
||||||
match client_secret {
|
client_secret
|
||||||
None => Ok(()),
|
.async_map(|cs| async move {
|
||||||
Some(cs) => {
|
let payment_id = get_payment_id_from_client_secret(&cs);
|
||||||
let payment_id = cs.split('_').take(2).collect::<Vec<&str>>().join("_");
|
|
||||||
|
|
||||||
let payment_intent = db
|
let payment_intent = db
|
||||||
.find_payment_intent_by_payment_id_merchant_id(
|
.find_payment_intent_by_payment_id_merchant_id(
|
||||||
@ -1369,9 +1369,16 @@ pub(crate) async fn verify_client_secret(
|
|||||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
|
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||||
|
|
||||||
authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())
|
authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())
|
||||||
.map_err(|err| err.into())
|
.map_err(errors::ApiErrorResponse::from)?;
|
||||||
}
|
Ok(payment_intent)
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn get_payment_id_from_client_secret(cs: &str) -> String {
|
||||||
|
cs.split('_').take(2).collect::<Vec<&str>>().join("_")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -181,8 +181,7 @@ impl MerchantConnectorAccount {
|
|||||||
.route(web::get().to(payment_connector_list)),
|
.route(web::get().to(payment_connector_list)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{merchant_id}/payment_methods")
|
web::resource("/payment_methods").route(web::get().to(list_payment_method_api)),
|
||||||
.route(web::get().to(list_payment_method_api)),
|
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{merchant_id}/connectors/{merchant_connector_id}")
|
web::resource("/{merchant_id}/connectors/{merchant_connector_id}")
|
||||||
|
|||||||
@ -32,7 +32,6 @@ pub async fn create_payment_method_api(
|
|||||||
pub async fn list_payment_method_api(
|
pub async fn list_payment_method_api(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
_merchant_id: web::Path<String>,
|
|
||||||
json_payload: web::Query<payment_methods::ListPaymentMethodRequest>,
|
json_payload: web::Query<payment_methods::ListPaymentMethodRequest>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let payload = json_payload.into_inner();
|
let payload = json_payload.into_inner();
|
||||||
|
|||||||
@ -279,6 +279,11 @@ impl From<F<api_enums::Currency>> for F<storage_enums::Currency> {
|
|||||||
Self(frunk::labelled_convert_from(currency.0))
|
Self(frunk::labelled_convert_from(currency.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<F<storage_enums::Currency>> for F<api_enums::Currency> {
|
||||||
|
fn from(currency: F<storage_enums::Currency>) -> Self {
|
||||||
|
Self(frunk::labelled_convert_from(currency.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<F<&'a api_types::Address>> for F<storage::AddressUpdate> {
|
impl<'a> From<F<&'a api_types::Address>> for F<storage::AddressUpdate> {
|
||||||
fn from(address: F<&api_types::Address>) -> Self {
|
fn from(address: F<&api_types::Address>) -> Self {
|
||||||
|
|||||||
Reference in New Issue
Block a user