mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
refactor(core): Refactor customer payment method list for v2 (#4856)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -24,6 +24,7 @@ customer_v2 = []
|
||||
merchant_account_v2 = []
|
||||
merchant_connector_account_v2 = []
|
||||
payment_v2 = []
|
||||
payment_methods_v2 = []
|
||||
routing_v2 = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
use crate::payment_methods::CustomerPaymentMethodsListResponse;
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
use crate::payment_methods::CustomerPaymentMethodsListResponse;
|
||||
use crate::{
|
||||
payment_methods::{
|
||||
CustomerDefaultPaymentMethodResponse, CustomerPaymentMethodsListResponse,
|
||||
DefaultPaymentMethod, ListCountriesCurrenciesRequest, ListCountriesCurrenciesResponse,
|
||||
PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest,
|
||||
PaymentMethodCollectLinkResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest,
|
||||
PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate,
|
||||
CustomerDefaultPaymentMethodResponse, DefaultPaymentMethod, ListCountriesCurrenciesRequest,
|
||||
ListCountriesCurrenciesResponse, PaymentMethodCollectLinkRenderRequest,
|
||||
PaymentMethodCollectLinkRequest, PaymentMethodCollectLinkResponse,
|
||||
PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse,
|
||||
PaymentMethodResponse, PaymentMethodUpdate,
|
||||
},
|
||||
payments::{
|
||||
ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints,
|
||||
|
||||
@ -994,6 +994,19 @@ impl serde::Serialize for PaymentMethodList {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
#[derive(Debug, serde::Serialize, ToSchema)]
|
||||
pub struct CustomerPaymentMethodsListResponse {
|
||||
/// List of payment methods for customer
|
||||
pub customer_payment_methods: Vec<CustomerPaymentMethod>,
|
||||
/// Returns whether a customer id is not tied to a payment intent (only when the request is made against a client secret)
|
||||
pub is_guest_customer: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
#[derive(Debug, serde::Serialize, ToSchema)]
|
||||
pub struct CustomerPaymentMethodsListResponse {
|
||||
/// List of payment methods for customer
|
||||
@ -1028,6 +1041,97 @@ pub struct CustomerDefaultPaymentMethodResponse {
|
||||
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
|
||||
pub struct CustomerPaymentMethod {
|
||||
/// Token for payment method in temporary card locker which gets refreshed often
|
||||
#[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")]
|
||||
pub payment_token: Option<String>,
|
||||
/// The unique identifier of the customer.
|
||||
#[schema(example = "pm_iouuy468iyuowqs")]
|
||||
pub payment_method_id: String,
|
||||
|
||||
/// The unique identifier of the customer.
|
||||
#[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")]
|
||||
pub customer_id: id_type::CustomerId,
|
||||
|
||||
/// The type of payment method use for the payment.
|
||||
#[schema(value_type = PaymentMethod,example = "card")]
|
||||
pub payment_method: api_enums::PaymentMethod,
|
||||
|
||||
/// This is a sub-category of payment method.
|
||||
#[schema(value_type = Option<PaymentMethodType>,example = "credit_card")]
|
||||
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
||||
|
||||
/// The name of the bank/ provider issuing the payment method to the end user
|
||||
#[schema(example = "Citibank")]
|
||||
pub payment_method_issuer: Option<String>,
|
||||
|
||||
/// A standard code representing the issuer of payment method
|
||||
#[schema(value_type = Option<PaymentMethodIssuerCode>,example = "jp_applepay")]
|
||||
pub payment_method_issuer_code: Option<api_enums::PaymentMethodIssuerCode>,
|
||||
|
||||
/// Indicates whether the payment method is eligible for recurring payments
|
||||
#[schema(example = true)]
|
||||
pub recurring_enabled: bool,
|
||||
|
||||
/// Indicates whether the payment method is eligible for installment payments
|
||||
#[schema(example = true)]
|
||||
pub installment_payment_enabled: bool,
|
||||
|
||||
/// Type of payment experience enabled with the connector
|
||||
#[schema(value_type = Option<Vec<PaymentExperience>>,example = json!(["redirect_to_url"]))]
|
||||
pub payment_experience: Option<Vec<api_enums::PaymentExperience>>,
|
||||
|
||||
/// PaymentMethod Data from locker
|
||||
pub payment_method_data: Option<PaymentMethodListData>,
|
||||
|
||||
/// Masked bank details from PM auth services
|
||||
#[schema(example = json!({"mask": "0000"}))]
|
||||
pub bank: Option<MaskedBankDetails>,
|
||||
|
||||
/// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.
|
||||
#[schema(value_type = Option<Object>,example = json!({ "city": "NY", "unit": "245" }))]
|
||||
pub metadata: Option<pii::SecretSerdeValue>,
|
||||
|
||||
/// A timestamp (ISO 8601 code) that determines when the customer was created
|
||||
#[schema(value_type = Option<PrimitiveDateTime>,example = "2023-01-18T11:04:09.922Z")]
|
||||
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
|
||||
pub created: Option<time::PrimitiveDateTime>,
|
||||
|
||||
/// Surcharge details for this saved card
|
||||
pub surcharge_details: Option<SurchargeDetailsResponse>,
|
||||
|
||||
/// Whether this payment method requires CVV to be collected
|
||||
#[schema(example = true)]
|
||||
pub requires_cvv: bool,
|
||||
|
||||
/// A timestamp (ISO 8601 code) that determines when the payment method was last used
|
||||
#[schema(value_type = Option<PrimitiveDateTime>,example = "2024-02-24T11:04:09.922Z")]
|
||||
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
|
||||
pub last_used_at: Option<time::PrimitiveDateTime>,
|
||||
/// Indicates if the payment method has been set to default or not
|
||||
#[schema(example = true)]
|
||||
pub default_payment_method_set: bool,
|
||||
|
||||
/// The billing details of the payment method
|
||||
#[schema(value_type = Option<Address>)]
|
||||
pub billing: Option<payments::Address>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PaymentMethodListData {
|
||||
Card(CardDetailFromLocker),
|
||||
#[cfg(feature = "payouts")]
|
||||
Bank(payouts::Bank),
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
|
||||
pub struct CustomerPaymentMethod {
|
||||
/// Token for payment method in temporary card locker which gets refreshed often
|
||||
|
||||
@ -37,6 +37,7 @@ v1 = ["api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "stor
|
||||
customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2"]
|
||||
merchant_account_v2 = ["api_models/merchant_account_v2", "diesel_models/merchant_account_v2", "hyperswitch_domain_models/merchant_account_v2"]
|
||||
payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2"]
|
||||
payment_methods_v2 = ["api_models/payment_methods_v2"]
|
||||
routing_v2 = ["api_models/routing_v2"]
|
||||
merchant_connector_account_v2 = ["api_models/merchant_connector_account_v2", "kgraph_utils/merchant_connector_account_v2", "hyperswitch_domain_models/merchant_connector_account_v2", "diesel_models/merchant_connector_account_v2"]
|
||||
|
||||
|
||||
@ -193,7 +193,7 @@ pub struct CustomerPaymentMethodListResponse {
|
||||
|
||||
#[derive(Default, Serialize, PartialEq, Eq)]
|
||||
pub struct PaymentMethodData {
|
||||
pub id: String,
|
||||
pub id: Option<String>,
|
||||
pub object: &'static str,
|
||||
pub card: Option<CardDetails>,
|
||||
pub created: Option<time::PrimitiveDateTime>,
|
||||
@ -222,12 +222,29 @@ impl From<api::CustomerPaymentMethodsListResponse> for CustomerPaymentMethodList
|
||||
}
|
||||
}
|
||||
|
||||
// Check this in review
|
||||
impl From<api_types::CustomerPaymentMethod> for PaymentMethodData {
|
||||
fn from(item: api_types::CustomerPaymentMethod) -> Self {
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
let card = item.card.map(From::from);
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
let card = match item.payment_method_data {
|
||||
Some(api_types::PaymentMethodListData::Card(card)) => Some(CardDetails::from(card)),
|
||||
_ => None,
|
||||
};
|
||||
Self {
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
id: Some(item.payment_token),
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
id: item.payment_token,
|
||||
object: "payment_method",
|
||||
card: item.card.map(From::from),
|
||||
card,
|
||||
created: item.created,
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,11 @@ use super::surcharge_decision_configs::{
|
||||
perform_surcharge_decision_management_for_payment_method_list,
|
||||
perform_surcharge_decision_management_for_saved_cards,
|
||||
};
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
use crate::routes::app::SessionStateInfo;
|
||||
#[cfg(feature = "payouts")]
|
||||
use crate::types::domain::types::AsyncLift;
|
||||
use crate::{
|
||||
@ -67,7 +72,7 @@ use crate::{
|
||||
},
|
||||
db, logger,
|
||||
pii::prelude::*,
|
||||
routes::{self, app::SessionStateInfo, metrics, payment_methods::ParentPaymentMethodToken},
|
||||
routes::{self, metrics, payment_methods::ParentPaymentMethodToken},
|
||||
services,
|
||||
types::{
|
||||
api::{self, routing as routing_types, PaymentMethodCreateExt},
|
||||
@ -3607,6 +3612,63 @@ fn filter_recurring_based(
|
||||
recurring_enabled.map_or(true, |enabled| payment_method.recurring_enabled == enabled)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
pub async fn list_customer_payment_method_util(
|
||||
state: routes::SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
req: Option<api::PaymentMethodListRequest>,
|
||||
customer_id: Option<id_type::CustomerId>,
|
||||
is_payment_associated: bool,
|
||||
) -> errors::RouterResponse<api::CustomerPaymentMethodsListResponse> {
|
||||
let limit = req.as_ref().and_then(|pml_req| pml_req.limit);
|
||||
|
||||
let (customer_id, payment_intent) = if is_payment_associated {
|
||||
let cloned_secret = req.and_then(|r| r.client_secret.clone());
|
||||
let payment_intent = helpers::verify_payment_intent_time_and_client_secret(
|
||||
&state,
|
||||
&merchant_account,
|
||||
&key_store,
|
||||
cloned_secret,
|
||||
)
|
||||
.await?;
|
||||
|
||||
(
|
||||
payment_intent
|
||||
.as_ref()
|
||||
.and_then(|pi| pi.customer_id.clone()),
|
||||
payment_intent,
|
||||
)
|
||||
} else {
|
||||
(customer_id, None)
|
||||
};
|
||||
|
||||
let resp = if let Some(cust) = customer_id {
|
||||
Box::pin(list_customer_payment_method(
|
||||
&state,
|
||||
merchant_account,
|
||||
key_store,
|
||||
payment_intent,
|
||||
&cust,
|
||||
limit,
|
||||
is_payment_associated,
|
||||
))
|
||||
.await?
|
||||
} else {
|
||||
let response = api::CustomerPaymentMethodsListResponse {
|
||||
customer_payment_methods: Vec::new(),
|
||||
is_guest_customer: Some(true),
|
||||
};
|
||||
services::ApplicationResponse::Json(response)
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
pub async fn do_list_customer_pm_fetch_customer_if_not_passed(
|
||||
state: routes::SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
@ -3678,6 +3740,10 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
pub async fn list_customer_payment_method(
|
||||
state: &routes::SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
@ -3761,88 +3827,21 @@ pub async fn list_customer_payment_method(
|
||||
|
||||
let payment_method = pm.payment_method.get_required_value("payment_method")?;
|
||||
|
||||
let payment_method_retrieval_context = match payment_method {
|
||||
enums::PaymentMethod::Card => {
|
||||
let card_details =
|
||||
get_card_details_with_locker_fallback(&pm, state, &key_store).await?;
|
||||
let pm_list_context = get_pm_list_context(
|
||||
state,
|
||||
&payment_method,
|
||||
&key_store,
|
||||
&pm,
|
||||
Some(parent_payment_method_token.clone()),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if card_details.is_some() {
|
||||
PaymentMethodListContext {
|
||||
card_details,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: PaymentTokenData::permanent_card(
|
||||
Some(pm.payment_method_id.clone()),
|
||||
pm.locker_id.clone().or(Some(pm.payment_method_id.clone())),
|
||||
pm.locker_id.clone().unwrap_or(pm.payment_method_id.clone()),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if pm_list_context.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
enums::PaymentMethod::BankDebit => {
|
||||
// Retrieve the pm_auth connector details so that it can be tokenized
|
||||
let bank_account_token_data =
|
||||
get_bank_account_connector_details(state, &pm, &key_store)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
logger::error!(?error);
|
||||
None
|
||||
});
|
||||
if let Some(data) = bank_account_token_data {
|
||||
let token_data = PaymentTokenData::AuthBankDebit(data);
|
||||
|
||||
PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: token_data,
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
enums::PaymentMethod::Wallet => PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: PaymentTokenData::wallet_token(
|
||||
pm.payment_method_id.clone(),
|
||||
),
|
||||
},
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
enums::PaymentMethod::BankTransfer => PaymentMethodListContext {
|
||||
card_details: None,
|
||||
bank_transfer_details: Some(
|
||||
get_bank_from_hs_locker(
|
||||
state,
|
||||
&key_store,
|
||||
&parent_payment_method_token,
|
||||
&pm.customer_id,
|
||||
&pm.merchant_id,
|
||||
pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
hyperswitch_token_data: PaymentTokenData::temporary_generic(
|
||||
parent_payment_method_token.clone(),
|
||||
),
|
||||
},
|
||||
|
||||
_ => PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: PaymentTokenData::temporary_generic(generate_id(
|
||||
consts::ID_LENGTH,
|
||||
"token",
|
||||
)),
|
||||
},
|
||||
};
|
||||
let pm_list_context = pm_list_context.get_required_value("PaymentMethodListContext")?;
|
||||
|
||||
// Retrieve the masked bank details to be sent as a response
|
||||
let bank_details = if payment_method == enums::PaymentMethod::BankDebit {
|
||||
@ -3898,7 +3897,7 @@ pub async fn list_customer_payment_method(
|
||||
payment_method,
|
||||
payment_method_type: pm.payment_method_type,
|
||||
payment_method_issuer: pm.payment_method_issuer,
|
||||
card: payment_method_retrieval_context.card_details,
|
||||
card: pm_list_context.card_details,
|
||||
metadata: pm.metadata,
|
||||
payment_method_issuer_code: pm.payment_method_issuer_code,
|
||||
recurring_enabled: mca_enabled,
|
||||
@ -3906,7 +3905,7 @@ pub async fn list_customer_payment_method(
|
||||
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
|
||||
created: Some(pm.created_at),
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer: payment_method_retrieval_context.bank_transfer_details,
|
||||
bank_transfer: pm_list_context.bank_transfer_details,
|
||||
bank: bank_details,
|
||||
surcharge_details: None,
|
||||
requires_cvv,
|
||||
@ -3928,15 +3927,15 @@ pub async fn list_customer_payment_method(
|
||||
.and_then(|b_profile| b_profile.intent_fulfillment_time)
|
||||
.unwrap_or(consts::DEFAULT_INTENT_FULFILLMENT_TIME);
|
||||
|
||||
let hyperswitch_token_data = pm_list_context
|
||||
.hyperswitch_token_data
|
||||
.get_required_value("PaymentTokenData")?;
|
||||
|
||||
ParentPaymentMethodToken::create_key_for_token((
|
||||
&parent_payment_method_token,
|
||||
pma.payment_method,
|
||||
))
|
||||
.insert(
|
||||
intent_fulfillment_time,
|
||||
payment_method_retrieval_context.hyperswitch_token_data,
|
||||
state,
|
||||
)
|
||||
.insert(intent_fulfillment_time, hyperswitch_token_data, state)
|
||||
.await?;
|
||||
|
||||
if let Some(metadata) = pma.metadata {
|
||||
@ -3967,6 +3966,115 @@ pub async fn list_customer_payment_method(
|
||||
customer_payment_methods: customer_pms,
|
||||
is_guest_customer: payment_intent.as_ref().map(|_| false), //to return this key only when the request is tied to a payment intent
|
||||
};
|
||||
|
||||
Box::pin(perform_surcharge_ops(
|
||||
payment_intent,
|
||||
state,
|
||||
merchant_account,
|
||||
key_store,
|
||||
business_profile,
|
||||
&mut response,
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(services::ApplicationResponse::Json(response))
|
||||
}
|
||||
|
||||
async fn get_pm_list_context(
|
||||
state: &routes::SessionState,
|
||||
payment_method: &enums::PaymentMethod,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
pm: &diesel_models::PaymentMethod,
|
||||
#[cfg(feature = "payouts")] parent_payment_method_token: Option<String>,
|
||||
#[cfg(not(feature = "payouts"))] _parent_payment_method_token: Option<String>,
|
||||
is_payment_associated: bool,
|
||||
) -> Result<Option<PaymentMethodListContext>, error_stack::Report<errors::ApiErrorResponse>> {
|
||||
let payment_method_retrieval_context = match payment_method {
|
||||
enums::PaymentMethod::Card => {
|
||||
let card_details = get_card_details_with_locker_fallback(pm, state, key_store).await?;
|
||||
|
||||
card_details.as_ref().map(|card| PaymentMethodListContext {
|
||||
card_details: Some(card.clone()),
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: is_payment_associated.then_some(
|
||||
PaymentTokenData::permanent_card(
|
||||
Some(pm.payment_method_id.clone()),
|
||||
pm.locker_id.clone().or(Some(pm.payment_method_id.clone())),
|
||||
pm.locker_id.clone().unwrap_or(pm.payment_method_id.clone()),
|
||||
),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
enums::PaymentMethod::BankDebit => {
|
||||
// Retrieve the pm_auth connector details so that it can be tokenized
|
||||
let bank_account_token_data = get_bank_account_connector_details(state, pm, key_store)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
logger::error!(error=?err);
|
||||
None
|
||||
});
|
||||
|
||||
bank_account_token_data.map(|data| {
|
||||
let token_data = PaymentTokenData::AuthBankDebit(data);
|
||||
|
||||
PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: is_payment_associated.then_some(token_data),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enums::PaymentMethod::Wallet => Some(PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: is_payment_associated
|
||||
.then_some(PaymentTokenData::wallet_token(pm.payment_method_id.clone())),
|
||||
}),
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
enums::PaymentMethod::BankTransfer => Some(PaymentMethodListContext {
|
||||
card_details: None,
|
||||
bank_transfer_details: Some(
|
||||
get_bank_from_hs_locker(
|
||||
state,
|
||||
key_store,
|
||||
parent_payment_method_token.as_ref(),
|
||||
&pm.customer_id,
|
||||
&pm.merchant_id,
|
||||
pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
hyperswitch_token_data: parent_payment_method_token
|
||||
.map(|token| PaymentTokenData::temporary_generic(token.clone())),
|
||||
}),
|
||||
|
||||
_ => Some(PaymentMethodListContext {
|
||||
card_details: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
bank_transfer_details: None,
|
||||
hyperswitch_token_data: is_payment_associated.then_some(
|
||||
PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(payment_method_retrieval_context)
|
||||
}
|
||||
|
||||
async fn perform_surcharge_ops(
|
||||
payment_intent: Option<storage::PaymentIntent>,
|
||||
state: &routes::SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
business_profile: Option<BusinessProfile>,
|
||||
response: &mut api::CustomerPaymentMethodsListResponse,
|
||||
) -> Result<(), error_stack::Report<errors::ApiErrorResponse>> {
|
||||
let payment_attempt = payment_intent
|
||||
.as_ref()
|
||||
.async_map(|payment_intent| async {
|
||||
@ -3983,7 +4091,6 @@ pub async fn list_customer_payment_method(
|
||||
})
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
if let Some((payment_attempt, payment_intent, business_profile)) = payment_attempt
|
||||
.zip(payment_intent)
|
||||
.zip(business_profile)
|
||||
@ -3996,13 +4103,355 @@ pub async fn list_customer_payment_method(
|
||||
&business_profile,
|
||||
&payment_attempt,
|
||||
payment_intent,
|
||||
&mut response,
|
||||
response,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
struct SavedPMLPaymentsInfo {
|
||||
pub payment_intent: storage::PaymentIntent,
|
||||
pub business_profile: Option<BusinessProfile>,
|
||||
pub requires_cvv: bool,
|
||||
pub off_session_payment_flag: bool,
|
||||
pub is_connector_agnostic_mit_enabled: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
impl SavedPMLPaymentsInfo {
|
||||
pub async fn form_payments_info(
|
||||
payment_intent: storage::PaymentIntent,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
db: &dyn db::StorageInterface,
|
||||
) -> errors::RouterResult<Self> {
|
||||
let requires_cvv = db
|
||||
.find_config_by_key_unwrap_or(
|
||||
format!("{}_requires_cvv", merchant_account.get_id()).as_str(),
|
||||
Some("true".to_string()),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch requires_cvv config")?
|
||||
.config
|
||||
!= "false";
|
||||
|
||||
let off_session_payment_flag = matches!(
|
||||
payment_intent.setup_future_usage,
|
||||
Some(common_enums::FutureUsage::OffSession)
|
||||
);
|
||||
|
||||
let profile_id = core_utils::get_profile_id_from_business_details(
|
||||
payment_intent.business_country,
|
||||
payment_intent.business_label.as_ref(),
|
||||
merchant_account,
|
||||
payment_intent.profile_id.as_ref(),
|
||||
db,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.attach_printable("Could not find profile id from business details")?;
|
||||
|
||||
let business_profile = core_utils::validate_and_get_business_profile(
|
||||
db,
|
||||
Some(profile_id).as_ref(),
|
||||
merchant_account.get_id(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_connector_agnostic_mit_enabled = business_profile
|
||||
.as_ref()
|
||||
.and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(Self {
|
||||
payment_intent,
|
||||
business_profile,
|
||||
requires_cvv,
|
||||
off_session_payment_flag,
|
||||
is_connector_agnostic_mit_enabled,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn perform_payment_ops(
|
||||
&self,
|
||||
state: &routes::SessionState,
|
||||
parent_payment_method_token: Option<String>,
|
||||
pma: &api::CustomerPaymentMethod,
|
||||
pm_list_context: PaymentMethodListContext,
|
||||
) -> errors::RouterResult<()> {
|
||||
let token = parent_payment_method_token
|
||||
.as_ref()
|
||||
.get_required_value("parent_payment_method_token")?;
|
||||
let hyperswitch_token_data = pm_list_context
|
||||
.hyperswitch_token_data
|
||||
.get_required_value("PaymentTokenData")?;
|
||||
|
||||
let intent_fulfillment_time = self
|
||||
.business_profile
|
||||
.as_ref()
|
||||
.and_then(|b_profile| b_profile.intent_fulfillment_time)
|
||||
.unwrap_or(consts::DEFAULT_INTENT_FULFILLMENT_TIME);
|
||||
|
||||
ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method))
|
||||
.insert(intent_fulfillment_time, hyperswitch_token_data, state)
|
||||
.await?;
|
||||
|
||||
if let Some(metadata) = pma.metadata.clone() {
|
||||
let pm_metadata_vec: payment_methods::PaymentMethodMetadata = metadata
|
||||
.parse_value("PaymentMethodMetadata")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"Failed to deserialize metadata to PaymentmethodMetadata struct",
|
||||
)?;
|
||||
|
||||
let redis_conn = state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get redis connection")?;
|
||||
|
||||
for pm_metadata in pm_metadata_vec.payment_method_tokenization {
|
||||
let key = format!(
|
||||
"pm_token_{}_{}_{}",
|
||||
token, pma.payment_method, pm_metadata.0
|
||||
);
|
||||
|
||||
redis_conn
|
||||
.set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time)
|
||||
.await
|
||||
.change_context(errors::StorageError::KVError)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to add data in redis")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
pub async fn list_customer_payment_method(
|
||||
state: &routes::SessionState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
payment_intent: Option<storage::PaymentIntent>,
|
||||
customer_id: &id_type::CustomerId,
|
||||
limit: Option<i64>,
|
||||
is_payment_associated: bool,
|
||||
) -> errors::RouterResponse<api::CustomerPaymentMethodsListResponse> {
|
||||
let db = &*state.store;
|
||||
let key_manager_state = &(state).into();
|
||||
// let key = key_store.key.get_inner().peek();
|
||||
|
||||
let customer = db
|
||||
.find_customer_by_customer_id_merchant_id(
|
||||
key_manager_state,
|
||||
customer_id,
|
||||
merchant_account.get_id(),
|
||||
&key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?;
|
||||
|
||||
let payments_info = payment_intent
|
||||
.async_map(|pi| SavedPMLPaymentsInfo::form_payments_info(pi, &merchant_account, db))
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
let saved_payment_methods = db
|
||||
.find_payment_method_by_customer_id_merchant_id_status(
|
||||
customer_id,
|
||||
merchant_account.get_id(),
|
||||
common_enums::PaymentMethodStatus::Active,
|
||||
limit,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
|
||||
|
||||
let mut filtered_saved_payment_methods_ctx = Vec::new();
|
||||
for pm in saved_payment_methods.into_iter() {
|
||||
let payment_method = pm.payment_method.get_required_value("payment_method")?;
|
||||
let parent_payment_method_token =
|
||||
is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token"));
|
||||
|
||||
let pm_list_context = get_pm_list_context(
|
||||
state,
|
||||
&payment_method,
|
||||
&key_store,
|
||||
&pm,
|
||||
parent_payment_method_token.clone(),
|
||||
is_payment_associated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ctx) = pm_list_context {
|
||||
filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm));
|
||||
}
|
||||
}
|
||||
|
||||
let pm_list_futures = filtered_saved_payment_methods_ctx
|
||||
.into_iter()
|
||||
.map(|ctx| {
|
||||
generate_saved_pm_response(
|
||||
state,
|
||||
&key_store,
|
||||
&merchant_account,
|
||||
ctx,
|
||||
&customer,
|
||||
payments_info.as_ref(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let final_result = futures::future::join_all(pm_list_futures).await;
|
||||
|
||||
let mut customer_pms = Vec::new();
|
||||
for result in final_result.into_iter() {
|
||||
let pma = result.attach_printable("saved pm list failed")?;
|
||||
customer_pms.push(pma);
|
||||
}
|
||||
|
||||
let mut response = api::CustomerPaymentMethodsListResponse {
|
||||
customer_payment_methods: customer_pms,
|
||||
is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent
|
||||
};
|
||||
|
||||
if is_payment_associated {
|
||||
Box::pin(perform_surcharge_ops(
|
||||
payments_info.as_ref().map(|pi| pi.payment_intent.clone()),
|
||||
state,
|
||||
merchant_account,
|
||||
key_store,
|
||||
payments_info.and_then(|pi| pi.business_profile),
|
||||
&mut response,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(services::ApplicationResponse::Json(response))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
async fn generate_saved_pm_response(
|
||||
state: &routes::SessionState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
pm_list_context: (
|
||||
PaymentMethodListContext,
|
||||
Option<String>,
|
||||
diesel_models::PaymentMethod,
|
||||
),
|
||||
customer: &domain::Customer,
|
||||
payment_info: Option<&SavedPMLPaymentsInfo>,
|
||||
) -> Result<api::CustomerPaymentMethod, error_stack::Report<errors::ApiErrorResponse>> {
|
||||
let (pm_list_context, parent_payment_method_token, pm) = pm_list_context;
|
||||
let payment_method = pm.payment_method.get_required_value("payment_method")?;
|
||||
|
||||
let bank_details = if payment_method == enums::PaymentMethod::BankDebit {
|
||||
get_masked_bank_details(state, &pm, key_store)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
logger::error!(error=?err);
|
||||
None
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let payment_method_billing = decrypt_generic_data::<api_models::payments::Address>(
|
||||
state,
|
||||
pm.payment_method_billing_address,
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
.attach_printable("unable to decrypt payment method billing address details")?;
|
||||
|
||||
let connector_mandate_details = pm
|
||||
.connector_mandate_details
|
||||
.clone()
|
||||
.map(|val| val.parse_value::<storage::PaymentsMandateReference>("PaymentsMandateReference"))
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to deserialize to Payment Mandate Reference ")?;
|
||||
|
||||
let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag) = payment_info
|
||||
.map(|pi| {
|
||||
(
|
||||
pi.is_connector_agnostic_mit_enabled,
|
||||
pi.requires_cvv,
|
||||
pi.off_session_payment_flag,
|
||||
)
|
||||
})
|
||||
.unwrap_or((false, false, false));
|
||||
|
||||
let mca_enabled = get_mca_status(
|
||||
state,
|
||||
key_store,
|
||||
merchant_account.get_id(),
|
||||
is_connector_agnostic_mit_enabled,
|
||||
connector_mandate_details,
|
||||
pm.network_transaction_id.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let requires_cvv = if is_connector_agnostic_mit_enabled {
|
||||
requires_cvv
|
||||
&& !(off_session_payment_flag
|
||||
&& (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some()))
|
||||
} else {
|
||||
requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some())
|
||||
};
|
||||
|
||||
let pmd = if let Some(card) = pm_list_context.card_details.as_ref() {
|
||||
Some(api::PaymentMethodListData::Card(card.clone()))
|
||||
} else if cfg!(feature = "payouts") {
|
||||
pm_list_context
|
||||
.bank_transfer_details
|
||||
.clone()
|
||||
.map(api::PaymentMethodListData::Bank)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pma = api::CustomerPaymentMethod {
|
||||
payment_token: parent_payment_method_token.clone(),
|
||||
payment_method_id: pm.payment_method_id.clone(),
|
||||
customer_id: pm.customer_id,
|
||||
payment_method,
|
||||
payment_method_type: pm.payment_method_type,
|
||||
payment_method_issuer: pm.payment_method_issuer,
|
||||
payment_method_data: pmd,
|
||||
metadata: pm.metadata,
|
||||
payment_method_issuer_code: pm.payment_method_issuer_code,
|
||||
recurring_enabled: mca_enabled,
|
||||
installment_payment_enabled: false,
|
||||
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
|
||||
created: Some(pm.created_at),
|
||||
bank: bank_details,
|
||||
surcharge_details: None,
|
||||
requires_cvv: requires_cvv
|
||||
&& !(off_session_payment_flag && pm.connector_mandate_details.is_some()),
|
||||
last_used_at: Some(pm.last_used_at),
|
||||
default_payment_method_set: customer.default_payment_method_id.is_some()
|
||||
&& customer.default_payment_method_id == Some(pm.payment_method_id),
|
||||
billing: payment_method_billing,
|
||||
};
|
||||
|
||||
payment_info
|
||||
.async_map(|pi| {
|
||||
pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context)
|
||||
})
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
Ok(pma)
|
||||
}
|
||||
|
||||
pub async fn get_mca_status(
|
||||
state: &routes::SessionState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
@ -4390,7 +4839,7 @@ pub async fn update_last_used_at(
|
||||
pub async fn get_bank_from_hs_locker(
|
||||
state: &routes::SessionState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
temp_token: &str,
|
||||
temp_token: Option<&String>,
|
||||
customer_id: &id_type::CustomerId,
|
||||
merchant_id: &id_type::MerchantId,
|
||||
token_ref: &str,
|
||||
@ -4413,16 +4862,18 @@ pub async fn get_bank_from_hs_locker(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
match &pm_parsed {
|
||||
api::PayoutMethodData::Bank(bank) => {
|
||||
vault::Vault::store_payout_method_data_in_locker(
|
||||
state,
|
||||
Some(temp_token.to_string()),
|
||||
&pm_parsed,
|
||||
Some(customer_id.to_owned()),
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error storing payout method data in temporary locker")?;
|
||||
if let Some(token) = temp_token {
|
||||
vault::Vault::store_payout_method_data_in_locker(
|
||||
state,
|
||||
Some(token.clone()),
|
||||
&pm_parsed,
|
||||
Some(customer_id.to_owned()),
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error storing payout method data in temporary locker")?;
|
||||
}
|
||||
Ok(bank.to_owned())
|
||||
}
|
||||
api::PayoutMethodData::Card(_) => Err(errors::ApiErrorResponse::InvalidRequestData {
|
||||
|
||||
@ -3,7 +3,16 @@ use api_models::{
|
||||
payments, routing,
|
||||
surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord},
|
||||
};
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
use common_utils::{ext_traits::StringExt, types as common_utils_types};
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
use common_utils::{
|
||||
ext_traits::{OptionExt, StringExt},
|
||||
types as common_utils_types,
|
||||
};
|
||||
use error_stack::{self, ResultExt};
|
||||
use euclid::{
|
||||
backend,
|
||||
@ -267,6 +276,11 @@ where
|
||||
}
|
||||
Ok(surcharge_metadata)
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
pub async fn perform_surcharge_decision_management_for_saved_cards(
|
||||
state: &SessionState,
|
||||
algorithm_ref: routing::RoutingAlgorithmRef,
|
||||
@ -303,10 +317,13 @@ pub async fn perform_surcharge_decision_management_for_saved_cards(
|
||||
.change_context(ConfigError::InputConstructionError)?;
|
||||
|
||||
for customer_payment_method in customer_payment_method_list.iter_mut() {
|
||||
let payment_token = customer_payment_method.payment_token.clone();
|
||||
|
||||
backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method);
|
||||
backend_input.payment_method.payment_method_type =
|
||||
customer_payment_method.payment_method_type;
|
||||
backend_input.payment_method.card_network = customer_payment_method
|
||||
|
||||
let card_network = customer_payment_method
|
||||
.card
|
||||
.as_ref()
|
||||
.and_then(|card| card.scheme.as_ref())
|
||||
@ -317,13 +334,98 @@ pub async fn perform_surcharge_decision_management_for_saved_cards(
|
||||
.change_context(ConfigError::DslExecutionError)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
backend_input.payment_method.card_network = card_network;
|
||||
|
||||
let surcharge_details = surcharge_source
|
||||
.generate_surcharge_details_and_populate_surcharge_metadata(
|
||||
&backend_input,
|
||||
payment_attempt,
|
||||
(
|
||||
&mut surcharge_metadata,
|
||||
types::SurchargeKey::Token(customer_payment_method.payment_token.clone()),
|
||||
types::SurchargeKey::Token(payment_token),
|
||||
),
|
||||
)?;
|
||||
customer_payment_method.surcharge_details = surcharge_details
|
||||
.map(|surcharge_details| {
|
||||
SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt))
|
||||
.change_context(ConfigError::DslParsingError)
|
||||
})
|
||||
.transpose()?;
|
||||
}
|
||||
Ok(surcharge_metadata)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
pub async fn perform_surcharge_decision_management_for_saved_cards(
|
||||
state: &SessionState,
|
||||
algorithm_ref: routing::RoutingAlgorithmRef,
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
payment_intent: &storage::PaymentIntent,
|
||||
customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod],
|
||||
) -> ConditionalConfigResult<types::SurchargeMetadata> {
|
||||
let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone());
|
||||
let surcharge_source = match (
|
||||
payment_attempt.get_surcharge_details(),
|
||||
algorithm_ref.surcharge_config_algo_id,
|
||||
) {
|
||||
(Some(request_surcharge_details), _) => {
|
||||
SurchargeSource::Predetermined(request_surcharge_details)
|
||||
}
|
||||
(None, Some(algorithm_id)) => {
|
||||
let cached_algo = ensure_algorithm_cached(
|
||||
&*state.store,
|
||||
&payment_attempt.merchant_id,
|
||||
algorithm_id.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
SurchargeSource::Generate(cached_algo)
|
||||
}
|
||||
(None, None) => return Ok(surcharge_metadata),
|
||||
};
|
||||
let surcharge_source_log_message = match &surcharge_source {
|
||||
SurchargeSource::Generate(_) => "Surcharge was calculated through surcharge rules",
|
||||
SurchargeSource::Predetermined(_) => "Surcharge was sent in payment create request",
|
||||
};
|
||||
logger::debug!(customer_saved_card_list_surcharge_source = surcharge_source_log_message);
|
||||
let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None)
|
||||
.change_context(ConfigError::InputConstructionError)?;
|
||||
|
||||
for customer_payment_method in customer_payment_method_list.iter_mut() {
|
||||
let payment_token = customer_payment_method
|
||||
.payment_token
|
||||
.clone()
|
||||
.get_required_value("payment_token")
|
||||
.change_context(ConfigError::InputConstructionError)?;
|
||||
|
||||
backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method);
|
||||
backend_input.payment_method.payment_method_type =
|
||||
customer_payment_method.payment_method_type;
|
||||
|
||||
let card_network = match &customer_payment_method.payment_method_data {
|
||||
Some(api_models::payment_methods::PaymentMethodListData::Card(card)) => card
|
||||
.scheme
|
||||
.as_ref()
|
||||
.map(|scheme| {
|
||||
scheme
|
||||
.clone()
|
||||
.parse_enum("CardNetwork")
|
||||
.change_context(ConfigError::DslExecutionError)
|
||||
})
|
||||
.transpose()?,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
backend_input.payment_method.card_network = card_network;
|
||||
|
||||
let surcharge_details = surcharge_source
|
||||
.generate_surcharge_details_and_populate_surcharge_metadata(
|
||||
&backend_input,
|
||||
payment_attempt,
|
||||
(
|
||||
&mut surcharge_metadata,
|
||||
types::SurchargeKey::Token(payment_token),
|
||||
),
|
||||
)?;
|
||||
customer_payment_method.surcharge_details = surcharge_details
|
||||
|
||||
@ -30,7 +30,7 @@ use super::blocklist;
|
||||
use super::currency;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
use super::dummy_connector::*;
|
||||
#[cfg(all(any(feature = "olap", feature = "oltp"), not(feature = "customer_v2")))]
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
use super::payment_methods::*;
|
||||
#[cfg(feature = "payouts")]
|
||||
use super::payout_link::*;
|
||||
@ -501,7 +501,30 @@ impl DummyConnector {
|
||||
|
||||
pub struct Payments;
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
#[cfg(all(
|
||||
any(feature = "olap", feature = "oltp"),
|
||||
feature = "v2",
|
||||
feature = "payment_methods_v2",
|
||||
feature = "payment_v2"
|
||||
))]
|
||||
impl Payments {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
let mut route = web::scope("/v2/payments").app_data(web::Data::new(state));
|
||||
route = route.service(
|
||||
web::resource("/{payment_id}/saved_payment_methods")
|
||||
.route(web::get().to(list_customer_payment_method_for_payment)),
|
||||
);
|
||||
|
||||
route
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "olap", feature = "oltp"),
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2"),
|
||||
not(feature = "payment_v2")
|
||||
))]
|
||||
impl Payments {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
let mut route = web::scope("/payments").app_data(web::Data::new(state));
|
||||
@ -590,7 +613,7 @@ impl Payments {
|
||||
)
|
||||
.service(
|
||||
web::resource("/{payment_id}/extended_card_info").route(web::get().to(retrieve_extended_card_info)),
|
||||
);
|
||||
)
|
||||
}
|
||||
route
|
||||
}
|
||||
@ -844,6 +867,7 @@ pub struct Customers;
|
||||
#[cfg(all(
|
||||
feature = "v2",
|
||||
feature = "customer_v2",
|
||||
feature = "payment_methods_v2",
|
||||
any(feature = "olap", feature = "oltp")
|
||||
))]
|
||||
impl Customers {
|
||||
@ -853,6 +877,13 @@ impl Customers {
|
||||
{
|
||||
route = route.service(web::resource("").route(web::post().to(customers_create)))
|
||||
}
|
||||
#[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))]
|
||||
{
|
||||
route = route.service(
|
||||
web::resource("/{customer_id}/saved_payment_methods")
|
||||
.route(web::get().to(list_customer_payment_method_api)),
|
||||
);
|
||||
}
|
||||
route
|
||||
}
|
||||
}
|
||||
@ -860,6 +891,7 @@ impl Customers {
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
not(feature = "customer_v2"),
|
||||
not(feature = "payment_methods_v2"),
|
||||
any(feature = "olap", feature = "oltp")
|
||||
))]
|
||||
impl Customers {
|
||||
@ -897,13 +929,12 @@ impl Customers {
|
||||
.route(web::get().to(customers_retrieve))
|
||||
.route(web::post().to(customers_update))
|
||||
.route(web::delete().to(customers_delete)),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
route
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Refunds;
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
|
||||
@ -229,6 +229,11 @@ pub async fn list_payment_method_api(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
/// List payment methods for a Customer
|
||||
///
|
||||
/// To filter and list the applicable payment methods for a particular Customer ID
|
||||
@ -287,6 +292,134 @@ pub async fn list_customer_payment_method_api(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
/// List payment methods for a Customer v2
|
||||
///
|
||||
/// To filter and list the applicable payment methods for a particular Customer ID, is to be associated with a payment
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "v2/payments/{payment_id}/saved_payment_methods",
|
||||
params (
|
||||
("client-secret" = String, Path, description = "A secret known only to your application and the authorization server"),
|
||||
("accepted_country" = Vec<String>, Query, description = "The two-letter ISO currency code"),
|
||||
("accepted_currency" = Vec<Currency>, Path, description = "The three-letter ISO currency code"),
|
||||
("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."),
|
||||
("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."),
|
||||
("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"),
|
||||
("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse),
|
||||
(status = 400, description = "Invalid Data"),
|
||||
(status = 404, description = "Payment Methods does not exist in records")
|
||||
),
|
||||
tag = "Payment Methods",
|
||||
operation_id = "List all Payment Methods for a Customer",
|
||||
security(("publishable_key" = []))
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))]
|
||||
pub async fn list_customer_payment_method_for_payment(
|
||||
state: web::Data<AppState>,
|
||||
payment_id: web::Path<(String,)>,
|
||||
req: HttpRequest,
|
||||
query_payload: web::Query<payment_methods::PaymentMethodListRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::CustomerPaymentMethodsList;
|
||||
let payload = query_payload.into_inner();
|
||||
let _payment_id = payment_id.into_inner().0.clone();
|
||||
|
||||
let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) {
|
||||
Ok((auth, _auth_flow)) => (auth, _auth_flow),
|
||||
Err(e) => return api::log_and_return_error_response(e),
|
||||
};
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
|state, auth, req, _| {
|
||||
cards::list_customer_payment_method_util(
|
||||
state,
|
||||
auth.merchant_account,
|
||||
auth.key_store,
|
||||
Some(req),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
},
|
||||
&*auth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
/// List payment methods for a Customer v2
|
||||
///
|
||||
/// To filter and list the applicable payment methods for a particular Customer ID, to be used in a non-payments context
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "v2/customers/{customer_id}/saved_payment_methods",
|
||||
params (
|
||||
("accepted_country" = Vec<String>, Query, description = "The two-letter ISO currency code"),
|
||||
("accepted_currency" = Vec<Currency>, Path, description = "The three-letter ISO currency code"),
|
||||
("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."),
|
||||
("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."),
|
||||
("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"),
|
||||
("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse),
|
||||
(status = 400, description = "Invalid Data"),
|
||||
(status = 404, description = "Payment Methods does not exist in records")
|
||||
),
|
||||
tag = "Payment Methods",
|
||||
operation_id = "List all Payment Methods for a Customer",
|
||||
security(("api_key" = []))
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))]
|
||||
pub async fn list_customer_payment_method_api(
|
||||
state: web::Data<AppState>,
|
||||
customer_id: web::Path<(id_type::CustomerId,)>,
|
||||
req: HttpRequest,
|
||||
query_payload: web::Query<payment_methods::PaymentMethodListRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::CustomerPaymentMethodsList;
|
||||
let payload = query_payload.into_inner();
|
||||
let customer_id = customer_id.into_inner().0.clone();
|
||||
|
||||
let ephemeral_or_api_auth = match auth::is_ephemeral_auth(req.headers()) {
|
||||
Ok(auth) => auth,
|
||||
Err(err) => return api::log_and_return_error_response(err),
|
||||
};
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
|state, auth, req, _| {
|
||||
cards::list_customer_payment_method_util(
|
||||
state,
|
||||
auth.merchant_account,
|
||||
auth.key_store,
|
||||
Some(req),
|
||||
Some(customer_id.clone()),
|
||||
false,
|
||||
)
|
||||
},
|
||||
&*ephemeral_or_api_auth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
/// List payment methods for a Customer
|
||||
///
|
||||
/// To filter and list the applicable payment methods for a particular Customer ID
|
||||
|
||||
@ -1,3 +1,19 @@
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
pub use api_models::payment_methods::{
|
||||
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod,
|
||||
CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest,
|
||||
GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest,
|
||||
PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate,
|
||||
PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList,
|
||||
PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse,
|
||||
PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData,
|
||||
TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2,
|
||||
TokenizedWalletValue1, TokenizedWalletValue2,
|
||||
};
|
||||
#[cfg(all(
|
||||
any(feature = "v2", feature = "v1"),
|
||||
not(feature = "payment_methods_v2")
|
||||
))]
|
||||
pub use api_models::payment_methods::{
|
||||
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod,
|
||||
CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest,
|
||||
|
||||
@ -81,7 +81,7 @@ impl PaymentTokenData {
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PaymentMethodListContext {
|
||||
pub card_details: Option<api::CardDetailFromLocker>,
|
||||
pub hyperswitch_token_data: PaymentTokenData,
|
||||
pub hyperswitch_token_data: Option<PaymentTokenData>,
|
||||
#[cfg(feature = "payouts")]
|
||||
pub bank_transfer_details: Option<api::BankPayout>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user