From 0c649158a8ee491e2ff6ff37e922266d5b64b22d Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 1 Jul 2025 16:30:42 +0530 Subject: [PATCH] feat(connector): [ADYENPLATFORM] add card payouts (#8504) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../adyenplatform/transformers/payouts.rs | 262 +++++++++++++----- .../defaults/payout_required_fields.rs | 18 +- 2 files changed, 194 insertions(+), 86 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs index 1b1f2b42b7..7262ec9f3f 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs @@ -1,6 +1,4 @@ -use api_models::payouts; -#[cfg(feature = "payouts")] -use api_models::webhooks; +use api_models::{payouts, webhooks}; use common_enums::enums; use common_utils::pii; use error_stack::{report, ResultExt}; @@ -13,7 +11,7 @@ use super::{AdyenPlatformRouterData, Error}; use crate::{ connectors::adyen::transformers as adyen, types::PayoutsResponseRouterData, - utils::{self, PayoutsData as _, RouterData as _}, + utils::{self, AddressDetailsData, PayoutsData as _, RouterData as _}, }; #[derive(Debug, Default, Serialize, Deserialize)] @@ -38,7 +36,7 @@ pub struct AdyenTransferRequest { balance_account_id: Secret, category: AdyenPayoutMethod, counterparty: AdyenPayoutMethodDetails, - priority: AdyenPayoutPriority, + priority: Option, reference: String, reference_for_beneficiary: String, description: Option, @@ -48,32 +46,50 @@ pub struct AdyenTransferRequest { #[serde(rename_all = "camelCase")] pub enum AdyenPayoutMethod { Bank, + Card, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct AdyenPayoutMethodDetails { - bank_account: AdyenBankAccountDetails, +pub enum AdyenPayoutMethodDetails { + BankAccount(AdyenBankAccountDetails), + Card(AdyenCardDetails), + #[serde(rename = "card")] + CardToken(AdyenCardTokenDetails), } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenBankAccountDetails { - account_holder: AdyenBankAccountHolder, + account_holder: AdyenAccountHolder, account_identification: AdyenBankAccountIdentification, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AdyenBankAccountHolder { - address: Option, - full_name: Secret, +pub struct AdyenAccountHolder { + address: AdyenAddress, + first_name: Option>, + last_name: Option>, + full_name: Option>, #[serde(rename = "reference")] customer_id: Option, #[serde(rename = "type")] entity_type: Option, } +#[serde_with::skip_serializing_none] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenAddress { + line1: Secret, + line2: Secret, + postal_code: Option>, + state_or_province: Option>, + city: String, + country: enums::CountryAlpha2, +} + #[derive(Debug, Serialize, Deserialize)] pub struct AdyenBankAccountIdentification { #[serde(rename = "type")] @@ -93,6 +109,38 @@ pub struct SepaDetails { iban: Secret, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenCardDetails { + card_holder: AdyenAccountHolder, + card_identification: AdyenCardIdentification, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenCardIdentification { + #[serde(rename = "number")] + card_number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + issue_number: Option, + start_month: Option, + start_year: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenCardTokenDetails { + card_holder: AdyenAccountHolder, + card_identification: AdyenCardTokenIdentification, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenCardTokenIdentification { + stored_payment_method_id: Secret, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum AdyenPayoutPriority { @@ -120,7 +168,7 @@ pub struct AdyenTransferResponse { amount: adyen::Amount, balance_account: AdyenBalanceAccount, category: AdyenPayoutMethod, - category_data: AdyenCategoryData, + category_data: Option, direction: AdyenTransactionDirection, reference: String, reference_for_beneficiary: String, @@ -168,25 +216,101 @@ pub enum AdyenTransferStatus { #[serde(rename_all = "camelCase")] pub enum AdyenTransactionType { BankTransfer, + CardTransfer, InternalTransfer, Payment, Refund, } +impl TryFrom<&hyperswitch_domain_models::address::AddressDetails> for AdyenAddress { + type Error = Error; + + fn try_from( + address: &hyperswitch_domain_models::address::AddressDetails, + ) -> Result { + let line1 = address + .get_line1() + .change_context(ConnectorError::MissingRequiredField { + field_name: "billing.address.line1", + })? + .clone(); + let line2 = address + .get_line2() + .change_context(ConnectorError::MissingRequiredField { + field_name: "billing.address.line2", + })? + .clone(); + Ok(Self { + line1, + line2, + postal_code: address.get_optional_zip(), + state_or_province: address.get_optional_state(), + city: address.get_city()?.to_owned(), + country: address.get_country()?.to_owned(), + }) + } +} + +impl TryFrom<(&types::PayoutsRouterData, enums::PayoutType)> for AdyenAccountHolder { + type Error = Error; + + fn try_from( + (router_data, payout_type): (&types::PayoutsRouterData, enums::PayoutType), + ) -> Result { + let billing_address = router_data.get_billing_address()?; + let (first_name, last_name, full_name) = match payout_type { + enums::PayoutType::Card => ( + Some(router_data.get_billing_first_name()?), + Some(router_data.get_billing_last_name()?), + None, + ), + enums::PayoutType::Bank => (None, None, Some(router_data.get_billing_full_name()?)), + _ => Err(ConnectorError::NotSupported { + message: "Payout method not supported".to_string(), + connector: "Adyen", + })?, + }; + Ok(Self { + address: billing_address.try_into()?, + first_name, + last_name, + full_name, + customer_id: Some(router_data.get_customer_id()?.get_string_repr().to_owned()), + entity_type: Some(EntityType::from(router_data.request.entity_type)), + }) + } +} + impl TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData>> for AdyenTransferRequest { type Error = Error; fn try_from( item: &AdyenPlatformRouterData<&types::PayoutsRouterData>, ) -> Result { - let request = item.router_data.request.to_owned(); - match item.router_data.get_payout_method_data()? { - payouts::PayoutMethodData::Card(_) | payouts::PayoutMethodData::Wallet(_) => { - Err(ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Adyenplatform"), - ))? + let request = &item.router_data.request; + let (counterparty, priority) = match item.router_data.get_payout_method_data()? { + payouts::PayoutMethodData::Wallet(_) => Err(ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Adyenplatform"), + ))?, + payouts::PayoutMethodData::Card(c) => { + let card_holder: AdyenAccountHolder = + (item.router_data, enums::PayoutType::Card).try_into()?; + let card_identification = AdyenCardIdentification { + card_number: c.card_number, + expiry_month: c.expiry_month, + expiry_year: c.expiry_year, + issue_number: None, + start_month: None, + start_year: None, + }; + let counterparty = AdyenPayoutMethodDetails::Card(AdyenCardDetails { + card_holder, + card_identification, + }); + (counterparty, None) } - payouts::PayoutMethodData::Bank(bd) => { + let account_holder: AdyenAccountHolder = + (item.router_data, enums::PayoutType::Bank).try_into()?; let bank_details = match bd { payouts::Bank::Sepa(b) => AdyenBankAccountIdentification { bank_type: "iban".to_string(), @@ -207,56 +331,39 @@ impl TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData>> for Adye connector: "Adyenplatform", })?, }; - let billing_address = item.router_data.get_optional_billing(); - let address = adyen::get_address_info(billing_address).transpose()?; - let account_holder = AdyenBankAccountHolder { - address, - full_name: item.router_data.get_billing_full_name()?, - customer_id: Some( - item.router_data - .get_customer_id()? - .get_string_repr() - .to_owned(), - ), - entity_type: Some(EntityType::from(request.entity_type)), - }; - let counterparty = AdyenPayoutMethodDetails { - bank_account: AdyenBankAccountDetails { - account_holder, - account_identification: bank_details, - }, - }; - - let adyen_connector_metadata_object = - AdyenPlatformConnectorMetadataObject::try_from( - &item.router_data.connector_meta_data, - )?; - let balance_account_id = adyen_connector_metadata_object - .source_balance_account - .ok_or(ConnectorError::InvalidConnectorConfig { - config: "metadata.source_balance_account", - })?; + let counterparty = AdyenPayoutMethodDetails::BankAccount(AdyenBankAccountDetails { + account_holder, + account_identification: bank_details, + }); let priority = request .priority .ok_or(ConnectorError::MissingRequiredField { field_name: "priority", })?; - let payout_type = request.get_payout_type()?; - Ok(Self { - amount: adyen::Amount { - value: item.amount, - currency: request.destination_currency, - }, - balance_account_id, - category: AdyenPayoutMethod::try_from(payout_type)?, - counterparty, - priority: AdyenPayoutPriority::from(priority), - reference: item.router_data.connector_request_reference_id.clone(), - reference_for_beneficiary: request.payout_id, - description: item.router_data.description.clone(), - }) + (counterparty, Some(AdyenPayoutPriority::from(priority))) } - } + }; + let adyen_connector_metadata_object = + AdyenPlatformConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?; + let balance_account_id = adyen_connector_metadata_object + .source_balance_account + .ok_or(ConnectorError::InvalidConnectorConfig { + config: "metadata.source_balance_account", + })?; + let payout_type = request.get_payout_type()?; + Ok(Self { + amount: adyen::Amount { + value: item.amount, + currency: request.destination_currency, + }, + balance_account_id, + category: AdyenPayoutMethod::try_from(payout_type)?, + counterparty, + priority, + reference: item.router_data.connector_request_reference_id.clone(), + reference_for_beneficiary: request.payout_id.clone(), + description: item.router_data.description.clone(), + }) } } @@ -332,17 +439,15 @@ impl TryFrom for AdyenPayoutMethod { fn try_from(payout_type: enums::PayoutType) -> Result { match payout_type { enums::PayoutType::Bank => Ok(Self::Bank), - enums::PayoutType::Card | enums::PayoutType::Wallet => { - Err(report!(ConnectorError::NotSupported { - message: "Card or wallet payouts".to_string(), - connector: "Adyenplatform", - })) - } + enums::PayoutType::Card => Ok(Self::Card), + enums::PayoutType::Wallet => Err(report!(ConnectorError::NotSupported { + message: "Card or wallet payouts".to_string(), + connector: "Adyenplatform", + })), } } } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformIncomingWebhook { @@ -351,7 +456,6 @@ pub struct AdyenplatformIncomingWebhook { pub webhook_type: AdyenplatformWebhookEventType, } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformIncomingWebhookData { @@ -360,7 +464,6 @@ pub struct AdyenplatformIncomingWebhookData { pub tracking: Option, } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformInstantStatus { @@ -368,7 +471,6 @@ pub struct AdyenplatformInstantStatus { estimated_arrival_time: Option, } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum InstantPriorityStatus { @@ -376,7 +478,6 @@ pub enum InstantPriorityStatus { Credited, } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] pub enum AdyenplatformWebhookEventType { #[serde(rename = "balancePlatform.transfer.created")] @@ -385,7 +486,6 @@ pub enum AdyenplatformWebhookEventType { PayoutUpdated, } -#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum AdyenplatformWebhookStatus { @@ -396,7 +496,6 @@ pub enum AdyenplatformWebhookStatus { Returned, Received, } -#[cfg(feature = "payouts")] pub fn get_adyen_webhook_event( event_type: AdyenplatformWebhookEventType, status: AdyenplatformWebhookStatus, @@ -434,4 +533,13 @@ pub struct AdyenTransferErrorResponse { pub title: String, pub detail: Option, pub request_id: Option, + pub invalid_fields: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenInvalidField { + pub name: Option, + pub value: Option, + pub message: Option, } diff --git a/crates/router/src/configs/defaults/payout_required_fields.rs b/crates/router/src/configs/defaults/payout_required_fields.rs index 5ec8161c59..a056a3ac93 100644 --- a/crates/router/src/configs/defaults/payout_required_fields.rs +++ b/crates/router/src/configs/defaults/payout_required_fields.rs @@ -437,15 +437,6 @@ fn get_billing_details(connector: PayoutConnectors) -> HashMap HashMap HashMap::from([ (