mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
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:
@ -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>,
|
||||
}
|
||||
|
||||
@ -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([
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user