mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
3778 lines
144 KiB
Rust
3778 lines
144 KiB
Rust
use std::borrow::Cow;
|
|
|
|
use api_models::payments::{CardToken, GetPaymentMethodType};
|
|
use base64::Engine;
|
|
use common_utils::{
|
|
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
|
|
fp_utils, generate_id, pii,
|
|
};
|
|
use data_models::{
|
|
mandates::MandateData,
|
|
payments::{payment_attempt::PaymentAttempt, PaymentIntent},
|
|
};
|
|
use diesel_models::enums;
|
|
// TODO : Evaluate all the helper functions ()
|
|
use error_stack::{report, IntoReport, ResultExt};
|
|
#[cfg(feature = "kms")]
|
|
use external_services::kms;
|
|
use josekit::jwe;
|
|
use masking::{ExposeInterface, PeekInterface};
|
|
use openssl::{
|
|
derive::Deriver,
|
|
pkey::PKey,
|
|
symm::{decrypt_aead, Cipher},
|
|
};
|
|
use router_env::{instrument, logger, tracing};
|
|
use time::Duration;
|
|
use uuid::Uuid;
|
|
use x509_parser::parse_x509_certificate;
|
|
|
|
use super::{
|
|
operations::{BoxedOperation, Operation, PaymentResponse},
|
|
CustomerDetails, PaymentData,
|
|
};
|
|
use crate::{
|
|
configs::settings::{ConnectorRequestReferenceIdConfig, Server, TempLockerEnableConfig},
|
|
connector,
|
|
consts::{self, BASE64_ENGINE},
|
|
core::{
|
|
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
|
payment_methods::{cards, vault, PaymentMethodRetrieve},
|
|
payments,
|
|
},
|
|
db::StorageInterface,
|
|
routes::{metrics, payment_methods, AppState},
|
|
services,
|
|
types::{
|
|
api::{self, admin, enums as api_enums, MandateValidationFieldsExt},
|
|
domain::{
|
|
self,
|
|
types::{self, AsyncLift},
|
|
},
|
|
storage::{self, enums as storage_enums, ephemeral_key, CustomerUpdate::Update},
|
|
transformers::{ForeignFrom, ForeignTryFrom},
|
|
ErrorResponse, MandateReference, RouterData,
|
|
},
|
|
utils::{
|
|
self,
|
|
crypto::{self, SignMessage},
|
|
OptionExt, StringExt,
|
|
},
|
|
};
|
|
|
|
pub fn create_identity_from_certificate_and_key(
|
|
encoded_certificate: String,
|
|
encoded_certificate_key: String,
|
|
) -> Result<reqwest::Identity, error_stack::Report<errors::ApiClientError>> {
|
|
let decoded_certificate = BASE64_ENGINE
|
|
.decode(encoded_certificate)
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::CertificateDecodeFailed)?;
|
|
|
|
let decoded_certificate_key = BASE64_ENGINE
|
|
.decode(encoded_certificate_key)
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::CertificateDecodeFailed)?;
|
|
|
|
let certificate = String::from_utf8(decoded_certificate)
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::CertificateDecodeFailed)?;
|
|
|
|
let certificate_key = String::from_utf8(decoded_certificate_key)
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::CertificateDecodeFailed)?;
|
|
|
|
reqwest::Identity::from_pkcs8_pem(certificate.as_bytes(), certificate_key.as_bytes())
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::CertificateDecodeFailed)
|
|
}
|
|
|
|
pub fn filter_mca_based_on_business_profile(
|
|
merchant_connector_accounts: Vec<domain::MerchantConnectorAccount>,
|
|
profile_id: Option<String>,
|
|
) -> Vec<domain::MerchantConnectorAccount> {
|
|
if let Some(profile_id) = profile_id {
|
|
merchant_connector_accounts
|
|
.into_iter()
|
|
.filter(|mca| mca.profile_id.as_ref() == Some(&profile_id))
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
merchant_connector_accounts
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn create_or_update_address_for_payment_by_request(
|
|
db: &dyn StorageInterface,
|
|
req_address: Option<&api::Address>,
|
|
address_id: Option<&str>,
|
|
merchant_id: &str,
|
|
customer_id: Option<&String>,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
payment_id: &str,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
|
|
let key = merchant_key_store.key.get_inner().peek();
|
|
|
|
Ok(match address_id {
|
|
Some(id) => match req_address {
|
|
Some(address) => {
|
|
let address_update = async {
|
|
Ok(storage::AddressUpdate::Update {
|
|
city: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.city.clone()),
|
|
country: address.address.as_ref().and_then(|value| value.country),
|
|
line1: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.line1.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
line2: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.line2.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
line3: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.line3.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
state: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.state.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
zip: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.zip.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
first_name: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.first_name.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
last_name: address
|
|
.address
|
|
.as_ref()
|
|
.and_then(|value| value.last_name.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
phone_number: address
|
|
.phone
|
|
.as_ref()
|
|
.and_then(|value| value.number.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
country_code: address
|
|
.phone
|
|
.as_ref()
|
|
.and_then(|value| value.country_code.clone()),
|
|
updated_by: storage_scheme.to_string(),
|
|
})
|
|
}
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while encrypting address")?;
|
|
let address = db
|
|
.find_address_by_merchant_id_payment_id_address_id(
|
|
merchant_id,
|
|
payment_id,
|
|
id,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while fetching address")?;
|
|
Some(
|
|
db.update_address_for_payments(
|
|
address,
|
|
address_update,
|
|
payment_id.to_string(),
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?,
|
|
)
|
|
}
|
|
None => Some(
|
|
db.find_address_by_merchant_id_payment_id_address_id(
|
|
merchant_id,
|
|
payment_id,
|
|
id,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await,
|
|
)
|
|
.transpose()
|
|
.to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?,
|
|
},
|
|
None => match req_address {
|
|
Some(address) => {
|
|
// generate a new address here
|
|
let address_details = address.address.clone().unwrap_or_default();
|
|
Some(
|
|
db.insert_address_for_payments(
|
|
payment_id,
|
|
get_domain_address_for_payments(
|
|
address_details,
|
|
address,
|
|
merchant_id,
|
|
customer_id,
|
|
payment_id,
|
|
key,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while encrypting address while insert")?,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while inserting new address")?,
|
|
)
|
|
}
|
|
None => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn create_or_find_address_for_payment_by_request(
|
|
db: &dyn StorageInterface,
|
|
req_address: Option<&api::Address>,
|
|
address_id: Option<&str>,
|
|
merchant_id: &str,
|
|
customer_id: Option<&String>,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
payment_id: &str,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
|
|
let key = merchant_key_store.key.get_inner().peek();
|
|
|
|
Ok(match address_id {
|
|
Some(id) => Some(
|
|
db.find_address_by_merchant_id_payment_id_address_id(
|
|
merchant_id,
|
|
payment_id,
|
|
id,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await,
|
|
)
|
|
.transpose()
|
|
.to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?,
|
|
None => match req_address {
|
|
Some(address) => {
|
|
// generate a new address here
|
|
|
|
let address_details = address.address.clone().unwrap_or_default();
|
|
Some(
|
|
db.insert_address_for_payments(
|
|
payment_id,
|
|
get_domain_address_for_payments(
|
|
address_details,
|
|
address,
|
|
merchant_id,
|
|
customer_id,
|
|
payment_id,
|
|
key,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while encrypting address while insert")?,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while inserting new address")?,
|
|
)
|
|
}
|
|
None => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub async fn get_domain_address_for_payments(
|
|
address_details: api_models::payments::AddressDetails,
|
|
address: &api_models::payments::Address,
|
|
merchant_id: &str,
|
|
customer_id: Option<&String>,
|
|
payment_id: &str,
|
|
key: &[u8],
|
|
storage_scheme: enums::MerchantStorageScheme,
|
|
) -> CustomResult<domain::Address, common_utils::errors::CryptoError> {
|
|
async {
|
|
Ok(domain::Address {
|
|
id: None,
|
|
phone_number: address
|
|
.phone
|
|
.as_ref()
|
|
.and_then(|a| a.number.clone())
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
country_code: address.phone.as_ref().and_then(|a| a.country_code.clone()),
|
|
customer_id: customer_id.cloned(),
|
|
merchant_id: merchant_id.to_string(),
|
|
address_id: generate_id(consts::ID_LENGTH, "add"),
|
|
city: address_details.city,
|
|
country: address_details.country,
|
|
line1: address_details
|
|
.line1
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
line2: address_details
|
|
.line2
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
line3: address_details
|
|
.line3
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
state: address_details
|
|
.state
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
created_at: common_utils::date_time::now(),
|
|
first_name: address_details
|
|
.first_name
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
last_name: address_details
|
|
.last_name
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
modified_at: common_utils::date_time::now(),
|
|
zip: address_details
|
|
.zip
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
payment_id: Some(payment_id.to_owned()),
|
|
updated_by: storage_scheme.to_string(),
|
|
})
|
|
}
|
|
.await
|
|
}
|
|
|
|
pub async fn get_address_by_id(
|
|
db: &dyn StorageInterface,
|
|
address_id: Option<String>,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
payment_id: String,
|
|
merchant_id: String,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
|
|
match address_id {
|
|
None => Ok(None),
|
|
Some(address_id) => Ok(db
|
|
.find_address_by_merchant_id_payment_id_address_id(
|
|
&merchant_id,
|
|
&payment_id,
|
|
&address_id,
|
|
merchant_key_store,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.ok()),
|
|
}
|
|
}
|
|
|
|
pub async fn get_token_pm_type_mandate_details(
|
|
state: &AppState,
|
|
request: &api::PaymentsRequest,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
merchant_account: &domain::MerchantAccount,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> RouterResult<(
|
|
Option<String>,
|
|
Option<storage_enums::PaymentMethod>,
|
|
Option<storage_enums::PaymentMethodType>,
|
|
Option<MandateData>,
|
|
Option<payments::RecurringMandatePaymentData>,
|
|
Option<payments::MandateConnectorDetails>,
|
|
)> {
|
|
let mandate_data = request.mandate_data.clone().map(MandateData::foreign_from);
|
|
match mandate_type {
|
|
Some(api::MandateTransactionType::NewMandateTransaction) => {
|
|
let setup_mandate = mandate_data.clone().get_required_value("mandate_data")?;
|
|
Ok((
|
|
request.payment_token.to_owned(),
|
|
request.payment_method,
|
|
request.payment_method_type,
|
|
Some(setup_mandate),
|
|
None,
|
|
None,
|
|
))
|
|
}
|
|
Some(api::MandateTransactionType::RecurringMandateTransaction) => {
|
|
let (
|
|
token_,
|
|
payment_method_,
|
|
recurring_mandate_payment_data,
|
|
payment_method_type_,
|
|
mandate_connector,
|
|
) = get_token_for_recurring_mandate(
|
|
state,
|
|
request,
|
|
merchant_account,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
Ok((
|
|
token_,
|
|
payment_method_,
|
|
payment_method_type_.or(request.payment_method_type),
|
|
None,
|
|
recurring_mandate_payment_data,
|
|
mandate_connector,
|
|
))
|
|
}
|
|
None => Ok((
|
|
request.payment_token.to_owned(),
|
|
request.payment_method,
|
|
request.payment_method_type,
|
|
mandate_data,
|
|
None,
|
|
None,
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub async fn get_token_for_recurring_mandate(
|
|
state: &AppState,
|
|
req: &api::PaymentsRequest,
|
|
merchant_account: &domain::MerchantAccount,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> RouterResult<(
|
|
Option<String>,
|
|
Option<storage_enums::PaymentMethod>,
|
|
Option<payments::RecurringMandatePaymentData>,
|
|
Option<storage_enums::PaymentMethodType>,
|
|
Option<payments::MandateConnectorDetails>,
|
|
)> {
|
|
let db = &*state.store;
|
|
let mandate_id = req.mandate_id.clone().get_required_value("mandate_id")?;
|
|
|
|
let mandate = db
|
|
.find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, mandate_id.as_str())
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
|
|
|
|
let customer = req.customer_id.clone().get_required_value("customer_id")?;
|
|
|
|
let payment_method_id = {
|
|
if mandate.customer_id != customer {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "customer_id must match mandate customer_id".into()
|
|
}))?
|
|
}
|
|
if mandate.mandate_status != storage_enums::MandateStatus::Active {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "mandate is not active".into()
|
|
}))?
|
|
};
|
|
mandate.payment_method_id.clone()
|
|
};
|
|
verify_mandate_details(
|
|
req.amount.get_required_value("amount")?.into(),
|
|
req.currency.get_required_value("currency")?,
|
|
mandate.clone(),
|
|
)?;
|
|
|
|
let payment_method = db
|
|
.find_payment_method(payment_method_id.as_str())
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
|
|
|
|
let token = Uuid::new_v4().to_string();
|
|
let payment_method_type = payment_method.payment_method_type;
|
|
let mandate_connector_details = payments::MandateConnectorDetails {
|
|
connector: mandate.connector,
|
|
merchant_connector_id: mandate.merchant_connector_id,
|
|
};
|
|
|
|
if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method {
|
|
let _ =
|
|
cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store)
|
|
.await?;
|
|
if let Some(payment_method_from_request) = req.payment_method {
|
|
let pm: storage_enums::PaymentMethod = payment_method_from_request;
|
|
if pm != payment_method.payment_method {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message:
|
|
"payment method in request does not match previously provided payment \
|
|
method information"
|
|
.into()
|
|
}))?
|
|
}
|
|
};
|
|
|
|
Ok((
|
|
Some(token),
|
|
Some(payment_method.payment_method),
|
|
Some(payments::RecurringMandatePaymentData {
|
|
payment_method_type,
|
|
}),
|
|
payment_method.payment_method_type,
|
|
Some(mandate_connector_details),
|
|
))
|
|
} else {
|
|
Ok((
|
|
None,
|
|
Some(payment_method.payment_method),
|
|
Some(payments::RecurringMandatePaymentData {
|
|
payment_method_type,
|
|
}),
|
|
payment_method.payment_method_type,
|
|
Some(mandate_connector_details),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
/// Check weather the merchant id in the request
|
|
/// and merchant id in the merchant account are same.
|
|
pub fn validate_merchant_id(
|
|
merchant_id: &str,
|
|
request_merchant_id: Option<&str>,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
// Get Merchant Id from the merchant
|
|
// or get from merchant account
|
|
|
|
let request_merchant_id = request_merchant_id.unwrap_or(merchant_id);
|
|
|
|
utils::when(merchant_id.ne(request_merchant_id), || {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!(
|
|
"Invalid `merchant_id`: {request_merchant_id} not found in merchant account"
|
|
)
|
|
}))
|
|
})
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub fn validate_request_amount_and_amount_to_capture(
|
|
op_amount: Option<api::Amount>,
|
|
op_amount_to_capture: Option<i64>,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
match (op_amount, op_amount_to_capture) {
|
|
(None, _) => Ok(()),
|
|
(Some(_amount), None) => Ok(()),
|
|
(Some(amount), Some(amount_to_capture)) => {
|
|
match amount {
|
|
api::Amount::Value(amount_inner) => {
|
|
// If both amount and amount to capture is present
|
|
// then amount to be capture should be less than or equal to request amount
|
|
utils::when(!amount_to_capture.le(&amount_inner.get()), || {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!(
|
|
"amount_to_capture is greater than amount capture_amount: {amount_to_capture:?} request_amount: {amount:?}"
|
|
)
|
|
}))
|
|
})
|
|
}
|
|
api::Amount::Zero => {
|
|
// If the amount is Null but still amount_to_capture is passed this is invalid and
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "amount_to_capture should not exist for when amount = 0"
|
|
.to_string()
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// if capture method = automatic, amount_to_capture(if provided) must be equal to amount
|
|
#[instrument(skip_all)]
|
|
pub fn validate_amount_to_capture_in_create_call_request(
|
|
request: &api_models::payments::PaymentsRequest,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic {
|
|
let total_capturable_amount = request.get_total_capturable_amount();
|
|
if let Some((amount_to_capture, total_capturable_amount)) =
|
|
request.amount_to_capture.zip(total_capturable_amount)
|
|
{
|
|
utils::when(amount_to_capture != total_capturable_amount, || {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "amount_to_capture must be equal to total_capturable_amount when capture_method = automatic".into()
|
|
}))
|
|
})
|
|
} else {
|
|
Ok(())
|
|
}
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub fn validate_card_data(
|
|
payment_method_data: Option<api::PaymentMethodData>,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
if let Some(api::PaymentMethodData::Card(card)) = payment_method_data {
|
|
let cvc = card.card_cvc.peek().to_string();
|
|
if cvc.len() < 3 || cvc.len() > 4 {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Invalid card_cvc length".to_string()
|
|
}))?
|
|
}
|
|
let card_cvc = cvc.parse::<u16>().into_report().change_context(
|
|
errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "card_cvc",
|
|
},
|
|
)?;
|
|
::cards::CardSecurityCode::try_from(card_cvc).change_context(
|
|
errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Invalid Card CVC".to_string(),
|
|
},
|
|
)?;
|
|
|
|
let exp_month = card
|
|
.card_exp_month
|
|
.peek()
|
|
.to_string()
|
|
.parse::<u8>()
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "card_exp_month",
|
|
})?;
|
|
let month = ::cards::CardExpirationMonth::try_from(exp_month).change_context(
|
|
errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Invalid Expiry Month".to_string(),
|
|
},
|
|
)?;
|
|
let mut year_str = card.card_exp_year.peek().to_string();
|
|
if year_str.len() == 2 {
|
|
year_str = format!("20{}", year_str);
|
|
}
|
|
let exp_year = year_str.parse::<u16>().into_report().change_context(
|
|
errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "card_exp_year",
|
|
},
|
|
)?;
|
|
let year = ::cards::CardExpirationYear::try_from(exp_year).change_context(
|
|
errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Invalid Expiry Year".to_string(),
|
|
},
|
|
)?;
|
|
|
|
let card_expiration = ::cards::CardExpiration { month, year };
|
|
let is_expired = card_expiration.is_expired().change_context(
|
|
errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Invalid card data".to_string(),
|
|
},
|
|
)?;
|
|
if is_expired {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "Card Expired".to_string()
|
|
}))?
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn infer_payment_type(
|
|
amount: &api::Amount,
|
|
mandate_type: Option<&api::MandateTransactionType>,
|
|
) -> api_enums::PaymentType {
|
|
match mandate_type {
|
|
Some(api::MandateTransactionType::NewMandateTransaction) => {
|
|
if let api::Amount::Value(_) = amount {
|
|
api_enums::PaymentType::NewMandate
|
|
} else {
|
|
api_enums::PaymentType::SetupMandate
|
|
}
|
|
}
|
|
|
|
Some(api::MandateTransactionType::RecurringMandateTransaction) => {
|
|
api_enums::PaymentType::RecurringMandate
|
|
}
|
|
|
|
None => api_enums::PaymentType::Normal,
|
|
}
|
|
}
|
|
|
|
pub fn validate_mandate(
|
|
req: impl Into<api::MandateValidationFields>,
|
|
is_confirm_operation: bool,
|
|
) -> CustomResult<Option<api::MandateTransactionType>, errors::ApiErrorResponse> {
|
|
let req: api::MandateValidationFields = req.into();
|
|
match req.validate_and_get_mandate_type().change_context(
|
|
errors::ApiErrorResponse::MandateValidationFailed {
|
|
reason: "Expected one out of mandate_id and mandate_data but got both".to_string(),
|
|
},
|
|
)? {
|
|
Some(api::MandateTransactionType::NewMandateTransaction) => {
|
|
validate_new_mandate_request(req, is_confirm_operation)?;
|
|
Ok(Some(api::MandateTransactionType::NewMandateTransaction))
|
|
}
|
|
Some(api::MandateTransactionType::RecurringMandateTransaction) => {
|
|
validate_recurring_mandate(req)?;
|
|
Ok(Some(
|
|
api::MandateTransactionType::RecurringMandateTransaction,
|
|
))
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
fn validate_new_mandate_request(
|
|
req: api::MandateValidationFields,
|
|
is_confirm_operation: bool,
|
|
) -> RouterResult<()> {
|
|
// We need not check for customer_id in the confirm request if it is already passed
|
|
// in create request
|
|
|
|
fp_utils::when(!is_confirm_operation && req.customer_id.is_none(), || {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`customer_id` is mandatory for mandates".into()
|
|
}))
|
|
})?;
|
|
|
|
let mandate_data = req
|
|
.mandate_data
|
|
.clone()
|
|
.get_required_value("mandate_data")?;
|
|
|
|
if api_enums::FutureUsage::OnSession
|
|
== req
|
|
.setup_future_usage
|
|
.get_required_value("setup_future_usage")?
|
|
{
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`setup_future_usage` must be `off_session` for mandates".into()
|
|
}))?
|
|
};
|
|
|
|
// Only use this validation if the customer_acceptance is present
|
|
if mandate_data
|
|
.customer_acceptance
|
|
.map(|inner| inner.acceptance_type == api::AcceptanceType::Online && inner.online.is_none())
|
|
.unwrap_or(false)
|
|
{
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`mandate_data.customer_acceptance.online` is required when \
|
|
`mandate_data.customer_acceptance.acceptance_type` is `online`"
|
|
.into()
|
|
}))?
|
|
}
|
|
|
|
let mandate_details = match mandate_data.mandate_type {
|
|
Some(api_models::payments::MandateType::SingleUse(details)) => Some(details),
|
|
Some(api_models::payments::MandateType::MultiUse(details)) => details,
|
|
None => None,
|
|
};
|
|
mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)|
|
|
utils::when (start_date >= end_date, || {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`mandate_data.mandate_type.{multi_use|single_use}.start_date` should be greater than \
|
|
`mandate_data.mandate_type.{multi_use|single_use}.end_date`"
|
|
.into()
|
|
}))
|
|
})).transpose()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn validate_customer_id_mandatory_cases(
|
|
has_setup_future_usage: bool,
|
|
customer_id: &Option<String>,
|
|
) -> RouterResult<()> {
|
|
match (has_setup_future_usage, customer_id) {
|
|
(true, None) => Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "customer_id is mandatory when setup_future_usage is given".to_string(),
|
|
})
|
|
.into_report(),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub fn create_startpay_url(
|
|
server: &Server,
|
|
payment_attempt: &PaymentAttempt,
|
|
payment_intent: &PaymentIntent,
|
|
) -> String {
|
|
format!(
|
|
"{}/payments/redirect/{}/{}/{}",
|
|
server.base_url,
|
|
payment_intent.payment_id,
|
|
payment_intent.merchant_id,
|
|
payment_attempt.attempt_id
|
|
)
|
|
}
|
|
|
|
pub fn create_redirect_url(
|
|
router_base_url: &String,
|
|
payment_attempt: &PaymentAttempt,
|
|
connector_name: &String,
|
|
creds_identifier: Option<&str>,
|
|
) -> String {
|
|
let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd));
|
|
format!(
|
|
"{}/payments/{}/{}/redirect/response/{}",
|
|
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name,
|
|
) + creds_identifier_path.as_ref()
|
|
}
|
|
|
|
pub fn create_webhook_url(
|
|
router_base_url: &String,
|
|
merchant_id: &String,
|
|
connector_name: &String,
|
|
) -> String {
|
|
format!(
|
|
"{}/webhooks/{}/{}",
|
|
router_base_url, merchant_id, connector_name
|
|
)
|
|
}
|
|
pub fn create_complete_authorize_url(
|
|
router_base_url: &String,
|
|
payment_attempt: &PaymentAttempt,
|
|
connector_name: &String,
|
|
) -> String {
|
|
format!(
|
|
"{}/payments/{}/{}/redirect/complete/{}",
|
|
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name
|
|
)
|
|
}
|
|
|
|
fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> {
|
|
req.mandate_id.check_value_present("mandate_id")?;
|
|
|
|
req.customer_id.check_value_present("customer_id")?;
|
|
|
|
let confirm = req.confirm.get_required_value("confirm")?;
|
|
if !confirm {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`confirm` must be `true` for mandates".into()
|
|
}))?
|
|
}
|
|
|
|
let off_session = req.off_session.get_required_value("off_session")?;
|
|
if !off_session {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "`off_session` should be `true` for mandates".into()
|
|
}))?
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn verify_mandate_details(
|
|
request_amount: i64,
|
|
request_currency: api_enums::Currency,
|
|
mandate: storage::Mandate,
|
|
) -> RouterResult<()> {
|
|
match mandate.mandate_type {
|
|
storage_enums::MandateType::SingleUse => utils::when(
|
|
mandate
|
|
.mandate_amount
|
|
.map(|mandate_amount| request_amount > mandate_amount)
|
|
.unwrap_or(true),
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::MandateValidationFailed {
|
|
reason: "request amount is greater than mandate amount".to_string()
|
|
}))
|
|
},
|
|
),
|
|
storage::enums::MandateType::MultiUse => utils::when(
|
|
mandate
|
|
.mandate_amount
|
|
.map(|mandate_amount| {
|
|
(mandate.amount_captured.unwrap_or(0) + request_amount) > mandate_amount
|
|
})
|
|
.unwrap_or(false),
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::MandateValidationFailed {
|
|
reason: "request amount is greater than mandate amount".to_string()
|
|
}))
|
|
},
|
|
),
|
|
}?;
|
|
utils::when(
|
|
mandate
|
|
.mandate_currency
|
|
.map(|mandate_currency| mandate_currency != request_currency)
|
|
.unwrap_or(false),
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::MandateValidationFailed {
|
|
reason: "cross currency mandates not supported".to_string()
|
|
}))
|
|
},
|
|
)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub fn payment_attempt_status_fsm(
|
|
payment_method_data: &Option<api::PaymentMethodData>,
|
|
confirm: Option<bool>,
|
|
) -> storage_enums::AttemptStatus {
|
|
match payment_method_data {
|
|
Some(_) => match confirm {
|
|
Some(true) => storage_enums::AttemptStatus::Pending,
|
|
_ => storage_enums::AttemptStatus::ConfirmationAwaited,
|
|
},
|
|
None => storage_enums::AttemptStatus::PaymentMethodAwaited,
|
|
}
|
|
}
|
|
|
|
pub fn payment_intent_status_fsm(
|
|
payment_method_data: &Option<api::PaymentMethodData>,
|
|
confirm: Option<bool>,
|
|
) -> storage_enums::IntentStatus {
|
|
match payment_method_data {
|
|
Some(_) => match confirm {
|
|
Some(true) => storage_enums::IntentStatus::RequiresCustomerAction,
|
|
_ => storage_enums::IntentStatus::RequiresConfirmation,
|
|
},
|
|
None => storage_enums::IntentStatus::RequiresPaymentMethod,
|
|
}
|
|
}
|
|
|
|
pub async fn add_domain_task_to_pt<Op>(
|
|
operation: &Op,
|
|
state: &AppState,
|
|
payment_attempt: &PaymentAttempt,
|
|
requeue: bool,
|
|
schedule_time: Option<time::PrimitiveDateTime>,
|
|
) -> CustomResult<(), errors::ApiErrorResponse>
|
|
where
|
|
Op: std::fmt::Debug,
|
|
{
|
|
if check_if_operation_confirm(operation) {
|
|
match schedule_time {
|
|
Some(stime) => {
|
|
if !requeue {
|
|
// scheduler_metrics::TASKS_ADDED_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics
|
|
super::add_process_sync_task(&*state.store, payment_attempt, stime)
|
|
.await
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while adding task to process tracker")
|
|
} else {
|
|
// scheduler_metrics::TASKS_RESET_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics
|
|
super::reset_process_sync_task(&*state.store, payment_attempt, stime)
|
|
.await
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while updating task in process tracker")
|
|
}
|
|
}
|
|
None => Ok(()),
|
|
}
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn response_operation<'a, F, R, Ctx>() -> BoxedOperation<'a, F, R, Ctx>
|
|
where
|
|
F: Send + Clone,
|
|
Ctx: PaymentMethodRetrieve,
|
|
PaymentResponse: Operation<F, R, Ctx>,
|
|
{
|
|
Box::new(PaymentResponse)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) async fn get_payment_method_create_request(
|
|
payment_method_data: Option<&api::PaymentMethodData>,
|
|
payment_method: Option<storage_enums::PaymentMethod>,
|
|
payment_method_type: Option<storage_enums::PaymentMethodType>,
|
|
customer: &domain::Customer,
|
|
) -> RouterResult<api::PaymentMethodCreate> {
|
|
match payment_method_data {
|
|
Some(pm_data) => match payment_method {
|
|
Some(payment_method) => match pm_data {
|
|
api::PaymentMethodData::Card(card) => {
|
|
let card_detail = api::CardDetail {
|
|
card_number: card.card_number.clone(),
|
|
card_exp_month: card.card_exp_month.clone(),
|
|
card_exp_year: card.card_exp_year.clone(),
|
|
card_holder_name: Some(card.card_holder_name.clone()),
|
|
nick_name: card.nick_name.clone(),
|
|
};
|
|
let customer_id = customer.customer_id.clone();
|
|
let payment_method_request = api::PaymentMethodCreate {
|
|
payment_method,
|
|
payment_method_type,
|
|
payment_method_issuer: card.card_issuer.clone(),
|
|
payment_method_issuer_code: None,
|
|
card: Some(card_detail),
|
|
metadata: None,
|
|
customer_id: Some(customer_id),
|
|
card_network: card
|
|
.card_network
|
|
.as_ref()
|
|
.map(|card_network| card_network.to_string()),
|
|
};
|
|
Ok(payment_method_request)
|
|
}
|
|
_ => {
|
|
let payment_method_request = api::PaymentMethodCreate {
|
|
payment_method,
|
|
payment_method_type,
|
|
payment_method_issuer: None,
|
|
payment_method_issuer_code: None,
|
|
card: None,
|
|
metadata: None,
|
|
customer_id: Some(customer.customer_id.to_owned()),
|
|
card_network: None,
|
|
};
|
|
Ok(payment_method_request)
|
|
}
|
|
},
|
|
None => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_method_type"
|
|
})
|
|
.attach_printable("PaymentMethodType Required")),
|
|
},
|
|
None => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_method_data"
|
|
})
|
|
.attach_printable("PaymentMethodData required Or Card is already saved")),
|
|
}
|
|
}
|
|
|
|
pub async fn get_customer_from_details<F: Clone>(
|
|
db: &dyn StorageInterface,
|
|
customer_id: Option<String>,
|
|
merchant_id: &str,
|
|
payment_data: &mut PaymentData<F>,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> CustomResult<Option<domain::Customer>, errors::StorageError> {
|
|
match customer_id {
|
|
None => Ok(None),
|
|
Some(c_id) => {
|
|
let customer = db
|
|
.find_customer_optional_by_customer_id_merchant_id(
|
|
&c_id,
|
|
merchant_id,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
payment_data.email = payment_data.email.clone().or_else(|| {
|
|
customer.as_ref().and_then(|inner| {
|
|
inner
|
|
.email
|
|
.clone()
|
|
.map(|encrypted_value| encrypted_value.into())
|
|
})
|
|
});
|
|
Ok(customer)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Checks if the inner values of two options are not equal and throws appropriate error
|
|
fn validate_options_for_inequality<T: PartialEq>(
|
|
first_option: Option<&T>,
|
|
second_option: Option<&T>,
|
|
field_name: &str,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
fp_utils::when(
|
|
first_option
|
|
.zip(second_option)
|
|
.map(|(value1, value2)| value1 != value2)
|
|
.unwrap_or(false),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!("The field name `{field_name}` sent in both places is ambiguous"),
|
|
})
|
|
},
|
|
)
|
|
}
|
|
|
|
// Checks if the customer details are passed in both places
|
|
// If so, raise an error
|
|
pub fn validate_customer_details_in_request(
|
|
request: &api_models::payments::PaymentsRequest,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
if let Some(customer_details) = request.customer.as_ref() {
|
|
validate_options_for_inequality(
|
|
request.customer_id.as_ref(),
|
|
Some(&customer_details.id),
|
|
"customer_id",
|
|
)?;
|
|
|
|
validate_options_for_inequality(
|
|
request.email.as_ref(),
|
|
customer_details.email.as_ref(),
|
|
"email",
|
|
)?;
|
|
|
|
validate_options_for_inequality(
|
|
request.name.as_ref(),
|
|
customer_details.name.as_ref(),
|
|
"name",
|
|
)?;
|
|
|
|
validate_options_for_inequality(
|
|
request.phone.as_ref(),
|
|
customer_details.phone.as_ref(),
|
|
"phone",
|
|
)?;
|
|
|
|
validate_options_for_inequality(
|
|
request.phone_country_code.as_ref(),
|
|
customer_details.phone_country_code.as_ref(),
|
|
"phone_country_code",
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the customer details from customer field if present
|
|
/// or from the individual fields in `PaymentsRequest`
|
|
#[instrument(skip_all)]
|
|
pub fn get_customer_details_from_request(
|
|
request: &api_models::payments::PaymentsRequest,
|
|
) -> CustomerDetails {
|
|
let customer_id = request
|
|
.customer
|
|
.as_ref()
|
|
.map(|customer_details| customer_details.id.clone())
|
|
.or(request.customer_id.clone());
|
|
|
|
let customer_name = request
|
|
.customer
|
|
.as_ref()
|
|
.and_then(|customer_details| customer_details.name.clone())
|
|
.or(request.name.clone());
|
|
|
|
let customer_email = request
|
|
.customer
|
|
.as_ref()
|
|
.and_then(|customer_details| customer_details.email.clone())
|
|
.or(request.email.clone());
|
|
|
|
let customer_phone = request
|
|
.customer
|
|
.as_ref()
|
|
.and_then(|customer_details| customer_details.phone.clone())
|
|
.or(request.phone.clone());
|
|
|
|
let customer_phone_code = request
|
|
.customer
|
|
.as_ref()
|
|
.and_then(|customer_details| customer_details.phone_country_code.clone())
|
|
.or(request.phone_country_code.clone());
|
|
|
|
CustomerDetails {
|
|
customer_id,
|
|
name: customer_name,
|
|
email: customer_email,
|
|
phone: customer_phone,
|
|
phone_country_code: customer_phone_code,
|
|
}
|
|
}
|
|
|
|
pub async fn get_connector_default(
|
|
_state: &AppState,
|
|
request_connector: Option<serde_json::Value>,
|
|
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
|
|
Ok(request_connector.map_or(
|
|
api::ConnectorChoice::Decide,
|
|
api::ConnectorChoice::StraightThrough,
|
|
))
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>(
|
|
operation: BoxedOperation<'a, F, R, Ctx>,
|
|
db: &dyn StorageInterface,
|
|
payment_data: &mut PaymentData<F>,
|
|
req: Option<CustomerDetails>,
|
|
merchant_id: &str,
|
|
key_store: &domain::MerchantKeyStore,
|
|
) -> CustomResult<(BoxedOperation<'a, F, R, Ctx>, Option<domain::Customer>), errors::StorageError> {
|
|
let request_customer_details = req
|
|
.get_required_value("customer")
|
|
.change_context(errors::StorageError::ValueNotFound("customer".to_owned()))?;
|
|
|
|
let customer_id = request_customer_details
|
|
.customer_id
|
|
.or(payment_data.payment_intent.customer_id.clone());
|
|
|
|
let optional_customer = match customer_id {
|
|
Some(customer_id) => {
|
|
let customer_data = db
|
|
.find_customer_optional_by_customer_id_merchant_id(
|
|
&customer_id,
|
|
merchant_id,
|
|
key_store,
|
|
)
|
|
.await?;
|
|
|
|
Some(match customer_data {
|
|
Some(c) => {
|
|
// Update the customer data if new data is passed in the request
|
|
if request_customer_details.email.is_some()
|
|
| request_customer_details.name.is_some()
|
|
| request_customer_details.phone.is_some()
|
|
| request_customer_details.phone_country_code.is_some()
|
|
{
|
|
let key = key_store.key.get_inner().peek();
|
|
let customer_update = async {
|
|
Ok(Update {
|
|
name: request_customer_details
|
|
.name
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
email: request_customer_details
|
|
.email
|
|
.clone()
|
|
.async_lift(|inner| {
|
|
types::encrypt_optional(
|
|
inner.map(|inner| inner.expose()),
|
|
key,
|
|
)
|
|
})
|
|
.await?,
|
|
phone: Box::new(
|
|
request_customer_details
|
|
.phone
|
|
.clone()
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
),
|
|
phone_country_code: request_customer_details.phone_country_code,
|
|
description: None,
|
|
connector_customer: None,
|
|
metadata: None,
|
|
address_id: None,
|
|
})
|
|
}
|
|
.await
|
|
.change_context(errors::StorageError::SerializationFailed)
|
|
.attach_printable("Failed while encrypting Customer while Update")?;
|
|
|
|
db.update_customer_by_customer_id_merchant_id(
|
|
customer_id,
|
|
merchant_id.to_string(),
|
|
customer_update,
|
|
key_store,
|
|
)
|
|
.await
|
|
} else {
|
|
Ok(c)
|
|
}
|
|
}
|
|
None => {
|
|
let new_customer = async {
|
|
let key = key_store.key.get_inner().peek();
|
|
Ok(domain::Customer {
|
|
customer_id: customer_id.to_string(),
|
|
merchant_id: merchant_id.to_string(),
|
|
name: request_customer_details
|
|
.name
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
email: request_customer_details
|
|
.email
|
|
.clone()
|
|
.async_lift(|inner| {
|
|
types::encrypt_optional(inner.map(|inner| inner.expose()), key)
|
|
})
|
|
.await?,
|
|
phone: request_customer_details
|
|
.phone
|
|
.clone()
|
|
.async_lift(|inner| types::encrypt_optional(inner, key))
|
|
.await?,
|
|
phone_country_code: request_customer_details.phone_country_code.clone(),
|
|
description: None,
|
|
created_at: common_utils::date_time::now(),
|
|
id: None,
|
|
metadata: None,
|
|
modified_at: common_utils::date_time::now(),
|
|
connector_customer: None,
|
|
address_id: None,
|
|
})
|
|
}
|
|
.await
|
|
.change_context(errors::StorageError::SerializationFailed)
|
|
.attach_printable("Failed while encrypting Customer while insert")?;
|
|
metrics::CUSTOMER_CREATED.add(&metrics::CONTEXT, 1, &[]);
|
|
db.insert_customer(new_customer, key_store).await
|
|
}
|
|
})
|
|
}
|
|
None => match &payment_data.payment_intent.customer_id {
|
|
None => None,
|
|
Some(customer_id) => db
|
|
.find_customer_optional_by_customer_id_merchant_id(
|
|
customer_id,
|
|
merchant_id,
|
|
key_store,
|
|
)
|
|
.await?
|
|
.map(Ok),
|
|
},
|
|
};
|
|
Ok((
|
|
operation,
|
|
match optional_customer {
|
|
Some(customer) => {
|
|
let customer = customer?;
|
|
|
|
payment_data.payment_intent.customer_id = Some(customer.customer_id.clone());
|
|
payment_data.email = payment_data.email.clone().or_else(|| {
|
|
customer
|
|
.email
|
|
.clone()
|
|
.map(|encrypted_value| encrypted_value.into())
|
|
});
|
|
|
|
Some(customer)
|
|
}
|
|
None => None,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn retrieve_payment_method_with_temporary_token(
|
|
state: &AppState,
|
|
token: &str,
|
|
payment_intent: &PaymentIntent,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
card_token_data: Option<&CardToken>,
|
|
) -> RouterResult<Option<(api::PaymentMethodData, enums::PaymentMethod)>> {
|
|
let (pm, supplementary_data) =
|
|
vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store)
|
|
.await
|
|
.attach_printable(
|
|
"Payment method for given token not found or there was a problem fetching it",
|
|
)?;
|
|
|
|
utils::when(
|
|
supplementary_data
|
|
.customer_id
|
|
.ne(&payment_intent.customer_id),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() })
|
|
},
|
|
)?;
|
|
|
|
Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(match pm {
|
|
Some(api::PaymentMethodData::Card(card)) => {
|
|
let mut updated_card = card.clone();
|
|
let mut is_card_updated = false;
|
|
|
|
let name_on_card = if card.card_holder_name.clone().expose().is_empty() {
|
|
card_token_data
|
|
.and_then(|token_data| token_data.card_holder_name.clone())
|
|
.filter(|name_on_card| !name_on_card.clone().expose().is_empty())
|
|
.map(|name_on_card| {
|
|
is_card_updated = true;
|
|
name_on_card
|
|
})
|
|
} else {
|
|
Some(card.card_holder_name.clone())
|
|
};
|
|
|
|
if let Some(name_on_card) = name_on_card {
|
|
updated_card.card_holder_name = name_on_card;
|
|
}
|
|
|
|
if let Some(token_data) = card_token_data {
|
|
if let Some(cvc) = token_data.card_cvc.clone() {
|
|
is_card_updated = true;
|
|
updated_card.card_cvc = cvc;
|
|
}
|
|
}
|
|
|
|
if is_card_updated {
|
|
let updated_pm = api::PaymentMethodData::Card(updated_card);
|
|
vault::Vault::store_payment_method_data_in_locker(
|
|
state,
|
|
Some(token.to_owned()),
|
|
&updated_pm,
|
|
payment_intent.customer_id.to_owned(),
|
|
enums::PaymentMethod::Card,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
|
|
Some((updated_pm, enums::PaymentMethod::Card))
|
|
} else {
|
|
Some((
|
|
api::PaymentMethodData::Card(card),
|
|
enums::PaymentMethod::Card,
|
|
))
|
|
}
|
|
}
|
|
|
|
Some(the_pm @ api::PaymentMethodData::Wallet(_)) => {
|
|
Some((the_pm, enums::PaymentMethod::Wallet))
|
|
}
|
|
|
|
Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => {
|
|
Some((the_pm, enums::PaymentMethod::BankTransfer))
|
|
}
|
|
|
|
Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => {
|
|
Some((the_pm, enums::PaymentMethod::BankRedirect))
|
|
}
|
|
|
|
Some(_) => Err(errors::ApiErrorResponse::InternalServerError)
|
|
.into_report()
|
|
.attach_printable("Payment method received from locker is unsupported by locker")?,
|
|
|
|
None => None,
|
|
})
|
|
}
|
|
|
|
pub async fn retrieve_card_with_permanent_token(
|
|
state: &AppState,
|
|
token: &str,
|
|
payment_intent: &PaymentIntent,
|
|
card_token_data: Option<&CardToken>,
|
|
) -> RouterResult<api::PaymentMethodData> {
|
|
let customer_id = payment_intent
|
|
.customer_id
|
|
.as_ref()
|
|
.get_required_value("customer_id")
|
|
.change_context(errors::ApiErrorResponse::UnprocessableEntity {
|
|
message: "no customer id provided for the payment".to_string(),
|
|
})?;
|
|
|
|
let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to fetch card information from the permanent locker")?;
|
|
|
|
let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() {
|
|
if card.name_on_card.unwrap_or_default().expose().is_empty() {
|
|
card_token_data
|
|
.and_then(|token_data| token_data.card_holder_name.clone())
|
|
.filter(|name_on_card| !name_on_card.clone().expose().is_empty())
|
|
} else {
|
|
Some(name_on_card)
|
|
}
|
|
} else {
|
|
card_token_data
|
|
.and_then(|token_data| token_data.card_holder_name.clone())
|
|
.filter(|name_on_card| !name_on_card.clone().expose().is_empty())
|
|
};
|
|
|
|
let api_card = api::Card {
|
|
card_number: card.card_number,
|
|
card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())),
|
|
card_exp_month: card.card_exp_month,
|
|
card_exp_year: card.card_exp_year,
|
|
card_cvc: card_token_data
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
.card_cvc
|
|
.unwrap_or_default(),
|
|
card_issuer: card.card_brand,
|
|
nick_name: card.nick_name.map(masking::Secret::new),
|
|
card_network: None,
|
|
card_type: None,
|
|
card_issuing_country: None,
|
|
bank_code: None,
|
|
};
|
|
|
|
Ok(api::PaymentMethodData::Card(api_card))
|
|
}
|
|
|
|
pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>(
|
|
operation: BoxedOperation<'a, F, R, Ctx>,
|
|
state: &'a AppState,
|
|
payment_data: &mut PaymentData<F>,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> RouterResult<(
|
|
BoxedOperation<'a, F, R, Ctx>,
|
|
Option<api::PaymentMethodData>,
|
|
)> {
|
|
let request = &payment_data.payment_method_data.clone();
|
|
|
|
let mut card_token_data = payment_data
|
|
.payment_method_data
|
|
.clone()
|
|
.and_then(|pmd| match pmd {
|
|
api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data),
|
|
_ => None,
|
|
})
|
|
.or(Some(CardToken::default()));
|
|
|
|
if let Some(cvc) = payment_data.card_cvc.clone() {
|
|
if let Some(token_data) = card_token_data.as_mut() {
|
|
token_data.card_cvc = Some(cvc);
|
|
}
|
|
}
|
|
|
|
let token = payment_data.token.clone();
|
|
|
|
let hyperswitch_token = match payment_data.mandate_id {
|
|
Some(_) => token.map(storage::PaymentTokenData::temporary_generic),
|
|
None => {
|
|
if let Some(token) = token {
|
|
let redis_conn = state
|
|
.store
|
|
.get_redis_conn()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to get redis connection")?;
|
|
|
|
let key = format!(
|
|
"pm_token_{}_{}_hyperswitch",
|
|
token,
|
|
payment_data
|
|
.payment_attempt
|
|
.payment_method
|
|
.to_owned()
|
|
.get_required_value("payment_method")?,
|
|
);
|
|
|
|
let token_data_string = redis_conn
|
|
.get_key::<Option<String>>(&key)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to fetch the token from redis")?
|
|
.ok_or(error_stack::Report::new(
|
|
errors::ApiErrorResponse::UnprocessableEntity {
|
|
message: "Token is invalid or expired".to_owned(),
|
|
},
|
|
))?;
|
|
|
|
let token_data_result = token_data_string
|
|
.clone()
|
|
.parse_struct("PaymentTokenData")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to deserialize hyperswitch token data");
|
|
|
|
let token_data = match token_data_result {
|
|
Ok(data) => data,
|
|
Err(e) => {
|
|
// The purpose of this logic is backwards compatibility to support tokens
|
|
// in redis that might be following the old format.
|
|
if token_data_string.starts_with('{') {
|
|
return Err(e);
|
|
} else {
|
|
storage::PaymentTokenData::temporary_generic(token_data_string)
|
|
}
|
|
}
|
|
};
|
|
|
|
Some(token_data)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: Handle case where payment method and token both are present in request properly.
|
|
let payment_method = match (request, hyperswitch_token) {
|
|
(_, Some(hyperswitch_token)) => {
|
|
let payment_method_details = Ctx::retrieve_payment_method_with_token(
|
|
state,
|
|
merchant_key_store,
|
|
&hyperswitch_token,
|
|
&payment_data.payment_intent,
|
|
card_token_data.as_ref(),
|
|
)
|
|
.await
|
|
.attach_printable("in 'make_pm_data'")?;
|
|
|
|
Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(
|
|
if let Some((payment_method_data, payment_method)) = payment_method_details {
|
|
payment_data.payment_attempt.payment_method = Some(payment_method);
|
|
Some(payment_method_data)
|
|
} else {
|
|
None
|
|
},
|
|
)
|
|
}
|
|
|
|
(Some(_), _) => {
|
|
let payment_method_data = Ctx::retrieve_payment_method(
|
|
request,
|
|
state,
|
|
&payment_data.payment_intent,
|
|
&payment_data.payment_attempt,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
|
|
payment_data.token = payment_method_data.1;
|
|
|
|
Ok(payment_method_data.0)
|
|
}
|
|
_ => Ok(None),
|
|
}?;
|
|
|
|
Ok((operation, payment_method))
|
|
}
|
|
|
|
pub async fn store_in_vault_and_generate_ppmt(
|
|
state: &AppState,
|
|
payment_method_data: &api_models::payments::PaymentMethodData,
|
|
payment_intent: &PaymentIntent,
|
|
payment_attempt: &PaymentAttempt,
|
|
payment_method: enums::PaymentMethod,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> RouterResult<String> {
|
|
let router_token = vault::Vault::store_payment_method_data_in_locker(
|
|
state,
|
|
None,
|
|
payment_method_data,
|
|
payment_intent.customer_id.to_owned(),
|
|
payment_method,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token");
|
|
let key_for_hyperswitch_token = payment_attempt.payment_method.map(|payment_method| {
|
|
payment_methods::ParentPaymentMethodToken::create_key_for_token((
|
|
&parent_payment_method_token,
|
|
payment_method,
|
|
))
|
|
});
|
|
if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token {
|
|
key_for_hyperswitch_token
|
|
.insert(
|
|
Some(payment_intent.created_at),
|
|
storage::PaymentTokenData::temporary_generic(router_token),
|
|
state,
|
|
)
|
|
.await?;
|
|
};
|
|
Ok(parent_payment_method_token)
|
|
}
|
|
|
|
pub async fn store_payment_method_data_in_vault(
|
|
state: &AppState,
|
|
payment_attempt: &PaymentAttempt,
|
|
payment_intent: &PaymentIntent,
|
|
payment_method: enums::PaymentMethod,
|
|
payment_method_data: &api::PaymentMethodData,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
) -> RouterResult<Option<String>> {
|
|
if should_store_payment_method_data_in_vault(
|
|
&state.conf.temp_locker_enable_config,
|
|
payment_attempt.connector.clone(),
|
|
payment_method,
|
|
) {
|
|
let parent_payment_method_token = store_in_vault_and_generate_ppmt(
|
|
state,
|
|
payment_method_data,
|
|
payment_intent,
|
|
payment_attempt,
|
|
payment_method,
|
|
merchant_key_store,
|
|
)
|
|
.await?;
|
|
|
|
return Ok(Some(parent_payment_method_token));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
pub fn should_store_payment_method_data_in_vault(
|
|
temp_locker_enable_config: &TempLockerEnableConfig,
|
|
option_connector: Option<String>,
|
|
payment_method: enums::PaymentMethod,
|
|
) -> bool {
|
|
option_connector
|
|
.map(|connector| {
|
|
temp_locker_enable_config
|
|
.0
|
|
.get(&connector)
|
|
.map(|config| config.payment_method.contains(&payment_method))
|
|
.unwrap_or(false)
|
|
})
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) fn validate_capture_method(
|
|
capture_method: storage_enums::CaptureMethod,
|
|
) -> RouterResult<()> {
|
|
utils::when(
|
|
capture_method == storage_enums::CaptureMethod::Automatic,
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
|
|
field_name: "capture_method".to_string(),
|
|
current_flow: "captured".to_string(),
|
|
current_value: capture_method.to_string(),
|
|
states: "manual, manual_multiple, scheduled".to_string()
|
|
}))
|
|
},
|
|
)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) fn validate_status_with_capture_method(
|
|
status: storage_enums::IntentStatus,
|
|
capture_method: storage_enums::CaptureMethod,
|
|
) -> RouterResult<()> {
|
|
if status == storage_enums::IntentStatus::Processing
|
|
&& !(capture_method == storage_enums::CaptureMethod::ManualMultiple)
|
|
{
|
|
return Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
|
|
field_name: "capture_method".to_string(),
|
|
current_flow: "captured".to_string(),
|
|
current_value: capture_method.to_string(),
|
|
states: "manual_multiple".to_string()
|
|
}));
|
|
}
|
|
utils::when(
|
|
status != storage_enums::IntentStatus::RequiresCapture
|
|
&& status != storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
|
&& status != storage_enums::IntentStatus::Processing,
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
|
|
field_name: "payment.status".to_string(),
|
|
current_flow: "captured".to_string(),
|
|
current_value: status.to_string(),
|
|
states: "requires_capture, partially_captured_and_capturable, processing"
|
|
.to_string()
|
|
}))
|
|
},
|
|
)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) fn validate_amount_to_capture(
|
|
amount: i64,
|
|
amount_to_capture: Option<i64>,
|
|
) -> RouterResult<()> {
|
|
utils::when(
|
|
amount_to_capture.is_some() && (Some(amount) < amount_to_capture),
|
|
|| {
|
|
Err(report!(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "amount_to_capture is greater than amount".to_string()
|
|
}))
|
|
},
|
|
)
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) fn validate_payment_method_fields_present(
|
|
req: &api::PaymentsRequest,
|
|
) -> RouterResult<()> {
|
|
utils::when(
|
|
req.payment_method.is_none() && req.payment_method_data.is_some(),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_method",
|
|
})
|
|
},
|
|
)?;
|
|
|
|
utils::when(
|
|
!matches!(
|
|
req.payment_method,
|
|
Some(api_enums::PaymentMethod::Card) | None
|
|
) && (req.payment_method_type.is_none()),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_method_type",
|
|
})
|
|
},
|
|
)?;
|
|
|
|
utils::when(
|
|
req.payment_method.is_some()
|
|
&& req.payment_method_data.is_none()
|
|
&& req.payment_token.is_none(),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_method_data",
|
|
})
|
|
},
|
|
)?;
|
|
utils::when(
|
|
req.payment_method.is_some() && req.payment_method_type.is_some(),
|
|
|| {
|
|
req.payment_method
|
|
.map_or(Ok(()), |req_payment_method| {
|
|
req.payment_method_type.map_or(Ok(()), |req_payment_method_type| {
|
|
if !validate_payment_method_type_against_payment_method(req_payment_method, req_payment_method_type) {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: ("payment_method_type doesn't correspond to the specified payment_method"
|
|
.to_string()),
|
|
})
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
})
|
|
},
|
|
)?;
|
|
|
|
let validate_payment_method_and_payment_method_data =
|
|
|req_payment_method_data, req_payment_method: api_enums::PaymentMethod| {
|
|
api_enums::PaymentMethod::foreign_try_from(req_payment_method_data).and_then(|payment_method|
|
|
if req_payment_method != payment_method {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: ("payment_method_data doesn't correspond to the specified payment_method"
|
|
.to_string()),
|
|
})
|
|
} else {
|
|
Ok(())
|
|
})
|
|
};
|
|
|
|
utils::when(
|
|
req.payment_method.is_some() && req.payment_method_data.is_some(),
|
|
|| {
|
|
req.payment_method_data
|
|
.clone()
|
|
.map_or(Ok(()), |req_payment_method_data| {
|
|
req.payment_method.map_or(Ok(()), |req_payment_method| {
|
|
validate_payment_method_and_payment_method_data(
|
|
req_payment_method_data,
|
|
req_payment_method,
|
|
)
|
|
})
|
|
})
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn validate_payment_method_type_against_payment_method(
|
|
payment_method: api_enums::PaymentMethod,
|
|
payment_method_type: api_enums::PaymentMethodType,
|
|
) -> bool {
|
|
match payment_method {
|
|
api_enums::PaymentMethod::Card => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit
|
|
),
|
|
api_enums::PaymentMethod::PayLater => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Affirm
|
|
| api_enums::PaymentMethodType::Alma
|
|
| api_enums::PaymentMethodType::AfterpayClearpay
|
|
| api_enums::PaymentMethodType::Klarna
|
|
| api_enums::PaymentMethodType::PayBright
|
|
| api_enums::PaymentMethodType::Atome
|
|
| api_enums::PaymentMethodType::Walley
|
|
),
|
|
api_enums::PaymentMethod::Wallet => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::ApplePay
|
|
| api_enums::PaymentMethodType::GooglePay
|
|
| api_enums::PaymentMethodType::Paypal
|
|
| api_enums::PaymentMethodType::AliPay
|
|
| api_enums::PaymentMethodType::AliPayHk
|
|
| api_enums::PaymentMethodType::Dana
|
|
| api_enums::PaymentMethodType::MbWay
|
|
| api_enums::PaymentMethodType::MobilePay
|
|
| api_enums::PaymentMethodType::SamsungPay
|
|
| api_enums::PaymentMethodType::Twint
|
|
| api_enums::PaymentMethodType::Vipps
|
|
| api_enums::PaymentMethodType::TouchNGo
|
|
| api_enums::PaymentMethodType::Swish
|
|
| api_enums::PaymentMethodType::WeChatPay
|
|
| api_enums::PaymentMethodType::GoPay
|
|
| api_enums::PaymentMethodType::Gcash
|
|
| api_enums::PaymentMethodType::Momo
|
|
| api_enums::PaymentMethodType::KakaoPay
|
|
| api_enums::PaymentMethodType::Cashapp
|
|
),
|
|
api_enums::PaymentMethod::BankRedirect => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Giropay
|
|
| api_enums::PaymentMethodType::Ideal
|
|
| api_enums::PaymentMethodType::Sofort
|
|
| api_enums::PaymentMethodType::Eps
|
|
| api_enums::PaymentMethodType::BancontactCard
|
|
| api_enums::PaymentMethodType::Blik
|
|
| api_enums::PaymentMethodType::OnlineBankingThailand
|
|
| api_enums::PaymentMethodType::OnlineBankingCzechRepublic
|
|
| api_enums::PaymentMethodType::OnlineBankingFinland
|
|
| api_enums::PaymentMethodType::OnlineBankingFpx
|
|
| api_enums::PaymentMethodType::OnlineBankingPoland
|
|
| api_enums::PaymentMethodType::OnlineBankingSlovakia
|
|
| api_enums::PaymentMethodType::Przelewy24
|
|
| api_enums::PaymentMethodType::Trustly
|
|
| api_enums::PaymentMethodType::Bizum
|
|
| api_enums::PaymentMethodType::Interac
|
|
| api_enums::PaymentMethodType::OpenBankingUk
|
|
),
|
|
api_enums::PaymentMethod::BankTransfer => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Ach
|
|
| api_enums::PaymentMethodType::Sepa
|
|
| api_enums::PaymentMethodType::Bacs
|
|
| api_enums::PaymentMethodType::Multibanco
|
|
| api_enums::PaymentMethodType::Pix
|
|
| api_enums::PaymentMethodType::Pse
|
|
| api_enums::PaymentMethodType::PermataBankTransfer
|
|
| api_enums::PaymentMethodType::BcaBankTransfer
|
|
| api_enums::PaymentMethodType::BniVa
|
|
| api_enums::PaymentMethodType::BriVa
|
|
| api_enums::PaymentMethodType::CimbVa
|
|
| api_enums::PaymentMethodType::DanamonVa
|
|
| api_enums::PaymentMethodType::MandiriVa
|
|
),
|
|
api_enums::PaymentMethod::BankDebit => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Ach
|
|
| api_enums::PaymentMethodType::Sepa
|
|
| api_enums::PaymentMethodType::Bacs
|
|
| api_enums::PaymentMethodType::Becs
|
|
),
|
|
api_enums::PaymentMethod::Crypto => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::CryptoCurrency
|
|
),
|
|
api_enums::PaymentMethod::Reward => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Evoucher | api_enums::PaymentMethodType::ClassicReward
|
|
),
|
|
api_enums::PaymentMethod::Upi => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::UpiCollect
|
|
),
|
|
api_enums::PaymentMethod::Voucher => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Boleto
|
|
| api_enums::PaymentMethodType::Efecty
|
|
| api_enums::PaymentMethodType::PagoEfectivo
|
|
| api_enums::PaymentMethodType::RedCompra
|
|
| api_enums::PaymentMethodType::RedPagos
|
|
| api_enums::PaymentMethodType::Indomaret
|
|
| api_enums::PaymentMethodType::Alfamart
|
|
| api_enums::PaymentMethodType::Oxxo
|
|
| api_enums::PaymentMethodType::SevenEleven
|
|
| api_enums::PaymentMethodType::Lawson
|
|
| api_enums::PaymentMethodType::MiniStop
|
|
| api_enums::PaymentMethodType::FamilyMart
|
|
| api_enums::PaymentMethodType::Seicomart
|
|
| api_enums::PaymentMethodType::PayEasy
|
|
),
|
|
api_enums::PaymentMethod::GiftCard => {
|
|
matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Givex | api_enums::PaymentMethodType::PaySafeCard
|
|
)
|
|
}
|
|
api_enums::PaymentMethod::CardRedirect => matches!(
|
|
payment_method_type,
|
|
api_enums::PaymentMethodType::Knet
|
|
| api_enums::PaymentMethodType::Benefit
|
|
| api_enums::PaymentMethodType::MomoAtm
|
|
| api_enums::PaymentMethodType::CardRedirect
|
|
),
|
|
}
|
|
}
|
|
|
|
pub fn check_force_psync_precondition(status: &storage_enums::AttemptStatus) -> bool {
|
|
!matches!(
|
|
status,
|
|
storage_enums::AttemptStatus::Charged
|
|
| storage_enums::AttemptStatus::AutoRefunded
|
|
| storage_enums::AttemptStatus::Voided
|
|
| storage_enums::AttemptStatus::CodInitiated
|
|
| storage_enums::AttemptStatus::Started
|
|
| storage_enums::AttemptStatus::Failure
|
|
)
|
|
}
|
|
|
|
pub fn append_option<T, U, F, V>(func: F, option1: Option<T>, option2: Option<U>) -> Option<V>
|
|
where
|
|
F: FnOnce(T, U) -> V,
|
|
{
|
|
Some(func(option1?, option2?))
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
pub(super) async fn filter_by_constraints(
|
|
db: &dyn StorageInterface,
|
|
constraints: &api::PaymentListConstraints,
|
|
merchant_id: &str,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
) -> CustomResult<Vec<PaymentIntent>, errors::DataStorageError> {
|
|
let result = db
|
|
.filter_payment_intent_by_constraints(
|
|
merchant_id,
|
|
&constraints.clone().into(),
|
|
storage_scheme,
|
|
)
|
|
.await?;
|
|
Ok(result)
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
pub(super) fn validate_payment_list_request(
|
|
req: &api::PaymentListConstraints,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
use common_utils::consts::PAYMENTS_LIST_MAX_LIMIT_V1;
|
|
|
|
utils::when(
|
|
req.limit > PAYMENTS_LIST_MAX_LIMIT_V1 || req.limit < 1,
|
|
|| {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"limit should be in between 1 and {}",
|
|
PAYMENTS_LIST_MAX_LIMIT_V1
|
|
),
|
|
})
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
#[cfg(feature = "olap")]
|
|
pub(super) fn validate_payment_list_request_for_joins(
|
|
limit: u32,
|
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
|
use common_utils::consts::PAYMENTS_LIST_MAX_LIMIT_V2;
|
|
|
|
utils::when(!(1..=PAYMENTS_LIST_MAX_LIMIT_V2).contains(&limit), || {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: format!(
|
|
"limit should be in between 1 and {}",
|
|
PAYMENTS_LIST_MAX_LIMIT_V2
|
|
),
|
|
})
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_handle_response_url(
|
|
payment_id: String,
|
|
business_profile: &diesel_models::business_profile::BusinessProfile,
|
|
response: api::PaymentsResponse,
|
|
connector: String,
|
|
) -> RouterResult<api::RedirectionResponse> {
|
|
let payments_return_url = response.return_url.as_ref();
|
|
|
|
let redirection_response = make_pg_redirect_response(payment_id, &response, connector);
|
|
|
|
let return_url = make_merchant_url_with_response(
|
|
business_profile,
|
|
redirection_response,
|
|
payments_return_url,
|
|
response.client_secret.as_ref(),
|
|
response.manual_retry_allowed,
|
|
)
|
|
.attach_printable("Failed to make merchant url with response")?;
|
|
|
|
make_url_with_signature(&return_url, business_profile)
|
|
}
|
|
|
|
pub fn make_merchant_url_with_response(
|
|
business_profile: &diesel_models::business_profile::BusinessProfile,
|
|
redirection_response: api::PgRedirectResponse,
|
|
request_return_url: Option<&String>,
|
|
client_secret: Option<&masking::Secret<String>>,
|
|
manual_retry_allowed: Option<bool>,
|
|
) -> RouterResult<String> {
|
|
// take return url if provided in the request else use merchant return url
|
|
let url = request_return_url
|
|
.or(business_profile.return_url.as_ref())
|
|
.get_required_value("return_url")?;
|
|
|
|
let status_check = redirection_response.status;
|
|
|
|
let payment_client_secret = client_secret
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.into_report()
|
|
.attach_printable("Expected client secret to be `Some`")?;
|
|
|
|
let merchant_url_with_response = if business_profile.redirect_to_merchant_with_http_post {
|
|
url::Url::parse_with_params(
|
|
url,
|
|
&[
|
|
("status", status_check.to_string()),
|
|
(
|
|
"payment_intent_client_secret",
|
|
payment_client_secret.peek().to_string(),
|
|
),
|
|
(
|
|
"manual_retry_allowed",
|
|
manual_retry_allowed.unwrap_or(false).to_string(),
|
|
),
|
|
],
|
|
)
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to parse the url with param")?
|
|
} else {
|
|
let amount = redirection_response.amount.get_required_value("amount")?;
|
|
url::Url::parse_with_params(
|
|
url,
|
|
&[
|
|
("status", status_check.to_string()),
|
|
(
|
|
"payment_intent_client_secret",
|
|
payment_client_secret.peek().to_string(),
|
|
),
|
|
("amount", amount.to_string()),
|
|
(
|
|
"manual_retry_allowed",
|
|
manual_retry_allowed.unwrap_or(false).to_string(),
|
|
),
|
|
],
|
|
)
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to parse the url with param")?
|
|
};
|
|
|
|
Ok(merchant_url_with_response.to_string())
|
|
}
|
|
|
|
pub async fn make_ephemeral_key(
|
|
state: AppState,
|
|
customer_id: String,
|
|
merchant_id: String,
|
|
) -> errors::RouterResponse<ephemeral_key::EphemeralKey> {
|
|
let store = &state.store;
|
|
let id = utils::generate_id(consts::ID_LENGTH, "eki");
|
|
let secret = format!("epk_{}", &Uuid::new_v4().simple().to_string());
|
|
let ek = ephemeral_key::EphemeralKeyNew {
|
|
id,
|
|
customer_id,
|
|
merchant_id,
|
|
secret,
|
|
};
|
|
let ek = store
|
|
.create_ephemeral_key(ek, state.conf.eph_key.validity)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to create ephemeral key")?;
|
|
Ok(services::ApplicationResponse::Json(ek))
|
|
}
|
|
|
|
pub async fn delete_ephemeral_key(
|
|
state: AppState,
|
|
ek_id: String,
|
|
) -> errors::RouterResponse<ephemeral_key::EphemeralKey> {
|
|
let db = state.store.as_ref();
|
|
let ek = db
|
|
.delete_ephemeral_key(&ek_id)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to delete ephemeral key")?;
|
|
Ok(services::ApplicationResponse::Json(ek))
|
|
}
|
|
|
|
pub fn make_pg_redirect_response(
|
|
payment_id: String,
|
|
response: &api::PaymentsResponse,
|
|
connector: String,
|
|
) -> api::PgRedirectResponse {
|
|
api::PgRedirectResponse {
|
|
payment_id,
|
|
status: response.status,
|
|
gateway_id: connector,
|
|
customer_id: response.customer_id.to_owned(),
|
|
amount: Some(response.amount),
|
|
}
|
|
}
|
|
|
|
pub fn make_url_with_signature(
|
|
redirect_url: &str,
|
|
business_profile: &diesel_models::business_profile::BusinessProfile,
|
|
) -> RouterResult<api::RedirectionResponse> {
|
|
let mut url = url::Url::parse(redirect_url)
|
|
.into_report()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to parse the url")?;
|
|
|
|
let mut base_url = url.clone();
|
|
base_url.query_pairs_mut().clear();
|
|
|
|
let url = if business_profile.enable_payment_response_hash {
|
|
let key = business_profile
|
|
.payment_response_hash_key
|
|
.as_ref()
|
|
.get_required_value("payment_response_hash_key")?;
|
|
let signature = hmac_sha512_sorted_query_params(
|
|
&mut url.query_pairs().collect::<Vec<_>>(),
|
|
key.as_str(),
|
|
)?;
|
|
|
|
url.query_pairs_mut()
|
|
.append_pair("signature", &signature)
|
|
.append_pair("signature_algorithm", "HMAC-SHA512");
|
|
url.to_owned()
|
|
} else {
|
|
url.to_owned()
|
|
};
|
|
|
|
let parameters = url
|
|
.query_pairs()
|
|
.collect::<Vec<_>>()
|
|
.iter()
|
|
.map(|(key, value)| (key.clone().into_owned(), value.clone().into_owned()))
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(api::RedirectionResponse {
|
|
return_url: base_url.to_string(),
|
|
params: parameters,
|
|
return_url_with_query_params: url.to_string(),
|
|
http_method: if business_profile.redirect_to_merchant_with_http_post {
|
|
services::Method::Post.to_string()
|
|
} else {
|
|
services::Method::Get.to_string()
|
|
},
|
|
headers: Vec::new(),
|
|
})
|
|
}
|
|
|
|
pub fn hmac_sha512_sorted_query_params(
|
|
params: &mut [(Cow<'_, str>, Cow<'_, str>)],
|
|
key: &str,
|
|
) -> RouterResult<String> {
|
|
params.sort();
|
|
let final_string = params
|
|
.iter()
|
|
.map(|(key, value)| format!("{key}={value}"))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
|
|
let signature = crypto::HmacSha512::sign_message(
|
|
&crypto::HmacSha512,
|
|
key.as_bytes(),
|
|
final_string.as_bytes(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to sign the message")?;
|
|
|
|
Ok(hex::encode(signature))
|
|
}
|
|
|
|
pub fn check_if_operation_confirm<Op: std::fmt::Debug>(operations: Op) -> bool {
|
|
format!("{operations:?}") == "PaymentConfirm"
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn generate_mandate(
|
|
merchant_id: String,
|
|
payment_id: String,
|
|
connector: String,
|
|
setup_mandate_details: Option<MandateData>,
|
|
customer: &Option<domain::Customer>,
|
|
payment_method_id: String,
|
|
connector_mandate_id: Option<pii::SecretSerdeValue>,
|
|
network_txn_id: Option<String>,
|
|
payment_method_data_option: Option<api_models::payments::PaymentMethodData>,
|
|
mandate_reference: Option<MandateReference>,
|
|
merchant_connector_id: Option<String>,
|
|
) -> CustomResult<Option<storage::MandateNew>, errors::ApiErrorResponse> {
|
|
match (setup_mandate_details, customer) {
|
|
(Some(data), Some(cus)) => {
|
|
let mandate_id = utils::generate_id(consts::ID_LENGTH, "man");
|
|
|
|
// The construction of the mandate new must be visible
|
|
let mut new_mandate = storage::MandateNew::default();
|
|
|
|
let customer_acceptance = data
|
|
.customer_acceptance
|
|
.get_required_value("customer_acceptance")?;
|
|
new_mandate
|
|
.set_mandate_id(mandate_id)
|
|
.set_customer_id(cus.customer_id.clone())
|
|
.set_merchant_id(merchant_id)
|
|
.set_original_payment_id(Some(payment_id))
|
|
.set_payment_method_id(payment_method_id)
|
|
.set_connector(connector)
|
|
.set_mandate_status(storage_enums::MandateStatus::Active)
|
|
.set_connector_mandate_ids(connector_mandate_id)
|
|
.set_network_transaction_id(network_txn_id)
|
|
.set_customer_ip_address(
|
|
customer_acceptance
|
|
.get_ip_address()
|
|
.map(masking::Secret::new),
|
|
)
|
|
.set_customer_user_agent(customer_acceptance.get_user_agent())
|
|
.set_customer_accepted_at(Some(customer_acceptance.get_accepted_at()))
|
|
.set_metadata(payment_method_data_option.map(|payment_method_data| {
|
|
pii::SecretSerdeValue::new(
|
|
serde_json::to_value(payment_method_data).unwrap_or_default(),
|
|
)
|
|
}))
|
|
.set_connector_mandate_id(
|
|
mandate_reference.and_then(|reference| reference.connector_mandate_id),
|
|
)
|
|
.set_merchant_connector_id(merchant_connector_id);
|
|
|
|
Ok(Some(
|
|
match data.mandate_type.get_required_value("mandate_type")? {
|
|
data_models::mandates::MandateDataType::SingleUse(data) => new_mandate
|
|
.set_mandate_amount(Some(data.amount))
|
|
.set_mandate_currency(Some(data.currency))
|
|
.set_mandate_type(storage_enums::MandateType::SingleUse)
|
|
.to_owned(),
|
|
|
|
data_models::mandates::MandateDataType::MultiUse(op_data) => match op_data {
|
|
Some(data) => new_mandate
|
|
.set_mandate_amount(Some(data.amount))
|
|
.set_mandate_currency(Some(data.currency))
|
|
.set_start_date(data.start_date)
|
|
.set_end_date(data.end_date),
|
|
// .set_metadata(data.metadata),
|
|
// we are storing PaymentMethodData in metadata of mandate
|
|
None => &mut new_mandate,
|
|
}
|
|
.set_mandate_type(storage_enums::MandateType::MultiUse)
|
|
.to_owned(),
|
|
},
|
|
))
|
|
}
|
|
(_, _) => Ok(None),
|
|
}
|
|
}
|
|
|
|
// A function to manually authenticate the client secret with intent fulfillment time
|
|
pub fn authenticate_client_secret(
|
|
request_client_secret: Option<&String>,
|
|
payment_intent: &PaymentIntent,
|
|
merchant_intent_fulfillment_time: Option<i64>,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
match (request_client_secret, &payment_intent.client_secret) {
|
|
(Some(req_cs), Some(pi_cs)) => {
|
|
if req_cs != pi_cs {
|
|
Err(errors::ApiErrorResponse::ClientSecretInvalid)
|
|
} else {
|
|
//This is done to check whether the merchant_account's intent fulfillment time has expired or not
|
|
let payment_intent_fulfillment_deadline =
|
|
payment_intent.created_at.saturating_add(Duration::seconds(
|
|
merchant_intent_fulfillment_time
|
|
.unwrap_or(consts::DEFAULT_FULFILLMENT_TIME),
|
|
));
|
|
let current_timestamp = common_utils::date_time::now();
|
|
fp_utils::when(
|
|
current_timestamp > payment_intent_fulfillment_deadline,
|
|
|| Err(errors::ApiErrorResponse::ClientSecretExpired),
|
|
)
|
|
}
|
|
}
|
|
// If there is no client in payment intent, then it has expired
|
|
(Some(_), None) => Err(errors::ApiErrorResponse::ClientSecretExpired),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub async fn get_merchant_fullfillment_time(
|
|
payment_link_id: Option<String>,
|
|
intent_fulfillment_time: Option<i64>,
|
|
db: &dyn StorageInterface,
|
|
) -> RouterResult<Option<i64>> {
|
|
if let Some(payment_link_id) = payment_link_id {
|
|
let payment_link_db = db
|
|
.find_payment_link_by_payment_link_id(&payment_link_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;
|
|
|
|
let curr_time = common_utils::date_time::now();
|
|
Ok(payment_link_db
|
|
.fulfilment_time
|
|
.map(|merchant_expiry_time| (merchant_expiry_time - curr_time).whole_seconds()))
|
|
} else {
|
|
Ok(intent_fulfillment_time)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_payment_status_against_not_allowed_statuses(
|
|
intent_status: &storage_enums::IntentStatus,
|
|
not_allowed_statuses: &[storage_enums::IntentStatus],
|
|
action: &'static str,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
fp_utils::when(not_allowed_statuses.contains(intent_status), || {
|
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!(
|
|
"You cannot {action} this payment because it has status {intent_status}",
|
|
),
|
|
})
|
|
})
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub(crate) fn validate_pm_or_token_given(
|
|
payment_method: &Option<api_enums::PaymentMethod>,
|
|
payment_method_data: &Option<api::PaymentMethodData>,
|
|
payment_method_type: &Option<api_enums::PaymentMethodType>,
|
|
mandate_type: &Option<api::MandateTransactionType>,
|
|
token: &Option<String>,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
utils::when(
|
|
!matches!(
|
|
payment_method_type,
|
|
Some(api_enums::PaymentMethodType::Paypal)
|
|
) && !matches!(
|
|
mandate_type,
|
|
Some(api::MandateTransactionType::RecurringMandateTransaction)
|
|
) && token.is_none()
|
|
&& (payment_method_data.is_none() || payment_method.is_none()),
|
|
|| {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "A payment token or payment method data is required".to_string(),
|
|
})
|
|
},
|
|
)
|
|
}
|
|
|
|
// A function to perform database lookup and then verify the client secret
|
|
pub async fn verify_payment_intent_time_and_client_secret(
|
|
db: &dyn StorageInterface,
|
|
merchant_account: &domain::MerchantAccount,
|
|
client_secret: Option<String>,
|
|
) -> error_stack::Result<Option<PaymentIntent>, errors::ApiErrorResponse> {
|
|
client_secret
|
|
.async_map(|cs| async move {
|
|
let payment_id = get_payment_id_from_client_secret(&cs)?;
|
|
|
|
let payment_intent = db
|
|
.find_payment_intent_by_payment_id_merchant_id(
|
|
&payment_id,
|
|
&merchant_account.merchant_id,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
let intent_fulfillment_time = get_merchant_fullfillment_time(
|
|
payment_intent.payment_link_id.clone(),
|
|
merchant_account.intent_fulfillment_time,
|
|
db,
|
|
)
|
|
.await?;
|
|
|
|
authenticate_client_secret(Some(&cs), &payment_intent, intent_fulfillment_time)?;
|
|
Ok(payment_intent)
|
|
})
|
|
.await
|
|
.transpose()
|
|
}
|
|
|
|
/// Check whether the business details are configured in the merchant account
|
|
pub fn validate_business_details(
|
|
business_country: Option<api_enums::CountryAlpha2>,
|
|
business_label: Option<&String>,
|
|
merchant_account: &domain::MerchantAccount,
|
|
) -> RouterResult<()> {
|
|
let primary_business_details = merchant_account
|
|
.primary_business_details
|
|
.clone()
|
|
.parse_value::<Vec<api_models::admin::PrimaryBusinessDetails>>("PrimaryBusinessDetails")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to parse primary business details")?;
|
|
|
|
business_country
|
|
.zip(business_label)
|
|
.map(|(business_country, business_label)| {
|
|
primary_business_details
|
|
.iter()
|
|
.find(|business_details| {
|
|
&business_details.business == business_label
|
|
&& business_details.country == business_country
|
|
})
|
|
.ok_or(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: "business_details are not configured in the merchant account"
|
|
.to_string(),
|
|
})
|
|
})
|
|
.transpose()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Do lazy parsing of primary business details
|
|
/// If both country and label are passed, no need to parse business details from merchant_account
|
|
/// If any one is missing, get it from merchant_account
|
|
/// If there is more than one label or country configured in merchant account, then
|
|
/// passing business details for payment is mandatory to avoid ambiguity
|
|
pub fn get_business_details(
|
|
business_country: Option<api_enums::CountryAlpha2>,
|
|
business_label: Option<&String>,
|
|
merchant_account: &domain::MerchantAccount,
|
|
) -> RouterResult<(api_enums::CountryAlpha2, String)> {
|
|
let primary_business_details = merchant_account
|
|
.primary_business_details
|
|
.clone()
|
|
.parse_value::<Vec<api_models::admin::PrimaryBusinessDetails>>("PrimaryBusinessDetails")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to parse primary business details")?;
|
|
|
|
match business_country.zip(business_label) {
|
|
Some((business_country, business_label)) => {
|
|
Ok((business_country.to_owned(), business_label.to_owned()))
|
|
}
|
|
_ => match primary_business_details.first() {
|
|
Some(business_details) if primary_business_details.len() == 1 => Ok((
|
|
business_country.unwrap_or_else(|| business_details.country.to_owned()),
|
|
business_label
|
|
.map(ToString::to_string)
|
|
.unwrap_or_else(|| business_details.business.to_owned()),
|
|
)),
|
|
_ => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "business_country, business_label"
|
|
})),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn get_payment_id_from_client_secret(cs: &str) -> RouterResult<String> {
|
|
let (payment_id, _) = cs
|
|
.rsplit_once("_secret_")
|
|
.ok_or(errors::ApiErrorResponse::ClientSecretInvalid)
|
|
.into_report()?;
|
|
Ok(payment_id.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_authenticate_client_secret_fulfillment_time_not_expired() {
|
|
let payment_intent = PaymentIntent {
|
|
id: 21,
|
|
payment_id: "23".to_string(),
|
|
merchant_id: "22".to_string(),
|
|
status: storage_enums::IntentStatus::RequiresCapture,
|
|
amount: 200,
|
|
currency: None,
|
|
amount_captured: None,
|
|
customer_id: None,
|
|
description: None,
|
|
return_url: None,
|
|
metadata: None,
|
|
connector_id: None,
|
|
shipping_address_id: None,
|
|
billing_address_id: None,
|
|
statement_descriptor_name: None,
|
|
statement_descriptor_suffix: None,
|
|
created_at: common_utils::date_time::now(),
|
|
modified_at: common_utils::date_time::now(),
|
|
last_synced: None,
|
|
setup_future_usage: None,
|
|
off_session: None,
|
|
client_secret: Some("1".to_string()),
|
|
active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()),
|
|
business_country: None,
|
|
business_label: None,
|
|
order_details: None,
|
|
allowed_payment_method_types: None,
|
|
connector_metadata: None,
|
|
feature_metadata: None,
|
|
attempt_count: 1,
|
|
payment_link_id: None,
|
|
profile_id: None,
|
|
merchant_decision: None,
|
|
payment_confirm_source: None,
|
|
surcharge_applicable: None,
|
|
updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(),
|
|
request_incremental_authorization:
|
|
common_enums::RequestIncrementalAuthorization::default(),
|
|
incremental_authorization_allowed: None,
|
|
};
|
|
let req_cs = Some("1".to_string());
|
|
let merchant_fulfillment_time = Some(900);
|
|
assert!(authenticate_client_secret(
|
|
req_cs.as_ref(),
|
|
&payment_intent,
|
|
merchant_fulfillment_time,
|
|
)
|
|
.is_ok()); // Check if the result is an Ok variant
|
|
}
|
|
|
|
#[test]
|
|
fn test_authenticate_client_secret_fulfillment_time_expired() {
|
|
let payment_intent = PaymentIntent {
|
|
id: 21,
|
|
payment_id: "23".to_string(),
|
|
merchant_id: "22".to_string(),
|
|
status: storage_enums::IntentStatus::RequiresCapture,
|
|
amount: 200,
|
|
currency: None,
|
|
amount_captured: None,
|
|
customer_id: None,
|
|
description: None,
|
|
return_url: None,
|
|
metadata: None,
|
|
connector_id: None,
|
|
shipping_address_id: None,
|
|
billing_address_id: None,
|
|
statement_descriptor_name: None,
|
|
statement_descriptor_suffix: None,
|
|
created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)),
|
|
modified_at: common_utils::date_time::now(),
|
|
last_synced: None,
|
|
setup_future_usage: None,
|
|
off_session: None,
|
|
client_secret: Some("1".to_string()),
|
|
active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()),
|
|
business_country: None,
|
|
business_label: None,
|
|
order_details: None,
|
|
allowed_payment_method_types: None,
|
|
connector_metadata: None,
|
|
feature_metadata: None,
|
|
attempt_count: 1,
|
|
payment_link_id: None,
|
|
profile_id: None,
|
|
merchant_decision: None,
|
|
payment_confirm_source: None,
|
|
surcharge_applicable: None,
|
|
updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(),
|
|
request_incremental_authorization:
|
|
common_enums::RequestIncrementalAuthorization::default(),
|
|
incremental_authorization_allowed: None,
|
|
};
|
|
let req_cs = Some("1".to_string());
|
|
let merchant_fulfillment_time = Some(10);
|
|
assert!(authenticate_client_secret(
|
|
req_cs.as_ref(),
|
|
&payment_intent,
|
|
merchant_fulfillment_time,
|
|
)
|
|
.is_err())
|
|
}
|
|
|
|
#[test]
|
|
fn test_authenticate_client_secret_expired() {
|
|
let payment_intent = PaymentIntent {
|
|
id: 21,
|
|
payment_id: "23".to_string(),
|
|
merchant_id: "22".to_string(),
|
|
status: storage_enums::IntentStatus::RequiresCapture,
|
|
amount: 200,
|
|
currency: None,
|
|
amount_captured: None,
|
|
customer_id: None,
|
|
description: None,
|
|
return_url: None,
|
|
metadata: None,
|
|
connector_id: None,
|
|
shipping_address_id: None,
|
|
billing_address_id: None,
|
|
statement_descriptor_name: None,
|
|
statement_descriptor_suffix: None,
|
|
created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)),
|
|
modified_at: common_utils::date_time::now(),
|
|
last_synced: None,
|
|
setup_future_usage: None,
|
|
off_session: None,
|
|
client_secret: None,
|
|
active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()),
|
|
business_country: None,
|
|
business_label: None,
|
|
order_details: None,
|
|
allowed_payment_method_types: None,
|
|
connector_metadata: None,
|
|
feature_metadata: None,
|
|
attempt_count: 1,
|
|
payment_link_id: None,
|
|
profile_id: None,
|
|
merchant_decision: None,
|
|
payment_confirm_source: None,
|
|
surcharge_applicable: None,
|
|
updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(),
|
|
request_incremental_authorization:
|
|
common_enums::RequestIncrementalAuthorization::default(),
|
|
incremental_authorization_allowed: None,
|
|
};
|
|
let req_cs = Some("1".to_string());
|
|
let merchant_fulfillment_time = Some(10);
|
|
assert!(authenticate_client_secret(
|
|
req_cs.as_ref(),
|
|
&payment_intent,
|
|
merchant_fulfillment_time,
|
|
)
|
|
.is_err())
|
|
}
|
|
}
|
|
|
|
// This function will be removed after moving this functionality to server_wrap and using cache instead of config
|
|
#[instrument(skip_all)]
|
|
pub async fn insert_merchant_connector_creds_to_config(
|
|
db: &dyn StorageInterface,
|
|
merchant_id: &str,
|
|
merchant_connector_details: admin::MerchantConnectorDetailsWrap,
|
|
) -> RouterResult<()> {
|
|
if let Some(encoded_data) = merchant_connector_details.encoded_data {
|
|
match db
|
|
.insert_config(storage::ConfigNew {
|
|
key: format!(
|
|
"mcd_{merchant_id}_{}",
|
|
merchant_connector_details.creds_identifier
|
|
),
|
|
config: encoded_data.peek().to_owned(),
|
|
})
|
|
.await
|
|
{
|
|
Ok(_) => Ok(()),
|
|
Err(err) => {
|
|
if err.current_context().is_db_unique_violation() {
|
|
Ok(())
|
|
} else {
|
|
Err(err
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to insert connector_creds to config"))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum MerchantConnectorAccountType {
|
|
DbVal(domain::MerchantConnectorAccount),
|
|
CacheVal(api_models::admin::MerchantConnectorDetails),
|
|
}
|
|
|
|
impl MerchantConnectorAccountType {
|
|
pub fn get_metadata(&self) -> Option<masking::Secret<serde_json::Value>> {
|
|
match self {
|
|
Self::DbVal(val) => val.metadata.to_owned(),
|
|
Self::CacheVal(val) => val.metadata.to_owned(),
|
|
}
|
|
}
|
|
|
|
pub fn get_connector_account_details(&self) -> serde_json::Value {
|
|
match self {
|
|
Self::DbVal(val) => val.connector_account_details.peek().to_owned(),
|
|
Self::CacheVal(val) => val.connector_account_details.peek().to_owned(),
|
|
}
|
|
}
|
|
|
|
pub fn is_disabled(&self) -> bool {
|
|
match self {
|
|
Self::DbVal(ref inner) => inner.disabled.unwrap_or(false),
|
|
// Cached merchant connector account, only contains the account details,
|
|
// the merchant connector account must only be cached if it's not disabled
|
|
Self::CacheVal(_) => false,
|
|
}
|
|
}
|
|
|
|
pub fn is_test_mode_on(&self) -> Option<bool> {
|
|
match self {
|
|
Self::DbVal(val) => val.test_mode,
|
|
Self::CacheVal(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_mca_id(&self) -> Option<String> {
|
|
match self {
|
|
Self::DbVal(db_val) => Some(db_val.merchant_connector_id.to_string()),
|
|
Self::CacheVal(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Query for merchant connector account either by business label or profile id
|
|
/// If profile_id is passed use it, or use connector_label to query merchant connector account
|
|
#[instrument(skip_all)]
|
|
pub async fn get_merchant_connector_account(
|
|
state: &AppState,
|
|
merchant_id: &str,
|
|
creds_identifier: Option<String>,
|
|
key_store: &domain::MerchantKeyStore,
|
|
profile_id: &String,
|
|
connector_name: &str,
|
|
merchant_connector_id: Option<&String>,
|
|
) -> RouterResult<MerchantConnectorAccountType> {
|
|
let db = &*state.store;
|
|
match creds_identifier {
|
|
Some(creds_identifier) => {
|
|
let mca_config = db
|
|
.find_config_by_key(format!("mcd_{merchant_id}_{creds_identifier}").as_str())
|
|
.await
|
|
.to_not_found_response(
|
|
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: format!("mcd_{merchant_id}_{creds_identifier}"),
|
|
},
|
|
)?;
|
|
|
|
#[cfg(feature = "kms")]
|
|
let private_key = state
|
|
.kms_secrets
|
|
.jwekey
|
|
.peek()
|
|
.tunnel_private_key
|
|
.as_bytes();
|
|
|
|
#[cfg(not(feature = "kms"))]
|
|
let private_key = state.conf.jwekey.tunnel_private_key.as_bytes();
|
|
|
|
let decrypted_mca = services::decrypt_jwe(mca_config.config.as_str(), services::KeyIdCheck::SkipKeyIdCheck, private_key, jwe::RSA_OAEP_256)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::UnprocessableEntity{
|
|
message: "decoding merchant_connector_details failed due to invalid data format!".into()})
|
|
.attach_printable(
|
|
"Failed to decrypt merchant_connector_details sent in request and then put in cache",
|
|
)?;
|
|
|
|
let res = String::into_bytes(decrypted_mca)
|
|
.parse_struct("MerchantConnectorDetails")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"Failed to parse merchant_connector_details sent in request and then put in cache",
|
|
)?;
|
|
|
|
Ok(MerchantConnectorAccountType::CacheVal(res))
|
|
}
|
|
None => {
|
|
if let Some(merchant_connector_id) = merchant_connector_id {
|
|
db.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
|
|
merchant_id,
|
|
merchant_connector_id,
|
|
key_store,
|
|
)
|
|
.await
|
|
.to_not_found_response(
|
|
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: merchant_connector_id.to_string(),
|
|
},
|
|
)
|
|
} else {
|
|
db.find_merchant_connector_account_by_profile_id_connector_name(
|
|
profile_id,
|
|
connector_name,
|
|
key_store,
|
|
)
|
|
.await
|
|
.to_not_found_response(
|
|
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: format!("profile id {profile_id} and connector name {connector_name}"),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
.map(MerchantConnectorAccountType::DbVal),
|
|
}
|
|
}
|
|
|
|
/// This function replaces the request and response type of routerdata with the
|
|
/// request and response type passed
|
|
/// # Arguments
|
|
///
|
|
/// * `router_data` - original router data
|
|
/// * `request` - new request
|
|
/// * `response` - new response
|
|
pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
|
|
router_data: RouterData<F1, Req1, Res1>,
|
|
request: Req2,
|
|
response: Result<Res2, ErrorResponse>,
|
|
) -> RouterData<F2, Req2, Res2> {
|
|
RouterData {
|
|
flow: std::marker::PhantomData,
|
|
request,
|
|
response,
|
|
merchant_id: router_data.merchant_id,
|
|
address: router_data.address,
|
|
amount_captured: router_data.amount_captured,
|
|
auth_type: router_data.auth_type,
|
|
connector: router_data.connector,
|
|
connector_auth_type: router_data.connector_auth_type,
|
|
connector_meta_data: router_data.connector_meta_data,
|
|
description: router_data.description,
|
|
payment_id: router_data.payment_id,
|
|
payment_method: router_data.payment_method,
|
|
payment_method_id: router_data.payment_method_id,
|
|
return_url: router_data.return_url,
|
|
status: router_data.status,
|
|
attempt_id: router_data.attempt_id,
|
|
access_token: router_data.access_token,
|
|
session_token: router_data.session_token,
|
|
reference_id: router_data.reference_id,
|
|
payment_method_token: router_data.payment_method_token,
|
|
customer_id: router_data.customer_id,
|
|
connector_customer: router_data.connector_customer,
|
|
preprocessing_id: router_data.preprocessing_id,
|
|
payment_method_balance: router_data.payment_method_balance,
|
|
recurring_mandate_payment_data: router_data.recurring_mandate_payment_data,
|
|
connector_request_reference_id: router_data.connector_request_reference_id,
|
|
#[cfg(feature = "payouts")]
|
|
payout_method_data: None,
|
|
#[cfg(feature = "payouts")]
|
|
quote_id: None,
|
|
test_mode: router_data.test_mode,
|
|
connector_api_version: router_data.connector_api_version,
|
|
connector_http_status_code: router_data.connector_http_status_code,
|
|
external_latency: router_data.external_latency,
|
|
apple_pay_flow: router_data.apple_pay_flow,
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub fn get_attempt_type(
|
|
payment_intent: &PaymentIntent,
|
|
payment_attempt: &PaymentAttempt,
|
|
request: &api::PaymentsRequest,
|
|
action: &str,
|
|
) -> RouterResult<AttemptType> {
|
|
match payment_intent.status {
|
|
enums::IntentStatus::Failed => {
|
|
if matches!(
|
|
request.retry_action,
|
|
Some(api_models::enums::RetryAction::ManualRetry)
|
|
) {
|
|
metrics::MANUAL_RETRY_REQUEST_COUNT.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes(
|
|
"merchant_id",
|
|
payment_attempt.merchant_id.clone(),
|
|
)],
|
|
);
|
|
match payment_attempt.status {
|
|
enums::AttemptStatus::Started
|
|
| enums::AttemptStatus::AuthenticationPending
|
|
| enums::AttemptStatus::AuthenticationSuccessful
|
|
| enums::AttemptStatus::Authorized
|
|
| enums::AttemptStatus::Charged
|
|
| enums::AttemptStatus::Authorizing
|
|
| enums::AttemptStatus::CodInitiated
|
|
| enums::AttemptStatus::VoidInitiated
|
|
| enums::AttemptStatus::CaptureInitiated
|
|
| enums::AttemptStatus::Unresolved
|
|
| enums::AttemptStatus::Pending
|
|
| enums::AttemptStatus::ConfirmationAwaited
|
|
| enums::AttemptStatus::PartialCharged
|
|
| enums::AttemptStatus::PartialChargedAndChargeable
|
|
| enums::AttemptStatus::Voided
|
|
| enums::AttemptStatus::AutoRefunded
|
|
| enums::AttemptStatus::PaymentMethodAwaited
|
|
| enums::AttemptStatus::DeviceDataCollectionPending => {
|
|
metrics::MANUAL_RETRY_VALIDATION_FAILED.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes(
|
|
"merchant_id",
|
|
payment_attempt.merchant_id.clone(),
|
|
)],
|
|
);
|
|
Err(errors::ApiErrorResponse::InternalServerError)
|
|
.into_report()
|
|
.attach_printable("Payment Attempt unexpected state")
|
|
}
|
|
|
|
storage_enums::AttemptStatus::VoidFailed
|
|
| storage_enums::AttemptStatus::RouterDeclined
|
|
| storage_enums::AttemptStatus::CaptureFailed => {
|
|
metrics::MANUAL_RETRY_VALIDATION_FAILED.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes(
|
|
"merchant_id",
|
|
payment_attempt.merchant_id.clone(),
|
|
)],
|
|
);
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message:
|
|
format!("You cannot {action} this payment because it has status {}, and the previous attempt has the status {}", payment_intent.status, payment_attempt.status)
|
|
}
|
|
))
|
|
}
|
|
|
|
storage_enums::AttemptStatus::AuthenticationFailed
|
|
| storage_enums::AttemptStatus::AuthorizationFailed
|
|
| storage_enums::AttemptStatus::Failure => {
|
|
metrics::MANUAL_RETRY_COUNT.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes(
|
|
"merchant_id",
|
|
payment_attempt.merchant_id.clone(),
|
|
)],
|
|
);
|
|
Ok(AttemptType::New)
|
|
}
|
|
}
|
|
} else {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message:
|
|
format!("You cannot {action} this payment because it has status {}, you can pass `retry_action` as `manual_retry` in request to try this payment again", payment_intent.status)
|
|
}
|
|
))
|
|
}
|
|
}
|
|
enums::IntentStatus::Cancelled
|
|
| enums::IntentStatus::RequiresCapture
|
|
| enums::IntentStatus::PartiallyCaptured
|
|
| enums::IntentStatus::PartiallyCapturedAndCapturable
|
|
| enums::IntentStatus::Processing
|
|
| enums::IntentStatus::Succeeded => {
|
|
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
|
|
message: format!(
|
|
"You cannot {action} this payment because it has status {}",
|
|
payment_intent.status,
|
|
),
|
|
}))
|
|
}
|
|
|
|
enums::IntentStatus::RequiresCustomerAction
|
|
| enums::IntentStatus::RequiresMerchantAction
|
|
| enums::IntentStatus::RequiresPaymentMethod
|
|
| enums::IntentStatus::RequiresConfirmation => Ok(AttemptType::SameOld),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
|
pub enum AttemptType {
|
|
New,
|
|
SameOld,
|
|
}
|
|
|
|
impl AttemptType {
|
|
// The function creates a new payment_attempt from the previous payment attempt but doesn't populate fields like payment_method, error_code etc.
|
|
// Logic to override the fields with data provided in the request should be done after this if required.
|
|
// In case if fields are not overridden by the request then they contain the same data that was in the previous attempt provided it is populated in this function.
|
|
#[inline(always)]
|
|
fn make_new_payment_attempt(
|
|
payment_method_data: &Option<api_models::payments::PaymentMethodData>,
|
|
old_payment_attempt: PaymentAttempt,
|
|
new_attempt_count: i16,
|
|
storage_scheme: enums::MerchantStorageScheme,
|
|
) -> storage::PaymentAttemptNew {
|
|
let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now());
|
|
|
|
storage::PaymentAttemptNew {
|
|
attempt_id: utils::get_payment_attempt_id(
|
|
&old_payment_attempt.payment_id,
|
|
new_attempt_count,
|
|
),
|
|
payment_id: old_payment_attempt.payment_id,
|
|
merchant_id: old_payment_attempt.merchant_id,
|
|
|
|
// A new payment attempt is getting created so, used the same function which is used to populate status in PaymentCreate Flow.
|
|
status: payment_attempt_status_fsm(payment_method_data, Some(true)),
|
|
|
|
amount: old_payment_attempt.amount,
|
|
currency: old_payment_attempt.currency,
|
|
save_to_locker: old_payment_attempt.save_to_locker,
|
|
|
|
connector: None,
|
|
|
|
error_message: None,
|
|
offer_amount: old_payment_attempt.offer_amount,
|
|
surcharge_amount: old_payment_attempt.surcharge_amount,
|
|
tax_amount: old_payment_attempt.tax_amount,
|
|
payment_method_id: None,
|
|
payment_method: None,
|
|
capture_method: old_payment_attempt.capture_method,
|
|
capture_on: old_payment_attempt.capture_on,
|
|
confirm: old_payment_attempt.confirm,
|
|
authentication_type: old_payment_attempt.authentication_type,
|
|
created_at,
|
|
modified_at,
|
|
last_synced,
|
|
cancellation_reason: None,
|
|
amount_to_capture: old_payment_attempt.amount_to_capture,
|
|
|
|
// Once the payment_attempt is authorised then mandate_id is created. If this payment attempt is authorised then mandate_id will be overridden.
|
|
// Since mandate_id is a contract between merchant and customer to debit customers amount adding it to newly created attempt
|
|
mandate_id: old_payment_attempt.mandate_id,
|
|
|
|
// The payment could be done from a different browser or same browser, it would probably be overridden by request data.
|
|
browser_info: None,
|
|
|
|
error_code: None,
|
|
payment_token: None,
|
|
connector_metadata: None,
|
|
payment_experience: None,
|
|
payment_method_type: None,
|
|
payment_method_data: None,
|
|
|
|
// In case it is passed in create and not in confirm,
|
|
business_sub_label: old_payment_attempt.business_sub_label,
|
|
// If the algorithm is entered in Create call from server side, it needs to be populated here, however it could be overridden from the request.
|
|
straight_through_algorithm: old_payment_attempt.straight_through_algorithm,
|
|
mandate_details: old_payment_attempt.mandate_details,
|
|
preprocessing_step_id: None,
|
|
error_reason: None,
|
|
multiple_capture_count: None,
|
|
connector_response_reference_id: None,
|
|
amount_capturable: old_payment_attempt.amount,
|
|
updated_by: storage_scheme.to_string(),
|
|
authentication_data: None,
|
|
encoded_data: None,
|
|
merchant_connector_id: None,
|
|
unified_code: None,
|
|
unified_message: None,
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn modify_payment_intent_and_payment_attempt(
|
|
&self,
|
|
request: &api::PaymentsRequest,
|
|
fetched_payment_intent: PaymentIntent,
|
|
fetched_payment_attempt: PaymentAttempt,
|
|
db: &dyn StorageInterface,
|
|
storage_scheme: storage::enums::MerchantStorageScheme,
|
|
) -> RouterResult<(PaymentIntent, PaymentAttempt)> {
|
|
match self {
|
|
Self::SameOld => Ok((fetched_payment_intent, fetched_payment_attempt)),
|
|
Self::New => {
|
|
let new_attempt_count = fetched_payment_intent.attempt_count + 1;
|
|
let new_payment_attempt = db
|
|
.insert_payment_attempt(
|
|
Self::make_new_payment_attempt(
|
|
&request.payment_method_data,
|
|
fetched_payment_attempt,
|
|
new_attempt_count,
|
|
storage_scheme,
|
|
),
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment {
|
|
payment_id: fetched_payment_intent.payment_id.to_owned(),
|
|
})?;
|
|
|
|
let updated_payment_intent = db
|
|
.update_payment_intent(
|
|
fetched_payment_intent,
|
|
storage::PaymentIntentUpdate::StatusAndAttemptUpdate {
|
|
status: payment_intent_status_fsm(
|
|
&request.payment_method_data,
|
|
Some(true),
|
|
),
|
|
active_attempt_id: new_payment_attempt.attempt_id.clone(),
|
|
attempt_count: new_attempt_count,
|
|
updated_by: storage_scheme.to_string(),
|
|
},
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
logger::info!(
|
|
"manual_retry payment for {} with attempt_id {}",
|
|
updated_payment_intent.payment_id,
|
|
new_payment_attempt.attempt_id
|
|
);
|
|
|
|
Ok((updated_payment_intent, new_payment_attempt))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn is_manual_retry_allowed(
|
|
intent_status: &storage_enums::IntentStatus,
|
|
attempt_status: &storage_enums::AttemptStatus,
|
|
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
|
|
merchant_id: &str,
|
|
) -> Option<bool> {
|
|
let is_payment_status_eligible_for_retry = match intent_status {
|
|
enums::IntentStatus::Failed => match attempt_status {
|
|
enums::AttemptStatus::Started
|
|
| enums::AttemptStatus::AuthenticationPending
|
|
| enums::AttemptStatus::AuthenticationSuccessful
|
|
| enums::AttemptStatus::Authorized
|
|
| enums::AttemptStatus::Charged
|
|
| enums::AttemptStatus::Authorizing
|
|
| enums::AttemptStatus::CodInitiated
|
|
| enums::AttemptStatus::VoidInitiated
|
|
| enums::AttemptStatus::CaptureInitiated
|
|
| enums::AttemptStatus::Unresolved
|
|
| enums::AttemptStatus::Pending
|
|
| enums::AttemptStatus::ConfirmationAwaited
|
|
| enums::AttemptStatus::PartialCharged
|
|
| enums::AttemptStatus::PartialChargedAndChargeable
|
|
| enums::AttemptStatus::Voided
|
|
| enums::AttemptStatus::AutoRefunded
|
|
| enums::AttemptStatus::PaymentMethodAwaited
|
|
| enums::AttemptStatus::DeviceDataCollectionPending => {
|
|
logger::error!("Payment Attempt should not be in this state because Attempt to Intent status mapping doesn't allow it");
|
|
None
|
|
}
|
|
|
|
storage_enums::AttemptStatus::VoidFailed
|
|
| storage_enums::AttemptStatus::RouterDeclined
|
|
| storage_enums::AttemptStatus::CaptureFailed => Some(false),
|
|
|
|
storage_enums::AttemptStatus::AuthenticationFailed
|
|
| storage_enums::AttemptStatus::AuthorizationFailed
|
|
| storage_enums::AttemptStatus::Failure => Some(true),
|
|
},
|
|
enums::IntentStatus::Cancelled
|
|
| enums::IntentStatus::RequiresCapture
|
|
| enums::IntentStatus::PartiallyCaptured
|
|
| enums::IntentStatus::PartiallyCapturedAndCapturable
|
|
| enums::IntentStatus::Processing
|
|
| enums::IntentStatus::Succeeded => Some(false),
|
|
|
|
enums::IntentStatus::RequiresCustomerAction
|
|
| enums::IntentStatus::RequiresMerchantAction
|
|
| enums::IntentStatus::RequiresPaymentMethod
|
|
| enums::IntentStatus::RequiresConfirmation => None,
|
|
};
|
|
let is_merchant_id_enabled_for_retries = !connector_request_reference_id_config
|
|
.merchant_ids_send_payment_id_as_connector_request_id
|
|
.contains(merchant_id);
|
|
is_payment_status_eligible_for_retry
|
|
.map(|payment_status_check| payment_status_check && is_merchant_id_enabled_for_retries)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
#![allow(clippy::unwrap_used)]
|
|
#[test]
|
|
fn test_client_secret_parse() {
|
|
let client_secret1 = "pay_3TgelAms4RQec8xSStjF_secret_fc34taHLw1ekPgNh92qr";
|
|
let client_secret2 = "pay_3Tgel__Ams4RQ_secret_ec8xSStjF_secret_fc34taHLw1ekPgNh92qr";
|
|
let client_secret3 =
|
|
"pay_3Tgel__Ams4RQ_secret_ec8xSStjF_secret__secret_fc34taHLw1ekPgNh92qr";
|
|
|
|
assert_eq!(
|
|
"pay_3TgelAms4RQec8xSStjF",
|
|
super::get_payment_id_from_client_secret(client_secret1).unwrap()
|
|
);
|
|
assert_eq!(
|
|
"pay_3Tgel__Ams4RQ_secret_ec8xSStjF",
|
|
super::get_payment_id_from_client_secret(client_secret2).unwrap()
|
|
);
|
|
assert_eq!(
|
|
"pay_3Tgel__Ams4RQ_secret_ec8xSStjF_secret_",
|
|
super::get_payment_id_from_client_secret(client_secret3).unwrap()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn get_additional_payment_data(
|
|
pm_data: &api_models::payments::PaymentMethodData,
|
|
db: &dyn StorageInterface,
|
|
) -> api_models::payments::AdditionalPaymentData {
|
|
match pm_data {
|
|
api_models::payments::PaymentMethodData::Card(card_data) => {
|
|
let card_isin = Some(card_data.card_number.clone().get_card_isin());
|
|
let last4 = Some(card_data.card_number.clone().get_last4());
|
|
if card_data.card_issuer.is_some()
|
|
&& card_data.card_network.is_some()
|
|
&& card_data.card_type.is_some()
|
|
&& card_data.card_issuing_country.is_some()
|
|
&& card_data.bank_code.is_some()
|
|
{
|
|
api_models::payments::AdditionalPaymentData::Card(Box::new(
|
|
api_models::payments::AdditionalCardInfo {
|
|
card_issuer: card_data.card_issuer.to_owned(),
|
|
card_network: card_data.card_network.clone(),
|
|
card_type: card_data.card_type.to_owned(),
|
|
card_issuing_country: card_data.card_issuing_country.to_owned(),
|
|
bank_code: card_data.bank_code.to_owned(),
|
|
card_exp_month: Some(card_data.card_exp_month.clone()),
|
|
card_exp_year: Some(card_data.card_exp_year.clone()),
|
|
card_holder_name: Some(card_data.card_holder_name.clone()),
|
|
last4: last4.clone(),
|
|
card_isin: card_isin.clone(),
|
|
},
|
|
))
|
|
} else {
|
|
let card_info = card_isin
|
|
.clone()
|
|
.async_and_then(|card_isin| async move {
|
|
db.get_card_info(&card_isin)
|
|
.await
|
|
.map_err(|error| services::logger::warn!(card_info_error=?error))
|
|
.ok()
|
|
})
|
|
.await
|
|
.flatten()
|
|
.map(|card_info| {
|
|
api_models::payments::AdditionalPaymentData::Card(Box::new(
|
|
api_models::payments::AdditionalCardInfo {
|
|
card_issuer: card_info.card_issuer,
|
|
card_network: card_info.card_network.clone(),
|
|
bank_code: card_info.bank_code,
|
|
card_type: card_info.card_type,
|
|
card_issuing_country: card_info.card_issuing_country,
|
|
last4: last4.clone(),
|
|
card_isin: card_isin.clone(),
|
|
card_exp_month: Some(card_data.card_exp_month.clone()),
|
|
card_exp_year: Some(card_data.card_exp_year.clone()),
|
|
card_holder_name: Some(card_data.card_holder_name.clone()),
|
|
},
|
|
))
|
|
});
|
|
card_info.unwrap_or(api_models::payments::AdditionalPaymentData::Card(Box::new(
|
|
api_models::payments::AdditionalCardInfo {
|
|
card_issuer: None,
|
|
card_network: None,
|
|
bank_code: None,
|
|
card_type: None,
|
|
card_issuing_country: None,
|
|
last4,
|
|
card_isin,
|
|
card_exp_month: Some(card_data.card_exp_month.clone()),
|
|
card_exp_year: Some(card_data.card_exp_year.clone()),
|
|
card_holder_name: Some(card_data.card_holder_name.clone()),
|
|
},
|
|
)))
|
|
}
|
|
}
|
|
api_models::payments::PaymentMethodData::BankRedirect(bank_redirect_data) => {
|
|
match bank_redirect_data {
|
|
api_models::payments::BankRedirectData::Eps { bank_name, .. } => {
|
|
api_models::payments::AdditionalPaymentData::BankRedirect {
|
|
bank_name: bank_name.to_owned(),
|
|
}
|
|
}
|
|
api_models::payments::BankRedirectData::Ideal { bank_name, .. } => {
|
|
api_models::payments::AdditionalPaymentData::BankRedirect {
|
|
bank_name: bank_name.to_owned(),
|
|
}
|
|
}
|
|
_ => api_models::payments::AdditionalPaymentData::BankRedirect { bank_name: None },
|
|
}
|
|
}
|
|
api_models::payments::PaymentMethodData::Wallet(_) => {
|
|
api_models::payments::AdditionalPaymentData::Wallet {}
|
|
}
|
|
api_models::payments::PaymentMethodData::PayLater(_) => {
|
|
api_models::payments::AdditionalPaymentData::PayLater {}
|
|
}
|
|
api_models::payments::PaymentMethodData::BankTransfer(_) => {
|
|
api_models::payments::AdditionalPaymentData::BankTransfer {}
|
|
}
|
|
api_models::payments::PaymentMethodData::Crypto(_) => {
|
|
api_models::payments::AdditionalPaymentData::Crypto {}
|
|
}
|
|
api_models::payments::PaymentMethodData::BankDebit(_) => {
|
|
api_models::payments::AdditionalPaymentData::BankDebit {}
|
|
}
|
|
api_models::payments::PaymentMethodData::MandatePayment => {
|
|
api_models::payments::AdditionalPaymentData::MandatePayment {}
|
|
}
|
|
api_models::payments::PaymentMethodData::Reward => {
|
|
api_models::payments::AdditionalPaymentData::Reward {}
|
|
}
|
|
api_models::payments::PaymentMethodData::Upi(_) => {
|
|
api_models::payments::AdditionalPaymentData::Upi {}
|
|
}
|
|
api_models::payments::PaymentMethodData::CardRedirect(_) => {
|
|
api_models::payments::AdditionalPaymentData::CardRedirect {}
|
|
}
|
|
api_models::payments::PaymentMethodData::Voucher(_) => {
|
|
api_models::payments::AdditionalPaymentData::Voucher {}
|
|
}
|
|
api_models::payments::PaymentMethodData::GiftCard(_) => {
|
|
api_models::payments::AdditionalPaymentData::GiftCard {}
|
|
}
|
|
api_models::payments::PaymentMethodData::CardToken(_) => {
|
|
api_models::payments::AdditionalPaymentData::CardToken {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_customer_access(
|
|
payment_intent: &PaymentIntent,
|
|
auth_flow: services::AuthFlow,
|
|
request: &api::PaymentsRequest,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
if auth_flow == services::AuthFlow::Client && request.customer_id.is_some() {
|
|
let is_same_customer = request.customer_id == payment_intent.customer_id;
|
|
if !is_same_customer {
|
|
Err(errors::ApiErrorResponse::GenericUnauthorized {
|
|
message: "Unauthorised access to update customer".to_string(),
|
|
})?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
pub struct ApplePayData {
|
|
version: masking::Secret<String>,
|
|
data: masking::Secret<String>,
|
|
signature: masking::Secret<String>,
|
|
header: ApplePayHeader,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ApplePayHeader {
|
|
ephemeral_public_key: masking::Secret<String>,
|
|
public_key_hash: masking::Secret<String>,
|
|
transaction_id: masking::Secret<String>,
|
|
}
|
|
|
|
impl ApplePayData {
|
|
pub fn token_json(
|
|
wallet_data: api_models::payments::WalletData,
|
|
) -> CustomResult<Self, errors::ConnectorError> {
|
|
let json_wallet_data: Self =
|
|
connector::utils::WalletData::get_wallet_token_as_json(&wallet_data)?;
|
|
Ok(json_wallet_data)
|
|
}
|
|
|
|
pub async fn decrypt(
|
|
&self,
|
|
state: &AppState,
|
|
) -> CustomResult<serde_json::Value, errors::ApplePayDecryptionError> {
|
|
let merchant_id = self.merchant_id(state).await?;
|
|
let shared_secret = self.shared_secret(state).await?;
|
|
let symmetric_key = self.symmetric_key(&merchant_id, &shared_secret)?;
|
|
let decrypted = self.decrypt_ciphertext(&symmetric_key)?;
|
|
let parsed_decrypted: serde_json::Value = serde_json::from_str(&decrypted)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DecryptionFailed)?;
|
|
Ok(parsed_decrypted)
|
|
}
|
|
|
|
pub async fn merchant_id(
|
|
&self,
|
|
state: &AppState,
|
|
) -> CustomResult<String, errors::ApplePayDecryptionError> {
|
|
#[cfg(feature = "kms")]
|
|
let cert_data = kms::get_kms_client(&state.conf.kms)
|
|
.await
|
|
.decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc)
|
|
.await
|
|
.change_context(errors::ApplePayDecryptionError::DecryptionFailed)?;
|
|
|
|
#[cfg(not(feature = "kms"))]
|
|
let cert_data = &state.conf.applepay_decrypt_keys.apple_pay_ppc;
|
|
|
|
let base64_decode_cert_data = BASE64_ENGINE
|
|
.decode(cert_data)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?;
|
|
|
|
// Parsing the certificate using x509-parser
|
|
let (_, certificate) = parse_x509_certificate(&base64_decode_cert_data)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::CertificateParsingFailed)
|
|
.attach_printable("Error parsing apple pay PPC")?;
|
|
|
|
// Finding the merchant ID extension
|
|
let apple_pay_m_id = certificate
|
|
.extensions()
|
|
.iter()
|
|
.find(|extension| {
|
|
extension
|
|
.oid
|
|
.to_string()
|
|
.eq(consts::MERCHANT_ID_FIELD_EXTENSION_ID)
|
|
})
|
|
.map(|ext| {
|
|
let merchant_id = String::from_utf8_lossy(ext.value)
|
|
.trim()
|
|
.trim_start_matches('@')
|
|
.to_string();
|
|
|
|
merchant_id
|
|
})
|
|
.ok_or(errors::ApplePayDecryptionError::MissingMerchantId)
|
|
.into_report()
|
|
.attach_printable("Unable to find merchant ID extension in the certificate")?;
|
|
|
|
Ok(apple_pay_m_id)
|
|
}
|
|
|
|
pub async fn shared_secret(
|
|
&self,
|
|
state: &AppState,
|
|
) -> CustomResult<Vec<u8>, errors::ApplePayDecryptionError> {
|
|
let public_ec_bytes = BASE64_ENGINE
|
|
.decode(self.header.ephemeral_public_key.peek().as_bytes())
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?;
|
|
|
|
let public_key = PKey::public_key_from_der(&public_ec_bytes)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed)
|
|
.attach_printable("Failed to deserialize the public key")?;
|
|
|
|
#[cfg(feature = "kms")]
|
|
let decrypted_apple_pay_ppc_key = kms::get_kms_client(&state.conf.kms)
|
|
.await
|
|
.decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc_key)
|
|
.await
|
|
.change_context(errors::ApplePayDecryptionError::DecryptionFailed)?;
|
|
|
|
#[cfg(not(feature = "kms"))]
|
|
let decrypted_apple_pay_ppc_key = &state.conf.applepay_decrypt_keys.apple_pay_ppc_key;
|
|
// Create PKey objects from EcKey
|
|
let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes())
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed)
|
|
.attach_printable("Failed to deserialize the private key")?;
|
|
|
|
// Create the Deriver object and set the peer public key
|
|
let mut deriver = Deriver::new(&private_key)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed)
|
|
.attach_printable("Failed to create a deriver for the private key")?;
|
|
|
|
deriver
|
|
.set_peer(&public_key)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed)
|
|
.attach_printable("Failed to set the peer key for the secret derivation")?;
|
|
|
|
// Compute the shared secret
|
|
let shared_secret = deriver
|
|
.derive_to_vec()
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DerivingSharedSecretKeyFailed)
|
|
.attach_printable("Final key derivation failed")?;
|
|
Ok(shared_secret)
|
|
}
|
|
|
|
pub fn symmetric_key(
|
|
&self,
|
|
merchant_id: &str,
|
|
shared_secret: &[u8],
|
|
) -> CustomResult<Vec<u8>, errors::ApplePayDecryptionError> {
|
|
let kdf_algorithm = b"\x0did-aes256-GCM";
|
|
let kdf_party_v = hex::decode(merchant_id)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?;
|
|
let kdf_party_u = b"Apple";
|
|
let kdf_info = [&kdf_algorithm[..], kdf_party_u, &kdf_party_v[..]].concat();
|
|
|
|
let mut hash = openssl::sha::Sha256::new();
|
|
hash.update(b"\x00\x00\x00");
|
|
hash.update(b"\x01");
|
|
hash.update(shared_secret);
|
|
hash.update(&kdf_info[..]);
|
|
let symmetric_key = hash.finish();
|
|
Ok(symmetric_key.to_vec())
|
|
}
|
|
|
|
pub fn decrypt_ciphertext(
|
|
&self,
|
|
symmetric_key: &[u8],
|
|
) -> CustomResult<String, errors::ApplePayDecryptionError> {
|
|
let data = BASE64_ENGINE
|
|
.decode(self.data.peek().as_bytes())
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?;
|
|
let iv = [0u8; 16]; //Initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process.
|
|
let ciphertext = &data[..data.len() - 16];
|
|
let tag = &data[data.len() - 16..];
|
|
let cipher = Cipher::aes_256_gcm();
|
|
let decrypted_data = decrypt_aead(cipher, symmetric_key, Some(&iv), &[], ciphertext, tag)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DecryptionFailed)?;
|
|
let decrypted = String::from_utf8(decrypted_data)
|
|
.into_report()
|
|
.change_context(errors::ApplePayDecryptionError::DecryptionFailed)?;
|
|
|
|
Ok(decrypted)
|
|
}
|
|
}
|
|
|
|
pub fn get_key_params_for_surcharge_details(
|
|
payment_method_data: api_models::payments::PaymentMethodData,
|
|
) -> RouterResult<(
|
|
common_enums::PaymentMethod,
|
|
common_enums::PaymentMethodType,
|
|
Option<common_enums::CardNetwork>,
|
|
)> {
|
|
match payment_method_data {
|
|
api_models::payments::PaymentMethodData::Card(card) => {
|
|
let card_type = card
|
|
.card_type
|
|
.get_required_value("payment_method_data.card.card_type")?;
|
|
let card_network = card
|
|
.card_network
|
|
.get_required_value("payment_method_data.card.card_network")?;
|
|
match card_type.to_lowercase().as_str() {
|
|
"credit" => Ok((
|
|
common_enums::PaymentMethod::Card,
|
|
common_enums::PaymentMethodType::Credit,
|
|
Some(card_network),
|
|
)),
|
|
"debit" => Ok((
|
|
common_enums::PaymentMethod::Card,
|
|
common_enums::PaymentMethodType::Debit,
|
|
Some(card_network),
|
|
)),
|
|
_ => {
|
|
logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable");
|
|
Err(errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "payment_method_data.card.card_type",
|
|
}
|
|
.into())
|
|
}
|
|
}
|
|
}
|
|
api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok((
|
|
common_enums::PaymentMethod::CardRedirect,
|
|
card_redirect_data.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::Wallet(wallet) => Ok((
|
|
common_enums::PaymentMethod::Wallet,
|
|
wallet.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok((
|
|
common_enums::PaymentMethod::PayLater,
|
|
pay_later.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok((
|
|
common_enums::PaymentMethod::BankRedirect,
|
|
bank_redirect.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok((
|
|
common_enums::PaymentMethod::BankDebit,
|
|
bank_debit.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok((
|
|
common_enums::PaymentMethod::BankTransfer,
|
|
bank_transfer.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::Crypto(crypto) => Ok((
|
|
common_enums::PaymentMethod::Crypto,
|
|
crypto.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::MandatePayment => {
|
|
Err(errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "payment_method_data",
|
|
}
|
|
.into())
|
|
}
|
|
api_models::payments::PaymentMethodData::Reward => {
|
|
Err(errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "payment_method_data",
|
|
}
|
|
.into())
|
|
}
|
|
api_models::payments::PaymentMethodData::Upi(_) => Ok((
|
|
common_enums::PaymentMethod::Upi,
|
|
common_enums::PaymentMethodType::UpiCollect,
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::Voucher(voucher) => Ok((
|
|
common_enums::PaymentMethod::Voucher,
|
|
voucher.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok((
|
|
common_enums::PaymentMethod::GiftCard,
|
|
gift_card.get_payment_method_type(),
|
|
None,
|
|
)),
|
|
api_models::payments::PaymentMethodData::CardToken(_) => {
|
|
Err(errors::ApiErrorResponse::InvalidDataValue {
|
|
field_name: "payment_method_data",
|
|
}
|
|
.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_payment_link_request(
|
|
payment_link_object: &api_models::payments::PaymentLinkObject,
|
|
confirm: Option<bool>,
|
|
order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
if let Some(cnf) = confirm {
|
|
if !cnf {
|
|
let current_time = Some(common_utils::date_time::now());
|
|
if current_time > payment_link_object.link_expiry {
|
|
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "link_expiry time cannot be less than current time".to_string(),
|
|
});
|
|
} else if order_details.is_none() {
|
|
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "cannot create payment link without order details".to_string(),
|
|
});
|
|
}
|
|
} else {
|
|
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "cannot confirm a payment while creating a payment link".to_string(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_gsm_record(
|
|
state: &AppState,
|
|
error_code: Option<String>,
|
|
error_message: Option<String>,
|
|
connector_name: String,
|
|
flow: String,
|
|
) -> Option<storage::gsm::GatewayStatusMap> {
|
|
let get_gsm = || async {
|
|
state.store.find_gsm_rule(
|
|
connector_name.clone(),
|
|
flow.clone(),
|
|
"sub_flow".to_string(),
|
|
error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response
|
|
error_message.clone().unwrap_or_default(),
|
|
)
|
|
.await
|
|
.map_err(|err| {
|
|
if err.current_context().is_db_not_found() {
|
|
logger::warn!(
|
|
"GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}",
|
|
connector_name,
|
|
flow,
|
|
error_code,
|
|
error_message
|
|
);
|
|
metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]);
|
|
} else {
|
|
metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]);
|
|
};
|
|
err.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to fetch decision from gsm")
|
|
})
|
|
};
|
|
get_gsm()
|
|
.await
|
|
.map_err(|err| {
|
|
// warn log should suffice here because we are not propagating this error
|
|
logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision");
|
|
err
|
|
})
|
|
.ok()
|
|
}
|
|
|
|
pub fn validate_order_details_amount(
|
|
order_details: Vec<api_models::payments::OrderDetailsWithAmount>,
|
|
amount: i64,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
let total_order_details_amount: i64 = order_details
|
|
.iter()
|
|
.map(|order| order.amount * i64::from(order.quantity))
|
|
.sum();
|
|
|
|
if total_order_details_amount != amount {
|
|
Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "Total sum of order details doesn't match amount in payment request"
|
|
.to_string(),
|
|
})
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|