use std::{collections::HashSet, marker::PhantomData, str::FromStr}; use api_models::enums::{DisputeStage, DisputeStatus}; #[cfg(feature = "payouts")] use api_models::payouts::PayoutVendorAccountDetails; use common_enums::{IntentStatus, RequestIncrementalAuthorization}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{ errors::CustomResult, ext_traits::AsyncExt, types::{keymanager::KeyManagerState, ConnectorTransactionIdTrait, MinorUnit}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ merchant_connector_account::MerchantConnectorAccount, payment_address::PaymentAddress, router_data::ErrorResponse, router_request_types, types::OrderDetailsWithAmount, }; #[cfg(feature = "payouts")] use masking::{ExposeInterface, PeekInterface}; use maud::{html, PreEscaped}; use router_env::{instrument, tracing}; use uuid::Uuid; use super::payments::helpers; #[cfg(feature = "payouts")] use super::payouts::{helpers as payout_helpers, PayoutData}; #[cfg(feature = "payouts")] use crate::core::payments; use crate::{ configs::Settings, consts, core::{ errors::{self, RouterResult, StorageErrorExt}, payments::PaymentData, }, db::StorageInterface, routes::SessionState, types::{ self, api, domain, storage::{self, enums}, PollConfig, }, utils::{generate_id, generate_uuid, OptionExt, ValueExt}, }; pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW: &str = "irrelevant_connector_request_reference_id_in_dispute_flow"; #[cfg(feature = "payouts")] pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW: &str = "irrelevant_connector_request_reference_id_in_payouts_flow"; const IRRELEVANT_ATTEMPT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_attempt_id_in_dispute_flow"; #[cfg(all(feature = "payouts", feature = "v2", feature = "customer_v2"))] #[instrument(skip_all)] pub async fn construct_payout_router_data<'a, F>( _state: &SessionState, _connector_data: &api::ConnectorData, _merchant_account: &domain::MerchantAccount, _payout_data: &mut PayoutData, ) -> RouterResult> { todo!() } #[cfg(all( feature = "payouts", any(feature = "v1", feature = "v2"), not(feature = "customer_v2") ))] #[instrument(skip_all)] pub async fn construct_payout_router_data<'a, F>( state: &SessionState, connector_data: &api::ConnectorData, merchant_account: &domain::MerchantAccount, payout_data: &mut PayoutData, ) -> RouterResult> { let merchant_connector_account = payout_data .merchant_connector_account .clone() .get_required_value("merchant_connector_account")?; let connector_name = connector_data.connector_name; let connector_auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let billing = payout_data.billing_address.to_owned(); let billing_address = billing.map(|a| { let phone_details = api_models::payments::PhoneDetails { number: a.phone_number.clone().map(Encryptable::into_inner), country_code: a.country_code.to_owned(), }; let address_details = api_models::payments::AddressDetails { city: a.city.to_owned(), country: a.country.to_owned(), line1: a.line1.clone().map(Encryptable::into_inner), line2: a.line2.clone().map(Encryptable::into_inner), line3: a.line3.clone().map(Encryptable::into_inner), zip: a.zip.clone().map(Encryptable::into_inner), first_name: a.first_name.clone().map(Encryptable::into_inner), last_name: a.last_name.clone().map(Encryptable::into_inner), state: a.state.map(Encryptable::into_inner), }; api_models::payments::Address { phone: Some(phone_details), address: Some(address_details), email: a.email.to_owned().map(Email::from), } }); let address = PaymentAddress::new(None, billing_address.map(From::from), None, None); let test_mode: Option = merchant_connector_account.is_test_mode_on(); let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; let connector_label = format!( "{}_{}", payout_data.profile_id.get_string_repr(), connector_name ); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) .and_then(|connector_customer_value| { connector_customer_value .clone() .expose() .get(connector_label) .cloned() }) .and_then(|id| serde_json::from_value::(id).ok()); let vendor_details: Option = match api_models::enums::PayoutConnectors::try_from(connector_name.to_owned()).map_err( |err| report!(errors::ApiErrorResponse::InternalServerError).attach_printable(err), )? { api_models::enums::PayoutConnectors::Stripe => { payout_data.payouts.metadata.to_owned().and_then(|meta| { let val = meta .peek() .to_owned() .parse_value("PayoutVendorAccountDetails") .ok(); val }) } _ => None, }; let connector_transfer_method_id = payout_helpers::should_create_connector_transfer_method(&*payout_data, connector_data)?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), customer_id: customer_details.to_owned().map(|c| c.customer_id), tenant_id: state.tenant.tenant_id.clone(), connector_customer: connector_customer_id, connector: connector_name.to_string(), payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("payout") .get_string_repr() .to_owned(), attempt_id: "".to_string(), status: enums::AttemptStatus::Failure, payment_method: enums::PaymentMethod::default(), connector_auth_type, description: None, address, auth_type: enums::AuthenticationType::default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, minor_amount_captured: None, payment_method_status: None, request: types::PayoutsData { payout_id: payouts.payout_id.to_owned(), amount: payouts.amount.get_amount_as_i64(), minor_amount: payouts.amount, connector_payout_id: payout_attempt.connector_payout_id.clone(), destination_currency: payouts.destination_currency, source_currency: payouts.source_currency, entity_type: payouts.entity_type.to_owned(), payout_type: payouts.payout_type, vendor_details, priority: payouts.priority, customer_details: customer_details .to_owned() .map(|c| payments::CustomerDetails { customer_id: Some(c.customer_id), name: c.name.map(Encryptable::into_inner), email: c.email.map(Email::from), phone: c.phone.map(Encryptable::into_inner), phone_country_code: c.phone_country_code, }), connector_transfer_method_id, }, response: Ok(types::PayoutsResponseData::default()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, recurring_mandate_payment_data: None, preprocessing_id: None, connector_request_reference_id: payout_attempt.payout_attempt_id.clone(), payout_method_data: payout_data.payout_method_data.to_owned(), quote_id: None, test_mode, payment_method_balance: None, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: None, connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn construct_refund_router_data<'a, F>( _state: &'a SessionState, _connector_id: &str, _merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, _money: (MinorUnit, enums::Currency), _payment_intent: &'a storage::PaymentIntent, _payment_attempt: &storage::PaymentAttempt, _refund: &'a storage::Refund, _creds_identifier: Option, _split_refunds: Option, ) -> RouterResult> { todo!() } #[cfg(feature = "v1")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn construct_refund_router_data<'a, F>( state: &'a SessionState, connector_id: &str, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, money: (MinorUnit, enums::Currency), payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, refund: &'a storage::Refund, creds_identifier: Option, split_refunds: Option, ) -> RouterResult> { let profile_id = payment_intent .profile_id .as_ref() .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in payment_intent")?; let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), creds_identifier.as_deref(), key_store, profile_id, connector_id, payment_attempt.merchant_connector_id.as_ref(), ) .await?; let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let status = payment_attempt.status; let (payment_amount, currency) = money; let payment_method_type = payment_attempt .payment_method .get_required_value("payment_method_type") .change_context(errors::ApiErrorResponse::InternalServerError)?; let merchant_connector_account_id_or_connector_name = payment_attempt .merchant_connector_id .as_ref() .map(|mca_id| mca_id.get_string_repr()) .unwrap_or(connector_id); let webhook_url = Some(helpers::create_webhook_url( &state.base_url.clone(), merchant_account.get_id(), merchant_connector_account_id_or_connector_name, )); let test_mode: Option = merchant_connector_account.is_test_mode_on(); let supported_connector = &state .conf .multiple_api_version_supported_connectors .supported_connectors; let connector_enum = api_models::enums::Connector::from_str(connector_id) .change_context(errors::ConnectorError::InvalidConnectorName) .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "connector", }) .attach_printable_lazy(|| format!("unable to parse connector name {connector_id:?}"))?; let connector_api_version = if supported_connector.contains(&connector_enum) { state .store .find_config_by_key(&format!("connector_api_version_{connector_id}")) .await .map(|value| value.config) .ok() } else { None }; let browser_info: Option = payment_attempt .browser_info .clone() .map(|b| b.parse_value("BrowserInformation")) .transpose() .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; let connector_refund_id = refund.get_optional_connector_refund_id().cloned(); let braintree_metadata = payment_intent .connector_metadata .clone() .map(|cm| { cm.parse_value::("ConnectorMetadata") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed parsing ConnectorMetadata") }) .transpose()? .and_then(|cm| cm.braintree); let merchant_account_id = braintree_metadata .as_ref() .and_then(|braintree| braintree.merchant_account_id.clone()); let merchant_config_currency = braintree_metadata.and_then(|braintree| braintree.merchant_config_currency); let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), customer_id: payment_intent.customer_id.to_owned(), tenant_id: state.tenant.tenant_id.clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), status, payment_method: payment_method_type, connector_auth_type: auth_type, description: None, // Does refund need shipping/billing address ? address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), payment_method_status: None, minor_amount_captured: payment_intent.amount_captured, request: types::RefundsData { refund_id: refund.refund_id.clone(), connector_transaction_id: refund.get_connector_transaction_id().clone(), refund_amount: refund.refund_amount.get_amount_as_i64(), minor_refund_amount: refund.refund_amount, currency, payment_amount: payment_amount.get_amount_as_i64(), minor_payment_amount: payment_amount, webhook_url, connector_metadata: payment_attempt.connector_metadata.clone(), reason: refund.refund_reason.clone(), connector_refund_id: connector_refund_id.clone(), browser_info, split_refunds, integrity_object: None, refund_status: refund.refund_status, merchant_account_id, merchant_config_currency, }, response: Ok(types::RefundsResponseData { connector_refund_id: connector_refund_id.unwrap_or_default(), refund_status: refund.refund_status, }), access_token: None, session_token: None, reference_id: None, payment_method_token: None, connector_customer: None, recurring_mandate_payment_data: None, preprocessing_id: None, connector_request_reference_id: refund.refund_id.clone(), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, payment_method_balance: None, connector_api_version, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: Some(refund.refund_id.clone()), dispute_id: None, connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } pub fn get_or_generate_id( key: &str, provided_id: &Option, prefix: &str, ) -> Result { let validate_id = |id| validate_id(id, key); provided_id .clone() .map_or(Ok(generate_id(consts::ID_LENGTH, prefix)), validate_id) } pub fn get_or_generate_uuid( key: &str, provided_id: Option<&String>, ) -> Result { let validate_id = |id: String| validate_uuid(id, key); provided_id .cloned() .map_or(Ok(generate_uuid()), validate_id) } fn invalid_id_format_error(key: &str) -> errors::ApiErrorResponse { errors::ApiErrorResponse::InvalidDataFormat { field_name: key.to_string(), expected_format: format!( "length should be less than {} characters", consts::MAX_ID_LENGTH ), } } pub fn validate_id(id: String, key: &str) -> Result { if id.len() > consts::MAX_ID_LENGTH { Err(invalid_id_format_error(key)) } else { Ok(id) } } pub fn validate_uuid(uuid: String, key: &str) -> Result { match (Uuid::parse_str(&uuid), uuid.len() > consts::MAX_ID_LENGTH) { (Ok(_), false) => Ok(uuid), (_, _) => Err(invalid_id_format_error(key)), } } #[cfg(feature = "v1")] pub fn get_split_refunds( split_refund_input: super::refunds::transformers::SplitRefundInput, ) -> RouterResult> { match split_refund_input.split_payment_request.as_ref() { Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(stripe_payment)) => { let (charge_id_option, charge_type_option) = match ( &split_refund_input.payment_charges, &split_refund_input.split_payment_request, ) { ( Some(common_types::payments::ConnectorChargeResponseData::StripeSplitPayment( stripe_split_payment_response, )), _, ) => ( stripe_split_payment_response.charge_id.clone(), Some(stripe_split_payment_response.charge_type.clone()), ), ( _, Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment( stripe_split_payment_request, )), ) => ( split_refund_input.charge_id, Some(stripe_split_payment_request.charge_type.clone()), ), (_, _) => (None, None), }; if let Some(charge_id) = charge_id_option { let options = super::refunds::validator::validate_stripe_charge_refund( charge_type_option, &split_refund_input.refund_request, )?; Ok(Some( router_request_types::SplitRefundsRequest::StripeSplitRefund( router_request_types::StripeSplitRefund { charge_id, charge_type: stripe_payment.charge_type.clone(), transfer_account_id: stripe_payment.transfer_account_id.clone(), options, }, ), )) } else { Ok(None) } } Some(common_types::payments::SplitPaymentsRequest::AdyenSplitPayment(_)) => { match &split_refund_input.payment_charges { Some(common_types::payments::ConnectorChargeResponseData::AdyenSplitPayment( adyen_split_payment_response, )) => { if let Some(common_types::refunds::SplitRefund::AdyenSplitRefund( split_refund_request, )) = split_refund_input.refund_request.clone() { super::refunds::validator::validate_adyen_charge_refund( adyen_split_payment_response, &split_refund_request, )?; Ok(Some( router_request_types::SplitRefundsRequest::AdyenSplitRefund( split_refund_request, ), )) } else { Ok(None) } } _ => Ok(None), } } Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(_)) => { match ( &split_refund_input.payment_charges, &split_refund_input.refund_request, ) { ( Some(common_types::payments::ConnectorChargeResponseData::XenditSplitPayment( xendit_split_payment_response, )), Some(common_types::refunds::SplitRefund::XenditSplitRefund( split_refund_request, )), ) => { let user_id = super::refunds::validator::validate_xendit_charge_refund( xendit_split_payment_response, split_refund_request, )?; Ok(user_id.map(|for_user_id| { router_request_types::SplitRefundsRequest::XenditSplitRefund( common_types::domain::XenditSplitSubMerchantData { for_user_id }, ) })) } ( Some(common_types::payments::ConnectorChargeResponseData::XenditSplitPayment( xendit_split_payment_response, )), None, ) => { let option_for_user_id = match xendit_split_payment_response { common_types::payments::XenditChargeResponseData::MultipleSplits( common_types::payments::XenditMultipleSplitResponse { for_user_id, .. }, ) => for_user_id.clone(), common_types::payments::XenditChargeResponseData::SingleSplit( common_types::domain::XenditSplitSubMerchantData { for_user_id }, ) => Some(for_user_id.clone()), }; if option_for_user_id.is_some() { Err(errors::ApiErrorResponse::MissingRequiredField { field_name: "split_refunds.xendit_split_refund.for_user_id", })? } else { Ok(None) } } _ => Ok(None), } } _ => Ok(None), } } #[cfg(test)] mod tests { #![allow(clippy::expect_used)] use super::*; #[test] fn validate_id_length_constraint() { let payment_id = "abcdefghijlkmnopqrstuvwzyzabcdefghijknlmnopsjkdnfjsknfkjsdnfspoig".to_string(); //length = 65 let result = validate_id(payment_id, "payment_id"); assert!(result.is_err()); } #[test] fn validate_id_proper_response() { let payment_id = "abcdefghijlkmnopqrstjhbjhjhkhbhgcxdfxvmhb".to_string(); let result = validate_id(payment_id.clone(), "payment_id"); assert!(result.is_ok()); let result = result.unwrap_or_default(); assert_eq!(result, payment_id); } #[test] fn test_generate_id() { let generated_id = generate_id(consts::ID_LENGTH, "ref"); assert_eq!(generated_id.len(), consts::ID_LENGTH + 4) } #[test] fn test_filter_objects_based_on_profile_id_list() { #[derive(PartialEq, Debug, Clone)] struct Object { profile_id: Option, } impl Object { pub fn new(profile_id: &'static str) -> Self { Self { profile_id: Some( common_utils::id_type::ProfileId::try_from(std::borrow::Cow::from( profile_id, )) .expect("invalid profile ID"), ), } } } impl GetProfileId for Object { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } } fn new_profile_id(profile_id: &'static str) -> common_utils::id_type::ProfileId { common_utils::id_type::ProfileId::try_from(std::borrow::Cow::from(profile_id)) .expect("invalid profile ID") } // non empty object_list and profile_id_list let object_list = vec![ Object::new("p1"), Object::new("p2"), Object::new("p2"), Object::new("p4"), Object::new("p5"), ]; let profile_id_list = vec![ new_profile_id("p1"), new_profile_id("p2"), new_profile_id("p3"), ]; let filtered_list = filter_objects_based_on_profile_id_list(Some(profile_id_list), object_list.clone()); let expected_result = vec![Object::new("p1"), Object::new("p2"), Object::new("p2")]; assert_eq!(filtered_list, expected_result); // non empty object_list and empty profile_id_list let empty_profile_id_list = vec![]; let filtered_list = filter_objects_based_on_profile_id_list( Some(empty_profile_id_list), object_list.clone(), ); let expected_result = vec![]; assert_eq!(filtered_list, expected_result); // non empty object_list and None profile_id_list let profile_id_list_as_none = None; let filtered_list = filter_objects_based_on_profile_id_list(profile_id_list_as_none, object_list); let expected_result = vec![ Object::new("p1"), Object::new("p2"), Object::new("p2"), Object::new("p4"), Object::new("p5"), ]; assert_eq!(filtered_list, expected_result); } } // Dispute Stage can move linearly from PreDispute -> Dispute -> PreArbitration pub fn validate_dispute_stage( prev_dispute_stage: DisputeStage, dispute_stage: DisputeStage, ) -> bool { match prev_dispute_stage { DisputeStage::PreDispute => true, DisputeStage::Dispute => !matches!(dispute_stage, DisputeStage::PreDispute), DisputeStage::PreArbitration => matches!(dispute_stage, DisputeStage::PreArbitration), } } //Dispute status can go from Opened -> (Expired | Accepted | Cancelled | Challenged -> (Won | Lost)) pub fn validate_dispute_status( prev_dispute_status: DisputeStatus, dispute_status: DisputeStatus, ) -> bool { match prev_dispute_status { DisputeStatus::DisputeOpened => true, DisputeStatus::DisputeExpired => { matches!(dispute_status, DisputeStatus::DisputeExpired) } DisputeStatus::DisputeAccepted => { matches!(dispute_status, DisputeStatus::DisputeAccepted) } DisputeStatus::DisputeCancelled => { matches!(dispute_status, DisputeStatus::DisputeCancelled) } DisputeStatus::DisputeChallenged => matches!( dispute_status, DisputeStatus::DisputeChallenged | DisputeStatus::DisputeWon | DisputeStatus::DisputeLost ), DisputeStatus::DisputeWon => matches!(dispute_status, DisputeStatus::DisputeWon), DisputeStatus::DisputeLost => matches!(dispute_status, DisputeStatus::DisputeLost), } } pub fn validate_dispute_stage_and_dispute_status( prev_dispute_stage: DisputeStage, prev_dispute_status: DisputeStatus, dispute_stage: DisputeStage, dispute_status: DisputeStatus, ) -> CustomResult<(), errors::WebhooksFlowError> { let dispute_stage_validation = validate_dispute_stage(prev_dispute_stage, dispute_stage); let dispute_status_validation = if dispute_stage == prev_dispute_stage { validate_dispute_status(prev_dispute_status, dispute_status) } else { true }; common_utils::fp_utils::when( !(dispute_stage_validation && dispute_status_validation), || { super::metrics::INCOMING_DISPUTE_WEBHOOK_VALIDATION_FAILURE_METRIC.add(1, &[]); Err(errors::WebhooksFlowError::DisputeWebhookValidationFailed)? }, ) } #[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn construct_accept_dispute_router_data<'a>( state: &'a SessionState, payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, dispute: &storage::Dispute, ) -> RouterResult { let profile_id = payment_intent .profile_id .as_ref() .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in payment_intent")? .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), None, key_store, &profile_id, &dispute.connector, payment_attempt.merchant_connector_id.as_ref(), ) .await?; let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let payment_method = payment_attempt .payment_method .get_required_value("payment_method_type")?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: dispute.connector.to_string(), tenant_id: state.tenant.tenant_id.clone(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, connector_auth_type: auth_type, description: None, address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), minor_amount_captured: payment_intent.amount_captured, payment_method_status: None, request: types::AcceptDisputeRequestData { dispute_id: dispute.dispute_id.clone(), connector_dispute_id: dispute.connector_dispute_id.clone(), }, response: Err(ErrorResponse::default()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, connector_customer: None, customer_id: None, recurring_mandate_payment_data: None, preprocessing_id: None, connector_request_reference_id: get_connector_request_reference_id( &state.conf, merchant_account.get_id(), payment_attempt, ), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, payment_method_balance: None, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, dispute_id: Some(dispute.dispute_id.clone()), refund_id: None, connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn construct_submit_evidence_router_data<'a>( state: &'a SessionState, payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, dispute: &storage::Dispute, submit_evidence_request_data: types::SubmitEvidenceRequestData, ) -> RouterResult { let connector_id = &dispute.connector; let profile_id = payment_intent .profile_id .as_ref() .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in payment_intent")? .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), None, key_store, &profile_id, connector_id, payment_attempt.merchant_connector_id.as_ref(), ) .await?; let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let payment_method = payment_attempt .payment_method .get_required_value("payment_method_type")?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, connector_auth_type: auth_type, description: None, address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), minor_amount_captured: payment_intent.amount_captured, request: submit_evidence_request_data, response: Err(ErrorResponse::default()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, connector_customer: None, customer_id: None, recurring_mandate_payment_data: None, preprocessing_id: None, payment_method_balance: None, payment_method_status: None, connector_request_reference_id: get_connector_request_reference_id( &state.conf, merchant_account.get_id(), payment_attempt, ), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: Some(dispute.dispute_id.clone()), connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[cfg(feature = "v1")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn construct_upload_file_router_data<'a>( state: &'a SessionState, payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, create_file_request: &api::CreateFileRequest, connector_id: &str, file_key: String, ) -> RouterResult { let profile_id = payment_intent .profile_id .as_ref() .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in payment_intent")? .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), None, key_store, &profile_id, connector_id, payment_attempt.merchant_connector_id.as_ref(), ) .await?; let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let payment_method = payment_attempt .payment_method .get_required_value("payment_method_type")?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, connector_auth_type: auth_type, description: None, address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), minor_amount_captured: payment_intent.amount_captured, payment_method_status: None, request: types::UploadFileRequestData { file_key, file: create_file_request.file.clone(), file_type: create_file_request.file_type.clone(), file_size: create_file_request.file_size, }, response: Err(ErrorResponse::default()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, connector_customer: None, customer_id: None, recurring_mandate_payment_data: None, preprocessing_id: None, payment_method_balance: None, connector_request_reference_id: get_connector_request_reference_id( &state.conf, merchant_account.get_id(), payment_attempt, ), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: None, connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[cfg(feature = "v2")] pub async fn construct_payments_dynamic_tax_calculation_router_data( state: &SessionState, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, merchant_connector_account: &MerchantConnectorAccount, ) -> RouterResult { todo!() } #[cfg(feature = "v1")] pub async fn construct_payments_dynamic_tax_calculation_router_data( state: &SessionState, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, merchant_connector_account: &MerchantConnectorAccount, ) -> RouterResult { let payment_intent = &payment_data.payment_intent.clone(); let payment_attempt = &payment_data.payment_attempt.clone(); #[cfg(feature = "v1")] let test_mode: Option = merchant_connector_account.test_mode; #[cfg(feature = "v2")] let test_mode = None; let connector_auth_type: types::ConnectorAuthType = merchant_connector_account .connector_account_details .clone() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while parsing value for ConnectorAuthType")?; let shipping_address = payment_data .tax_data .clone() .map(|tax_data| tax_data.shipping_details) .clone() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Missing shipping_details")?; let order_details: Option> = payment_intent .order_details .clone() .map(|order_details| { order_details .iter() .map(|data| { data.to_owned() .parse_value("OrderDetailsWithAmount") .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "OrderDetailsWithAmount", }) .attach_printable("Unable to parse OrderDetailsWithAmount") }) .collect::, _>>() }) .transpose()?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), customer_id: None, connector_customer: None, connector: merchant_connector_account.connector_name.clone(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), tenant_id: state.tenant.tenant_id.clone(), status: payment_attempt.status, payment_method: diesel_models::enums::PaymentMethod::default(), connector_auth_type, description: None, address: payment_data.address.clone(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: None, connector_wallets_details: None, amount_captured: None, access_token: None, session_token: None, reference_id: None, payment_method_token: None, recurring_mandate_payment_data: None, preprocessing_id: None, payment_method_balance: None, connector_api_version: None, request: types::PaymentsTaxCalculationData { amount: payment_intent.amount, shipping_cost: payment_intent.shipping_cost, order_details, currency: payment_data.currency, shipping_address, }, response: Err(ErrorResponse::default()), connector_request_reference_id: get_connector_request_reference_id( &state.conf, merchant_account.get_id(), payment_attempt, ), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: None, connector_response: None, payment_method_status: None, minor_amount_captured: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn construct_defend_dispute_router_data<'a>( state: &'a SessionState, payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, dispute: &storage::Dispute, ) -> RouterResult { let _db = &*state.store; let connector_id = &dispute.connector; let profile_id = payment_intent .profile_id .as_ref() .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in payment_intent")? .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), None, key_store, &profile_id, connector_id, payment_attempt.merchant_connector_id.as_ref(), ) .await?; let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let payment_method = payment_attempt .payment_method .get_required_value("payment_method_type")?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, connector_auth_type: auth_type, description: None, address: PaymentAddress::default(), auth_type: payment_attempt.authentication_type.unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: payment_intent .amount_captured .map(|amt| amt.get_amount_as_i64()), minor_amount_captured: payment_intent.amount_captured, payment_method_status: None, request: types::DefendDisputeRequestData { dispute_id: dispute.dispute_id.clone(), connector_dispute_id: dispute.connector_dispute_id.clone(), }, response: Err(ErrorResponse::get_not_implemented()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, customer_id: None, connector_customer: None, recurring_mandate_payment_data: None, preprocessing_id: None, payment_method_balance: None, connector_request_reference_id: get_connector_request_reference_id( &state.conf, merchant_account.get_id(), payment_attempt, ), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: Some(dispute.dispute_id.clone()), connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } #[instrument(skip_all)] pub async fn construct_retrieve_file_router_data<'a>( state: &'a SessionState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, file_metadata: &diesel_models::file::FileMetadata, connector_id: &str, ) -> RouterResult { let profile_id = file_metadata .profile_id .as_ref() .ok_or(errors::ApiErrorResponse::MissingRequiredField { field_name: "profile_id", }) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("profile_id is not set in file_metadata")?; let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), None, key_store, profile_id, connector_id, file_metadata.merchant_connector_id.as_ref(), ) .await?; let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account .get_connector_account_details() .parse_value("ConnectorAuthType") .change_context(errors::ApiErrorResponse::InternalServerError)?; let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), tenant_id: state.tenant.tenant_id.clone(), customer_id: None, connector_customer: None, payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("dispute") .get_string_repr() .to_owned(), attempt_id: IRRELEVANT_ATTEMPT_ID_IN_DISPUTE_FLOW.to_string(), status: diesel_models::enums::AttemptStatus::default(), payment_method: diesel_models::enums::PaymentMethod::default(), connector_auth_type: auth_type, description: None, address: PaymentAddress::default(), auth_type: diesel_models::enums::AuthenticationType::default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, minor_amount_captured: None, payment_method_status: None, request: types::RetrieveFileRequestData { provider_file_id: file_metadata .provider_file_id .clone() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Missing provider file id")?, }, response: Err(ErrorResponse::default()), access_token: None, session_token: None, reference_id: None, payment_method_token: None, recurring_mandate_payment_data: None, preprocessing_id: None, payment_method_balance: None, connector_request_reference_id: IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW .to_string(), #[cfg(feature = "payouts")] payout_method_data: None, #[cfg(feature = "payouts")] quote_id: None, test_mode, connector_api_version: None, connector_http_status_code: None, external_latency: None, apple_pay_flow: None, frm_metadata: None, refund_id: None, dispute_id: None, connector_response: None, integrity_check: Ok(()), additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, authentication_id: None, psd2_sca_exemption_type: None, }; Ok(router_data) } pub fn is_merchant_enabled_for_payment_id_as_connector_request_id( conf: &Settings, merchant_id: &common_utils::id_type::MerchantId, ) -> bool { let config_map = &conf .connector_request_reference_id_config .merchant_ids_send_payment_id_as_connector_request_id; config_map.contains(merchant_id) } #[cfg(feature = "v1")] pub fn get_connector_request_reference_id( conf: &Settings, merchant_id: &common_utils::id_type::MerchantId, payment_attempt: &hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt, ) -> String { let is_config_enabled_for_merchant = is_merchant_enabled_for_payment_id_as_connector_request_id(conf, merchant_id); // Send payment_id if config is enabled for a merchant, else send attempt_id if is_config_enabled_for_merchant { payment_attempt.payment_id.get_string_repr().to_owned() } else { payment_attempt.attempt_id.to_owned() } } // TODO: Based on the connector configuration, the connector_request_reference_id should be generated #[cfg(feature = "v2")] pub fn get_connector_request_reference_id( conf: &Settings, merchant_id: &common_utils::id_type::MerchantId, payment_attempt: &hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt, ) -> String { todo!() } /// Validate whether the profile_id exists and is associated with the merchant_id pub async fn validate_and_get_business_profile( db: &dyn StorageInterface, key_manager_state: &KeyManagerState, merchant_key_store: &domain::MerchantKeyStore, profile_id: Option<&common_utils::id_type::ProfileId>, merchant_id: &common_utils::id_type::MerchantId, ) -> RouterResult> { profile_id .async_map(|profile_id| async { db.find_business_profile_by_profile_id( key_manager_state, merchant_key_store, profile_id, ) .await .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { id: profile_id.get_string_repr().to_owned(), }) }) .await .transpose()? .map(|business_profile| { // Check if the merchant_id of business profile is same as the current merchant_id if business_profile.merchant_id.ne(merchant_id) { Err(errors::ApiErrorResponse::AccessForbidden { resource: business_profile.get_id().get_string_repr().to_owned(), } .into()) } else { Ok(business_profile) } }) .transpose() } fn connector_needs_business_sub_label(connector_name: &str) -> bool { let connectors_list = [api_models::enums::Connector::Cybersource]; connectors_list .map(|connector| connector.to_string()) .contains(&connector_name.to_string()) } /// Create the connector label /// {connector_name}_{country}_{business_label} pub fn get_connector_label( business_country: Option, business_label: Option<&String>, business_sub_label: Option<&String>, connector_name: &str, ) -> Option { business_country .zip(business_label) .map(|(business_country, business_label)| { let mut connector_label = format!("{connector_name}_{business_country}_{business_label}"); // Business sub label is currently being used only for cybersource // To ensure backwards compatibality, cybersource mca's created before this change // will have the business_sub_label value as default. // // Even when creating the connector account, if no sub label is provided, default will be used if connector_needs_business_sub_label(connector_name) { if let Some(sub_label) = business_sub_label { connector_label.push_str(&format!("_{sub_label}")); } else { connector_label.push_str("_default"); // For backwards compatibality } } connector_label }) } #[cfg(feature = "v1")] /// If profile_id is not passed, use default profile if available, or /// If business_details (business_country and business_label) are passed, get the business_profile /// or return a `MissingRequiredField` error #[allow(clippy::too_many_arguments)] pub async fn get_profile_id_from_business_details( key_manager_state: &KeyManagerState, merchant_key_store: &domain::MerchantKeyStore, business_country: Option, business_label: Option<&String>, merchant_account: &domain::MerchantAccount, request_profile_id: Option<&common_utils::id_type::ProfileId>, db: &dyn StorageInterface, should_validate: bool, ) -> RouterResult { match request_profile_id.or(merchant_account.default_profile.as_ref()) { Some(profile_id) => { // Check whether this business profile belongs to the merchant if should_validate { let _ = validate_and_get_business_profile( db, key_manager_state, merchant_key_store, Some(profile_id), merchant_account.get_id(), ) .await?; } Ok(profile_id.clone()) } None => match business_country.zip(business_label) { Some((business_country, business_label)) => { let profile_name = format!("{business_country}_{business_label}"); let business_profile = db .find_business_profile_by_profile_name_merchant_id( key_manager_state, merchant_key_store, &profile_name, merchant_account.get_id(), ) .await .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { id: profile_name, })?; Ok(business_profile.get_id().to_owned()) } _ => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "profile_id or business_country, business_label" })), }, } } pub fn get_poll_id(merchant_id: &common_utils::id_type::MerchantId, unique_id: String) -> String { merchant_id.get_poll_id(&unique_id) } pub fn get_external_authentication_request_poll_id( payment_id: &common_utils::id_type::PaymentId, ) -> String { payment_id.get_external_authentication_request_poll_id() } #[cfg(feature = "v1")] pub fn get_html_redirect_response_for_external_authentication( return_url_with_query_params: String, payment_response: &api_models::payments::PaymentsResponse, payment_id: common_utils::id_type::PaymentId, poll_config: &PollConfig, ) -> RouterResult { // if intent_status is requires_customer_action then set poll_id, fetch poll config and do a poll_status post message, else do open_url post message to redirect to return_url let html = match payment_response.status { IntentStatus::RequiresCustomerAction => { // Request poll id sent to client for retrieve_poll_status api let req_poll_id = get_external_authentication_request_poll_id(&payment_id); let poll_frequency = poll_config.frequency; let poll_delay_in_secs = poll_config.delay_in_secs; html! { head { title { "Redirect Form" } (PreEscaped(format!(r#" "#))) } } .into_string() }, _ => { html! { head { title { "Redirect Form" } (PreEscaped(format!(r#" "#))) } } .into_string() }, }; Ok(html) } #[inline] pub fn get_flow_name() -> RouterResult { Ok(std::any::type_name::() .to_string() .rsplit("::") .next() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Flow stringify failed")? .to_string()) } pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, ) -> RouterResult> { Some(request_incremental_authorization .map(|request_incremental_authorization| { if request_incremental_authorization { if matches!( capture_method, Some(common_enums::CaptureMethod::Automatic) | Some(common_enums::CaptureMethod::SequentialAutomatic) ) { Err(errors::ApiErrorResponse::NotSupported { message: "incremental authorization is not supported when capture_method is automatic".to_owned() })? } Ok(RequestIncrementalAuthorization::True) } else { Ok(RequestIncrementalAuthorization::False) } }) .unwrap_or(Ok(RequestIncrementalAuthorization::default()))).transpose() } pub fn get_incremental_authorization_allowed_value( incremental_authorization_allowed: Option, request_incremental_authorization: Option, ) -> Option { if request_incremental_authorization == Some(RequestIncrementalAuthorization::False) { Some(false) } else { incremental_authorization_allowed } } pub(crate) trait GetProfileId { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId>; } impl GetProfileId for MerchantConnectorAccount { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(&self.profile_id) } } impl GetProfileId for storage::PaymentIntent { #[cfg(feature = "v1")] fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } // TODO: handle this in a better way for v2 #[cfg(feature = "v2")] fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(&self.profile_id) } } impl GetProfileId for (storage::PaymentIntent, A) { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.0.get_profile_id() } } impl GetProfileId for diesel_models::Dispute { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } } impl GetProfileId for diesel_models::Refund { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } } #[cfg(feature = "v1")] impl GetProfileId for api_models::routing::RoutingConfigRequest { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } } #[cfg(feature = "v2")] impl GetProfileId for api_models::routing::RoutingConfigRequest { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(&self.profile_id) } } impl GetProfileId for api_models::routing::RoutingRetrieveLinkQuery { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.profile_id.as_ref() } } impl GetProfileId for diesel_models::routing_algorithm::RoutingProfileMetadata { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(&self.profile_id) } } impl GetProfileId for domain::Profile { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(self.get_id()) } } #[cfg(feature = "payouts")] impl GetProfileId for storage::Payouts { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { Some(&self.profile_id) } } #[cfg(feature = "payouts")] impl GetProfileId for (storage::Payouts, T, F, R) { fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { self.0.get_profile_id() } } /// Filter Objects based on profile ids pub(super) fn filter_objects_based_on_profile_id_list( profile_id_list_auth_layer: Option>, object_list: Vec, ) -> Vec { if let Some(profile_id_list) = profile_id_list_auth_layer { let profile_ids_to_filter: HashSet<_> = profile_id_list.iter().collect(); object_list .into_iter() .filter_map(|item| { if item .get_profile_id() .is_some_and(|profile_id| profile_ids_to_filter.contains(profile_id)) { Some(item) } else { None } }) .collect() } else { object_list } } pub(crate) fn validate_profile_id_from_auth_layer( profile_id_auth_layer: Option, object: &T, ) -> RouterResult<()> { match (profile_id_auth_layer, object.get_profile_id()) { (Some(auth_profile_id), Some(object_profile_id)) => { auth_profile_id.eq(object_profile_id).then_some(()).ok_or( errors::ApiErrorResponse::PreconditionFailed { message: "Profile id authentication failed. Please use the correct JWT token" .to_string(), } .into(), ) } (Some(_auth_profile_id), None) => RouterResult::Err( errors::ApiErrorResponse::PreconditionFailed { message: "Couldn't find profile_id in record for authentication".to_string(), } .into(), ) .attach_printable(format!("Couldn't find profile_id in entity {:?}", object)), (None, None) | (None, Some(_)) => Ok(()), } } pub(crate) trait ValidatePlatformMerchant { fn get_platform_merchant_id(&self) -> Option<&common_utils::id_type::MerchantId>; fn validate_platform_merchant( &self, auth_platform_merchant_id: Option<&common_utils::id_type::MerchantId>, ) -> CustomResult<(), errors::ApiErrorResponse> { let data_platform_merchant_id = self.get_platform_merchant_id(); match (data_platform_merchant_id, auth_platform_merchant_id) { (Some(data_platform_merchant_id), Some(auth_platform_merchant_id)) => { common_utils::fp_utils::when( data_platform_merchant_id != auth_platform_merchant_id, || { Err(report!(errors::ApiErrorResponse::PaymentNotFound)).attach_printable(format!( "Data platform merchant id: {data_platform_merchant_id:?} does not match with auth platform merchant id: {auth_platform_merchant_id:?}")) }, ) } (Some(_), None) | (None, Some(_)) => { Err(report!(errors::ApiErrorResponse::InvalidPlatformOperation)) .attach_printable("Platform merchant id is missing in either data or auth") } (None, None) => Ok(()), } } } impl ValidatePlatformMerchant for storage::PaymentIntent { fn get_platform_merchant_id(&self) -> Option<&common_utils::id_type::MerchantId> { self.platform_merchant_id.as_ref() } }