From 1a984f91c37d67f4e52e941e95024988bfc9787e Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 7 Mar 2024 21:47:01 +0530 Subject: [PATCH 1/3] fix(locker): handle card duplication in payouts flow --- .../src/core/payment_methods/transformers.rs | 9 + crates/router/src/core/payouts.rs | 11 +- crates/router/src/core/payouts/helpers.rs | 351 ++++++++++++------ 3 files changed, 261 insertions(+), 110 deletions(-) diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 35491d747ef..bc111cfe6aa 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -27,6 +27,15 @@ pub enum StoreLockerReq<'a> { LockerGeneric(StoreGenericReq<'a>), } +impl StoreLockerReq<'_> { + pub fn update_requestor_card_reference(&mut self, card_reference: Option) { + 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, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 0e5d3652be8..47233829c2d 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -1245,7 +1245,13 @@ pub async fn fulfill_payout( match router_data_resp.response { Ok(payout_response_data) => { - if payout_data.payouts.recurring && payout_data.payouts.payout_method_id.is_none() { + let status = payout_response_data + .status + .unwrap_or(payout_attempt.status.to_owned()); + 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_attempt, @@ -1258,9 +1264,6 @@ pub async fn fulfill_payout( ) .await?; } - let status = payout_response_data - .status - .unwrap_or(payout_attempt.status.to_owned()); let updated_payouts = storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_attempt.connector_payout_id.to_owned(), status, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9fd7458155a..6c3441b88d3 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -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, - transformers::{StoreCardReq, StoreGenericReq, StoreLockerReq}, + cards, + transformers::{ + self, DataDuplicationCheck, StoreCardReq, StoreGenericReq, StoreLockerReq, + }, vault, }, payments::{ @@ -186,9 +188,9 @@ pub async fn save_payout_data_to_locker( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { - 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(), @@ -201,7 +203,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(), @@ -246,31 +248,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, @@ -281,8 +284,237 @@ 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 card's metadata whenever insertion or metadata update is required + let card_details_encrypted = 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 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, + }, + ) + }); + cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await + } else { + None + }; + + // Insert new entry in payment_methods table + if should_insert_in_pm_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.clone(), + 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.clone()), + &merchant_account.merchant_id, + None, + None, + card_details_encrypted.clone(), + key_store, + 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) + .into_report() + .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: Some(stored_resp.card_reference.to_owned()), last_modified_at: Some(common_utils::date_time::now()), @@ -296,99 +528,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, - ) - .await?; - Ok(()) } @@ -624,7 +763,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 From ec142774e739dab0e8602530773cf37dfefd9e6e Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 22 Mar 2024 13:14:17 +0530 Subject: [PATCH 2/3] refactor(payouts): persist card's issuer and network in payment methods table for cards --- crates/router/src/core/payouts/helpers.rs | 191 ++++++++++++---------- 1 file changed, 108 insertions(+), 83 deletions(-) diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index d71c20ac1e1..a03e9915b2c 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -355,96 +355,121 @@ pub async fn save_payout_data_to_locker( None => (true, None), }; - // Form card's metadata whenever insertion or metadata update is required - let card_details_encrypted = 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()); + // 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| { - 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 - } else { - 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 = 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.clone(), - 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, + &new_payment_method, &payout_attempt.customer_id, &payment_method_id, Some(stored_resp.card_reference.clone()), From 9ed87a0703c4bcf579e57893110e2856cf6b7ca7 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:21:17 +0000 Subject: [PATCH 3/3] chore: run formatter --- crates/router/src/core/payouts/helpers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 19f551000d7..5681999ae9f 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -527,10 +527,9 @@ pub async fn save_payout_data_to_locker( .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", - )? + 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