diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index fd46c76476..7ffed5fa05 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1990,6 +1990,48 @@ } } }, + "AchBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for ach bank transfer payout method", + "required": [ + "bank_account_number", + "bank_routing_number" + ], + "properties": { + "bank_account_number": { + "type": "string", + "description": "Partially masked account number for ach bank debit payment", + "example": "0001****3456" + }, + "bank_routing_number": { + "type": "string", + "description": "Partially masked routing number for ach bank debit payment", + "example": "110***000" + }, + "bank_name": { + "allOf": [ + { + "$ref": "#/components/schemas/BankNames" + } + ], + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + } + } + }, "AchBillingDetails": { "type": "object", "properties": { @@ -2042,6 +2084,44 @@ } ] }, + "AdditionalPayoutMethodData": { + "oneOf": [ + { + "type": "object", + "required": [ + "Card" + ], + "properties": { + "Card": { + "$ref": "#/components/schemas/CardAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "Bank" + ], + "properties": { + "Bank": { + "$ref": "#/components/schemas/BankAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "Wallet" + ], + "properties": { + "Wallet": { + "$ref": "#/components/schemas/WalletAdditionalData" + } + } + } + ], + "description": "Masked payout method details for storing in db" + }, "Address": { "type": "object", "properties": { @@ -2649,6 +2729,46 @@ } } }, + "BacsBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for bacs bank transfer payout method", + "required": [ + "bank_sort_code", + "bank_account_number" + ], + "properties": { + "bank_sort_code": { + "type": "string", + "description": "Partially masked sort code for Bacs payment method", + "example": "108800" + }, + "bank_account_number": { + "type": "string", + "description": "Bank account's owner name", + "example": "0001****3456" + }, + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + } + } + }, "BacsBankTransferInstructions": { "type": "object", "required": [ @@ -2716,6 +2836,23 @@ } ] }, + "BankAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/AchBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/BacsBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/SepaBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/PixBankTransferAdditionalData" + } + ], + "description": "Masked payout method details for bank payout method" + }, "BankDebitAdditionalData": { "oneOf": [ { @@ -4688,6 +4825,75 @@ } } }, + "CardAdditionalData": { + "type": "object", + "description": "Masked payout method details for card payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_issuer": { + "type": "string", + "description": "Issuer of the card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card type, can be either `credit` or `debit`", + "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Card issuing country", + "nullable": true + }, + "bank_code": { + "type": "string", + "description": "Code for Card issuing bank", + "nullable": true + }, + "last4": { + "type": "string", + "description": "Last 4 digits of the card number", + "nullable": true + }, + "card_isin": { + "type": "string", + "description": "The ISIN of the card", + "nullable": true + }, + "card_extended_bin": { + "type": "string", + "description": "Extended bin of card, contains the first 8 digits of card number", + "nullable": true + }, + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "CardDetail": { "type": "object", "required": [ @@ -16247,6 +16453,14 @@ ], "nullable": true }, + "payout_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PayoutMethodDataResponse" + } + ], + "nullable": true + }, "billing": { "allOf": [ { @@ -16677,6 +16891,44 @@ ], "description": "The payout method information required for carrying out a payout" }, + "PayoutMethodDataResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/CardAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/components/schemas/BankAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "wallet" + ], + "properties": { + "wallet": { + "$ref": "#/components/schemas/WalletAdditionalData" + } + } + } + ], + "description": "The payout method information for response" + }, "PayoutRequest": { "oneOf": [ { @@ -16818,6 +17070,30 @@ } } }, + "PaypalAdditionalData": { + "type": "object", + "description": "Masked payout method details for paypal wallet payout method", + "properties": { + "email": { + "type": "string", + "description": "Email linked with paypal account", + "example": "john.doe@example.com", + "nullable": true + }, + "telephone_number": { + "type": "string", + "description": "mobile number linked to paypal account", + "example": "******* 3349", + "nullable": true + }, + "paypal_id": { + "type": "string", + "description": "id of the paypal account", + "example": "G83K ***** HCQ2", + "nullable": true + } + } + }, "PaypalRedirection": { "type": "object", "properties": { @@ -18890,6 +19166,46 @@ } } }, + "SepaBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for sepa bank transfer payout method", + "required": [ + "iban" + ], + "properties": { + "iban": { + "type": "string", + "description": "Partially masked international bank account number (iban) for SEPA", + "example": "DE8937******013000" + }, + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + }, + "bic": { + "type": "string", + "description": "[8 / 11 digits] Bank Identifier Code (bic) / Swift Code - used in many countries for identifying a bank and it's branches", + "example": "HSBCGB2LXXX", + "nullable": true + } + } + }, "SepaBankTransferInstructions": { "type": "object", "required": [ @@ -19798,6 +20114,18 @@ } } }, + "VenmoAdditionalData": { + "type": "object", + "description": "Masked payout method details for venmo wallet payout method", + "properties": { + "telephone_number": { + "type": "string", + "description": "mobile number linked to venmo account", + "example": "******* 3349", + "nullable": true + } + } + }, "VoucherData": { "oneOf": [ { @@ -19972,6 +20300,17 @@ } ] }, + "WalletAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/PaypalAdditionalData" + }, + { + "$ref": "#/components/schemas/VenmoAdditionalData" + } + ], + "description": "Masked payout method details for wallet payout method" + }, "WalletAdditionalDataForCard": { "type": "object", "required": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index dfcaf157c2..4d8d523923 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -5378,6 +5378,48 @@ } } }, + "AchBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for ach bank transfer payout method", + "required": [ + "bank_account_number", + "bank_routing_number" + ], + "properties": { + "bank_account_number": { + "type": "string", + "description": "Partially masked account number for ach bank debit payment", + "example": "0001****3456" + }, + "bank_routing_number": { + "type": "string", + "description": "Partially masked routing number for ach bank debit payment", + "example": "110***000" + }, + "bank_name": { + "allOf": [ + { + "$ref": "#/components/schemas/BankNames" + } + ], + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + } + } + }, "AchBillingDetails": { "type": "object", "properties": { @@ -5430,6 +5472,44 @@ } ] }, + "AdditionalPayoutMethodData": { + "oneOf": [ + { + "type": "object", + "required": [ + "Card" + ], + "properties": { + "Card": { + "$ref": "#/components/schemas/CardAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "Bank" + ], + "properties": { + "Bank": { + "$ref": "#/components/schemas/BankAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "Wallet" + ], + "properties": { + "Wallet": { + "$ref": "#/components/schemas/WalletAdditionalData" + } + } + } + ], + "description": "Masked payout method details for storing in db" + }, "Address": { "type": "object", "properties": { @@ -5981,6 +6061,46 @@ } } }, + "BacsBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for bacs bank transfer payout method", + "required": [ + "bank_sort_code", + "bank_account_number" + ], + "properties": { + "bank_sort_code": { + "type": "string", + "description": "Partially masked sort code for Bacs payment method", + "example": "108800" + }, + "bank_account_number": { + "type": "string", + "description": "Bank account's owner name", + "example": "0001****3456" + }, + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + } + } + }, "BacsBankTransferInstructions": { "type": "object", "required": [ @@ -6048,6 +6168,23 @@ } ] }, + "BankAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/AchBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/BacsBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/SepaBankTransferAdditionalData" + }, + { + "$ref": "#/components/schemas/PixBankTransferAdditionalData" + } + ], + "description": "Masked payout method details for bank payout method" + }, "BankDebitAdditionalData": { "oneOf": [ { @@ -8020,6 +8157,75 @@ } } }, + "CardAdditionalData": { + "type": "object", + "description": "Masked payout method details for card payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_issuer": { + "type": "string", + "description": "Issuer of the card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card type, can be either `credit` or `debit`", + "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Card issuing country", + "nullable": true + }, + "bank_code": { + "type": "string", + "description": "Code for Card issuing bank", + "nullable": true + }, + "last4": { + "type": "string", + "description": "Last 4 digits of the card number", + "nullable": true + }, + "card_isin": { + "type": "string", + "description": "The ISIN of the card", + "nullable": true + }, + "card_extended_bin": { + "type": "string", + "description": "Extended bin of card, contains the first 8 digits of card number", + "nullable": true + }, + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "CardDetail": { "type": "object", "required": [ @@ -19552,6 +19758,14 @@ ], "nullable": true }, + "payout_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PayoutMethodDataResponse" + } + ], + "nullable": true + }, "billing": { "allOf": [ { @@ -20036,6 +20250,44 @@ ], "description": "The payout method information required for carrying out a payout" }, + "PayoutMethodDataResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/CardAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/components/schemas/BankAdditionalData" + } + } + }, + { + "type": "object", + "required": [ + "wallet" + ], + "properties": { + "wallet": { + "$ref": "#/components/schemas/WalletAdditionalData" + } + } + } + ], + "description": "The payout method information for response" + }, "PayoutRetrieveBody": { "type": "object", "properties": { @@ -20573,6 +20825,30 @@ } } }, + "PaypalAdditionalData": { + "type": "object", + "description": "Masked payout method details for paypal wallet payout method", + "properties": { + "email": { + "type": "string", + "description": "Email linked with paypal account", + "example": "john.doe@example.com", + "nullable": true + }, + "telephone_number": { + "type": "string", + "description": "mobile number linked to paypal account", + "example": "******* 3349", + "nullable": true + }, + "paypal_id": { + "type": "string", + "description": "id of the paypal account", + "example": "G83K ***** HCQ2", + "nullable": true + } + } + }, "PaypalRedirection": { "type": "object", "properties": { @@ -22637,6 +22913,46 @@ } } }, + "SepaBankTransferAdditionalData": { + "type": "object", + "description": "Masked payout method details for sepa bank transfer payout method", + "required": [ + "iban" + ], + "properties": { + "iban": { + "type": "string", + "description": "Partially masked international bank account number (iban) for SEPA", + "example": "DE8937******013000" + }, + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + }, + "bic": { + "type": "string", + "description": "[8 / 11 digits] Bank Identifier Code (bic) / Swift Code - used in many countries for identifying a bank and it's branches", + "example": "HSBCGB2LXXX", + "nullable": true + } + } + }, "SepaBankTransferInstructions": { "type": "object", "required": [ @@ -23627,6 +23943,18 @@ } } }, + "VenmoAdditionalData": { + "type": "object", + "description": "Masked payout method details for venmo wallet payout method", + "properties": { + "telephone_number": { + "type": "string", + "description": "mobile number linked to venmo account", + "example": "******* 3349", + "nullable": true + } + } + }, "VoucherData": { "oneOf": [ { @@ -23801,6 +24129,17 @@ } ] }, + "WalletAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/PaypalAdditionalData" + }, + { + "$ref": "#/components/schemas/VenmoAdditionalData" + } + ], + "description": "Masked payout method details for wallet payout method" + }, "WalletAdditionalDataForCard": { "type": "object", "required": [ diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 34e1bbe544..a7be9703d3 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ consts::default_payouts_list_limit, - crypto, id_type, link_utils, + crypto, id_type, link_utils, payout_method_utils, pii::{self, Email}, + transformers::ForeignFrom, types::{UnifiedCode, UnifiedMessage}, }; use masking::Secret; @@ -418,6 +419,25 @@ pub struct PayoutCreateResponse { #[schema(value_type = Option, example = "bank")] pub payout_type: Option, + /// The payout method details for the payout + #[schema(value_type = Option, example = json!(r#"{ + "card": { + "last4": "2503", + "card_type": null, + "card_network": null, + "card_issuer": null, + "card_issuing_country": null, + "card_isin": "400000", + "card_extended_bin": null, + "card_exp_month": "08", + "card_exp_year": "25", + "card_holder_name": null, + "payment_checks": null, + "authentication_data": null + } + }"#))] + pub payout_method_data: Option, + /// The billing address for the payout #[schema(value_type = Option
, example = json!(r#"{ "address": { @@ -550,6 +570,18 @@ pub struct PayoutCreateResponse { pub unified_message: Option, } +/// The payout method information for response +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum PayoutMethodDataResponse { + #[schema(value_type = CardAdditionalData)] + Card(Box), + #[schema(value_type = BankAdditionalData)] + Bank(Box), + #[schema(value_type = WalletAdditionalData)] + Wallet(Box), +} + #[derive( Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema, )] @@ -844,3 +876,107 @@ pub struct PayoutLinkStatusDetails { pub ui_config: link_utils::GenericLinkUiConfigFormData, pub test_mode: bool, } + +impl From for payout_method_utils::BankAdditionalData { + fn from(bank_data: Bank) -> Self { + match bank_data { + Bank::Ach(AchBankTransfer { + bank_name, + bank_country_code, + bank_city, + bank_account_number, + bank_routing_number, + }) => Self::Ach(Box::new( + payout_method_utils::AchBankTransferAdditionalData { + bank_name, + bank_country_code, + bank_city, + bank_account_number: bank_account_number.into(), + bank_routing_number: bank_routing_number.into(), + }, + )), + Bank::Bacs(BacsBankTransfer { + bank_name, + bank_country_code, + bank_city, + bank_account_number, + bank_sort_code, + }) => Self::Bacs(Box::new( + payout_method_utils::BacsBankTransferAdditionalData { + bank_name, + bank_country_code, + bank_city, + bank_account_number: bank_account_number.into(), + bank_sort_code: bank_sort_code.into(), + }, + )), + Bank::Sepa(SepaBankTransfer { + bank_name, + bank_country_code, + bank_city, + iban, + bic, + }) => Self::Sepa(Box::new( + payout_method_utils::SepaBankTransferAdditionalData { + bank_name, + bank_country_code, + bank_city, + iban: iban.into(), + bic: bic.map(From::from), + }, + )), + Bank::Pix(PixBankTransfer { + bank_name, + bank_branch, + bank_account_number, + pix_key, + tax_id, + }) => Self::Pix(Box::new( + payout_method_utils::PixBankTransferAdditionalData { + bank_name, + bank_branch, + bank_account_number: bank_account_number.into(), + pix_key: pix_key.into(), + tax_id: tax_id.map(From::from), + }, + )), + } + } +} + +impl From for payout_method_utils::WalletAdditionalData { + fn from(wallet_data: Wallet) -> Self { + match wallet_data { + Wallet::Paypal(Paypal { + email, + telephone_number, + paypal_id, + }) => Self::Paypal(Box::new(payout_method_utils::PaypalAdditionalData { + email: email.map(ForeignFrom::foreign_from), + telephone_number: telephone_number.map(From::from), + paypal_id: paypal_id.map(From::from), + })), + Wallet::Venmo(Venmo { telephone_number }) => { + Self::Venmo(Box::new(payout_method_utils::VenmoAdditionalData { + telephone_number: telephone_number.map(From::from), + })) + } + } + } +} + +impl From for PayoutMethodDataResponse { + fn from(additional_data: payout_method_utils::AdditionalPayoutMethodData) -> Self { + match additional_data { + payout_method_utils::AdditionalPayoutMethodData::Card(card_data) => { + Self::Card(card_data) + } + payout_method_utils::AdditionalPayoutMethodData::Bank(bank_data) => { + Self::Bank(bank_data) + } + payout_method_utils::AdditionalPayoutMethodData::Wallet(wallet_data) => { + Self::Wallet(wallet_data) + } + } + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 238e333644..ecffa259bc 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -20,6 +20,7 @@ pub mod keymanager; pub mod link_utils; pub mod macros; pub mod new_type; +pub mod payout_method_utils; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; diff --git a/crates/common_utils/src/new_type.rs b/crates/common_utils/src/new_type.rs index 9d05e0be27..96d558bfa3 100644 --- a/crates/common_utils/src/new_type.rs +++ b/crates/common_utils/src/new_type.rs @@ -1,7 +1,11 @@ //! Contains new types with restrictions -use masking::{ExposeInterface, Secret}; +use masking::{ExposeInterface, PeekInterface, Secret}; -use crate::{consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH, pii::UpiVpaMaskingStrategy}; +use crate::{ + consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH, + pii::{Email, UpiVpaMaskingStrategy}, + transformers::ForeignFrom, +}; #[nutype::nutype( derive(Clone, Serialize, Deserialize, Debug), @@ -131,6 +135,21 @@ impl From> for MaskedIban { } } +/// Masked IBAN +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MaskedBic(Secret); +impl From for MaskedBic { + fn from(src: String) -> Self { + let masked_value = apply_mask(src.as_ref(), 3, 2); + Self(Secret::from(masked_value)) + } +} +impl From> for MaskedBic { + fn from(secret: Secret) -> Self { + Self::from(secret.expose()) + } +} + /// Masked UPI ID #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct MaskedUpiVpaId(Secret); @@ -159,6 +178,64 @@ impl From> for MaskedUpiVpaId { } } +/// Masked Email +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MaskedEmail(Secret); +impl From for MaskedEmail { + fn from(src: String) -> Self { + let unmasked_char_count = 2; + let masked_value = if let Some((user_identifier, domain)) = src.split_once('@') { + let masked_user_identifier = user_identifier + .to_string() + .chars() + .take(unmasked_char_count) + .collect::() + + &"*".repeat(user_identifier.len() - unmasked_char_count); + format!("{}@{}", masked_user_identifier, domain) + } else { + let masked_value = apply_mask(src.as_ref(), unmasked_char_count, 8); + masked_value + }; + Self(Secret::from(masked_value)) + } +} +impl From> for MaskedEmail { + fn from(secret: Secret) -> Self { + Self::from(secret.expose()) + } +} +impl ForeignFrom for MaskedEmail { + fn foreign_from(email: Email) -> Self { + let email_value: String = email.expose().peek().to_owned(); + Self::from(email_value) + } +} + +/// Masked Phone Number +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MaskedPhoneNumber(Secret); +impl From for MaskedPhoneNumber { + fn from(src: String) -> Self { + let unmasked_char_count = 2; + let masked_value = if unmasked_char_count <= src.len() { + let len = src.len(); + // mask every character except the last 2 + "*".repeat(len - unmasked_char_count).to_string() + + src + .get(len.saturating_sub(unmasked_char_count)..) + .unwrap_or("") + } else { + src + }; + Self(Secret::from(masked_value)) + } +} +impl From> for MaskedPhoneNumber { + fn from(secret: Secret) -> Self { + Self::from(secret.expose()) + } +} + #[cfg(test)] mod apply_mask_fn_test { use masking::PeekInterface; diff --git a/crates/common_utils/src/payout_method_utils.rs b/crates/common_utils/src/payout_method_utils.rs new file mode 100644 index 0000000000..3a2a9e4c38 --- /dev/null +++ b/crates/common_utils/src/payout_method_utils.rs @@ -0,0 +1,240 @@ +//! This module has common utilities for payout method data in HyperSwitch + +use common_enums; +use diesel::{sql_types::Jsonb, AsExpression, FromSqlRow}; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::new_type::{ + MaskedBankAccount, MaskedBic, MaskedEmail, MaskedIban, MaskedPhoneNumber, MaskedRoutingNumber, + MaskedSortCode, +}; + +/// Masked payout method details for storing in db +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub enum AdditionalPayoutMethodData { + /// Additional data for card payout method + Card(Box), + /// Additional data for bank payout method + Bank(Box), + /// Additional data for wallet payout method + Wallet(Box), +} + +crate::impl_to_sql_from_sql_json!(AdditionalPayoutMethodData); + +/// Masked payout method details for card payout method +#[derive( + Eq, PartialEq, Clone, Debug, Serialize, Deserialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct CardAdditionalData { + /// Issuer of the card + pub card_issuer: Option, + + /// Card network of the card + #[schema(value_type = Option)] + pub card_network: Option, + + /// Card type, can be either `credit` or `debit` + pub card_type: Option, + + /// Card issuing country + pub card_issuing_country: Option, + + /// Code for Card issuing bank + pub bank_code: Option, + + /// Last 4 digits of the card number + pub last4: Option, + + /// The ISIN of the card + pub card_isin: Option, + + /// Extended bin of card, contains the first 8 digits of card number + pub card_extended_bin: Option, + + /// Card expiry month + #[schema(value_type = String, example = "01")] + pub card_exp_month: Option>, + + /// Card expiry year + #[schema(value_type = String, example = "2026")] + pub card_exp_year: Option>, + + /// Card holder name + #[schema(value_type = String, example = "John Doe")] + pub card_holder_name: Option>, +} + +/// Masked payout method details for bank payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +pub enum BankAdditionalData { + /// Additional data for ach bank transfer payout method + Ach(Box), + /// Additional data for bacs bank transfer payout method + Bacs(Box), + /// Additional data for sepa bank transfer payout method + Sepa(Box), + /// Additional data for pix bank transfer payout method + Pix(Box), +} + +/// Masked payout method details for ach bank transfer payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct AchBankTransferAdditionalData { + /// Partially masked account number for ach bank debit payment + #[schema(value_type = String, example = "0001****3456")] + pub bank_account_number: MaskedBankAccount, + + /// Partially masked routing number for ach bank debit payment + #[schema(value_type = String, example = "110***000")] + pub bank_routing_number: MaskedRoutingNumber, + + /// Name of the bank + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, + + /// Bank country code + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, + + /// Bank city + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, +} + +/// Masked payout method details for bacs bank transfer payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct BacsBankTransferAdditionalData { + /// Partially masked sort code for Bacs payment method + #[schema(value_type = String, example = "108800")] + pub bank_sort_code: MaskedSortCode, + + /// Bank account's owner name + #[schema(value_type = String, example = "0001****3456")] + pub bank_account_number: MaskedBankAccount, + + /// Bank name + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, + + /// Bank country code + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, + + /// Bank city + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, +} + +/// Masked payout method details for sepa bank transfer payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct SepaBankTransferAdditionalData { + /// Partially masked international bank account number (iban) for SEPA + #[schema(value_type = String, example = "DE8937******013000")] + pub iban: MaskedIban, + + /// Bank name + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, + + /// Bank country code + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, + + /// Bank city + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, + + /// [8 / 11 digits] Bank Identifier Code (bic) / Swift Code - used in many countries for identifying a bank and it's branches + #[schema(value_type = Option, example = "HSBCGB2LXXX")] + pub bic: Option, +} + +/// Masked payout method details for pix bank transfer payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct PixBankTransferAdditionalData { + /// Partially masked unique key for pix transfer + #[schema(value_type = String, example = "a1f4102e ****** 6fa48899c1d1")] + pub pix_key: MaskedBankAccount, + + /// Partially masked CPF - CPF is a Brazilian tax identification number + #[schema(value_type = Option, example = "**** 124689")] + pub tax_id: Option, + + /// Bank account number is an unique identifier assigned by a bank to a customer. + #[schema(value_type = String, example = "**** 23456")] + pub bank_account_number: MaskedBankAccount, + + /// Bank name + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, + + /// Bank branch + #[schema(value_type = Option, example = "3707")] + pub bank_branch: Option, +} + +/// Masked payout method details for wallet payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +pub enum WalletAdditionalData { + /// Additional data for paypal wallet payout method + Paypal(Box), + /// Additional data for venmo wallet payout method + Venmo(Box), +} + +/// Masked payout method details for paypal wallet payout method +#[derive( + Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct PaypalAdditionalData { + /// Email linked with paypal account + #[schema(value_type = Option, example = "john.doe@example.com")] + pub email: Option, + + /// mobile number linked to paypal account + #[schema(value_type = Option, example = "******* 3349")] + pub telephone_number: Option, + + /// id of the paypal account + #[schema(value_type = Option, example = "G83K ***** HCQ2")] + pub paypal_id: Option, +} + +/// Masked payout method details for venmo wallet payout method +#[derive( + Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct VenmoAdditionalData { + /// mobile number linked to venmo account + #[schema(value_type = Option, example = "******* 3349")] + pub telephone_number: Option, +} diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index edb60bc1f4..0e12a9ed80 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -1,4 +1,7 @@ -use common_utils::types::{UnifiedCode, UnifiedMessage}; +use common_utils::{ + payout_method_utils, + types::{UnifiedCode, UnifiedMessage}, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use serde::{self, Deserialize, Serialize}; use time::PrimitiveDateTime; @@ -33,6 +36,7 @@ pub struct PayoutAttempt { pub routing_info: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } #[derive( @@ -71,6 +75,7 @@ pub struct PayoutAttemptNew { pub routing_info: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -98,6 +103,9 @@ pub enum PayoutAttemptUpdate { routing_info: Option, merchant_connector_id: Option, }, + AdditionalPayoutMethodDataUpdate { + additional_payout_method_data: Option, + }, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -119,6 +127,7 @@ pub struct PayoutAttemptUpdateInternal { pub merchant_connector_id: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } impl Default for PayoutAttemptUpdateInternal { @@ -140,6 +149,7 @@ impl Default for PayoutAttemptUpdateInternal { customer_id: None, unified_code: None, unified_message: None, + additional_payout_method_data: None, } } } @@ -191,6 +201,12 @@ impl From for PayoutAttemptUpdateInternal { merchant_connector_id, ..Default::default() }, + PayoutAttemptUpdate::AdditionalPayoutMethodDataUpdate { + additional_payout_method_data, + } => Self { + additional_payout_method_data, + ..Default::default() + }, } } } @@ -214,6 +230,7 @@ impl PayoutAttemptUpdate { merchant_connector_id, unified_code, unified_message, + additional_payout_method_data, } = self.into(); PayoutAttempt { payout_token: payout_token.or(source.payout_token), @@ -232,6 +249,8 @@ impl PayoutAttemptUpdate { merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), unified_code: unified_code.or(source.unified_code), unified_message: unified_message.or(source.unified_message), + additional_payout_method_data: additional_payout_method_data + .or(source.additional_payout_method_data), ..source } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9ff7c958df..77140891ad 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1058,6 +1058,7 @@ diesel::table! { unified_code -> Nullable, #[max_length = 1024] unified_message -> Nullable, + additional_payout_method_data -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index a66286e02d..4c44bb18b7 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1013,6 +1013,7 @@ diesel::table! { unified_code -> Nullable, #[max_length = 1024] unified_message -> Nullable, + additional_payout_method_data -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs index 9c51614284..22629fede7 100644 --- a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs @@ -1,7 +1,7 @@ use api_models::enums::PayoutConnectors; use common_enums as storage_enums; use common_utils::{ - id_type, + id_type, payout_method_utils, types::{UnifiedCode, UnifiedMessage}, }; use serde::{Deserialize, Serialize}; @@ -83,6 +83,7 @@ pub struct PayoutAttempt { pub routing_info: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } #[derive(Clone, Debug, PartialEq)] @@ -108,6 +109,7 @@ pub struct PayoutAttemptNew { pub routing_info: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } #[derive(Debug, Clone)] @@ -136,6 +138,9 @@ pub enum PayoutAttemptUpdate { routing_info: Option, merchant_connector_id: Option, }, + AdditionalPayoutMethodDataUpdate { + additional_payout_method_data: Option, + }, } #[derive(Clone, Debug, Default)] @@ -155,6 +160,7 @@ pub struct PayoutAttemptUpdateInternal { pub merchant_connector_id: Option, pub unified_code: Option, pub unified_message: Option, + pub additional_payout_method_data: Option, } impl From for PayoutAttemptUpdateInternal { @@ -204,6 +210,12 @@ impl From for PayoutAttemptUpdateInternal { merchant_connector_id, ..Default::default() }, + PayoutAttemptUpdate::AdditionalPayoutMethodDataUpdate { + additional_payout_method_data, + } => Self { + additional_payout_method_data, + ..Default::default() + }, } } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index ae999bb060..a76726eb8a 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -198,6 +198,16 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::types::TimeRange, common_utils::link_utils::GenericLinkUiConfig, common_utils::link_utils::EnabledPaymentMethod, + common_utils::payout_method_utils::AdditionalPayoutMethodData, + common_utils::payout_method_utils::CardAdditionalData, + common_utils::payout_method_utils::BankAdditionalData, + common_utils::payout_method_utils::WalletAdditionalData, + common_utils::payout_method_utils::AchBankTransferAdditionalData, + common_utils::payout_method_utils::BacsBankTransferAdditionalData, + common_utils::payout_method_utils::SepaBankTransferAdditionalData, + common_utils::payout_method_utils::PixBankTransferAdditionalData, + common_utils::payout_method_utils::PaypalAdditionalData, + common_utils::payout_method_utils::VenmoAdditionalData, api_models::refunds::RefundRequest, api_models::refunds::RefundType, api_models::refunds::RefundResponse, @@ -510,6 +520,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutListResponse, api_models::payouts::PayoutRetrieveBody, api_models::payouts::PayoutMethodData, + api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, api_models::payouts::PayoutCreatePayoutLinkConfig, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index c08ea9bf7c..715af43def 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -126,6 +126,16 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::types::TimeRange, common_utils::link_utils::GenericLinkUiConfig, common_utils::link_utils::EnabledPaymentMethod, + common_utils::payout_method_utils::AdditionalPayoutMethodData, + common_utils::payout_method_utils::CardAdditionalData, + common_utils::payout_method_utils::BankAdditionalData, + common_utils::payout_method_utils::WalletAdditionalData, + common_utils::payout_method_utils::AchBankTransferAdditionalData, + common_utils::payout_method_utils::BacsBankTransferAdditionalData, + common_utils::payout_method_utils::SepaBankTransferAdditionalData, + common_utils::payout_method_utils::PixBankTransferAdditionalData, + common_utils::payout_method_utils::PaypalAdditionalData, + common_utils::payout_method_utils::VenmoAdditionalData, api_models::refunds::RefundRequest, api_models::refunds::RefundType, api_models::refunds::RefundResponse, @@ -439,6 +449,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutRetrieveBody, api_models::payouts::PayoutRetrieveRequest, api_models::payouts::PayoutMethodData, + api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, api_models::payouts::PayoutCreatePayoutLinkConfig, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index c96da24443..c1b8f78eeb 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -2353,6 +2353,11 @@ pub async fn response_handler( ) .await?; + let additional_payout_method_data = payout_attempt.additional_payout_method_data.clone(); + + let payout_method_data = + additional_payout_method_data.map(payouts::PayoutMethodDataResponse::from); + let response = api::PayoutCreateResponse { payout_id: payouts.payout_id.to_owned(), merchant_id: merchant_account.get_id().to_owned(), @@ -2360,6 +2365,7 @@ pub async fn response_handler( currency: payouts.destination_currency.to_owned(), connector: payout_attempt.connector, payout_type: payouts.payout_type.to_owned(), + payout_method_data, billing, auto_fulfill: payouts.auto_fulfill, customer_id, @@ -2548,9 +2554,20 @@ pub async fn payout_create_db_entries( // Make payout_attempt entry let payout_attempt_id = utils::get_payout_attempt_id(payout_id, 1); + let additional_pm_data_value = req + .payout_method_data + .clone() + .or(stored_payout_method_data.cloned()) + .async_and_then(|payout_method_data| async move { + helpers::get_additional_payout_data(&payout_method_data, &*state.store, profile_id) + .await + }) + .await; + let payout_attempt_req = storage::PayoutAttemptNew { payout_attempt_id: payout_attempt_id.to_string(), payout_id: payout_id.to_owned(), + additional_payout_method_data: additional_pm_data_value, merchant_id: merchant_id.to_owned(), status, business_country: req.business_country.to_owned(), @@ -2644,7 +2661,7 @@ pub async fn make_payout_data( let payout_attempt_id = utils::get_payout_attempt_id(payout_id, payouts.attempt_count); - let payout_attempt = db + let mut payout_attempt = db .find_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, &payout_attempt_id, @@ -2705,7 +2722,7 @@ pub async fn make_payout_data( // Validate whether profile_id passed in request is valid and is linked to the merchant let business_profile = validate_and_get_business_profile(state, key_store, &profile_id, merchant_id).await?; - let payout_method_data = match req { + let payout_method_data_req = match req { payouts::PayoutRequest::PayoutCreateRequest(r) => r.payout_method_data.to_owned(), payouts::PayoutRequest::PayoutActionRequest(_) => { match payout_attempt.payout_token.to_owned() { @@ -2733,6 +2750,28 @@ pub async fn make_payout_data( payouts::PayoutRequest::PayoutRetrieveRequest(_) => None, }; + if let Some(payout_method_data) = payout_method_data_req.clone() { + let additional_payout_method_data = + helpers::get_additional_payout_data(&payout_method_data, &*state.store, &profile_id) + .await; + + let update_additional_payout_method_data = + storage::PayoutAttemptUpdate::AdditionalPayoutMethodDataUpdate { + additional_payout_method_data, + }; + + payout_attempt = db + .update_payout_attempt( + &payout_attempt, + update_additional_payout_method_data, + &payouts, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating additional payout method data in payout_attempt")?; + }; + let merchant_connector_account = if payout_attempt.connector.is_some() && payout_attempt.merchant_connector_id.is_some() { let connector_name = payout_attempt @@ -2773,7 +2812,7 @@ pub async fn make_payout_data( customer_details, payouts, payout_attempt, - payout_method_data: payout_method_data.to_owned(), + payout_method_data: payout_method_data_req.to_owned(), merchant_connector_account, should_terminate: false, profile_id, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 43412ebfd7..7417947cac 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -5,7 +5,7 @@ use common_utils::{ encryption::Encryption, errors::CustomResult, ext_traits::{AsyncExt, StringExt}, - fp_utils, id_type, type_name, + fp_utils, id_type, payout_method_utils as payout_additional, type_name, types::{ keymanager::{Identifier, KeyManagerState}, MinorUnit, UnifiedCode, UnifiedMessage, @@ -1296,3 +1296,83 @@ pub async fn get_translated_unified_code_and_message( })? .or_else(|| unified_message.cloned())) } + +pub async fn get_additional_payout_data( + pm_data: &api::PayoutMethodData, + db: &dyn StorageInterface, + profile_id: &id_type::ProfileId, +) -> Option { + match pm_data { + api::PayoutMethodData::Card(card_data) => { + let card_isin = Some(card_data.card_number.get_card_isin()); + let enable_extended_bin =db + .find_config_by_key_unwrap_or( + format!("{}_enable_extended_card_bin", profile_id.get_string_repr()).as_str(), + Some("false".to_string())) + .await.map_err(|err| services::logger::error!(message="Failed to fetch the config", extended_card_bin_error=?err)).ok(); + + let card_extended_bin = match enable_extended_bin { + Some(config) if config.config == "true" => { + Some(card_data.card_number.get_extended_card_bin()) + } + _ => None, + }; + let last4 = Some(card_data.card_number.get_last4()); + + let card_info = 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| { + payout_additional::AdditionalPayoutMethodData::Card(Box::new( + payout_additional::CardAdditionalData { + card_issuer: card_info.card_issuer, + card_network: card_info.card_network.clone(), + bank_code: card_info.bank_code, + card_type: card_info.card_type, + card_issuing_country: card_info.card_issuing_country, + last4: last4.clone(), + card_isin: card_isin.clone(), + card_extended_bin: card_extended_bin.clone(), + card_exp_month: Some(card_data.expiry_month.clone()), + card_exp_year: Some(card_data.expiry_year.clone()), + card_holder_name: card_data.card_holder_name.clone(), + }, + )) + }); + Some(card_info.unwrap_or_else(|| { + payout_additional::AdditionalPayoutMethodData::Card(Box::new( + payout_additional::CardAdditionalData { + card_issuer: None, + card_network: None, + bank_code: None, + card_type: None, + card_issuing_country: None, + last4, + card_isin, + card_extended_bin, + card_exp_month: Some(card_data.expiry_month.clone()), + card_exp_year: Some(card_data.expiry_year.clone()), + card_holder_name: card_data.card_holder_name.clone(), + }, + )) + })) + } + api::PayoutMethodData::Bank(bank_data) => { + Some(payout_additional::AdditionalPayoutMethodData::Bank( + Box::new(bank_data.to_owned().into()), + )) + } + api::PayoutMethodData::Wallet(wallet_data) => { + Some(payout_additional::AdditionalPayoutMethodData::Wallet( + Box::new(wallet_data.to_owned().into()), + )) + } + } +} diff --git a/crates/router/src/core/payouts/retry.rs b/crates/router/src/core/payouts/retry.rs index 00c25b0d67..ed7e4a9b4b 100644 --- a/crates/router/src/core/payouts/retry.rs +++ b/crates/router/src/core/payouts/retry.rs @@ -300,6 +300,10 @@ pub async fn modify_trackers( routing_info: None, unified_code: None, unified_message: None, + additional_payout_method_data: payout_data + .payout_attempt + .additional_payout_method_data + .to_owned(), }; payout_data.payout_attempt = db .insert_payout_attempt( diff --git a/crates/router/src/core/payouts/transformers.rs b/crates/router/src/core/payouts/transformers.rs index 080cf775b2..c1f3140430 100644 --- a/crates/router/src/core/payouts/transformers.rs +++ b/crates/router/src/core/payouts/transformers.rs @@ -99,6 +99,7 @@ impl connector_transaction_id: attempt.connector_transaction_id.clone(), priority: payout.priority, billing: address, + payout_method_data: payout_attempt.additional_payout_method_data.map(From::from), client_secret: None, payout_link: None, unified_code: attempt.unified_code.clone(), diff --git a/crates/router/src/types/api/payouts.rs b/crates/router/src/types/api/payouts.rs index 984fba1c8d..a9630beb25 100644 --- a/crates/router/src/types/api/payouts.rs +++ b/crates/router/src/types/api/payouts.rs @@ -3,8 +3,8 @@ pub use api_models::payouts::{ PayoutActionRequest, PayoutAttemptResponse, PayoutCreateRequest, PayoutCreateResponse, PayoutEnabledPaymentMethodsInfo, PayoutLinkResponse, PayoutListConstraints, PayoutListFilterConstraints, PayoutListFilters, PayoutListResponse, PayoutMethodData, - PayoutRequest, PayoutRetrieveBody, PayoutRetrieveRequest, PixBankTransfer, - RequiredFieldsOverrideRequest, SepaBankTransfer, Wallet as WalletPayout, + PayoutMethodDataResponse, PayoutRequest, PayoutRetrieveBody, PayoutRetrieveRequest, + PixBankTransfer, RequiredFieldsOverrideRequest, SepaBankTransfer, Wallet as WalletPayout, }; pub use hyperswitch_domain_models::router_flow_types::payouts::{ PoCancel, PoCreate, PoEligibility, PoFulfill, PoQuote, PoRecipient, PoRecipientAccount, PoSync, diff --git a/crates/storage_impl/src/payouts/payout_attempt.rs b/crates/storage_impl/src/payouts/payout_attempt.rs index e5bfe29af5..33d73f99e5 100644 --- a/crates/storage_impl/src/payouts/payout_attempt.rs +++ b/crates/storage_impl/src/payouts/payout_attempt.rs @@ -67,6 +67,9 @@ impl PayoutAttemptInterface for KVRouterStore { let created_attempt = PayoutAttempt { payout_attempt_id: new_payout_attempt.payout_attempt_id.clone(), payout_id: new_payout_attempt.payout_id.clone(), + additional_payout_method_data: new_payout_attempt + .additional_payout_method_data + .clone(), customer_id: new_payout_attempt.customer_id.clone(), merchant_id: new_payout_attempt.merchant_id.clone(), address_id: new_payout_attempt.address_id.clone(), @@ -529,6 +532,7 @@ impl DataModelExt for PayoutAttempt { routing_info: self.routing_info, unified_code: self.unified_code, unified_message: self.unified_message, + additional_payout_method_data: self.additional_payout_method_data, } } @@ -555,6 +559,7 @@ impl DataModelExt for PayoutAttempt { routing_info: storage_model.routing_info, unified_code: storage_model.unified_code, unified_message: storage_model.unified_message, + additional_payout_method_data: storage_model.additional_payout_method_data, } } } @@ -584,6 +589,7 @@ impl DataModelExt for PayoutAttemptNew { routing_info: self.routing_info, unified_code: self.unified_code, unified_message: self.unified_message, + additional_payout_method_data: self.additional_payout_method_data, } } @@ -610,6 +616,7 @@ impl DataModelExt for PayoutAttemptNew { routing_info: storage_model.routing_info, unified_code: storage_model.unified_code, unified_message: storage_model.unified_message, + additional_payout_method_data: storage_model.additional_payout_method_data, } } } @@ -657,6 +664,11 @@ impl DataModelExt for PayoutAttemptUpdate { routing_info, merchant_connector_id, }, + Self::AdditionalPayoutMethodDataUpdate { + additional_payout_method_data, + } => DieselPayoutAttemptUpdate::AdditionalPayoutMethodDataUpdate { + additional_payout_method_data, + }, } } diff --git a/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/down.sql b/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/down.sql new file mode 100644 index 0000000000..97d1657196 --- /dev/null +++ b/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payout_attempt DROP COLUMN IF EXISTS additional_payout_method_data; \ No newline at end of file diff --git a/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/up.sql b/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/up.sql new file mode 100644 index 0000000000..c630b321d8 --- /dev/null +++ b/migrations/2024-09-15-080630_add_addtional_payout_method_data_column_to_payout_attempt_table/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payout_attempt +ADD COLUMN IF NOT EXISTS additional_payout_method_data JSONB DEFAULT NULL; \ No newline at end of file