diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index a907fff601..3467777da7 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -55,6 +55,11 @@ pub struct PaymentMethodCreate { /// The card network #[schema(example = "Visa")] pub card_network: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -72,6 +77,11 @@ pub struct PaymentMethodUpdate { #[schema(value_type = Option,example = "Visa")] pub card_network: Option, + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -147,6 +157,11 @@ pub struct PaymentMethodResponse { #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index f7dba2446e..9e771b4712 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -181,7 +181,7 @@ pub struct Card { /// The card holder's name #[schema(value_type = String, example = "John Doe")] - pub card_holder_name: Secret, + pub card_holder_name: Option>, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -195,16 +195,16 @@ pub enum Bank { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct AchBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -218,16 +218,16 @@ pub struct AchBankTransfer { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct BacsBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -242,16 +242,16 @@ pub struct BacsBankTransfer { // 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 { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// International Bank Account Number (iban) - used in many countries for identifying a bank along with it's customer. #[schema(value_type = String, example = "DE89370400440532013000")] diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index e00b829f28..1e1cfa8fe5 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -9,9 +9,7 @@ use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime, PrimitiveDateTime}; #[cfg(feature = "payouts")] -use crate::connector::utils::AddressDetailsData; -#[cfg(feature = "payouts")] -use crate::types::api::payouts; +use crate::{connector::utils::AddressDetailsData, types::api::payouts, utils::OptionExt}; use crate::{ connector::utils::{ self, BrowserInformationData, CardData, MandateReferenceData, PaymentsAuthorizeRequestData, @@ -1707,20 +1705,6 @@ fn get_country_code( address.and_then(|billing| billing.address.as_ref().and_then(|address| address.country)) } -#[cfg(feature = "payouts")] -fn get_payout_card_details(payout_method_data: &PayoutMethodData) -> Option { - match payout_method_data { - PayoutMethodData::Card(card) => Some(PayoutCardDetails { - _type: "scheme".to_string(), // FIXME: Remove hardcoding - number: card.card_number.peek().to_string(), - expiry_month: card.expiry_month.peek().to_string(), - expiry_year: card.expiry_year.peek().to_string(), - holder_name: card.card_holder_name.peek().to_string(), - }), - _ => None, - } -} - fn get_social_security_number( voucher_data: &api_models::payments::VoucherData, ) -> Option> { @@ -3980,12 +3964,12 @@ pub struct AdyenPayoutCreateRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct PayoutBankDetails { - bank_name: String, + iban: Secret, + owner_name: Secret, + bank_city: Option, + bank_name: Option, bic: Option>, - country_code: storage_enums::CountryAlpha2, - iban: Option>, - owner_name: Option>, - bank_city: String, + country_code: Option, tax_id: Option>, } @@ -4036,11 +4020,11 @@ pub struct AdyenPayoutEligibilityRequest { #[serde(rename_all = "camelCase")] pub struct PayoutCardDetails { #[serde(rename = "type")] - _type: String, - number: String, - expiry_month: String, - expiry_year: String, - holder_name: String, + payment_method_type: String, + number: CardNumber, + expiry_month: Secret, + expiry_year: Secret, + holder_name: Secret, } #[cfg(feature = "payouts")] @@ -4095,6 +4079,31 @@ pub struct AdyenPayoutCancelRequest { merchant_account: Secret, } +#[cfg(feature = "payouts")] +impl TryFrom<&PayoutMethodData> for PayoutCardDetails { + type Error = Error; + fn try_from(item: &PayoutMethodData) -> Result { + match item { + PayoutMethodData::Card(card) => Ok(Self { + payment_method_type: "scheme".to_string(), // FIXME: Remove hardcoding + number: card.card_number.clone(), + expiry_month: card.expiry_month.clone(), + expiry_year: card.expiry_year.clone(), + holder_name: card + .card_holder_name + .clone() + .get_required_value("card_holder_name") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card.holder_name", + })?, + }), + _ => Err(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card", + })?, + } + } +} + // Payouts eligibility request transform #[cfg(feature = "payouts")] impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutEligibilityRequest { @@ -4102,12 +4111,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE fn try_from(item: &AdyenRouterData<&types::PayoutsRouterData>) -> Result { let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let payout_method_data = - get_payout_card_details(&item.router_data.get_payout_method_data()?).map_or( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?; + PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?; Ok(Self { amount: Amount { currency: item.router_data.request.destination_currency, @@ -4155,6 +4159,11 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC .customer_details .to_owned() .map_or((None, None), |c| (c.name, c.email)); + let owner_name = owner_name.get_required_value("owner_name").change_context( + errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.bank.owner_name", + }, + )?; match item.router_data.get_payout_method_data()? { PayoutMethodData::Card(_) => Err(errors::ConnectorError::NotSupported { @@ -4169,7 +4178,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC bank_city: b.bank_city, owner_name, bic: b.bic, - iban: Some(b.iban), + iban: b.iban, tax_id: None, }, payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { @@ -4234,13 +4243,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF value: item.amount.to_owned(), currency: item.router_data.request.destination_currency, }, - card: get_payout_card_details(&item.router_data.get_payout_method_data()?) - .map_or( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?, + card: PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?, billing_address: get_address_info(item.router_data.get_billing().ok()), merchant_account, reference: item.router_data.request.payout_id.clone(), diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index 3f56cddee1..e3e308a8a0 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -109,6 +109,7 @@ pub async fn call_to_locker( payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, card: Some(card_details.clone()), + bank_transfer: None, metadata: pm.metadata, customer_id: Some(pm.customer_id), card_network: card.card_brand, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 39bc54fa15..51f5435363 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -105,6 +105,29 @@ pub async fn create_payment_method( Ok(response) } +pub fn store_default_payment_method( + req: &api::PaymentMethodCreate, + customer_id: &str, + merchant_id: &String, +) -> (api::PaymentMethodResponse, bool) { + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id.to_owned()), + payment_method_id: pm_id, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: None, + card: None, + metadata: req.metadata.clone(), + created: Some(common_utils::date_time::now()), + recurring_enabled: false, //[#219] + installment_payment_enabled: false, //[#219] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + }; + (payment_method_response, false) +} + #[instrument(skip_all)] pub async fn add_payment_method( state: routes::AppState, @@ -115,30 +138,44 @@ pub async fn add_payment_method( req.validate()?; let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; - let response = match req.card.clone() { - Some(card) => { - add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Card Failed") - } - None => { - let pm_id = generate_id(consts::ID_LENGTH, "pm"); - let payment_method_response = api::PaymentMethodResponse { - merchant_id: merchant_id.to_string(), - customer_id: Some(customer_id.clone()), - payment_method_id: pm_id, - payment_method: req.payment_method, - payment_method_type: req.payment_method_type, - card: None, - metadata: req.metadata.clone(), - created: Some(common_utils::date_time::now()), - recurring_enabled: false, //[#219] - installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] - }; - Ok((payment_method_response, false)) - } + + let response = match req.payment_method { + api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { + Some(bank) => add_bank_to_locker( + &state, + req.clone(), + merchant_account, + key_store, + &bank, + &customer_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add PaymentMethod Failed"), + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + api_enums::PaymentMethod::Card => match req.card.clone() { + Some(card) => { + add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), }; let (resp, is_duplicate) = response?; @@ -199,6 +236,7 @@ pub async fn update_customer_payment_method( payment_method_type: pm.payment_method_type, payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, + bank_transfer: req.bank_transfer, card: req.card, metadata: req.metadata, customer_id: Some(pm.customer_id), @@ -212,6 +250,64 @@ pub async fn update_customer_payment_method( // Wrapper function to switch lockers +pub async fn add_bank_to_locker( + state: &routes::AppState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + bank: &api::BankPayout, + customer_id: &String, +) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { + let key = key_store.key.get_inner().peek(); + let payout_method_data = api::PayoutMethodData::Bank(bank.clone()); + let enc_data = async { + serde_json::to_value(payout_method_data.to_owned()) + .map_err(|err| { + logger::error!("Error while encoding payout method data: {}", err); + errors::VaultError::SavePaymentMethodFailed + }) + .into_report() + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Unable to encode payout method data") + .ok() + .map(|v| { + let secret: Secret = Secret::new(v.to_string()); + secret + }) + .async_lift(|inner| encrypt_optional(inner, key)) + .await + } + .await + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Failed to encrypt payout method data")? + .map(Encryption::from) + .map(|e| e.into_inner()) + .map_or(Err(errors::VaultError::SavePaymentMethodFailed), |e| { + Ok(hex::encode(e.peek())) + })?; + + let payload = + payment_methods::StoreLockerReq::LockerGeneric(payment_methods::StoreGenericReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: customer_id.to_owned(), + enc_data, + }); + let store_resp = call_to_locker_hs( + state, + &payload, + customer_id, + api_enums::LockerChoice::Basilisk, + ) + .await?; + let payment_method_resp = payment_methods::mk_add_bank_response_hs( + bank.clone(), + store_resp.card_reference, + req, + &merchant_account.merchant_id, + ); + Ok((payment_method_resp, store_resp.duplicate.unwrap_or(false))) +} + /// The response will be the tuple of PaymentMethodResponse and the duplication check of payment_method pub async fn add_card_to_locker( state: &routes::AppState, @@ -2424,7 +2520,17 @@ pub async fn list_customer_payment_method( let token_data = PaymentTokenData::temporary_generic(token.clone()); ( None, - Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + Some( + get_bank_from_hs_locker( + state, + &key_store, + &token, + &pm.customer_id, + &pm.customer_id, + &pm.payment_method_id, + ) + .await?, + ), token_data, ) } @@ -2738,18 +2844,20 @@ async fn get_bank_account_connector_details( } #[cfg(feature = "payouts")] -pub async fn get_lookup_key_for_payout_method( +pub async fn get_bank_from_hs_locker( state: &routes::AppState, key_store: &domain::MerchantKeyStore, - payout_token: &str, - pm: &storage::PaymentMethod, + temp_token: &str, + customer_id: &str, + merchant_id: &str, + token_ref: &str, ) -> errors::RouterResult { let payment_method = get_payment_method_from_hs_locker( state, key_store, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, + customer_id, + merchant_id, + token_ref, None, ) .await @@ -2764,9 +2872,9 @@ pub async fn get_lookup_key_for_payout_method( api::PayoutMethodData::Bank(bank) => { vault::Vault::store_payout_method_data_in_locker( state, - Some(payout_token.to_string()), + Some(temp_token.to_string()), &pm_parsed, - Some(pm.customer_id.to_owned()), + Some(customer_id.to_owned()), key_store, ) .await @@ -2893,6 +3001,7 @@ pub async fn retrieve_payment_method( payment_method_id: pm.payment_method_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, + bank_transfer: None, card, metadata: pm.metadata, created: Some(pm.created_at), diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 5506dc7eb9..da4f03b49c 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -324,6 +324,28 @@ pub async fn mk_add_locker_request_hs<'a>( Ok(request) } +pub fn mk_add_bank_response_hs( + bank: api::BankPayout, + bank_reference: String, + req: api::PaymentMethodCreate, + merchant_id: &str, +) -> api::PaymentMethodResponse { + api::PaymentMethodResponse { + merchant_id: merchant_id.to_owned(), + customer_id: req.customer_id, + payment_method_id: bank_reference, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: Some(bank), + card: None, + metadata: req.metadata, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, // [#256] + installment_payment_enabled: false, // #[#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + } +} + pub fn mk_add_card_response_hs( card: api::CardDetail, card_reference: String, @@ -349,6 +371,7 @@ pub fn mk_add_card_response_hs( payment_method_id: card_reference, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), @@ -383,6 +406,7 @@ pub fn mk_add_card_response( payment_method_id: response.card_id, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 070bca234c..063b696875 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -351,7 +351,7 @@ impl Vaultable for api::CardPayout { card_number: self.card_number.peek().clone(), exp_year: self.expiry_year.peek().clone(), exp_month: self.expiry_month.peek().clone(), - name_on_card: Some(self.card_holder_name.peek().clone()), + name_on_card: self.card_holder_name.clone().map(|n| n.peek().to_string()), nickname: None, card_last_four: None, card_token: None, @@ -397,7 +397,7 @@ impl Vaultable for api::CardPayout { .map_err(|_| errors::VaultError::FetchCardFailed)?, expiry_month: value1.exp_month.into(), expiry_year: value1.exp_year.into(), - card_holder_name: value1.name_on_card.unwrap_or_default().into(), + card_holder_name: value1.name_on_card.map(masking::Secret::new), }; let supp_data = SupplementaryVaultData { @@ -421,9 +421,9 @@ pub struct TokenizedBankSensitiveValues { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenizedBankInsensitiveValues { pub customer_id: Option, - pub bank_name: String, - pub bank_country_code: api::enums::CountryAlpha2, - pub bank_city: String, + pub bank_name: Option, + pub bank_country_code: Option, + pub bank_city: Option, } #[cfg(feature = "payouts")] @@ -702,7 +702,8 @@ impl Vault { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Value2 for locker")?; - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); + let lookup_key = + token_id.unwrap_or_else(|| generate_id_with_default_len("temporary_token")); let lookup_key = create_tokenize( state, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 7230d74e9a..92dc1bf5f4 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1041,6 +1041,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: card.card_issuer.clone(), payment_method_issuer_code: None, + bank_transfer: None, card: Some(card_detail), metadata: None, customer_id: Some(customer_id), @@ -1057,6 +1058,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: None, payment_method_issuer_code: None, + bank_transfer: None, card: None, metadata: None, customer_id: Some(customer.customer_id.to_owned()), diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 551d1c8abb..f884cb79e7 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -198,6 +198,7 @@ pub async fn save_in_locker( payment_method_id: pm_id, payment_method: payment_method_request.payment_method, payment_method_type: payment_method_request.payment_method_type, + bank_transfer: None, card: None, metadata: None, created: Some(common_utils::date_time::now()), diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9ddc839573..56e3a6faf5 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,4 +1,7 @@ -use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{StringExt, ValueExt}, +}; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; @@ -40,28 +43,50 @@ pub async fn make_payout_method_data<'a>( merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { 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 { - let key = format!( - "pm_token_{}_{}_hyperswitch", - payout_token, - api_enums::PaymentMethod::foreign_from( - payout_type.get_required_value("payout_type")?.to_owned() - ) - ); + 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 redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; - let hyperswitch_token_option = redis_conn - .get_key::>(&key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch the token from redis")?; + let hyperswitch_token = redis_conn + .get_key::>(&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")?; - hyperswitch_token_option.or(Some(payout_token.to_string())) + let payment_token = match payment_token_data { + storage::PaymentTokenData::PermanentCard(storage::CardTokenData { token }) => { + Some(token) + } + storage::PaymentTokenData::TemporaryGeneric(storage::GenericTokenData { + token, + }) => Some(token), + _ => None, + }; + payment_token.or(Some(payout_token.to_string())) + } } else { None }; @@ -69,8 +94,10 @@ pub async fn make_payout_method_data<'a>( match (payout_method_data.to_owned(), hyperswitch_token) { // Get operation (None, Some(payout_token)) => { - let (pm, supplementary_data) = - vault::Vault::get_payout_method_data_from_temporary_locker( + 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, @@ -79,15 +106,33 @@ pub async fn make_payout_method_data<'a>( .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) + 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 @@ -131,11 +176,11 @@ pub async fn save_payout_data_to_locker( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { - let (locker_req, card_details, payment_method_type) = match payout_method_data { + let (locker_req, card_details, bank_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_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, @@ -145,7 +190,7 @@ pub async fn save_payout_data_to_locker( 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()), + 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, @@ -157,6 +202,7 @@ pub async fn save_payout_data_to_locker( ( payload, Some(card_detail), + None, api_enums::PaymentMethodType::Debit, ) } @@ -191,6 +237,7 @@ pub async fn save_payout_data_to_locker( ( payload, None, + Some(bank.to_owned()), api_enums::PaymentMethodType::foreign_from(bank.to_owned()), ) } @@ -244,6 +291,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, + bank_transfer: bank_details, card: card_details, metadata: None, customer_id: Some(payout_attempt.customer_id.to_owned()), diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 5acb66b506..ca852f832e 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,4 +1,3 @@ -use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, @@ -9,9 +8,9 @@ pub use api_models::payment_methods::{ }; use error_stack::report; -use crate::{ - core::errors::{self, RouterResult}, - types::transformers::ForeignFrom, +use crate::core::{ + errors::{self, RouterResult}, + payments::helpers::validate_payment_method_type_against_payment_method, }; pub(crate) trait PaymentMethodCreateExt { @@ -21,16 +20,16 @@ pub(crate) trait PaymentMethodCreateExt { // convert self.payment_method_type to payment_method and compare it against self.payment_method impl PaymentMethodCreateExt for PaymentMethodCreate { fn validate(&self) -> RouterResult<()> { - let payment_method: Option = - self.payment_method_type.map(ForeignFrom::foreign_from); - if payment_method - .map(|payment_method| payment_method != self.payment_method) - .unwrap_or(false) - { - return Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid 'payment_method_type' provided".to_string() - }) - .attach_printable("Invalid payment method type")); + if let Some(payment_method_type) = self.payment_method_type { + if !validate_payment_method_type_against_payment_method( + self.payment_method, + payment_method_type, + ) { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_type' provided".to_string() + }) + .attach_printable("Invalid payment method type")); + } } Ok(()) } diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 97dca3baa5..4907508050 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -95,16 +95,16 @@ impl AdyenTest { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), expiry_month: Secret::new("3".to_string()), expiry_year: Secret::new("2030".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(Secret::new("John Doe".to_string())), })) } enums::PayoutType::Bank => Some(api::PayoutMethodData::Bank( api::payouts::BankPayout::Sepa(api::SepaBankTransfer { iban: "NL46TEST0136169112".to_string().into(), bic: Some("ABNANL2A".to_string().into()), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }), )), }, diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index fb65397e1a..de30352304 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -73,9 +73,9 @@ impl WiseTest { api::BacsBankTransfer { bank_sort_code: "231470".to_string().into(), bank_account_number: "28821822".to_string().into(), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }, ))), ..Default::default() diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 466489e2f9..b2f5d3ea52 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2541,9 +2541,6 @@ "AchBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "bank_account_number", "bank_routing_number" ], @@ -2551,15 +2548,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "bank_account_number": { "type": "string", @@ -2993,9 +2997,6 @@ "BacsBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "bank_account_number", "bank_sort_code" ], @@ -3003,15 +3004,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "bank_account_number": { "type": "string", @@ -9139,6 +9147,14 @@ "description": "The card network", "example": "Visa", "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true } } }, @@ -9475,6 +9491,14 @@ "description": "A timestamp (ISO 8601 code) that determines when the customer was created", "example": "2023-01-18T11:04:09.922Z", "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true } } }, @@ -9585,6 +9609,14 @@ ], "nullable": true }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true + }, "metadata": { "type": "object", "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", @@ -12294,9 +12326,6 @@ "SepaBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "iban", "bic" ], @@ -12304,15 +12333,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "iban": { "type": "string",