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> { 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, payment_intent: Option<&diesel_models::payment_intent::PaymentIntent>, ) -> Vec { 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::>() } 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, 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, merchant_key_store: &domain::MerchantKeyStore, ) -> CustomResult, 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, merchant_account: &domain::MerchantAccount, ) -> RouterResult<( Option, Option, Option, Option, Option, Option, )> { 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, Option, Option, Option, Option, )> { 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, op_amount_to_capture: Option, ) -> 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, ) -> 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::().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::() .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::().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, is_confirm_operation: bool, ) -> CustomResult, 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, ) -> 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, confirm: Option, ) -> 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, confirm: Option, ) -> 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( operation: &Op, state: &AppState, payment_attempt: &storage::PaymentAttempt, requeue: bool, schedule_time: Option, ) -> 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, { Box::new(PaymentResponse) } #[instrument(skip_all)] pub(crate) async fn get_payment_method_create_request( payment_method_data: Option<&api::PaymentMethodData>, payment_method: Option, payment_method_type: Option, customer: &domain::Customer, ) -> RouterResult { 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( db: &dyn StorageInterface, customer_id: Option, merchant_id: &str, payment_data: &mut PaymentData, merchant_key_store: &domain::MerchantKeyStore, ) -> CustomResult, 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( 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, ) -> CustomResult { 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, req: Option, merchant_id: &str, key_store: &domain::MerchantKeyStore, ) -> CustomResult<(BoxedOperation<'a, F, R>, Option), 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, ) -> RouterResult<(BoxedOperation<'a, F, R>, Option)> { 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::>(&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>(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, ) -> 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 = (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, ) -> 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(func: F, option1: Option, option2: Option) -> Option 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, 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 { 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>, manual_retry_allowed: Option, ) -> RouterResult { // 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 { 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 { 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 { 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::>(), 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::>() .iter() .map(|(key, value)| (key.clone().into_owned(), value.clone().into_owned())) .collect::>(); 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 { params.sort(); let final_string = params .iter() .map(|(key, value)| format!("{key}={value}")) .collect::>() .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(operations: Op) -> bool { format!("{operations:?}") == "PaymentConfirm" } #[allow(clippy::too_many_arguments)] pub fn generate_mandate( merchant_id: String, connector: String, setup_mandate_details: Option, customer: &Option, payment_method_id: String, connector_mandate_id: Option, network_txn_id: Option, payment_method_data_option: Option, ) -> CustomResult, 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, ) -> 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, payment_method_data: &Option, payment_method_type: &Option, mandate_type: &Option, token: &Option, ) -> 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, ) -> error_stack::Result, 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::>("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, 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::>("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 { 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> { 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 { 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, key_store: &domain::MerchantKeyStore, ) -> RouterResult { 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( router_data: RouterData, request: Req2, response: Result, ) -> RouterData { 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 { 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, 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 { 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 { 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(()) }