mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-04 05:59:48 +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