Files
Chethan Rao 3333bbfe7f refactor(payment_methods): revamp payment methods update endpoint (#4305)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
2024-04-16 16:38:47 +00:00

919 lines
36 KiB
Rust

use api_models::{enums, payment_methods::Card, payouts};
use common_utils::{
errors::CustomResult,
ext_traits::{AsyncExt, StringExt},
};
use diesel_models::encryption::Encryption;
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};
use router_env::logger;
use super::PayoutData;
use crate::{
core::{
errors::{self, RouterResult, StorageErrorExt},
payment_methods::{
cards,
transformers::{DataDuplicationCheck, StoreCardReq, StoreGenericReq, StoreLockerReq},
vault,
},
payments::{
customers::get_connector_customer_details_if_present, route_connector_v1, routing,
CustomerDetails,
},
routing::TransactionData,
utils as core_utils,
},
db::StorageInterface,
routes::{metrics, AppState},
services,
types::{
api::{self, enums as api_enums},
domain::{
self,
types::{self as domain_types, AsyncLift},
},
storage,
transformers::ForeignFrom,
},
utils::{self, OptionExt},
};
#[allow(clippy::too_many_arguments)]
pub async fn make_payout_method_data<'a>(
state: &'a AppState,
payout_method_data: Option<&api::PayoutMethodData>,
payout_token: Option<&str>,
customer_id: &str,
merchant_id: &str,
payout_type: Option<&api_enums::PayoutType>,
merchant_key_store: &domain::MerchantKeyStore,
payout_data: Option<&mut PayoutData>,
storage_scheme: storage::enums::MerchantStorageScheme,
) -> RouterResult<Option<api::PayoutMethodData>> {
let db = &*state.store;
let certain_payout_type = payout_type.get_required_value("payout_type")?.to_owned();
let hyperswitch_token = if let Some(payout_token) = payout_token {
if payout_token.starts_with("temporary_token_") {
Some(payout_token.to_string())
} else {
let key = format!(
"pm_token_{}_{}_hyperswitch",
payout_token,
api_enums::PaymentMethod::foreign_from(certain_payout_type)
);
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let hyperswitch_token = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?
.ok_or(error_stack::Report::new(
errors::ApiErrorResponse::UnprocessableEntity {
message: "Token is invalid or expired".to_owned(),
},
))?;
let payment_token_data = hyperswitch_token
.clone()
.parse_struct("PaymentTokenData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to deserialize hyperswitch token data")?;
let payment_token = match payment_token_data {
storage::PaymentTokenData::PermanentCard(storage::CardTokenData {
locker_id,
token,
..
}) => locker_id.or(Some(token)),
storage::PaymentTokenData::TemporaryGeneric(storage::GenericTokenData {
token,
}) => Some(token),
_ => None,
};
payment_token.or(Some(payout_token.to_string()))
}
} else {
None
};
match (
payout_method_data.to_owned(),
hyperswitch_token,
payout_data,
) {
// Get operation
(None, Some(payout_token), _) => {
if payout_token.starts_with("temporary_token_")
|| certain_payout_type == api_enums::PayoutType::Bank
{
let (pm, supplementary_data) = vault::Vault::get_payout_method_data_from_temporary_locker(
state,
&payout_token,
merchant_key_store,
)
.await
.attach_printable(
"Payout method for given token not found or there was a problem fetching it",
)?;
utils::when(
supplementary_data
.customer_id
.ne(&Some(customer_id.to_owned())),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payout method and customer passed in payout are not same".into() })
},
)?;
Ok(pm)
} else {
let resp = cards::get_card_from_locker(
state,
customer_id,
merchant_id,
payout_token.as_ref(),
)
.await
.attach_printable("Payout method [card] could not be fetched from HS locker")?;
Ok(Some({
api::PayoutMethodData::Card(api::CardPayout {
card_number: resp.card_number,
expiry_month: resp.card_exp_month,
expiry_year: resp.card_exp_year,
card_holder_name: resp.name_on_card,
})
}))
}
}
// Create / Update operation
(Some(payout_method), payout_token, Some(payout_data)) => {
let lookup_key = vault::Vault::store_payout_method_data_in_locker(
state,
payout_token.to_owned(),
payout_method,
Some(customer_id.to_owned()),
merchant_key_store,
)
.await?;
// Update payout_token in payout_attempt table
if payout_token.is_none() {
let updated_payout_attempt = storage::PayoutAttemptUpdate::PayoutTokenUpdate {
payout_token: lookup_key,
};
payout_data.payout_attempt = db
.update_payout_attempt(
&payout_data.payout_attempt,
updated_payout_attempt,
&payout_data.payouts,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error updating token in payout attempt")?;
}
Ok(Some(payout_method.clone()))
}
// Ignore if nothing is passed
_ => Ok(None),
}
}
pub async fn save_payout_data_to_locker(
state: &AppState,
payout_data: &mut PayoutData,
payout_method_data: &api::PayoutMethodData,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<()> {
let payout_attempt = &payout_data.payout_attempt;
let (mut locker_req, card_details, bank_details, wallet_details, payment_method_type) =
match payout_method_data {
payouts::PayoutMethodData::Card(card) => {
let card_detail = api::CardDetail {
card_number: card.card_number.to_owned(),
card_holder_name: card.card_holder_name.to_owned(),
card_exp_month: card.expiry_month.to_owned(),
card_exp_year: card.expiry_year.to_owned(),
nick_name: None,
card_issuing_country: None,
card_network: None,
card_issuer: None,
card_type: None,
};
let payload = StoreLockerReq::LockerCard(StoreCardReq {
merchant_id: merchant_account.merchant_id.as_ref(),
merchant_customer_id: payout_attempt.customer_id.to_owned(),
card: Card {
card_number: card.card_number.to_owned(),
name_on_card: card.card_holder_name.to_owned(),
card_exp_month: card.expiry_month.to_owned(),
card_exp_year: card.expiry_year.to_owned(),
card_brand: None,
card_isin: None,
nick_name: None,
},
requestor_card_reference: None,
});
(
payload,
Some(card_detail),
None,
None,
api_enums::PaymentMethodType::Debit,
)
}
_ => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: merchant_account.merchant_id.as_ref(),
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
match payout_method_data {
payouts::PayoutMethodData::Bank(bank) => (
payload,
None,
Some(bank.to_owned()),
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
),
payouts::PayoutMethodData::Wallet(wallet) => (
payload,
None,
None,
Some(wallet.to_owned()),
api_enums::PaymentMethodType::foreign_from(wallet.to_owned()),
),
payouts::PayoutMethodData::Card(_) => {
Err(errors::ApiErrorResponse::InternalServerError)?
}
}
}
};
// Store payout method in locker
let stored_resp = cards::call_to_locker_hs(
state,
&locker_req,
&payout_attempt.customer_id,
api_enums::LockerChoice::HyperswitchCardVault,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let db = &*state.store;
// Handle duplicates
let (should_insert_in_pm_table, metadata_update) = match stored_resp.duplication_check {
// Check if equivalent entry exists in payment_methods
Some(duplication_check) => {
let locker_ref = stored_resp.card_reference.clone();
// Use locker ref as payment_method_id
let existing_pm_by_pmid = db
.find_payment_method(&locker_ref, merchant_account.storage_scheme)
.await;
match existing_pm_by_pmid {
// If found, update locker's metadata [DELETE + INSERT OP], don't insert in payment_method's table
Ok(pm) => (
false,
if duplication_check == DataDuplicationCheck::MetaDataChanged {
Some(pm.clone())
} else {
None
},
),
// If not found, use locker ref as locker_id
Err(err) => {
if err.current_context().is_db_not_found() {
match db
.find_payment_method_by_locker_id(
&locker_ref,
merchant_account.storage_scheme,
)
.await
{
// If found, update locker's metadata [DELETE + INSERT OP], don't insert in payment_methods table
Ok(pm) => (
false,
if duplication_check == DataDuplicationCheck::MetaDataChanged {
Some(pm.clone())
} else {
None
},
),
Err(err) => {
// If not found, update locker's metadata [DELETE + INSERT OP], and insert in payment_methods table
if err.current_context().is_db_not_found() {
(true, None)
// Misc. DB errors
} else {
Err(err)
.change_context(
errors::ApiErrorResponse::InternalServerError,
)
.attach_printable(
"DB failures while finding payment method by locker ID",
)?
}
}
}
// Misc. DB errors
} else {
Err(err)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("DB failures while finding payment method by pm ID")?
}
}
}
}
// Not duplicate, should be inserted in payment_methods table
None => (true, None),
};
// Form payment method entry and card's metadata whenever insertion or metadata update is required
let (card_details_encrypted, new_payment_method) =
if let (api::PayoutMethodData::Card(_), true, _)
| (api::PayoutMethodData::Card(_), _, Some(_)) = (
payout_method_data,
should_insert_in_pm_table,
metadata_update.as_ref(),
) {
// Fetch card info from db
let card_isin = card_details
.as_ref()
.map(|c| c.card_number.clone().get_card_isin());
let mut payment_method = api::PaymentMethodCreate {
payment_method: api_enums::PaymentMethod::foreign_from(
payout_method_data.to_owned(),
),
payment_method_type: Some(payment_method_type),
payment_method_issuer: None,
payment_method_issuer_code: None,
bank_transfer: None,
card: card_details.clone(),
wallet: None,
metadata: None,
customer_id: Some(payout_attempt.customer_id.to_owned()),
card_network: None,
};
let pm_data = card_isin
.clone()
.async_and_then(|card_isin| async move {
db.get_card_info(&card_isin)
.await
.map_err(|error| services::logger::warn!(card_info_error=?error))
.ok()
})
.await
.flatten()
.map(|card_info| {
payment_method.payment_method_issuer = card_info.card_issuer.clone();
payment_method.card_network =
card_info.card_network.clone().map(|cn| cn.to_string());
api::payment_methods::PaymentMethodsData::Card(
api::payment_methods::CardDetailsPaymentMethod {
last4_digits: card_details
.as_ref()
.map(|c| c.card_number.clone().get_last4()),
issuer_country: card_info.card_issuing_country,
expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()),
expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()),
nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()),
card_holder_name: card_details
.as_ref()
.and_then(|c| c.card_holder_name.clone()),
card_isin: card_isin.clone(),
card_issuer: card_info.card_issuer,
card_network: card_info.card_network,
card_type: card_info.card_type,
saved_to_locker: true,
},
)
})
.unwrap_or_else(|| {
api::payment_methods::PaymentMethodsData::Card(
api::payment_methods::CardDetailsPaymentMethod {
last4_digits: card_details
.as_ref()
.map(|c| c.card_number.clone().get_last4()),
issuer_country: None,
expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()),
expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()),
nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()),
card_holder_name: card_details
.as_ref()
.and_then(|c| c.card_holder_name.clone()),
card_isin: card_isin.clone(),
card_issuer: None,
card_network: None,
card_type: None,
saved_to_locker: true,
},
)
});
(
cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await,
payment_method,
)
} else {
(
None,
api::PaymentMethodCreate {
payment_method: api_enums::PaymentMethod::foreign_from(
payout_method_data.to_owned(),
),
payment_method_type: Some(payment_method_type),
payment_method_issuer: None,
payment_method_issuer_code: None,
bank_transfer: bank_details,
card: None,
wallet: wallet_details,
metadata: None,
customer_id: Some(payout_attempt.customer_id.to_owned()),
card_network: None,
},
)
};
// Insert new entry in payment_methods table
if should_insert_in_pm_table {
let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm");
cards::create_payment_method(
db,
&new_payment_method,
&payout_attempt.customer_id,
&payment_method_id,
Some(stored_resp.card_reference.clone()),
&merchant_account.merchant_id,
None,
None,
card_details_encrypted.clone(),
key_store,
None,
None,
merchant_account.storage_scheme,
)
.await?;
}
/* 1. Delete from locker
* 2. Create new entry in locker
* 3. Handle creation response from locker
* 4. Update card's metadata in payment_methods table
*/
if let Some(existing_pm) = metadata_update {
let card_reference = &existing_pm
.locker_id
.clone()
.unwrap_or(existing_pm.payment_method_id.clone());
// Delete from locker
cards::delete_card_from_hs_locker(
state,
&payout_attempt.customer_id,
&merchant_account.merchant_id,
card_reference,
)
.await
.attach_printable(
"Failed to delete PMD from locker as a part of metadata update operation",
)?;
locker_req.update_requestor_card_reference(Some(card_reference.to_string()));
// Store in locker
let stored_resp = cards::call_to_locker_hs(
state,
&locker_req,
&payout_attempt.customer_id,
api_enums::LockerChoice::HyperswitchCardVault,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError);
// Check if locker operation was successful or not, if not, delete the entry from payment_methods table
if let Err(err) = stored_resp {
logger::error!(vault_err=?err);
db.delete_payment_method_by_merchant_id_payment_method_id(
&merchant_account.merchant_id,
&existing_pm.payment_method_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
Err(errors::ApiErrorResponse::InternalServerError).attach_printable(
"Failed to insert PMD from locker as a part of metadata update operation",
)?
};
// Update card's metadata in payment_methods table
let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
payment_method_data: card_details_encrypted,
};
db.update_payment_method(existing_pm, pm_update, merchant_account.storage_scheme)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to add payment method in db")?;
};
// Store card_reference in payouts table
let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate {
payout_method_id: stored_resp.card_reference.to_owned(),
};
payout_data.payouts = db
.update_payout(
&payout_data.payouts,
updated_payout,
payout_attempt,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error updating payouts in saved payout method")?;
Ok(())
}
pub async fn get_or_create_customer_details(
state: &AppState,
customer_details: &CustomerDetails,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Option<domain::Customer>> {
let db: &dyn StorageInterface = &*state.store;
// Create customer_id if not passed in request
let customer_id =
core_utils::get_or_generate_id("customer_id", &customer_details.customer_id, "cust")?;
let merchant_id = &merchant_account.merchant_id;
let key = key_store.key.get_inner().peek();
match db
.find_customer_optional_by_customer_id_merchant_id(
&customer_id,
merchant_id,
key_store,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?
{
Some(customer) => Ok(Some(customer)),
None => {
let customer = domain::Customer {
customer_id,
merchant_id: merchant_id.to_string(),
name: domain_types::encrypt_optional(customer_details.name.to_owned(), key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?,
email: domain_types::encrypt_optional(
customer_details.email.to_owned().map(|e| e.expose()),
key,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?,
phone: domain_types::encrypt_optional(customer_details.phone.to_owned(), key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?,
description: None,
phone_country_code: customer_details.phone_country_code.to_owned(),
metadata: None,
connector_customer: None,
id: None,
created_at: common_utils::date_time::now(),
modified_at: common_utils::date_time::now(),
address_id: None,
default_payment_method_id: None,
};
Ok(Some(
db.insert_customer(customer, key_store, merchant_account.storage_scheme)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?,
))
}
}
}
pub async fn decide_payout_connector(
state: &AppState,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
request_straight_through: Option<api::routing::StraightThroughAlgorithm>,
routing_data: &mut storage::RoutingData,
payout_data: &mut PayoutData,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
) -> RouterResult<api::ConnectorCallType> {
// 1. For existing attempts, use stored connector
let payout_attempt = &payout_data.payout_attempt;
if let Some(connector_name) = payout_attempt.connector.clone() {
// Connector was already decided previously, use the same connector
let connector_data = api::ConnectorData::get_payout_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
payout_attempt.merchant_connector_id.clone(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in 'routed_through'")?;
routing_data.routed_through = Some(connector_name.clone());
return Ok(api::ConnectorCallType::PreDetermined(connector_data));
}
// 2. Check routing algorithm passed in the request
if let Some(routing_algorithm) = request_straight_through {
let (mut connectors, check_eligibility) =
routing::perform_straight_through_routing(&routing_algorithm, None)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
connectors = routing::perform_eligibility_analysis_with_fallback(
state,
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
&TransactionData::<()>::Payout(payout_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
Some(payout_attempt.profile_id.clone()),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("Empty connector list returned")?
.clone();
let connector_data = connectors
.into_iter()
.map(|conn| {
api::ConnectorData::get_payout_connector_by_name(
&state.conf.connectors,
&conn.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
payout_attempt.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
})
.collect::<CustomResult<Vec<_>, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label.clone();
}
routing_data.routing_info.algorithm = Some(routing_algorithm);
return Ok(api::ConnectorCallType::Retryable(connector_data));
}
// 3. Check algorithm passed in routing data
if let Some(ref routing_algorithm) = routing_data.algorithm {
let (mut connectors, check_eligibility) =
routing::perform_straight_through_routing(routing_algorithm, None)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
connectors = routing::perform_eligibility_analysis_with_fallback(
state,
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
&TransactionData::<()>::Payout(payout_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
Some(payout_attempt.profile_id.clone()),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("Empty connector list returned")?
.clone();
connectors.remove(0);
let connector_data = connectors
.into_iter()
.map(|conn| {
api::ConnectorData::get_payout_connector_by_name(
&state.conf.connectors,
&conn.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
payout_attempt.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
})
.collect::<CustomResult<Vec<_>, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label.clone();
}
return Ok(api::ConnectorCallType::Retryable(connector_data));
}
// 4. Route connector
route_connector_v1(
state,
merchant_account,
&payout_data.business_profile,
key_store,
TransactionData::<()>::Payout(payout_data),
routing_data,
eligible_connectors,
None,
)
.await
}
pub async fn get_default_payout_connector(
_state: &AppState,
request_connector: Option<serde_json::Value>,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
Ok(request_connector.map_or(
api::ConnectorChoice::Decide,
api::ConnectorChoice::StraightThrough,
))
}
pub fn should_call_payout_connector_create_customer<'a>(
state: &AppState,
connector: &api::ConnectorData,
customer: &'a Option<domain::Customer>,
connector_label: &str,
) -> (bool, Option<&'a str>) {
// Check if create customer is required for the connector
match enums::PayoutConnectors::try_from(connector.connector_name) {
Ok(connector) => {
let connector_needs_customer = state
.conf
.connector_customer
.payout_connector_list
.contains(&connector);
if connector_needs_customer {
let connector_customer_details = customer.as_ref().and_then(|customer| {
get_connector_customer_details_if_present(customer, connector_label)
});
let should_call_connector = connector_customer_details.is_none();
(should_call_connector, connector_customer_details)
} else {
(false, None)
}
}
_ => (false, None),
}
}
pub async fn get_gsm_record(
state: &AppState,
error_code: Option<String>,
error_message: Option<String>,
connector_name: Option<String>,
flow: String,
) -> Option<storage::gsm::GatewayStatusMap> {
let get_gsm = || async {
state.store.find_gsm_rule(
connector_name.clone().unwrap_or_default(),
flow.clone(),
"sub_flow".to_string(),
error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response
error_message.clone().unwrap_or_default(),
)
.await
.map_err(|err| {
if err.current_context().is_db_not_found() {
logger::warn!(
"GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}",
connector_name.unwrap_or_default(),
flow,
error_code,
error_message
);
metrics::AUTO_PAYOUT_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]);
} else {
metrics::AUTO_PAYOUT_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]);
};
err.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to fetch decision from gsm")
})
};
get_gsm()
.await
.map_err(|err| {
// warn log should suffice here because we are not propagating this error
logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision");
err
})
.ok()
}
pub fn is_payout_initiated(status: api_enums::PayoutStatus) -> bool {
matches!(
status,
api_enums::PayoutStatus::Pending | api_enums::PayoutStatus::RequiresFulfillment
)
}
pub fn is_payout_terminal_state(status: api_enums::PayoutStatus) -> bool {
!matches!(
status,
api_enums::PayoutStatus::Pending
| api_enums::PayoutStatus::RequiresCreation
| api_enums::PayoutStatus::RequiresFulfillment
| api_enums::PayoutStatus::RequiresPayoutMethodData
)
}
pub fn is_payout_err_state(status: api_enums::PayoutStatus) -> bool {
matches!(
status,
api_enums::PayoutStatus::Cancelled
| api_enums::PayoutStatus::Failed
| api_enums::PayoutStatus::Ineligible
)
}
pub fn is_eligible_for_local_payout_cancellation(status: api_enums::PayoutStatus) -> bool {
matches!(
status,
api_enums::PayoutStatus::RequiresCreation
| api_enums::PayoutStatus::RequiresPayoutMethodData,
)
}
#[cfg(feature = "olap")]
pub(super) async fn filter_by_constraints(
db: &dyn StorageInterface,
constraints: &api::PayoutListConstraints,
merchant_id: &str,
storage_scheme: storage::enums::MerchantStorageScheme,
) -> CustomResult<Vec<storage::Payouts>, errors::DataStorageError> {
let result = db
.filter_payouts_by_constraints(merchant_id, &constraints.clone().into(), storage_scheme)
.await?;
Ok(result)
}