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