diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index e45b2ad759..29dd089961 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -13,6 +13,7 @@ pub mod files; pub mod mandates; pub mod payment_methods; pub mod payments; +#[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; pub mod webhooks; diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 711b28057d..8fbed6bf68 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -1,21 +1,14 @@ -#[cfg(feature = "payouts")] use cards::CardNumber; -#[cfg(feature = "payouts")] use common_utils::{ crypto, pii::{self, Email}, }; -#[cfg(feature = "payouts")] use masking::Secret; -#[cfg(feature = "payouts")] use serde::{Deserialize, Serialize}; -#[cfg(feature = "payouts")] use utoipa::ToSchema; -#[cfg(feature = "payouts")] use crate::{admin, enums as api_enums, payments}; -#[cfg(feature = "payouts")] #[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] pub enum PayoutRequest { PayoutActionRequest(PayoutActionRequest), @@ -23,7 +16,6 @@ pub enum PayoutRequest { PayoutRetrieveRequest(PayoutRetrieveRequest), } -#[cfg(feature = "payouts")] #[derive(Default, Debug, Deserialize, Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PayoutCreateRequest { @@ -156,7 +148,6 @@ pub struct PayoutCreateRequest { pub payout_token: Option, } -#[cfg(feature = "payouts")] #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum PayoutMethodData { @@ -164,14 +155,12 @@ pub enum PayoutMethodData { Bank(Bank), } -#[cfg(feature = "payouts")] impl Default for PayoutMethodData { fn default() -> Self { Self::Card(Card::default()) } } -#[cfg(feature = "payouts")] #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct Card { /// The card number @@ -191,7 +180,6 @@ pub struct Card { pub card_holder_name: Secret, } -#[cfg(feature = "payouts")] #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] #[serde(untagged)] pub enum Bank { @@ -200,7 +188,6 @@ pub enum Bank { Sepa(SepaBankTransfer), } -#[cfg(feature = "payouts")] #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct AchBankTransfer { /// Bank name @@ -224,7 +211,6 @@ pub struct AchBankTransfer { pub bank_routing_number: Secret, } -#[cfg(feature = "payouts")] #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct BacsBankTransfer { /// Bank name @@ -248,7 +234,6 @@ pub struct BacsBankTransfer { pub bank_sort_code: Secret, } -#[cfg(feature = "payouts")] #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] // The SEPA (Single Euro Payments Area) is a pan-European network that allows you to send and receive payments in euros between two cross-border bank accounts in the eurozone. pub struct SepaBankTransfer { @@ -273,7 +258,6 @@ pub struct SepaBankTransfer { pub bic: Option>, } -#[cfg(feature = "payouts")] #[derive(Debug, ToSchema, Clone, Serialize)] #[serde(deny_unknown_fields)] pub struct PayoutCreateResponse { @@ -394,13 +378,11 @@ pub struct PayoutCreateResponse { pub error_code: Option, } -#[cfg(feature = "payouts")] #[derive(Default, Debug, Clone, Deserialize, ToSchema)] pub struct PayoutRetrieveBody { pub force_sync: Option, } -#[cfg(feature = "payouts")] #[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)] pub struct PayoutRetrieveRequest { /// Unique identifier for the payout. This ensures idempotency for multiple payouts @@ -419,7 +401,6 @@ pub struct PayoutRetrieveRequest { pub force_sync: Option, } -#[cfg(feature = "payouts")] #[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)] pub struct PayoutActionRequest { /// Unique identifier for the payout. This ensures idempotency for multiple payouts diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 65d59bf156..1028c37c25 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -13,8 +13,6 @@ use api_models::{ }, payments::BankCodeResponse, }; -#[cfg(feature = "payouts")] -use common_utils::ext_traits::ByteSliceExt; use common_utils::{ consts, ext_traits::{AsyncExt, StringExt, ValueExt}, @@ -257,8 +255,20 @@ pub async fn add_card_hs( customer_id: String, merchant_account: &domain::MerchantAccount, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { - let store_card_payload = - call_to_card_hs(state, &card, None, &customer_id, merchant_account).await?; + let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: customer_id.to_owned(), + card: payment_methods::Card { + card_number: card.card_number.to_owned(), + name_on_card: card.card_holder_name.to_owned(), + card_exp_month: card.card_exp_month.to_owned(), + card_exp_year: card.card_exp_year.to_owned(), + card_brand: None, + card_isin: None, + nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(), + }, + }); + let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?; let payment_method_resp = payment_methods::mk_add_card_response_hs( card, @@ -272,6 +282,28 @@ pub async fn add_card_hs( )) } +#[instrument(skip_all)] +pub async fn decode_and_decrypt_locker_data( + key_store: &domain::MerchantKeyStore, + enc_card_data: String, +) -> errors::CustomResult, errors::VaultError> { + // Fetch key + let key = key_store.key.get_inner().peek(); + // Decode + let decoded_bytes = hex::decode(&enc_card_data) + .into_report() + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Failed to decode hex string into bytes")?; + // Decrypt + decrypt(Some(Encryption::new(decoded_bytes.into())), key) + .await + .change_context(errors::VaultError::FetchPaymentMethodFailed)? + .map_or( + Err(report!(errors::VaultError::FetchPaymentMethodFailed)), + |d| Ok(d.into_inner()), + ) +} + #[instrument(skip_all)] pub async fn get_payment_method_from_hs_locker<'a>( state: &'a routes::AppState, @@ -286,7 +318,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( #[cfg(feature = "kms")] let jwekey = &state.kms_secrets; - if !locker.mock_locker { + let payment_method_data = if !locker.mock_locker { let request = payment_methods::mk_get_card_request_hs( jwekey, locker, @@ -315,44 +347,34 @@ pub async fn get_payment_method_from_hs_locker<'a>( .payload .get_required_value("RetrieveCardRespPayload") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; - retrieve_card_resp + let enc_card_data = retrieve_card_resp .enc_card_data .get_required_value("enc_card_data") - .change_context(errors::VaultError::FetchPaymentMethodFailed) + .change_context(errors::VaultError::FetchPaymentMethodFailed)?; + decode_and_decrypt_locker_data(key_store, enc_card_data.peek().to_string()).await? } else { - let get_card_resp = - mock_get_payment_method(&*state.store, key_store, payment_method_reference).await?; - Ok(get_card_resp.payment_method.payment_method_data) - } + mock_get_payment_method(&*state.store, key_store, payment_method_reference) + .await? + .payment_method + .payment_method_data + }; + Ok(payment_method_data) } #[instrument(skip_all)] -pub async fn call_to_card_hs( +pub async fn call_to_locker_hs<'a>( state: &routes::AppState, - card: &api::CardDetail, - enc_value: Option<&str>, + payload: &payment_methods::StoreLockerReq<'a>, customer_id: &str, - merchant_account: &domain::MerchantAccount, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] let jwekey = &state.conf.jwekey; #[cfg(feature = "kms")] let jwekey = &state.kms_secrets; - let db = &*state.store; - let merchant_id = &merchant_account.merchant_id; - let stored_card_response = if !locker.mock_locker { - let request = payment_methods::mk_add_card_request_hs( - jwekey, - locker, - card, - enc_value, - customer_id, - merchant_id, - ) - .await?; + let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?; let response = services::call_connector_api(state, request) .await .change_context(errors::VaultError::SaveCardFailed); @@ -371,7 +393,7 @@ pub async fn call_to_card_hs( stored_card_resp } else { let card_id = generate_id(consts::ID_LENGTH, "card"); - mock_add_card_hs(db, &card_id, card, None, enc_value, None, Some(customer_id)).await? + mock_call_to_locker_hs(db, &card_id, payload, None, None, Some(customer_id)).await? }; let stored_card = stored_card_response @@ -495,31 +517,47 @@ pub async fn delete_card_from_hs_locker<'a>( } ///Mock api for local testing -#[instrument(skip_all)] -pub async fn mock_add_card_hs( +pub async fn mock_call_to_locker_hs<'a>( db: &dyn db::StorageInterface, card_id: &str, - card: &api::CardDetail, + payload: &payment_methods::StoreLockerReq<'a>, card_cvc: Option, - enc_val: Option<&str>, payment_method_id: Option, customer_id: Option<&str>, ) -> errors::CustomResult { - let locker_mock_up = storage::LockerMockUpNew { + let mut locker_mock_up = storage::LockerMockUpNew { card_id: card_id.to_string(), external_id: uuid::Uuid::new_v4().to_string(), card_fingerprint: uuid::Uuid::new_v4().to_string(), card_global_fingerprint: uuid::Uuid::new_v4().to_string(), - merchant_id: "mm01".to_string(), - card_number: card.card_number.peek().to_string(), - card_exp_year: card.card_exp_year.peek().to_string(), - card_exp_month: card.card_exp_month.peek().to_string(), + merchant_id: "".to_string(), + card_number: "4111111111111111".to_string(), + card_exp_year: "2099".to_string(), + card_exp_month: "12".to_string(), card_cvc, payment_method_id, customer_id: customer_id.map(str::to_string), - name_on_card: card.card_holder_name.to_owned().expose_option(), - nickname: card.nick_name.to_owned().map(masking::Secret::expose), - enc_card_data: enc_val.map(|e| e.to_string()), + name_on_card: None, + nickname: None, + enc_card_data: None, + }; + locker_mock_up = match payload { + payment_methods::StoreLockerReq::LockerCard(store_card_req) => storage::LockerMockUpNew { + merchant_id: store_card_req.merchant_id.to_string(), + card_number: store_card_req.card.card_number.peek().to_string(), + card_exp_year: store_card_req.card.card_exp_year.peek().to_string(), + card_exp_month: store_card_req.card.card_exp_month.peek().to_string(), + name_on_card: store_card_req.card.name_on_card.to_owned().expose_option(), + nickname: store_card_req.card.nick_name.to_owned(), + ..locker_mock_up + }, + payment_methods::StoreLockerReq::LockerGeneric(store_generic_req) => { + storage::LockerMockUpNew { + merchant_id: store_generic_req.merchant_id.to_string(), + enc_card_data: Some(store_generic_req.enc_data.to_owned()), + ..locker_mock_up + } + } }; let response = db @@ -588,21 +626,7 @@ pub async fn mock_get_payment_method<'a>( .await .change_context(errors::VaultError::FetchPaymentMethodFailed)?; let dec_data = if let Some(e) = locker_mock_up.enc_card_data { - // Fetch key - let key = key_store.key.get_inner().peek(); - // Decode - let decoded_bytes = hex::decode(e) - .into_report() - .change_context(errors::VaultError::ResponseDeserializationFailed) - .attach_printable("Failed to decode hex string into bytes")?; - // Decrypt - async { decrypt(Some(Encryption::new(decoded_bytes.into())), key).await } - .await - .change_context(errors::VaultError::FetchPaymentMethodFailed)? - .map_or( - Err(report!(errors::VaultError::FetchPaymentMethodFailed)), - |d| Ok(d.into_inner()), - ) + decode_and_decrypt_locker_data(key_store, e).await } else { Err(report!(errors::VaultError::FetchPaymentMethodFailed)) }?; @@ -1894,8 +1918,7 @@ pub async fn get_lookup_key_for_payout_method( .attach_printable("Error getting payment method from locker")?; let pm_parsed: api::PayoutMethodData = payment_method .peek() - .as_bytes() - .to_vec() + .to_string() .parse_struct("PayoutMethodData") .change_context(errors::ApiErrorResponse::InternalServerError)?; match &pm_parsed { diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 2d8fe43d18..086133ec78 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -15,12 +15,26 @@ use crate::{ utils::{self, OptionExt}, }; +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum StoreLockerReq<'a> { + LockerCard(StoreCardReq<'a>), + LockerGeneric(StoreGenericReq<'a>), +} + #[derive(Debug, Deserialize, Serialize)] pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, pub card: Card, - pub enc_card_data: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct StoreGenericReq<'a> { + pub merchant_id: &'a str, + pub merchant_customer_id: String, + #[serde(rename = "enc_card_data")] + pub enc_data: String, } #[derive(Debug, Deserialize, Serialize)] @@ -253,32 +267,13 @@ pub async fn mk_basilisk_req( Ok(jwe_body) } -pub async fn mk_add_card_request_hs( +pub async fn mk_add_locker_request_hs<'a>( #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, locker: &settings::Locker, - card: &api::CardDetail, - enc_value: Option<&str>, - customer_id: &str, - merchant_id: &str, + payload: &StoreLockerReq<'a>, ) -> CustomResult { - let merchant_customer_id = customer_id.to_owned(); - let card = Card { - card_number: card.card_number.to_owned(), - name_on_card: card.card_holder_name.to_owned(), - card_exp_month: card.card_exp_month.to_owned(), - card_exp_year: card.card_exp_year.to_owned(), - card_brand: None, - card_isin: None, - nick_name: card.nick_name.to_owned().map(masking::Secret::expose), - }; - let store_card_req = StoreCardReq { - merchant_id, - merchant_customer_id, - card, - enc_card_data: enc_value.map(|e| e.to_string()), - }; - let payload = utils::Encode::>::encode_to_vec(&store_card_req) + let payload = utils::Encode::>::encode_to_vec(&payload) .change_context(errors::VaultError::RequestEncodingFailed)?; #[cfg(feature = "kms")] diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 462e569bb3..6959962a13 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,6 +1,3 @@ -use std::str::FromStr; - -use ::cards::CardNumber; use common_utils::{errors::CustomResult, ext_traits::ValueExt}; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; @@ -9,7 +6,11 @@ use masking::{ExposeInterface, PeekInterface, Secret}; use crate::{ core::{ errors::{self, RouterResult}, - payment_methods::{cards, vault}, + payment_methods::{ + cards, transformers, + transformers::{StoreCardReq, StoreGenericReq, StoreLockerReq}, + vault, + }, payments::{customers::get_connector_customer_details_if_present, CustomerDetails}, utils as core_utils, }, @@ -125,21 +126,37 @@ pub async fn save_payout_data_to_locker( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { - let mut enc_card_data = None; - let (card_details, payment_method_type) = match payout_method_data { - api_models::payouts::PayoutMethodData::Card(card) => ( - api::CardDetail { + let (locker_req, card_details, payment_method_type) = match payout_method_data { + api_models::payouts::PayoutMethodData::Card(card) => { + let card_detail = api::CardDetail { card_number: card.card_number.to_owned(), + card_holder_name: Some(card.card_holder_name.to_owned()), card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), - card_holder_name: Some(card.card_holder_name.to_owned()), nick_name: None, - }, - api_enums::PaymentMethodType::Debit, - ), + }; + let payload = StoreLockerReq::LockerCard(StoreCardReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: payout_attempt.customer_id.to_owned(), + card: transformers::Card { + card_number: card.card_number.to_owned(), + name_on_card: Some(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, + }, + }); + ( + payload, + Some(card_detail), + api_enums::PaymentMethodType::Debit, + ) + } api_models::payouts::PayoutMethodData::Bank(bank) => { let key = key_store.key.get_inner().peek(); - let enc_str = async { + let enc_data = async { serde_json::to_value(payout_method_data.to_owned()) .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -160,32 +177,22 @@ pub async fn save_payout_data_to_locker( .map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| { Ok(hex::encode(e.peek())) })?; - enc_card_data = Some(enc_str); + let payload = StoreLockerReq::LockerGeneric(StoreGenericReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: payout_attempt.customer_id.to_owned(), + enc_data, + }); ( - api::CardDetail { - card_number: CardNumber::from_str("4111111111111111") - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to form a sample card number")?, - card_exp_month: "12".to_string().into(), - card_exp_year: "99".to_string().into(), - card_holder_name: None, - nick_name: None, - }, + payload, + None, api_enums::PaymentMethodType::foreign_from(bank.to_owned()), ) } }; // Store payout method in locker - let stored_resp = cards::call_to_card_hs( - state, - &card_details, - enc_card_data.as_deref(), - &payout_attempt.customer_id, - merchant_account, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; + let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; // Store card_reference in payouts table let db = &*state.store; @@ -208,7 +215,7 @@ pub async fn save_payout_data_to_locker( payment_method_type: Some(payment_method_type), payment_method_issuer: None, payment_method_issuer_code: None, - card: Some(card_details), + card: card_details, metadata: None, customer_id: Some(payout_attempt.customer_id.to_owned()), card_network: None,