feat(connector): [ADYENPLATFORM] add card payouts (#8504)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2025-07-01 16:30:42 +05:30
committed by GitHub
parent f8dc3ecfe6
commit 0c649158a8
2 changed files with 194 additions and 86 deletions

View File

@ -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<String>,
category: AdyenPayoutMethod,
counterparty: AdyenPayoutMethodDetails,
priority: AdyenPayoutPriority,
priority: Option<AdyenPayoutPriority>,
reference: String,
reference_for_beneficiary: String,
description: Option<String>,
@ -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<adyen::Address>,
full_name: Secret<String>,
pub struct AdyenAccountHolder {
address: AdyenAddress,
first_name: Option<Secret<String>>,
last_name: Option<Secret<String>>,
full_name: Option<Secret<String>>,
#[serde(rename = "reference")]
customer_id: Option<String>,
#[serde(rename = "type")]
entity_type: Option<EntityType>,
}
#[serde_with::skip_serializing_none]
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenAddress {
line1: Secret<String>,
line2: Secret<String>,
postal_code: Option<Secret<String>>,
state_or_province: Option<Secret<String>>,
city: String,
country: enums::CountryAlpha2,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdyenBankAccountIdentification {
#[serde(rename = "type")]
@ -93,6 +109,38 @@ pub struct SepaDetails {
iban: Secret<String>,
}
#[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<String>,
expiry_year: Secret<String>,
issue_number: Option<String>,
start_month: Option<String>,
start_year: Option<String>,
}
#[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<String>,
}
#[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<AdyenCategoryData>,
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<Self, Self::Error> {
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<F> TryFrom<(&types::PayoutsRouterData<F>, enums::PayoutType)> for AdyenAccountHolder {
type Error = Error;
fn try_from(
(router_data, payout_type): (&types::PayoutsRouterData<F>, enums::PayoutType),
) -> Result<Self, Self::Error> {
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<F> TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData<F>>> for AdyenTransferRequest {
type Error = Error;
fn try_from(
item: &AdyenPlatformRouterData<&types::PayoutsRouterData<F>>,
) -> Result<Self, Self::Error> {
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<F> TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData<F>>> 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<enums::PayoutType> for AdyenPayoutMethod {
fn try_from(payout_type: enums::PayoutType) -> Result<Self, Self::Error> {
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<AdyenplatformInstantStatus>,
}
#[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<String>,
}
#[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<String>,
pub request_id: Option<String>,
pub invalid_fields: Option<Vec<AdyenInvalidField>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenInvalidField {
pub name: Option<String>,
pub value: Option<String>,
pub message: Option<String>,
}

View File

@ -437,15 +437,6 @@ fn get_billing_details(connector: PayoutConnectors) -> HashMap<String, RequiredF
value: None,
},
),
(
"billing.address.zip".to_string(),
RequiredFieldInfo {
required_field: "billing.address.zip".to_string(),
display_name: "billing_address_zip".to_string(),
field_type: FieldType::Text,
value: None,
},
),
(
"billing.address.country".to_string(),
RequiredFieldInfo {
@ -469,6 +460,15 @@ fn get_billing_details(connector: PayoutConnectors) -> HashMap<String, RequiredF
value: None,
},
),
(
"billing.address.last_name".to_string(),
RequiredFieldInfo {
required_field: "billing.address.last_name".to_string(),
display_name: "billing_address_last_name".to_string(),
field_type: FieldType::Text,
value: None,
},
),
]),
PayoutConnectors::Wise => HashMap::from([
(