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; use api_models::{payouts, webhooks};
#[cfg(feature = "payouts")]
use api_models::webhooks;
use common_enums::enums; use common_enums::enums;
use common_utils::pii; use common_utils::pii;
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
@ -13,7 +11,7 @@ use super::{AdyenPlatformRouterData, Error};
use crate::{ use crate::{
connectors::adyen::transformers as adyen, connectors::adyen::transformers as adyen,
types::PayoutsResponseRouterData, types::PayoutsResponseRouterData,
utils::{self, PayoutsData as _, RouterData as _}, utils::{self, AddressDetailsData, PayoutsData as _, RouterData as _},
}; };
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@ -38,7 +36,7 @@ pub struct AdyenTransferRequest {
balance_account_id: Secret<String>, balance_account_id: Secret<String>,
category: AdyenPayoutMethod, category: AdyenPayoutMethod,
counterparty: AdyenPayoutMethodDetails, counterparty: AdyenPayoutMethodDetails,
priority: AdyenPayoutPriority, priority: Option<AdyenPayoutPriority>,
reference: String, reference: String,
reference_for_beneficiary: String, reference_for_beneficiary: String,
description: Option<String>, description: Option<String>,
@ -48,32 +46,50 @@ pub struct AdyenTransferRequest {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum AdyenPayoutMethod { pub enum AdyenPayoutMethod {
Bank, Bank,
Card,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenPayoutMethodDetails { pub enum AdyenPayoutMethodDetails {
bank_account: AdyenBankAccountDetails, BankAccount(AdyenBankAccountDetails),
Card(AdyenCardDetails),
#[serde(rename = "card")]
CardToken(AdyenCardTokenDetails),
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenBankAccountDetails { pub struct AdyenBankAccountDetails {
account_holder: AdyenBankAccountHolder, account_holder: AdyenAccountHolder,
account_identification: AdyenBankAccountIdentification, account_identification: AdyenBankAccountIdentification,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenBankAccountHolder { pub struct AdyenAccountHolder {
address: Option<adyen::Address>, address: AdyenAddress,
full_name: Secret<String>, first_name: Option<Secret<String>>,
last_name: Option<Secret<String>>,
full_name: Option<Secret<String>>,
#[serde(rename = "reference")] #[serde(rename = "reference")]
customer_id: Option<String>, customer_id: Option<String>,
#[serde(rename = "type")] #[serde(rename = "type")]
entity_type: Option<EntityType>, 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct AdyenBankAccountIdentification { pub struct AdyenBankAccountIdentification {
#[serde(rename = "type")] #[serde(rename = "type")]
@ -93,6 +109,38 @@ pub struct SepaDetails {
iban: Secret<String>, 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)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum AdyenPayoutPriority { pub enum AdyenPayoutPriority {
@ -120,7 +168,7 @@ pub struct AdyenTransferResponse {
amount: adyen::Amount, amount: adyen::Amount,
balance_account: AdyenBalanceAccount, balance_account: AdyenBalanceAccount,
category: AdyenPayoutMethod, category: AdyenPayoutMethod,
category_data: AdyenCategoryData, category_data: Option<AdyenCategoryData>,
direction: AdyenTransactionDirection, direction: AdyenTransactionDirection,
reference: String, reference: String,
reference_for_beneficiary: String, reference_for_beneficiary: String,
@ -168,25 +216,101 @@ pub enum AdyenTransferStatus {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum AdyenTransactionType { pub enum AdyenTransactionType {
BankTransfer, BankTransfer,
CardTransfer,
InternalTransfer, InternalTransfer,
Payment, Payment,
Refund, 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 { impl<F> TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData<F>>> for AdyenTransferRequest {
type Error = Error; type Error = Error;
fn try_from( fn try_from(
item: &AdyenPlatformRouterData<&types::PayoutsRouterData<F>>, item: &AdyenPlatformRouterData<&types::PayoutsRouterData<F>>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let request = item.router_data.request.to_owned(); let request = &item.router_data.request;
match item.router_data.get_payout_method_data()? { let (counterparty, priority) = match item.router_data.get_payout_method_data()? {
payouts::PayoutMethodData::Card(_) | payouts::PayoutMethodData::Wallet(_) => { payouts::PayoutMethodData::Wallet(_) => Err(ConnectorError::NotImplemented(
Err(ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Adyenplatform"),
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) => { payouts::PayoutMethodData::Bank(bd) => {
let account_holder: AdyenAccountHolder =
(item.router_data, enums::PayoutType::Bank).try_into()?;
let bank_details = match bd { let bank_details = match bd {
payouts::Bank::Sepa(b) => AdyenBankAccountIdentification { payouts::Bank::Sepa(b) => AdyenBankAccountIdentification {
bank_type: "iban".to_string(), bank_type: "iban".to_string(),
@ -207,56 +331,39 @@ impl<F> TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData<F>>> for Adye
connector: "Adyenplatform", connector: "Adyenplatform",
})?, })?,
}; };
let billing_address = item.router_data.get_optional_billing(); let counterparty = AdyenPayoutMethodDetails::BankAccount(AdyenBankAccountDetails {
let address = adyen::get_address_info(billing_address).transpose()?; account_holder,
let account_holder = AdyenBankAccountHolder { account_identification: bank_details,
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 priority = request let priority = request
.priority .priority
.ok_or(ConnectorError::MissingRequiredField { .ok_or(ConnectorError::MissingRequiredField {
field_name: "priority", field_name: "priority",
})?; })?;
let payout_type = request.get_payout_type()?; (counterparty, Some(AdyenPayoutPriority::from(priority)))
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(),
})
} }
} };
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> { fn try_from(payout_type: enums::PayoutType) -> Result<Self, Self::Error> {
match payout_type { match payout_type {
enums::PayoutType::Bank => Ok(Self::Bank), enums::PayoutType::Bank => Ok(Self::Bank),
enums::PayoutType::Card | enums::PayoutType::Wallet => { enums::PayoutType::Card => Ok(Self::Card),
Err(report!(ConnectorError::NotSupported { enums::PayoutType::Wallet => Err(report!(ConnectorError::NotSupported {
message: "Card or wallet payouts".to_string(), message: "Card or wallet payouts".to_string(),
connector: "Adyenplatform", connector: "Adyenplatform",
})) })),
}
} }
} }
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenplatformIncomingWebhook { pub struct AdyenplatformIncomingWebhook {
@ -351,7 +456,6 @@ pub struct AdyenplatformIncomingWebhook {
pub webhook_type: AdyenplatformWebhookEventType, pub webhook_type: AdyenplatformWebhookEventType,
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenplatformIncomingWebhookData { pub struct AdyenplatformIncomingWebhookData {
@ -360,7 +464,6 @@ pub struct AdyenplatformIncomingWebhookData {
pub tracking: Option<AdyenplatformInstantStatus>, pub tracking: Option<AdyenplatformInstantStatus>,
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AdyenplatformInstantStatus { pub struct AdyenplatformInstantStatus {
@ -368,7 +471,6 @@ pub struct AdyenplatformInstantStatus {
estimated_arrival_time: Option<String>, estimated_arrival_time: Option<String>,
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum InstantPriorityStatus { pub enum InstantPriorityStatus {
@ -376,7 +478,6 @@ pub enum InstantPriorityStatus {
Credited, Credited,
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum AdyenplatformWebhookEventType { pub enum AdyenplatformWebhookEventType {
#[serde(rename = "balancePlatform.transfer.created")] #[serde(rename = "balancePlatform.transfer.created")]
@ -385,7 +486,6 @@ pub enum AdyenplatformWebhookEventType {
PayoutUpdated, PayoutUpdated,
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum AdyenplatformWebhookStatus { pub enum AdyenplatformWebhookStatus {
@ -396,7 +496,6 @@ pub enum AdyenplatformWebhookStatus {
Returned, Returned,
Received, Received,
} }
#[cfg(feature = "payouts")]
pub fn get_adyen_webhook_event( pub fn get_adyen_webhook_event(
event_type: AdyenplatformWebhookEventType, event_type: AdyenplatformWebhookEventType,
status: AdyenplatformWebhookStatus, status: AdyenplatformWebhookStatus,
@ -434,4 +533,13 @@ pub struct AdyenTransferErrorResponse {
pub title: String, pub title: String,
pub detail: Option<String>, pub detail: Option<String>,
pub request_id: 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, 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(), "billing.address.country".to_string(),
RequiredFieldInfo { RequiredFieldInfo {
@ -469,6 +460,15 @@ fn get_billing_details(connector: PayoutConnectors) -> HashMap<String, RequiredF
value: None, 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([ PayoutConnectors::Wise => HashMap::from([
( (