Files
Abhishek Marrivagu a1cb255765 fix(payments): all AdditionalCardInfo fields optional (#1840)
Co-authored-by: Sangamesh Kulkarni <59434228+Sangamesh26@users.noreply.github.com>
2023-08-01 12:58:55 +00:00

2743 lines
106 KiB
Rust

use std::borrow::Cow;
use base64::Engine;
use common_utils::{
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
fp_utils, generate_id, pii,
};
use diesel_models::{enums, payment_intent};
// TODO : Evaluate all the helper functions ()
use error_stack::{report, IntoReport, ResultExt};
use josekit::jwe;
use masking::{ExposeInterface, PeekInterface};
use router_env::{instrument, logger, tracing};
use time::Duration;
use uuid::Uuid;
use super::{
operations::{BoxedOperation, Operation, PaymentResponse},
CustomerDetails, PaymentData,
};
use crate::{
configs::settings::{ConnectorRequestReferenceIdConfig, Server},
consts,
core::{
errors::{self, CustomResult, RouterResult, StorageErrorExt},
payment_methods::{cards, vault},
payments,
},
db::StorageInterface,
routes::{metrics, AppState},
scheduler::metrics as scheduler_metrics,
services,
types::{
api::{self, admin, enums as api_enums, CustomerAcceptanceExt, MandateValidationFieldsExt},
domain::{
self,
types::{self, AsyncLift},
},
storage::{self, enums as storage_enums, ephemeral_key, CustomerUpdate::Update},
transformers::ForeignInto,
ErrorResponse, RouterData,
},
utils::{
self,
crypto::{self, SignMessage},
OptionExt,
},
};
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 = consts::BASE64_ENGINE
.decode(encoded_certificate)
.into_report()
.change_context(errors::ApiClientError::CertificateDecodeFailed)?;
let decoded_certificate_key = consts::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_details(
merchant_connector_accounts: Vec<domain::MerchantConnectorAccount>,
payment_intent: Option<&diesel_models::payment_intent::PaymentIntent>,
) -> Vec<domain::MerchantConnectorAccount> {
if let Some(payment_intent) = payment_intent {
merchant_connector_accounts
.into_iter()
.filter(|mca| {
mca.business_country == payment_intent.business_country
&& mca.business_label == payment_intent.business_label
})
.collect::<Vec<_>>()
} else {
merchant_connector_accounts
}
}
pub async fn get_address_for_payment_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,
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
let key = merchant_key_store.key.get_inner().peek();
Ok(match req_address {
Some(address) => {
match address_id {
Some(id) => {
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()),
})
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while encrypting address")?;
Some(
db.update_address(id.to_owned(), address_update, merchant_key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?,
)
}
None => {
// generate a new address here
let customer_id = customer_id.get_required_value("customer_id")?;
let address_details = address.address.clone().unwrap_or_default();
Some(
db.insert_address(
async {
Ok(domain::Address {
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.to_string(),
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?,
id: None,
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?,
})
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while encrypting address while insert")?,
merchant_key_store,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while inserting new address")?,
)
}
}
}
None => match address_id {
Some(id) => Some(db.find_address(id, merchant_key_store).await)
.transpose()
.to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?,
None => None,
},
})
}
pub async fn get_address_by_id(
db: &dyn StorageInterface,
address_id: Option<String>,
merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Option<domain::Address>, errors::ApiErrorResponse> {
match address_id {
None => Ok(None),
Some(address_id) => Ok(db.find_address(&address_id, merchant_key_store).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,
) -> RouterResult<(
Option<String>,
Option<storage_enums::PaymentMethod>,
Option<storage_enums::PaymentMethodType>,
Option<api::MandateData>,
Option<payments::RecurringMandatePaymentData>,
Option<String>,
)> {
match mandate_type {
Some(api::MandateTransactionType::NewMandateTransaction) => {
let setup_mandate = request
.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).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,
request.mandate_data.clone(),
None,
None,
)),
}
}
pub async fn get_token_for_recurring_mandate(
state: &AppState,
req: &api::PaymentsRequest,
merchant_account: &domain::MerchantAccount,
) -> RouterResult<(
Option<String>,
Option<storage_enums::PaymentMethod>,
Option<payments::RecurringMandatePaymentData>,
Option<storage_enums::PaymentMethodType>,
Option<String>,
)> {
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;
if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method {
let _ = cards::get_lookup_key_from_locker(state, &token, &payment_method).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),
))
} else {
Ok((
None,
Some(payment_method.payment_method),
Some(payments::RecurringMandatePaymentData {
payment_method_type,
}),
payment_method.payment_method_type,
Some(mandate.connector),
))
}
}
#[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()
}))
}
}
}
}
}
#[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 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_shipping: bool,
has_billing: bool,
has_setup_future_usage: bool,
customer_id: &Option<String>,
) -> RouterResult<()> {
match (
has_shipping,
has_billing,
has_setup_future_usage,
customer_id,
) {
(true, _, _, None) | (_, true, _, None) | (_, _, true, None) => {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "customer_id is mandatory when shipping or billing \
address is given or when setup_future_usage is given"
.to_string(),
})
.into_report()
}
_ => Ok(()),
}
}
pub fn create_startpay_url(
server: &Server,
payment_attempt: &storage::PaymentAttempt,
payment_intent: &storage::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: &storage::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
}
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: &storage::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: &storage::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>() -> BoxedOperation<'a, F, R>
where
F: Send + Clone,
PaymentResponse: Operation<F, R>,
{
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`
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>(
operation: BoxedOperation<'a, F, R>,
db: &dyn StorageInterface,
payment_data: &mut PaymentData<F>,
req: Option<CustomerDetails>,
merchant_id: &str,
key_store: &domain::MerchantKeyStore,
) -> CustomResult<(BoxedOperation<'a, F, R>, 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: 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,
})
}
.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,
})
}
.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 make_pm_data<'a, F: Clone, R>(
operation: BoxedOperation<'a, F, R>,
state: &'a AppState,
payment_data: &mut PaymentData<F>,
) -> RouterResult<(BoxedOperation<'a, F, R>, Option<api::PaymentMethodData>)> {
let request = &payment_data.payment_method_data;
let token = payment_data.token.clone();
let hyperswitch_token = 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 hyperswitch_token_option = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?;
hyperswitch_token_option.or(Some(token))
} else {
None
};
let card_cvc = payment_data.card_cvc.clone();
// 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 (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker(
state,
&hyperswitch_token,
)
.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_data.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.clone() {
Some(api::PaymentMethodData::Card(card)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::Card);
if let Some(cvc) = card_cvc {
let mut updated_card = card;
updated_card.card_cvc = cvc;
let updated_pm = api::PaymentMethodData::Card(updated_card);
vault::Vault::store_payment_method_data_in_locker(
state,
Some(hyperswitch_token),
&updated_pm,
payment_data.payment_intent.customer_id.to_owned(),
enums::PaymentMethod::Card,
)
.await?;
Some(updated_pm)
} else {
pm
}
}
Some(api::PaymentMethodData::Wallet(_)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::Wallet);
pm
}
Some(api::PaymentMethodData::BankTransfer(_)) => {
payment_data.payment_attempt.payment_method =
Some(storage_enums::PaymentMethod::BankTransfer);
pm
}
Some(_) => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable(
"Payment method received from locker is unsupported by locker",
)?,
None => None,
})
}
(pm_opt @ Some(pm @ api::PaymentMethodData::Card(_)), _) => {
let token = vault::Vault::store_payment_method_data_in_locker(
state,
None,
pm,
payment_data.payment_intent.customer_id.to_owned(),
enums::PaymentMethod::Card,
)
.await?;
payment_data.token = Some(token);
Ok(pm_opt.to_owned())
}
(pm @ Some(api::PaymentMethodData::PayLater(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::BankRedirect(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Crypto(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::BankDebit(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Upi(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Voucher(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Reward(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::GiftCard(_)), _) => Ok(pm.to_owned()),
(pm_opt @ Some(pm @ api::PaymentMethodData::BankTransfer(_)), _) => {
let token = vault::Vault::store_payment_method_data_in_locker(
state,
None,
pm,
payment_data.payment_intent.customer_id.to_owned(),
enums::PaymentMethod::BankTransfer,
)
.await?;
payment_data.token = Some(token);
Ok(pm_opt.to_owned())
}
(pm_opt @ Some(pm @ api::PaymentMethodData::Wallet(_)), _) => {
let token = vault::Vault::store_payment_method_data_in_locker(
state,
None,
pm,
payment_data.payment_intent.customer_id.to_owned(),
enums::PaymentMethod::Wallet,
)
.await?;
payment_data.token = Some(token);
Ok(pm_opt.to_owned())
}
_ => Ok(None),
}?;
Ok((operation, payment_method))
}
#[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_single, manual_multiple, scheduled".to_string()
}))
},
)
}
#[instrument(skip_all)]
pub(crate) fn validate_status(status: storage_enums::IntentStatus) -> RouterResult<()> {
utils::when(
status != storage_enums::IntentStatus::RequiresCapture,
|| {
Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
field_name: "payment.status".to_string(),
current_flow: "captured".to_string(),
current_value: status.to_string(),
states: "requires_capture".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",
})
},
)?;
let payment_method: Option<api_enums::PaymentMethod> =
(req.payment_method_type).map(ForeignInto::foreign_into);
utils::when(
req.payment_method.is_some()
&& req.payment_method_type.is_some()
&& (req.payment_method != payment_method),
|| {
Err(errors::ApiErrorResponse::InvalidRequestData {
message: ("payment_method_type doesn't correspond to the specified payment_method"
.to_string()),
})
},
)?;
utils::when(
!matches!(
req.payment_method
.as_ref()
.zip(req.payment_method_data.as_ref()),
Some(
(
api_enums::PaymentMethod::Card,
api::PaymentMethodData::Card(..)
) | (
api_enums::PaymentMethod::Wallet,
api::PaymentMethodData::Wallet(..)
) | (
api_enums::PaymentMethod::PayLater,
api::PaymentMethodData::PayLater(..)
) | (
api_enums::PaymentMethod::BankRedirect,
api::PaymentMethodData::BankRedirect(..)
) | (
api_enums::PaymentMethod::BankDebit,
api::PaymentMethodData::BankDebit(..)
) | (
api_enums::PaymentMethod::Crypto,
api::PaymentMethodData::Crypto(..)
) | (
api_enums::PaymentMethod::Upi,
api::PaymentMethodData::Upi(..)
) | (
api_enums::PaymentMethod::Voucher,
api::PaymentMethodData::Voucher(..)
)
) | None
),
|| {
Err(errors::ApiErrorResponse::InvalidRequestData {
message: "payment_method_data doesn't correspond to the specified payment_method"
.to_string(),
})
},
)?;
Ok(())
}
pub fn check_force_psync_precondition(
status: &storage_enums::AttemptStatus,
connector_transaction_id: &Option<String>,
) -> bool {
!matches!(
status,
storage_enums::AttemptStatus::Charged
| storage_enums::AttemptStatus::AutoRefunded
| storage_enums::AttemptStatus::Voided
| storage_enums::AttemptStatus::CodInitiated
| storage_enums::AttemptStatus::Authorized
| storage_enums::AttemptStatus::Started
| storage_enums::AttemptStatus::Failure
) && connector_transaction_id.is_some()
}
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<storage::PaymentIntent>, errors::StorageError> {
let result = db
.filter_payment_intent_by_constraints(merchant_id, constraints, storage_scheme)
.await?;
Ok(result)
}
#[cfg(feature = "olap")]
pub(super) fn validate_payment_list_request(
req: &api::PaymentListConstraints,
) -> CustomResult<(), errors::ApiErrorResponse> {
utils::when(req.limit > 100 || req.limit < 1, || {
Err(errors::ApiErrorResponse::InvalidRequestData {
message: "limit should be in between 1 and 100".to_string(),
})
})?;
Ok(())
}
pub fn get_handle_response_url(
payment_id: String,
merchant_account: &domain::MerchantAccount,
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(
merchant_account,
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, merchant_account)
}
pub fn make_merchant_url_with_response(
merchant_account: &domain::MerchantAccount,
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(merchant_account.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 merchant_account.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(
store: &dyn StorageInterface,
ek_id: String,
) -> errors::RouterResponse<ephemeral_key::EphemeralKey> {
let ek = store
.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,
merchant_account: &domain::MerchantAccount,
) -> 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 merchant_account.enable_payment_response_hash {
let key = merchant_account
.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 merchant_account.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,
connector: String,
setup_mandate_details: Option<api::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>,
) -> 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_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(),
)
}));
Ok(Some(
match data.mandate_type.get_required_value("mandate_type")? {
api::MandateType::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(),
api::MandateType::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(crate) fn authenticate_client_secret(
request_client_secret: Option<&String>,
payment_intent: &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(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}",
),
})
})
}
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<storage::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)?;
authenticate_client_secret(
Some(&cs),
&payment_intent,
merchant_account.intent_fulfillment_time,
)?;
Ok(payment_intent)
})
.await
.transpose()
}
fn connector_needs_business_sub_label(connector_name: &str) -> bool {
let connectors_list = [api_models::enums::Connector::Cybersource];
connectors_list
.map(|connector| connector.to_string())
.contains(&connector_name.to_string())
}
/// Create the connector label
/// {connector_name}_{country}_{business_label}
pub fn get_connector_label(
business_country: api_models::enums::CountryAlpha2,
business_label: &str,
business_sub_label: Option<&String>,
connector_name: &str,
) -> String {
let mut connector_label = format!("{connector_name}_{business_country}_{business_label}");
// Business sub label is currently being used only for cybersource
// To ensure backwards compatibality, cybersource mca's created before this change
// will have the business_sub_label value as default.
//
// Even when creating the connector account, if no sub label is provided, default will be used
if connector_needs_business_sub_label(connector_name) {
if let Some(sub_label) = business_sub_label {
connector_label.push_str(&format!("_{sub_label}"));
} else {
connector_label.push_str("_default"); // For backwards compatibality
}
}
connector_label
}
/// Check whether the business details are configured in the merchant account
pub fn validate_business_details(
business_country: api_enums::CountryAlpha2,
business_label: &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")?;
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(),
})?;
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 = 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_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
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 = 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_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
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 = 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_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
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
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 async fn get_merchant_connector_account(
state: &AppState,
merchant_id: &str,
connector_label: &str,
creds_identifier: Option<String>,
key_store: &domain::MerchantKeyStore,
) -> 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: connector_label.to_string(),
},
)?;
#[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::InternalServerError)
.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 => db
.find_merchant_connector_account_by_merchant_id_connector_label(
merchant_id,
connector_label,
key_store,
)
.await
.map(MerchantConnectorAccountType::DbVal)
.change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: connector_label.to_string(),
}),
}
}
/// 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: None,
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,
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,
}
}
pub fn get_attempt_type(
payment_intent: &storage::PaymentIntent,
payment_attempt: &storage::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)
) {
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::Voided
| enums::AttemptStatus::AutoRefunded
| enums::AttemptStatus::PaymentMethodAwaited
| enums::AttemptStatus::DeviceDataCollectionPending => {
Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Payment Attempt unexpected state")
}
storage_enums::AttemptStatus::VoidFailed
| storage_enums::AttemptStatus::RouterDeclined
| storage_enums::AttemptStatus::CaptureFailed => 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 => 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::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: storage::PaymentAttempt,
new_attempt_count: i16,
) -> 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,
connector_response_reference_id: None,
}
}
pub async fn modify_payment_intent_and_payment_attempt(
&self,
request: &api::PaymentsRequest,
fetched_payment_intent: storage::PaymentIntent,
fetched_payment_attempt: storage::PaymentAttempt,
db: &dyn StorageInterface,
storage_scheme: storage::enums::MerchantStorageScheme,
) -> RouterResult<(storage::PaymentIntent, storage::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,
)
.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.to_owned(),
attempt_count: new_attempt_count,
},
storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
Ok((updated_payment_intent, new_payment_attempt))
}
}
}
pub async fn get_connector_response(
&self,
payment_attempt: &storage::PaymentAttempt,
db: &dyn StorageInterface,
storage_scheme: storage::enums::MerchantStorageScheme,
) -> RouterResult<storage::ConnectorResponse> {
match self {
Self::New => db
.insert_connector_response(
payments::PaymentCreate::make_connector_response(payment_attempt),
storage_scheme,
)
.await
.to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment {
payment_id: payment_attempt.payment_id.clone(),
}),
Self::SameOld => db
.find_connector_response_by_payment_id_merchant_id_attempt_id(
&payment_attempt.payment_id,
&payment_attempt.merchant_id,
&payment_attempt.attempt_id,
storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound),
}
}
}
#[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::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::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()
);
}
}
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::Voucher(_) => {
api_models::payments::AdditionalPaymentData::Voucher {}
}
api_models::payments::PaymentMethodData::GiftCard(_) => {
api_models::payments::AdditionalPaymentData::GiftCard {}
}
}
}
pub fn validate_customer_access(
payment_intent: &storage::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(())
}