Files
Sai Harsha Vardhan 56791c2743 refactor(router): add api_version and make profile_id mandatory in mca v2 (#5602)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
2024-08-14 15:19:26 +00:00

1429 lines
53 KiB
Rust

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::MinorUnit};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{
merchant_connector_account::MerchantConnectorAccount, payment_address::PaymentAddress,
router_data::ErrorResponse,
};
#[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::PayoutData;
#[cfg(feature = "payouts")]
use crate::core::payments;
use crate::{
configs::Settings,
consts,
core::errors::{self, RouterResult, StorageErrorExt},
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_PAYMENT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_payment_id_in_dispute_flow";
const IRRELEVANT_ATTEMPT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_attempt_id_in_dispute_flow";
#[cfg(feature = "payouts")]
#[instrument(skip_all)]
pub async fn get_mca_for_payout<'a>(
state: &'a SessionState,
connector_data: &api::ConnectorData,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
payout_data: &PayoutData,
) -> RouterResult<helpers::MerchantConnectorAccountType> {
match payout_data.merchant_connector_account.to_owned() {
Some(mca) => Ok(mca),
None => {
let merchant_connector_account = helpers::get_merchant_connector_account(
state,
merchant_account.get_id(),
None,
key_store,
&payout_data.profile_id,
&connector_data.connector_name.to_string(),
connector_data.merchant_connector_id.as_ref(),
)
.await?;
Ok(merchant_connector_account)
}
}
}
#[cfg(feature = "payouts")]
#[instrument(skip_all)]
pub async fn construct_payout_router_data<'a, F>(
state: &'a SessionState,
connector_data: &api::ConnectorData,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
payout_data: &mut PayoutData,
) -> RouterResult<types::PayoutsRouterData<F>> {
let merchant_connector_account = get_mca_for_payout(
state,
connector_data,
merchant_account,
key_store,
payout_data,
)
.await?;
let connector_name = connector_data.connector_name;
payout_data.merchant_connector_account = Some(merchant_connector_account.clone());
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, None, None);
let test_mode: Option<bool> = 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, 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::<String>(id).ok());
let vendor_details: Option<PayoutVendorAccountDetails> =
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 router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.get_id().to_owned(),
customer_id: customer_details.to_owned().map(|c| c.get_customer_id()),
connector_customer: connector_customer_id,
connector: connector_name.to_string(),
payment_id: "".to_string(),
attempt_id: "".to_string(),
status: enums::AttemptStatus::Failure,
payment_method: enums::PaymentMethod::default(),
connector_auth_type,
description: None,
return_url: payouts.return_url.to_owned(),
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.get_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,
}),
},
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(()),
};
Ok(router_data)
}
#[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<String>,
charges: Option<types::ChargeRefunds>,
) -> RouterResult<types::RefundsRouterData<F>> {
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,
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 webhook_url = Some(helpers::create_webhook_url(
&state.base_url.clone(),
merchant_account.get_id(),
&connector_id.to_string(),
));
let test_mode: Option<bool> = 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<types::BrowserInformation> = payment_attempt
.browser_info
.clone()
.map(|b| b.parse_value("BrowserInformation"))
.transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info",
})?;
let router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.get_id().clone(),
customer_id: payment_intent.customer_id.to_owned(),
connector: connector_id.to_string(),
payment_id: payment_attempt.payment_id.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status,
payment_method: payment_method_type,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
// 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.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: refund.connector_refund_id.clone(),
browser_info,
charges,
integrity_object: None,
},
response: Ok(types::RefundsResponseData {
connector_refund_id: refund.connector_refund_id.clone().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(()),
};
Ok(router_data)
}
pub fn get_or_generate_id(
key: &str,
provided_id: &Option<String>,
prefix: &str,
) -> Result<String, errors::ApiErrorResponse> {
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<String, errors::ApiErrorResponse> {
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<String, errors::ApiErrorResponse> {
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<String, errors::ApiErrorResponse> {
match (Uuid::parse_str(&uuid), uuid.len() > consts::MAX_ID_LENGTH) {
(Ok(_), false) => Ok(uuid),
(_, _) => Err(invalid_id_format_error(key)),
}
}
#[cfg(test)]
mod tests {
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<String>,
}
impl Object {
pub fn new(profile_id: &str) -> Self {
Self {
profile_id: Some(profile_id.to_string()),
}
}
}
impl GetProfileId for Object {
fn get_profile_id(&self) -> Option<&String> {
self.profile_id.as_ref()
}
}
// 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!["p1".to_string(), "p2".to_string(), "p3".to_string()];
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(
&super::metrics::CONTEXT,
1,
&[],
);
Err(errors::WebhooksFlowError::DisputeWebhookValidationFailed)?
},
)
}
#[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<types::AcceptDisputeRouterData> {
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<bool> = 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(),
payment_id: payment_attempt.payment_id.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status: payment_attempt.status,
payment_method,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
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(()),
};
Ok(router_data)
}
#[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<types::SubmitEvidenceRouterData> {
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<bool> = 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.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status: payment_attempt.status,
payment_method,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
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(()),
};
Ok(router_data)
}
#[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<types::UploadFileRouterData> {
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<bool> = 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.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status: payment_attempt.status,
payment_method,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
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(()),
};
Ok(router_data)
}
#[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<types::DefendDisputeRouterData> {
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<bool> = 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.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status: payment_attempt.status,
payment_method,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
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(()),
};
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<types::RetrieveFileRouterData> {
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<bool> = 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(),
customer_id: None,
connector_customer: None,
payment_id: IRRELEVANT_PAYMENT_ID_IN_DISPUTE_FLOW.to_string(),
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,
return_url: 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(()),
};
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)
}
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.clone()
} else {
payment_attempt.attempt_id.clone()
}
}
/// 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: &common_utils::types::keymanager::KeyManagerState,
merchant_key_store: &domain::MerchantKeyStore,
profile_id: Option<&String>,
merchant_id: &common_utils::id_type::MerchantId,
) -> RouterResult<Option<domain::BusinessProfile>> {
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::BusinessProfileNotFound {
id: profile_id.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.profile_id,
}
.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<api_models::enums::CountryAlpha2>,
business_label: Option<&String>,
business_sub_label: Option<&String>,
connector_name: &str,
) -> Option<String> {
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(all(
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
/// 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: &common_utils::types::keymanager::KeyManagerState,
merchant_key_store: &domain::MerchantKeyStore,
business_country: Option<api_models::enums::CountryAlpha2>,
business_label: Option<&String>,
merchant_account: &domain::MerchantAccount,
request_profile_id: Option<&String>,
db: &dyn StorageInterface,
should_validate: bool,
) -> RouterResult<String> {
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::BusinessProfileNotFound {
id: profile_name,
})?;
Ok(business_profile.profile_id)
}
_ => 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: &String) -> String {
format!("external_authentication_{}", payment_id)
}
pub fn get_html_redirect_response_for_external_authentication(
return_url_with_query_params: String,
payment_response: &api_models::payments::PaymentsResponse,
payment_id: String,
poll_config: &PollConfig,
) -> RouterResult<String> {
// 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#"
<script>
let return_url = "{return_url_with_query_params}";
let poll_status_data = {{
'poll_id': '{req_poll_id}',
'frequency': '{poll_frequency}',
'delay_in_secs': '{poll_delay_in_secs}',
'return_url_with_query_params': return_url
}};
try {{
// if inside iframe, send post message to parent for redirection
if (window.self !== window.parent) {{
window.top.postMessage({{poll_status: poll_status_data}}, '*')
// if parent, redirect self to return_url
}} else {{
window.location.href = return_url
}}
}}
catch(err) {{
// if error occurs, send post message to parent and wait for 10 secs to redirect. if doesn't redirect, redirect self to return_url
window.top.postMessage({{poll_status: poll_status_data}}, '*')
setTimeout(function() {{
window.location.href = return_url
}}, 10000);
console.log(err.message)
}}
</script>
"#)))
}
}
.into_string()
},
_ => {
html! {
head {
title { "Redirect Form" }
(PreEscaped(format!(r#"
<script>
let return_url = "{return_url_with_query_params}";
try {{
// if inside iframe, send post message to parent for redirection
if (window.self !== window.parent) {{
window.top.postMessage({{openurl_if_required: return_url}}, '*')
// if parent, redirect self to return_url
}} else {{
window.location.href = return_url
}}
}}
catch(err) {{
// if error occurs, send post message to parent and wait for 10 secs to redirect. if doesn't redirect, redirect self to return_url
window.top.postMessage({{openurl_if_required: return_url}}, '*')
setTimeout(function() {{
window.location.href = return_url
}}, 10000);
console.log(err.message)
}}
</script>
"#)))
}
}
.into_string()
},
};
Ok(html)
}
#[inline]
pub fn get_flow_name<F>() -> RouterResult<String> {
Ok(std::any::type_name::<F>()
.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<bool>,
capture_method: Option<common_enums::CaptureMethod>,
) -> RouterResult<Option<RequestIncrementalAuthorization>> {
Some(request_incremental_authorization
.map(|request_incremental_authorization| {
if request_incremental_authorization {
if capture_method == Some(common_enums::CaptureMethod::Automatic) {
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<bool>,
request_incremental_authorization: Option<RequestIncrementalAuthorization>,
) -> Option<bool> {
if request_incremental_authorization == Some(RequestIncrementalAuthorization::False) {
Some(false)
} else {
incremental_authorization_allowed
}
}
pub(super) trait GetProfileId {
fn get_profile_id(&self) -> Option<&String>;
}
impl GetProfileId for MerchantConnectorAccount {
fn get_profile_id(&self) -> Option<&String> {
Some(&self.profile_id)
}
}
impl GetProfileId for storage::PaymentIntent {
fn get_profile_id(&self) -> Option<&String> {
self.profile_id.as_ref()
}
}
impl<A> GetProfileId for (storage::PaymentIntent, A) {
fn get_profile_id(&self) -> Option<&String> {
self.0.get_profile_id()
}
}
impl GetProfileId for diesel_models::Dispute {
fn get_profile_id(&self) -> Option<&String> {
self.profile_id.as_ref()
}
}
impl GetProfileId for diesel_models::Refund {
fn get_profile_id(&self) -> Option<&String> {
self.profile_id.as_ref()
}
}
#[cfg(feature = "payouts")]
impl GetProfileId for storage::Payouts {
fn get_profile_id(&self) -> Option<&String> {
Some(&self.profile_id)
}
}
#[cfg(feature = "payouts")]
impl<T, F> GetProfileId for (storage::Payouts, T, F) {
fn get_profile_id(&self) -> Option<&String> {
self.0.get_profile_id()
}
}
/// Filter Objects based on profile ids
pub(super) fn filter_objects_based_on_profile_id_list<T: GetProfileId>(
profile_id_list_auth_layer: Option<Vec<String>>,
object_list: Vec<T>,
) -> Vec<T> {
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(super) fn validate_profile_id_from_auth_layer<T: GetProfileId + std::fmt::Debug>(
profile_id_auth_layer: Option<String>,
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(()),
}
}