From 9b9bce80a6419abdd5318d993f1abd6598853dd3 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:36:26 +0530 Subject: [PATCH] refactor(payment_method_data): add a trait to retrieve billing from payment method data (#4095) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 600 ++++++++++++++++++++++++++++-- 1 file changed, 576 insertions(+), 24 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 40d11543c0..349e3548eb 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -992,6 +992,49 @@ pub enum PayLaterData { AtomeRedirect {}, } +impl GetAddressFromPaymentMethodData for PayLaterData { + fn get_billing_address(&self) -> Option
{ + match self { + Self::KlarnaRedirect { + billing_email, + billing_country, + } => { + let address_details = AddressDetails { + country: Some(*billing_country), + ..AddressDetails::default() + }; + + Some(Address { + address: Some(address_details), + email: Some(billing_email.clone()), + phone: None, + }) + } + Self::AfterpayClearpayRedirect { + billing_email, + billing_name, + } => { + let address_details = AddressDetails { + first_name: Some(billing_name.clone()), + ..AddressDetails::default() + }; + + Some(Address { + address: Some(address_details), + email: Some(billing_email.clone()), + phone: None, + }) + } + Self::PayBrightRedirect {} + | Self::WalleyRedirect {} + | Self::AlmaRedirect {} + | Self::KlarnaSdk { .. } + | Self::AffirmRedirect {} + | Self::AtomeRedirect {} => None, + } + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, ToSchema, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum BankDebitData { @@ -1059,6 +1102,50 @@ pub enum BankDebitData { }, } +impl GetAddressFromPaymentMethodData for BankDebitData { + fn get_billing_address(&self) -> Option
{ + fn get_billing_address_inner( + bank_debit_billing: &BankDebitBilling, + bank_account_holder_name: Option<&Secret>, + ) -> Option
{ + // We will always have address here + let mut address = bank_debit_billing.get_billing_address()?; + + // Prefer `account_holder_name` over `name` + address.address.as_mut().map(|address| { + address.first_name = bank_account_holder_name + .or(address.first_name.as_ref()) + .cloned(); + }); + + Some(address) + } + + match self { + Self::AchBankDebit { + billing_details, + bank_account_holder_name, + .. + } + | Self::SepaBankDebit { + billing_details, + bank_account_holder_name, + .. + } + | Self::BecsBankDebit { + billing_details, + bank_account_holder_name, + .. + } + | Self::BacsBankDebit { + billing_details, + bank_account_holder_name, + .. + } => get_billing_address_inner(billing_details, bank_account_holder_name.as_ref()), + } + } +} + /// Custom serializer and deserializer for PaymentMethodData mod payment_method_data_serde { @@ -1196,35 +1283,44 @@ pub enum PaymentMethodData { CardToken(CardToken), } -impl PaymentMethodData { - pub fn get_payment_method_type_if_session_token_type( - &self, - ) -> Option { +pub trait GetAddressFromPaymentMethodData { + fn get_billing_address(&self) -> Option
; +} + +impl GetAddressFromPaymentMethodData for PaymentMethodData { + fn get_billing_address(&self) -> Option
{ match self { - Self::Wallet(wallet) => match wallet { - WalletData::ApplePay(_) => Some(api_enums::PaymentMethodType::ApplePay), - WalletData::GooglePay(_) => Some(api_enums::PaymentMethodType::GooglePay), - WalletData::PaypalSdk(_) => Some(api_enums::PaymentMethodType::Paypal), - _ => None, - }, - Self::PayLater(pay_later) => match pay_later { - PayLaterData::KlarnaSdk { .. } => Some(api_enums::PaymentMethodType::Klarna), - _ => None, - }, - Self::Card(_) - | Self::CardRedirect(_) - | Self::BankRedirect(_) - | Self::BankDebit(_) - | Self::BankTransfer(_) - | Self::Crypto(_) - | Self::MandatePayment - | Self::Reward {} + Self::Card(card_data) => { + card_data + .card_holder_name + .as_ref() + .map(|card_holder_name| Address { + address: Some(AddressDetails { + first_name: Some(card_holder_name.clone()), + ..AddressDetails::default() + }), + email: None, + phone: None, + }) + } + Self::CardRedirect(_) => None, + Self::Wallet(wallet_data) => wallet_data.get_billing_address(), + Self::PayLater(pay_later_data) => pay_later_data.get_billing_address(), + Self::BankRedirect(bank_redirect_data) => bank_redirect_data.get_billing_address(), + Self::BankDebit(bank_debit_data) => bank_debit_data.get_billing_address(), + Self::BankTransfer(bank_transfer_data) => bank_transfer_data.get_billing_address(), + Self::Voucher(voucher_data) => voucher_data.get_billing_address(), + Self::Crypto(_) + | Self::Reward | Self::Upi(_) - | Self::Voucher(_) | Self::GiftCard(_) - | Self::CardToken(_) => None, + | Self::CardToken(_) + | Self::MandatePayment => None, } } +} + +impl PaymentMethodData { pub fn apply_additional_payment_data( &self, additional_payment_data: AdditionalPaymentData, @@ -1647,6 +1743,123 @@ pub enum BankRedirectData { }, } +impl GetAddressFromPaymentMethodData for BankRedirectData { + fn get_billing_address(&self) -> Option
{ + let get_billing_address_inner = |bank_redirect_billing: Option<&BankRedirectBilling>, + billing_country: Option<&common_enums::CountryAlpha2>, + billing_email: Option<&Email>| + -> Option
{ + let address = bank_redirect_billing + .and_then(GetAddressFromPaymentMethodData::get_billing_address); + + let address = match (address, billing_country) { + (Some(mut address), Some(billing_country)) => { + address + .address + .as_mut() + .map(|address| address.country = Some(*billing_country)); + + Some(address) + } + (Some(address), None) => Some(address), + (None, Some(billing_country)) => Some(Address { + address: Some(AddressDetails { + country: Some(*billing_country), + ..AddressDetails::default() + }), + phone: None, + email: None, + }), + (None, None) => None, + }; + + match (address, billing_email) { + (Some(mut address), Some(email)) => { + address.email = Some(email.clone()); + Some(address) + } + (Some(address), None) => Some(address), + (None, Some(billing_email)) => Some(Address { + address: None, + phone: None, + email: Some(billing_email.clone()), + }), + (None, None) => None, + } + }; + + match self { + Self::BancontactCard { + billing_details, + card_holder_name, + .. + } => { + let address = get_billing_address_inner(billing_details.as_ref(), None, None); + + if let Some(mut address) = address { + address.address.as_mut().map(|address| { + address.first_name = card_holder_name + .as_ref() + .or(address.first_name.as_ref()) + .cloned(); + }); + + Some(address) + } else { + Some(Address { + address: Some(AddressDetails { + first_name: card_holder_name.clone(), + ..AddressDetails::default() + }), + phone: None, + email: None, + }) + } + } + Self::Eps { + billing_details, + country, + .. + } + | Self::Giropay { + billing_details, + country, + .. + } + | Self::Ideal { + billing_details, + country, + .. + } + | Self::Sofort { + billing_details, + country, + .. + } => get_billing_address_inner(billing_details.as_ref(), country.as_ref(), None), + Self::Interac { country, email } => { + get_billing_address_inner(None, Some(country), Some(email)) + } + Self::OnlineBankingFinland { email } => { + get_billing_address_inner(None, None, email.as_ref()) + } + Self::OpenBankingUk { country, .. } => { + get_billing_address_inner(None, country.as_ref(), None) + } + Self::Przelewy24 { + billing_details, .. + } => get_billing_address_inner(Some(billing_details), None, None), + Self::Trustly { country } => get_billing_address_inner(None, Some(country), None), + Self::OnlineBankingFpx { .. } + | Self::OnlineBankingThailand { .. } + | Self::Bizum {} + | Self::OnlineBankingPoland { .. } + | Self::OnlineBankingSlovakia { .. } + | Self::OnlineBankingCzechRepublic { .. } + | Self::Blik { .. } => None, + } + } +} + #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct AlfamartVoucherData { /// The billing first name for Alfamart @@ -1755,6 +1968,28 @@ pub struct BankRedirectBilling { pub email: Option, } +impl GetAddressFromPaymentMethodData for BankRedirectBilling { + fn get_billing_address(&self) -> Option
{ + let address_details = self + .billing_name + .as_ref() + .map(|billing_name| AddressDetails { + first_name: Some(billing_name.clone()), + ..AddressDetails::default() + }); + + if address_details.is_some() || self.email.is_some() { + Some(Address { + address: address_details, + phone: None, + email: self.email.clone(), + }) + } else { + None + } + } +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum BankTransferData { @@ -1810,6 +2045,59 @@ pub enum BankTransferData { Pse {}, } +impl GetAddressFromPaymentMethodData for BankTransferData { + fn get_billing_address(&self) -> Option
{ + match self { + Self::AchBankTransfer { billing_details } => Some(Address { + address: None, + phone: None, + email: Some(billing_details.email.clone()), + }), + Self::SepaBankTransfer { + billing_details, + country, + } => Some(Address { + address: Some(AddressDetails { + country: Some(*country), + first_name: Some(billing_details.name.clone()), + ..AddressDetails::default() + }), + phone: None, + email: Some(billing_details.email.clone()), + }), + Self::BacsBankTransfer { billing_details } => Some(Address { + address: Some(AddressDetails { + first_name: Some(billing_details.name.clone()), + ..AddressDetails::default() + }), + phone: None, + email: Some(billing_details.email.clone()), + }), + Self::MultibancoBankTransfer { billing_details } => Some(Address { + address: None, + phone: None, + email: Some(billing_details.email.clone()), + }), + Self::PermataBankTransfer { billing_details } + | Self::BcaBankTransfer { billing_details } + | Self::BniVaBankTransfer { billing_details } + | Self::BriVaBankTransfer { billing_details } + | Self::CimbVaBankTransfer { billing_details } + | Self::DanamonVaBankTransfer { billing_details } + | Self::MandiriVaBankTransfer { billing_details } => Some(Address { + address: Some(AddressDetails { + first_name: Some(billing_details.first_name.clone()), + last_name: billing_details.last_name.clone(), + ..AddressDetails::default() + }), + phone: None, + email: Some(billing_details.email.clone()), + }), + Self::Pix {} | Self::Pse {} => None, + } + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, ToSchema, Eq, PartialEq)] pub struct BankDebitBilling { /// The billing name for bank debits @@ -1822,6 +2110,30 @@ pub struct BankDebitBilling { pub address: Option, } +impl GetAddressFromPaymentMethodData for BankDebitBilling { + fn get_billing_address(&self) -> Option
{ + let address = if let Some(mut address) = self.address.clone() { + address.first_name = Some(self.name.clone()); + Address { + address: Some(address), + email: Some(self.email.clone()), + phone: None, + } + } else { + Address { + address: Some(AddressDetails { + first_name: Some(self.name.clone()), + ..AddressDetails::default() + }), + email: Some(self.email.clone()), + phone: None, + } + }; + + Some(address) + } +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum WalletData { @@ -1878,6 +2190,57 @@ pub enum WalletData { SwishQr(SwishQrData), } +impl GetAddressFromPaymentMethodData for WalletData { + fn get_billing_address(&self) -> Option
{ + match self { + Self::MbWayRedirect(mb_way_redirect) => { + let phone = PhoneDetails { + // Portuguese country code, this payment method is applicable only in portugal + country_code: Some("+351".into()), + number: Some(mb_way_redirect.telephone_number.clone()), + }; + + Some(Address { + phone: Some(phone), + address: None, + email: None, + }) + } + Self::MobilePayRedirect(_) => None, + Self::PaypalRedirect(paypal_redirect) => { + paypal_redirect.email.clone().map(|email| Address { + email: Some(email), + address: None, + phone: None, + }) + } + Self::AliPayQr(_) + | Self::AliPayRedirect(_) + | Self::AliPayHkRedirect(_) + | Self::MomoRedirect(_) + | Self::KakaoPayRedirect(_) + | Self::GoPayRedirect(_) + | Self::GcashRedirect(_) + | Self::ApplePay(_) + | Self::ApplePayRedirect(_) + | Self::ApplePayThirdPartySdk(_) + | Self::DanaRedirect {} + | Self::GooglePay(_) + | Self::GooglePayRedirect(_) + | Self::GooglePayThirdPartySdk(_) + | Self::PaypalSdk(_) + | Self::SamsungPay(_) + | Self::TwintRedirect {} + | Self::VippsRedirect {} + | Self::TouchNGoRedirect(_) + | Self::WeChatPayRedirect(_) + | Self::WeChatPayQr(_) + | Self::CashappQr(_) + | Self::SwishQr(_) => None, + } + } +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SamsungPayWalletData { @@ -2062,6 +2425,54 @@ pub enum VoucherData { PayEasy(Box), } +impl GetAddressFromPaymentMethodData for VoucherData { + fn get_billing_address(&self) -> Option
{ + match self { + Self::Alfamart(voucher_data) => Some(Address { + address: Some(AddressDetails { + first_name: Some(voucher_data.first_name.clone()), + last_name: voucher_data.last_name.clone(), + ..AddressDetails::default() + }), + phone: None, + email: Some(voucher_data.email.clone()), + }), + Self::Indomaret(voucher_data) => Some(Address { + address: Some(AddressDetails { + first_name: Some(voucher_data.first_name.clone()), + last_name: voucher_data.last_name.clone(), + ..AddressDetails::default() + }), + phone: None, + email: Some(voucher_data.email.clone()), + }), + Self::Lawson(voucher_data) + | Self::MiniStop(voucher_data) + | Self::FamilyMart(voucher_data) + | Self::Seicomart(voucher_data) + | Self::PayEasy(voucher_data) + | Self::SevenEleven(voucher_data) => Some(Address { + address: Some(AddressDetails { + first_name: Some(voucher_data.first_name.clone()), + last_name: voucher_data.last_name.clone(), + ..AddressDetails::default() + }), + phone: Some(PhoneDetails { + number: Some(voucher_data.phone_number.clone().into()), + country_code: None, + }), + email: Some(voucher_data.email.clone()), + }), + Self::Boleto(_) + | Self::Efecty + | Self::PagoEfectivo + | Self::RedCompra + | Self::RedPagos + | Self::Oxxo => None, + } + } +} + /// Use custom serializer to provide backwards compatible response for `reward` payment_method_data pub fn serialize_payment_method_data_response( payment_method_data_response: &Option, @@ -4133,3 +4544,144 @@ mod payments_request_api_contract { ); } } + +/// Set of tests to extract billing details from payment method data +/// These are required for backwards compatibility +#[cfg(test)] +mod billing_from_payment_method_data { + #![allow(clippy::unwrap_used)] + use common_enums::CountryAlpha2; + + use super::*; + + const TEST_COUNTRY: CountryAlpha2 = CountryAlpha2::US; + const TEST_FIRST_NAME: &str = "John"; + + #[test] + fn test_wallet_payment_method_data_paypal() { + let test_email: Email = Email::try_from("example@example.com".to_string()).unwrap(); + + let paypal_wallet_payment_method_data = + PaymentMethodData::Wallet(WalletData::PaypalRedirect(PaypalRedirection { + email: Some(test_email.clone()), + })); + + let billing_address = paypal_wallet_payment_method_data + .get_billing_address() + .unwrap(); + + assert_eq!(billing_address.email.unwrap(), test_email); + + assert!(billing_address.address.is_none()); + assert!(billing_address.phone.is_none()); + } + + #[test] + fn test_bank_redirect_payment_method_data_eps() { + let test_email = Email::try_from("example@example.com".to_string()).unwrap(); + let test_first_name = Secret::new(String::from("Chaser")); + + let bank_redirect_billing = BankRedirectBilling { + billing_name: Some(test_first_name.clone()), + email: Some(test_email.clone()), + }; + + let eps_bank_redirect_payment_method_data = + PaymentMethodData::BankRedirect(BankRedirectData::Eps { + billing_details: Some(bank_redirect_billing), + bank_name: None, + country: Some(TEST_COUNTRY), + }); + + let billing_address = eps_bank_redirect_payment_method_data + .get_billing_address() + .unwrap(); + + let address_details = billing_address.address.unwrap(); + + assert_eq!(billing_address.email.unwrap(), test_email); + assert_eq!(address_details.country.unwrap(), TEST_COUNTRY); + assert_eq!(address_details.first_name.unwrap(), test_first_name); + assert!(billing_address.phone.is_none()); + } + + #[test] + fn test_paylater_payment_method_data_klarna() { + let test_email: Email = Email::try_from("example@example.com".to_string()).unwrap(); + + let klarna_paylater_payment_method_data = + PaymentMethodData::PayLater(PayLaterData::KlarnaRedirect { + billing_email: test_email.clone(), + billing_country: TEST_COUNTRY, + }); + + let billing_address = klarna_paylater_payment_method_data + .get_billing_address() + .unwrap(); + + assert_eq!(billing_address.email.unwrap(), test_email); + assert_eq!( + billing_address.address.unwrap().country.unwrap(), + TEST_COUNTRY + ); + assert!(billing_address.phone.is_none()); + } + + #[test] + fn test_bank_debit_payment_method_data_ach() { + let test_email = Email::try_from("example@example.com".to_string()).unwrap(); + let test_first_name = Secret::new(String::from("Chaser")); + + let bank_redirect_billing = BankDebitBilling { + name: test_first_name.clone(), + address: None, + email: test_email.clone(), + }; + + let ach_bank_debit_payment_method_data = + PaymentMethodData::BankDebit(BankDebitData::AchBankDebit { + billing_details: bank_redirect_billing, + account_number: Secret::new("1234".to_string()), + routing_number: Secret::new("1235".to_string()), + card_holder_name: None, + bank_account_holder_name: None, + bank_name: None, + bank_type: None, + bank_holder_type: None, + }); + + let billing_address = ach_bank_debit_payment_method_data + .get_billing_address() + .unwrap(); + + let address_details = billing_address.address.unwrap(); + + assert_eq!(billing_address.email.unwrap(), test_email); + assert_eq!(address_details.first_name.unwrap(), test_first_name); + assert!(billing_address.phone.is_none()); + } + + #[test] + fn test_card_payment_method_data() { + let card_payment_method_data = PaymentMethodData::Card(Card { + card_holder_name: Some(Secret::new(TEST_FIRST_NAME.into())), + ..Default::default() + }); + + let billing_address = card_payment_method_data.get_billing_address().unwrap(); + + assert_eq!( + billing_address.address.unwrap().first_name.unwrap(), + Secret::new(TEST_FIRST_NAME.into()) + ); + } + + #[test] + fn test_card_payment_method_data_empty() { + let card_payment_method_data = PaymentMethodData::Card(Card::default()); + + let billing_address = card_payment_method_data.get_billing_address(); + + assert!(billing_address.is_none()); + } +}