fix(locker): handle card duplication in payouts flow (#4013)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-04-05 12:19:36 +05:30
committed by GitHub
parent ca0aad3cf9
commit 2fac436683
3 changed files with 281 additions and 107 deletions

View File

@ -27,6 +27,15 @@ pub enum StoreLockerReq<'a> {
LockerGeneric(StoreGenericReq<'a>),
}
impl StoreLockerReq<'_> {
pub fn update_requestor_card_reference(&mut self, card_reference: Option<String>) {
match self {
Self::LockerCard(c) => c.requestor_card_reference = card_reference,
Self::LockerGeneric(_) => (),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StoreCardReq<'a> {
pub merchant_id: &'a str,

View File

@ -1452,7 +1452,10 @@ pub async fn fulfill_payout(
.status
.unwrap_or(payout_attempt.status.to_owned());
payout_data.payouts.status = status;
if payout_data.payouts.recurring && payout_data.payouts.payout_method_id.is_none() {
if payout_data.payouts.recurring
&& payout_data.payouts.payout_method_id.is_none()
&& !helpers::is_payout_err_state(status)
{
helpers::save_payout_data_to_locker(
state,
payout_data,

View File

@ -1,4 +1,4 @@
use api_models::enums::PayoutConnectors;
use api_models::{enums, payouts};
use common_utils::{
errors::CustomResult,
ext_traits::{AsyncExt, StringExt},
@ -11,10 +11,12 @@ use router_env::logger;
use super::PayoutData;
use crate::{
core::{
errors::{self, RouterResult},
errors::{self, RouterResult, StorageErrorExt},
payment_methods::{
cards,
transformers::{self, StoreCardReq, StoreGenericReq, StoreLockerReq},
transformers::{
self, DataDuplicationCheck, StoreCardReq, StoreGenericReq, StoreLockerReq,
},
vault,
},
payments::{
@ -191,9 +193,9 @@ pub async fn save_payout_data_to_locker(
key_store: &domain::MerchantKeyStore,
) -> RouterResult<()> {
let payout_attempt = &payout_data.payout_attempt;
let (locker_req, card_details, bank_details, wallet_details, payment_method_type) =
let (mut locker_req, card_details, bank_details, wallet_details, payment_method_type) =
match payout_method_data {
api_models::payouts::PayoutMethodData::Card(card) => {
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(),
@ -206,7 +208,7 @@ pub async fn save_payout_data_to_locker(
card_type: None,
};
let payload = StoreLockerReq::LockerCard(StoreCardReq {
merchant_id: &merchant_account.merchant_id,
merchant_id: merchant_account.merchant_id.as_ref(),
merchant_customer_id: payout_attempt.customer_id.to_owned(),
card: transformers::Card {
card_number: card.card_number.to_owned(),
@ -250,31 +252,32 @@ pub async fn save_payout_data_to_locker(
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_id: merchant_account.merchant_id.as_ref(),
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
match payout_method_data {
api_models::payouts::PayoutMethodData::Bank(bank) => (
payouts::PayoutMethodData::Bank(bank) => (
payload,
None,
Some(bank.to_owned()),
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
),
api_models::payouts::PayoutMethodData::Wallet(wallet) => (
payouts::PayoutMethodData::Wallet(wallet) => (
payload,
None,
None,
Some(wallet.to_owned()),
api_enums::PaymentMethodType::foreign_from(wallet.to_owned()),
),
api_models::payouts::PayoutMethodData::Card(_) => {
payouts::PayoutMethodData::Card(_) => {
Err(errors::ApiErrorResponse::InternalServerError)?
}
}
}
};
// Store payout method in locker
let stored_resp = cards::call_to_locker_hs(
state,
@ -285,8 +288,261 @@ pub async fn save_payout_data_to_locker(
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
// Store card_reference in payouts table
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).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).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,
)
.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)
.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(),
};
@ -299,100 +555,6 @@ pub async fn save_payout_data_to_locker(
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error updating payouts in saved payout method")?;
// fetch card info from db
let card_isin = card_details
.as_ref()
.map(|c| c.card_number.clone().get_card_isin());
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| {
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,
},
)
});
let card_details_encrypted =
cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await;
// Insert in payment_method table
let 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: bank_details,
card: card_details,
wallet: wallet_details,
metadata: None,
customer_id: Some(payout_attempt.customer_id.to_owned()),
card_network: None,
};
let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm");
cards::create_payment_method(
db,
&payment_method,
&payout_attempt.customer_id,
&payment_method_id,
Some(stored_resp.card_reference),
&merchant_account.merchant_id,
None,
None,
card_details_encrypted,
key_store,
None,
None,
)
.await?;
Ok(())
}
@ -626,7 +788,7 @@ pub fn should_call_payout_connector_create_customer<'a>(
connector_label: &str,
) -> (bool, Option<&'a str>) {
// Check if create customer is required for the connector
match PayoutConnectors::try_from(connector.connector_name) {
match enums::PayoutConnectors::try_from(connector.connector_name) {
Ok(connector) => {
let connector_needs_customer = state
.conf