diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index f639aa5921..357f077332 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 644848ac0e..0f435a2e26 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -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, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index e7dd60e241..9b54bb03d1 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::{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