diff --git a/config/config.example.toml b/config/config.example.toml index ac428298e7..206ba392b7 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -166,6 +166,7 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" @@ -267,6 +268,7 @@ wallets = ["klarna", "mifinity", "braintree", "applepay"] rewards = ["cashtocode", "zen"] cards = [ "adyen", + "adyenplatform", "authorizedotnet", "coinbase", "cryptopay", diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 3248021578..1ea7d4f448 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -20,6 +20,7 @@ przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_peka aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 0e3ebd1c1f..9b9b6aac22 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -24,6 +24,7 @@ payout_connector_list = "stripe,wise" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/checkout/" adyen.secondary_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" +adyenplatform.base_url = "https://balanceplatform-api-live.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://api.authorize.net/xml/v1/request.api" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 28ac1dca02..891e4b1f17 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -24,6 +24,7 @@ payout_connector_list = "stripe,wise" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" diff --git a/config/development.toml b/config/development.toml index e9d84395e6..a3f9eb54bb 100644 --- a/config/development.toml +++ b/config/development.toml @@ -94,6 +94,7 @@ rewards = ["cashtocode", "zen"] cards = [ "aci", "adyen", + "adyenplatform", "airwallex", "authorizedotnet", "bambora", @@ -166,6 +167,7 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index f43ceca893..e6e2fd3815 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -104,6 +104,7 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" @@ -186,6 +187,7 @@ rewards = ["cashtocode", "zen"] cards = [ "aci", "adyen", + "adyenplatform", "airwallex", "authorizedotnet", "bambora", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c2969a1661..ce039c1156 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -44,6 +44,7 @@ pub enum RoutingAlgorithm { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum Connector { + Adyenplatform, #[cfg(feature = "dummy_connector")] #[serde(rename = "phonypay")] #[strum(serialize = "phonypay")] @@ -142,7 +143,7 @@ impl Connector { pub fn supports_instant_payout(&self, payout_method: PayoutType) -> bool { matches!( (self, payout_method), - (Self::Paypal, PayoutType::Wallet) | (_, PayoutType::Card) + (Self::Paypal, PayoutType::Wallet) | (_, PayoutType::Card) | (Self::Adyenplatform, _) ) } #[cfg(feature = "payouts")] @@ -191,6 +192,7 @@ impl Connector { | Self::DummyConnector7 => false, Self::Aci | Self::Adyen + | Self::Adyenplatform | Self::Airwallex | Self::Authorizedotnet | Self::Bambora @@ -302,11 +304,12 @@ impl AuthenticationConnectors { #[strum(serialize_all = "snake_case")] pub enum PayoutConnectors { Adyen, - Stripe, + Adyenplatform, + Cybersource, + Ebanx, Payone, Paypal, - Ebanx, - Cybersource, + Stripe, Wise, } @@ -315,11 +318,12 @@ impl From for RoutableConnectors { fn from(value: PayoutConnectors) -> Self { match value { PayoutConnectors::Adyen => Self::Adyen, - PayoutConnectors::Stripe => Self::Stripe, + PayoutConnectors::Adyenplatform => Self::Adyenplatform, + PayoutConnectors::Cybersource => Self::Cybersource, + PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Payone => Self::Payone, PayoutConnectors::Paypal => Self::Paypal, - PayoutConnectors::Ebanx => Self::Ebanx, - PayoutConnectors::Cybersource => Self::Cybersource, + PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, } } @@ -330,11 +334,12 @@ impl From for Connector { fn from(value: PayoutConnectors) -> Self { match value { PayoutConnectors::Adyen => Self::Adyen, - PayoutConnectors::Stripe => Self::Stripe, + PayoutConnectors::Adyenplatform => Self::Adyenplatform, + PayoutConnectors::Cybersource => Self::Cybersource, + PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Payone => Self::Payone, PayoutConnectors::Paypal => Self::Paypal, - PayoutConnectors::Ebanx => Self::Ebanx, - PayoutConnectors::Cybersource => Self::Cybersource, + PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, } } @@ -346,12 +351,13 @@ impl TryFrom for PayoutConnectors { fn try_from(value: Connector) -> Result { match value { Connector::Adyen => Ok(Self::Adyen), + Connector::Adyenplatform => Ok(Self::Adyenplatform), + Connector::Cybersource => Ok(Self::Cybersource), + Connector::Ebanx => Ok(Self::Ebanx), + Connector::Payone => Ok(Self::Payone), + Connector::Paypal => Ok(Self::Paypal), Connector::Stripe => Ok(Self::Stripe), Connector::Wise => Ok(Self::Wise), - Connector::Paypal => Ok(Self::Paypal), - Connector::Ebanx => Ok(Self::Ebanx), - Connector::Cybersource => Ok(Self::Cybersource), - Connector::Payone => Ok(Self::Payone), _ => Err(format!("Invalid payout connector {}", value)), } } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 2df40bd675..a068518007 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -148,6 +148,10 @@ pub struct PayoutCreateRequest { /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. pub profile_id: Option, + + /// The send method for processing payouts + #[schema(value_type = PayoutSendPriority, example = "instant")] + pub priority: Option, } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] @@ -441,6 +445,14 @@ pub struct PayoutCreateResponse { #[serde(with = "common_utils::custom_serde::iso8601::option")] pub created: Option, + /// Underlying processor's payout resource ID + #[schema(value_type = Option, example = "S3FC9G9M2MVFDXT5")] + pub connector_transaction_id: Option, + + /// Payout's send priority (if applicable) + #[schema(value_type = Option, example = "instant")] + pub priority: Option, + /// List of attempts #[schema(value_type = Option>)] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e0ed251b7a..edb088032c 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -83,6 +83,7 @@ pub enum AttemptStatus { #[strum(serialize_all = "snake_case")] /// Connectors eligible for payments routing pub enum RoutableConnectors { + Adyenplatform, #[cfg(feature = "dummy_connector")] #[serde(rename = "phonypay")] #[strum(serialize = "phonypay")] @@ -2196,6 +2197,31 @@ pub enum PayoutEntityType { Personal, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum PayoutSendPriority { + Instant, + Fast, + Regular, + Wire, + CrossBorder, + Internal, +} + #[derive( Clone, Copy, diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs index 950af06752..cd1d82f441 100644 --- a/crates/connector_configs/src/common_config.rs +++ b/crates/connector_configs/src/common_config.rs @@ -98,6 +98,7 @@ pub struct ApiModelMetaData { pub three_ds_requestor_id: Option, pub pull_mechanism_for_external_3ds_enabled: Option, pub klarna_region: Option, + pub source_balance_account: Option, } #[serde_with::skip_serializing_none] @@ -211,4 +212,5 @@ pub struct DashboardMetaData { pub three_ds_requestor_id: Option, pub pull_mechanism_for_external_3ds_enabled: Option, pub klarna_region: Option, + pub source_balance_account: Option, } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index d7c505f794..b386d52517 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -101,6 +101,7 @@ pub struct ConfigMetadata { pub three_ds_requestor_id: Option, pub pull_mechanism_for_external_3ds_enabled: Option, pub klarna_region: Option>, + pub source_balance_account: Option, } #[serde_with::skip_serializing_none] @@ -131,6 +132,8 @@ pub struct ConnectorConfig { pub adyen: Option, #[cfg(feature = "payouts")] pub adyen_payout: Option, + #[cfg(feature = "payouts")] + pub adyenplatform_payout: Option, pub airwallex: Option, pub authorizedotnet: Option, pub bankofamerica: Option, @@ -235,12 +238,13 @@ impl ConnectorConfig { let connector_data = Self::new()?; match connector { PayoutConnectors::Adyen => Ok(connector_data.adyen_payout), + PayoutConnectors::Adyenplatform => Ok(connector_data.adyenplatform_payout), + PayoutConnectors::Cybersource => Ok(connector_data.cybersource_payout), + PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout), + PayoutConnectors::Payone => Ok(connector_data.payone_payout), + PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), PayoutConnectors::Stripe => Ok(connector_data.stripe_payout), PayoutConnectors::Wise => Ok(connector_data.wise_payout), - PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), - PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout), - PayoutConnectors::Cybersource => Ok(connector_data.cybersource_payout), - PayoutConnectors::Payone => Ok(connector_data.payone_payout), } } @@ -262,6 +266,7 @@ impl ConnectorConfig { match connector { Connector::Aci => Ok(connector_data.aci), Connector::Adyen => Ok(connector_data.adyen), + Connector::Adyenplatform => Err("Use get_payout_connector_config".to_string()), Connector::Airwallex => Ok(connector_data.airwallex), Connector::Authorizedotnet => Ok(connector_data.authorizedotnet), Connector::Bankofamerica => Ok(connector_data.bankofamerica), diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs index 16a50c3dcd..51da723d71 100644 --- a/crates/connector_configs/src/response_modifier.rs +++ b/crates/connector_configs/src/response_modifier.rs @@ -334,6 +334,7 @@ impl From for DashboardMetaData { pull_mechanism_for_external_3ds_enabled: api_model .pull_mechanism_for_external_3ds_enabled, klarna_region: api_model.klarna_region, + source_balance_account: api_model.source_balance_account, } } } diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs index be7f31416f..06730f54f7 100644 --- a/crates/connector_configs/src/transformer.rs +++ b/crates/connector_configs/src/transformer.rs @@ -200,6 +200,7 @@ impl DashboardRequestPayload { pull_mechanism_for_external_3ds_enabled: None, paypal_sdk: None, klarna_region: None, + source_balance_account: None, }; let meta_data = match request.metadata { Some(data) => data, @@ -225,6 +226,7 @@ impl DashboardRequestPayload { let pull_mechanism_for_external_3ds_enabled = meta_data.pull_mechanism_for_external_3ds_enabled; let klarna_region = meta_data.klarna_region; + let source_balance_account = meta_data.source_balance_account; Some(ApiModelMetaData { google_pay, @@ -246,6 +248,7 @@ impl DashboardRequestPayload { three_ds_requestor_id, pull_mechanism_for_external_3ds_enabled, klarna_region, + source_balance_account, }) } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index e614ce45dc..9b4d7b921a 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -249,6 +249,14 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" +[adyenplatform_payout] +[[adyenplatform_payout.bank_transfer]] + payment_method_type = "sepa" +[adyenplatform_payout.metadata] +source_balance_account="Source balance account ID" +[adyenplatform_payout.connector_auth.HeaderKey] +api_key="Adyen platform's API Key" + [airwallex] [[airwallex.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index e337dba4e7..348c463f56 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -249,6 +249,14 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" +[adyenplatform_payout] +[[adyenplatform_payout.bank_transfer]] + payment_method_type = "sepa" +[adyenplatform_payout.metadata] + source_balance_account = "Source balance account ID" +[adyenplatform_payout.connector_auth.HeaderKey] +api_key = "Adyen platform's API Key" + [airwallex] [[airwallex.credit]] payment_method_type = "Mastercard" diff --git a/crates/diesel_models/src/payouts.rs b/crates/diesel_models/src/payouts.rs index 1549610d4e..11b2cf366e 100644 --- a/crates/diesel_models/src/payouts.rs +++ b/crates/diesel_models/src/payouts.rs @@ -33,6 +33,7 @@ pub struct Payouts { pub profile_id: String, pub status: storage_enums::PayoutStatus, pub confirm: Option, + pub priority: Option, } #[derive( @@ -71,6 +72,7 @@ pub struct PayoutsNew { pub profile_id: String, pub status: storage_enums::PayoutStatus, pub confirm: Option, + pub priority: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2c57feb4cd..ff5f6aef0e 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1023,6 +1023,8 @@ diesel::table! { profile_id -> Varchar, status -> PayoutStatus, confirm -> Nullable, + #[max_length = 32] + priority -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payouts/payouts.rs b/crates/hyperswitch_domain_models/src/payouts/payouts.rs index c9017fff5b..c86fb7d5f5 100644 --- a/crates/hyperswitch_domain_models/src/payouts/payouts.rs +++ b/crates/hyperswitch_domain_models/src/payouts/payouts.rs @@ -90,6 +90,7 @@ pub struct Payouts { pub profile_id: String, pub status: storage_enums::PayoutStatus, pub confirm: Option, + pub priority: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -115,6 +116,7 @@ pub struct PayoutsNew { pub profile_id: String, pub status: storage_enums::PayoutStatus, pub confirm: Option, + pub priority: Option, } impl Default for PayoutsNew { @@ -143,6 +145,7 @@ impl Default for PayoutsNew { profile_id: String::default(), status: storage_enums::PayoutStatus::default(), confirm: None, + priority: None, } } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index f06f336b84..f6e7e891ad 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -636,6 +636,7 @@ pub struct PayoutsData { pub entity_type: storage_enums::PayoutEntityType, pub customer_details: Option, pub vendor_details: Option, + pub priority: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index c15704afa3..8016e1411a 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -461,6 +461,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutMethodData, api_models::payouts::Bank, api_models::enums::PayoutEntityType, + api_models::enums::PayoutSendPriority, api_models::enums::PayoutStatus, api_models::enums::PayoutType, api_models::enums::TransactionType, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 9aa879a438..fb892f79ec 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -535,6 +535,7 @@ pub struct Connectors { pub aci: ConnectorParams, #[cfg(feature = "payouts")] pub adyen: ConnectorParamsWithSecondaryBaseUrl, + pub adyenplatform: ConnectorParams, #[cfg(not(feature = "payouts"))] pub adyen: ConnectorParams, pub airwallex: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index c3fb730b12..a423decdb9 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -1,5 +1,6 @@ pub mod aci; pub mod adyen; +pub mod adyenplatform; pub mod airwallex; pub mod authorizedotnet; pub mod bambora; @@ -66,13 +67,13 @@ pub mod zsl; #[cfg(feature = "dummy_connector")] pub use self::dummyconnector::DummyConnector; pub use self::{ - aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet, - bambora::Bambora, bankofamerica::Bankofamerica, billwerk::Billwerk, bitpay::Bitpay, - bluesnap::Bluesnap, boku::Boku, braintree::Braintree, cashtocode::Cashtocode, - checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay, cybersource::Cybersource, - dlocal::Dlocal, ebanx::Ebanx, fiserv::Fiserv, forte::Forte, globalpay::Globalpay, - globepay::Globepay, gocardless::Gocardless, gpayments::Gpayments, helcim::Helcim, - iatapay::Iatapay, klarna::Klarna, mifinity::Mifinity, mollie::Mollie, + aci::Aci, adyen::Adyen, adyenplatform::Adyenplatform, airwallex::Airwallex, + authorizedotnet::Authorizedotnet, bambora::Bambora, bankofamerica::Bankofamerica, + billwerk::Billwerk, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, braintree::Braintree, + cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay, + cybersource::Cybersource, dlocal::Dlocal, ebanx::Ebanx, fiserv::Fiserv, forte::Forte, + globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, gpayments::Gpayments, + helcim::Helcim, iatapay::Iatapay, klarna::Klarna, mifinity::Mifinity, mollie::Mollie, multisafepay::Multisafepay, netcetera::Netcetera, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, payone::Payone, paypal::Paypal, payu::Payu, placetopay::Placetopay, powertranz::Powertranz, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index c8b8ced7bd..91969b00c9 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1726,7 +1726,7 @@ fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) } } -fn get_address_info( +pub fn get_address_info( address: Option<&payments::Address>, ) -> Option>> { address.and_then(|add| { diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs new file mode 100644 index 0000000000..e085b0bbc9 --- /dev/null +++ b/crates/router/src/connector/adyenplatform.rs @@ -0,0 +1,294 @@ +pub mod transformers; +use std::fmt::Debug; + +#[cfg(feature = "payouts")] +use common_utils::request::RequestContent; +use error_stack::{report, ResultExt}; +#[cfg(feature = "payouts")] +use router_env::{instrument, tracing}; + +use self::transformers as adyenplatform; +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon}, + }, +}; +#[cfg(feature = "payouts")] +use crate::{events::connector_api_logs::ConnectorEvent, utils::BytesExt}; + +#[derive(Debug, Clone)] +pub struct Adyenplatform; + +impl ConnectorCommon for Adyenplatform { + fn id(&self) -> &'static str { + "adyenplatform" + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = adyenplatform::AdyenplatformAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.into_masked(), + )]) + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.adyenplatform.base_url.as_ref() + } + + #[cfg(feature = "payouts")] + fn build_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: adyenplatform::AdyenTransferErrorResponse = res + .response + .parse_struct("AdyenTransferErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(types::ErrorResponse { + status_code: res.status_code, + code: response.error_code, + message: response.title, + reason: response.detail, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl api::Payment for Adyenplatform {} +impl api::PaymentAuthorize for Adyenplatform {} +impl api::PaymentSync for Adyenplatform {} +impl api::PaymentVoid for Adyenplatform {} +impl api::PaymentCapture for Adyenplatform {} +impl api::MandateSetup for Adyenplatform {} +impl api::ConnectorAccessToken for Adyenplatform {} +impl api::PaymentToken for Adyenplatform {} +impl ConnectorValidation for Adyenplatform {} + +impl + services::ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl + services::ConnectorIntegration< + api::AccessTokenAuth, + types::AccessTokenRequestData, + types::AccessToken, + > for Adyenplatform +{ +} + +impl + services::ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl api::PaymentSession for Adyenplatform {} + +impl + services::ConnectorIntegration< + api::Session, + types::PaymentsSessionData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl + services::ConnectorIntegration< + api::Capture, + types::PaymentsCaptureData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl + services::ConnectorIntegration + for Adyenplatform +{ +} + +impl + services::ConnectorIntegration< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl + services::ConnectorIntegration< + api::Void, + types::PaymentsCancelData, + types::PaymentsResponseData, + > for Adyenplatform +{ +} + +impl api::Payouts for Adyenplatform {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Adyenplatform {} + +#[cfg(feature = "payouts")] +impl services::ConnectorIntegration + for Adyenplatform +{ + fn get_url( + &self, + _req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}btl/v4/transfers", + connectors.adyenplatform.base_url, + )) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PayoutFulfillType::get_content_type(self) + .to_string() + .into(), + )]; + let auth = adyenplatform::AdyenplatformAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let mut api_key = vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.into_masked(), + )]; + header.append(&mut api_key); + Ok(header) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = adyenplatform::AdyenTransferRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutFulfillType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutFulfillType::get_headers( + self, req, connectors, + )?) + .set_body(types::PayoutFulfillType::get_request_body( + self, req, connectors, + )?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + let response: adyenplatform::AdyenTransferResponse = res + .response + .parse_struct("AdyenTransferResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl api::Refund for Adyenplatform {} +impl api::RefundExecute for Adyenplatform {} +impl api::RefundSync for Adyenplatform {} + +impl services::ConnectorIntegration + for Adyenplatform +{ +} + +impl services::ConnectorIntegration + for Adyenplatform +{ +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Adyenplatform { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} diff --git a/crates/router/src/connector/adyenplatform/transformers.rs b/crates/router/src/connector/adyenplatform/transformers.rs new file mode 100644 index 0000000000..4b74de3f7f --- /dev/null +++ b/crates/router/src/connector/adyenplatform/transformers.rs @@ -0,0 +1,29 @@ +use error_stack::Report; +use masking::Secret; + +#[cfg(feature = "payouts")] +pub mod payouts; +#[cfg(feature = "payouts")] +pub use payouts::*; + +use crate::{core::errors, types}; + +// Error signature +type Error = Report; + +// Auth Struct +pub struct AdyenplatformAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for AdyenplatformAuthType { + type Error = Error; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/adyenplatform/transformers/payouts.rs b/crates/router/src/connector/adyenplatform/transformers/payouts.rs new file mode 100644 index 0000000000..e927b0d96b --- /dev/null +++ b/crates/router/src/connector/adyenplatform/transformers/payouts.rs @@ -0,0 +1,338 @@ +use common_utils::pii; +use error_stack::{report, ResultExt}; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use super::Error; +use crate::{ + connector::{ + adyen::transformers as adyen, + utils::{self, RouterData}, + }, + core::errors, + types::{self, api::payouts, storage::enums}, +}; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct AdyenPlatformConnectorMetadataObject { + source_balance_account: Option>, +} + +impl TryFrom<&Option> for AdyenPlatformConnectorMetadataObject { + type Error = Error; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenTransferRequest { + amount: adyen::Amount, + balance_account_id: Secret, + category: AdyenPayoutMethod, + counterparty: AdyenPayoutMethodDetails, + priority: AdyenPayoutPriority, + reference: String, + reference_for_beneficiary: String, + description: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AdyenPayoutMethod { + Bank, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenPayoutMethodDetails { + bank_account: AdyenBankAccountDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenBankAccountDetails { + account_holder: AdyenBankAccountHolder, + account_identification: AdyenBankAccountIdentification, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenBankAccountHolder { + address: Option, + full_name: Secret, + #[serde(rename = "reference")] + customer_id: Option, + #[serde(rename = "type")] + entity_type: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdyenBankAccountIdentification { + #[serde(rename = "type")] + bank_type: String, + #[serde(flatten)] + account_details: AdyenBankAccountIdentificationDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AdyenBankAccountIdentificationDetails { + Sepa(SepaDetails), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SepaDetails { + iban: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AdyenPayoutPriority { + Instant, + Fast, + Regular, + Wire, + CrossBorder, + Internal, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EntityType { + Individual, + Organization, + Unknown, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenTransferResponse { + id: String, + account_holder: AdyenPlatformAccountHolder, + amount: adyen::Amount, + balance_account: AdyenBalanceAccount, + category: AdyenPayoutMethod, + category_data: AdyenCategoryData, + direction: AdyenTransactionDirection, + reference: String, + reference_for_beneficiary: String, + status: AdyenTransferStatus, + #[serde(rename = "type")] + transaction_type: AdyenTransactionType, + reason: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdyenPlatformAccountHolder { + description: String, + id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdyenCategoryData { + priority: AdyenPayoutPriority, + #[serde(rename = "type")] + category: AdyenPayoutMethod, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdyenBalanceAccount { + description: String, + id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AdyenTransactionDirection { + Incoming, + Outgoing, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AdyenTransferStatus { + Authorised, + Refused, + Error, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AdyenTransactionType { + BankTransfer, + InternalTransfer, + Payment, + Refund, +} + +impl TryFrom<&types::PayoutsRouterData> for AdyenTransferRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + let request = item.request.to_owned(); + match item.get_payout_method_data()? { + payouts::PayoutMethodData::Card(_) | payouts::PayoutMethodData::Wallet(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Adyenplatform"), + ))? + } + + payouts::PayoutMethodData::Bank(bd) => { + let bank_details = match bd { + payouts::BankPayout::Sepa(b) => AdyenBankAccountIdentification { + bank_type: "iban".to_string(), + account_details: AdyenBankAccountIdentificationDetails::Sepa(SepaDetails { + iban: b.iban, + }), + }, + payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via ACH is not supported".to_string(), + connector: "Adyenplatform", + })?, + payouts::BankPayout::Bacs(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via Bacs is not supported".to_string(), + connector: "Adyenplatform", + })?, + payouts::BankPayout::Pix(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via Pix is not supported".to_string(), + connector: "Adyenplatform", + })?, + }; + let billing_address = item.get_optional_billing(); + let address = adyen::get_address_info(billing_address).transpose()?; + let account_holder = AdyenBankAccountHolder { + address, + full_name: item.get_billing_full_name()?, + customer_id: Some(item.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.connector_meta_data)?; + let balance_account_id = adyen_connector_metadata_object + .source_balance_account + .ok_or(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata.source_balance_account", + })?; + let priority = + request + .priority + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "priority", + })?; + + Ok(Self { + amount: adyen::Amount { + value: request.amount, + currency: request.destination_currency, + }, + balance_account_id, + category: AdyenPayoutMethod::try_from(request.payout_type)?, + counterparty, + priority: AdyenPayoutPriority::from(priority), + reference: request.payout_id.clone(), + reference_for_beneficiary: request.payout_id, + description: item.description.clone(), + }) + } + } + } +} + +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: AdyenTransferResponse = item.response; + + Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::from(response.status)), + connector_payout_id: Some(response.id), + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + }), + ..item.data + }) + } +} + +impl From for enums::PayoutStatus { + fn from(adyen_status: AdyenTransferStatus) -> Self { + match adyen_status { + AdyenTransferStatus::Authorised => Self::Success, + AdyenTransferStatus::Refused => Self::Ineligible, + AdyenTransferStatus::Error => Self::Failed, + } + } +} + +impl From for EntityType { + fn from(entity: enums::PayoutEntityType) -> Self { + match entity { + enums::PayoutEntityType::Individual + | enums::PayoutEntityType::Personal + | enums::PayoutEntityType::NaturalPerson => Self::Individual, + + enums::PayoutEntityType::Company | enums::PayoutEntityType::Business => { + Self::Organization + } + _ => Self::Unknown, + } + } +} + +impl From for AdyenPayoutPriority { + fn from(entity: enums::PayoutSendPriority) -> Self { + match entity { + enums::PayoutSendPriority::Instant => Self::Instant, + enums::PayoutSendPriority::Fast => Self::Fast, + enums::PayoutSendPriority::Regular => Self::Regular, + enums::PayoutSendPriority::Wire => Self::Wire, + enums::PayoutSendPriority::CrossBorder => Self::CrossBorder, + enums::PayoutSendPriority::Internal => Self::Internal, + } + } +} + +impl TryFrom for AdyenPayoutMethod { + type Error = Error; + 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!(errors::ConnectorError::NotSupported { + message: "Card or wallet payouts".to_string(), + connector: "Adyenplatform", + })) + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenTransferErrorResponse { + pub error_code: String, + #[serde(rename = "type")] + pub error_type: String, + pub status: u16, + pub title: String, + pub detail: Option, + pub request_id: Option, +} diff --git a/crates/router/src/connector/stripe/transformers/connect.rs b/crates/router/src/connector/stripe/transformers/connect.rs index 5145421675..7126404b6f 100644 --- a/crates/router/src/connector/stripe/transformers/connect.rs +++ b/crates/router/src/connector/stripe/transformers/connect.rs @@ -295,12 +295,10 @@ impl TryFrom, ) -> Result { - let response: StripeConnectReversalResponse = item.response; - Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(enums::PayoutStatus::Cancelled), - connector_payout_id: Some(response.id), + connector_payout_id: item.data.request.connector_payout_id.clone(), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), @@ -370,7 +368,6 @@ impl TryFrom, ) -> Result { let response: StripeConnectRecipientCreateResponse = item.response; - Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(enums::PayoutStatus::RequiresVendorAccountCreation), @@ -452,12 +449,10 @@ impl TryFrom, ) -> Result { - let response: StripeConnectRecipientAccountCreateResponse = item.response; - Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(enums::PayoutStatus::RequiresCreation), - connector_payout_id: Some(response.id), + connector_payout_id: item.data.request.connector_payout_id.clone(), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index c480a0bc84..589647c6bf 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -557,7 +557,13 @@ impl TryFrom> Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(storage_enums::PayoutStatus::from(response.status)), - connector_payout_id: None, + connector_payout_id: Some( + item.data + .request + .connector_payout_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, + ), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 6b582c2c95..0ba807fd68 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1819,6 +1819,10 @@ pub(crate) fn validate_auth_and_metadata_type( use crate::connector::*; match connector_name { + api_enums::Connector::Adyenplatform => { + adyenplatform::transformers::AdyenplatformAuthType::try_from(val)?; + Ok(()) + } // api_enums::Connector::Mifinity => { // mifinity::transformers::MifinityAuthType::try_from(val)?; // Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 4ca15b7a1f..64254ae14b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -156,6 +156,7 @@ impl } default_imp_for_complete_authorize!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Billwerk, @@ -229,6 +230,7 @@ impl { } default_imp_for_webhook_source_verification!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -319,6 +321,7 @@ impl } default_imp_for_create_customer!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -409,6 +412,7 @@ impl services::ConnectorRedirectResponse for connector::DummyConnec } default_imp_for_connector_redirect_response!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Bitpay, @@ -468,6 +472,7 @@ macro_rules! default_imp_for_connector_request_id { impl api::ConnectorTransactionId for connector::DummyConnector {} default_imp_for_connector_request_id!( + connector::Adyenplatform, connector::Zsl, connector::Aci, connector::Adyen, @@ -560,6 +565,7 @@ impl } default_imp_for_accept_dispute!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -672,6 +678,7 @@ impl } default_imp_for_file_upload!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -761,6 +768,7 @@ impl } default_imp_for_submit_evidence!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -850,6 +858,7 @@ impl } default_imp_for_defend_dispute!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -940,6 +949,7 @@ impl } default_imp_for_pre_processing_steps!( + connector::Adyenplatform, connector::Aci, connector::Authorizedotnet, connector::Bambora, @@ -1090,6 +1100,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_create!( + connector::Adyenplatform, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1180,6 +1191,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_eligibility!( + connector::Adyenplatform, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1354,6 +1366,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_cancel!( + connector::Adyenplatform, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1442,6 +1455,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_quote!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1532,6 +1546,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_recipient!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1624,6 +1639,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_recipient_account!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1714,6 +1730,7 @@ impl } default_imp_for_approve!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1805,6 +1822,7 @@ impl } default_imp_for_reject!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1880,6 +1898,7 @@ macro_rules! default_imp_for_fraud_check { impl api::FraudCheck for connector::DummyConnector {} default_imp_for_fraud_check!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1971,6 +1990,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_sale!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2062,6 +2082,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_checkout!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2153,6 +2174,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_transaction!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2244,6 +2266,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_fulfillment!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2335,6 +2358,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_record_return!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2424,6 +2448,7 @@ impl } default_imp_for_incremental_authorization!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2512,6 +2537,7 @@ impl { } default_imp_for_revoking_mandates!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2660,6 +2686,7 @@ impl { } default_imp_for_connector_authentication!( + connector::Adyenplatform, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2747,6 +2774,7 @@ impl default_imp_for_authorize_session_token!( connector::Aci, connector::Adyen, + connector::Adyenplatform, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 733002fe7c..fa1d68ca23 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -990,7 +990,6 @@ impl ForeignFrom<(storage::Payouts, storage::PayoutAttempt, domain::Customer)> unified_code: None, unified_message: None, }; - let attempts = vec![attempt]; Self { payout_id: payout.payout_id, merchant_id: payout.merchant_id, @@ -1016,7 +1015,9 @@ impl ForeignFrom<(storage::Payouts, storage::PayoutAttempt, domain::Customer)> error_code: payout_attempt.error_code, profile_id: payout.profile_id, created: Some(payout.created_at), - attempts: Some(attempts), + connector_transaction_id: attempt.connector_transaction_id.clone(), + priority: payout.priority, + attempts: Some(vec![attempt]), billing: None, client_secret: None, } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index eb1f54ab6c..4b60db6c79 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -502,7 +502,6 @@ pub async fn payouts_cancel_core( .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); - let connector_payout_id = payout_attempt.connector_payout_id.to_owned(); let status = payout_attempt.status; // Verify if cancellation can be triggered @@ -518,7 +517,7 @@ pub async fn payouts_cancel_core( } else if helpers::is_eligible_for_local_payout_cancellation(status) { let status = storage_enums::PayoutStatus::Cancelled; let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: connector_payout_id.to_owned(), + connector_payout_id: payout_attempt.connector_payout_id.to_owned(), status, error_message: Some("Cancelled by user".to_string()), error_code: None, @@ -1084,7 +1083,10 @@ pub async fn create_recipient( .status .unwrap_or(api_enums::PayoutStatus::RequiresVendorAccountCreation); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: recipient_create_data.connector_payout_id, + connector_payout_id: payout_data + .payout_attempt + .connector_payout_id + .to_owned(), status, error_code: None, error_message: None, @@ -1248,7 +1250,7 @@ pub async fn check_payout_eligibility( Err(err) => { let status = storage_enums::PayoutStatus::Failed; let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.clone(), + connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, error_code: Some(err.code), error_message: Some(err.message), @@ -1440,7 +1442,7 @@ pub async fn create_payout( Err(err) => { let status = storage_enums::PayoutStatus::Failed; let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.clone(), + connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, error_code: Some(err.code), error_message: Some(err.message), @@ -1563,7 +1565,7 @@ pub async fn create_recipient_disburse_account( } Err(err) => { let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.clone(), + connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status: storage_enums::PayoutStatus::Failed, error_code: Some(err.code), error_message: Some(err.message), @@ -1659,7 +1661,7 @@ pub async fn cancel_payout( Err(err) => { let status = storage_enums::PayoutStatus::Failed; let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.clone(), + connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, error_code: Some(err.code), error_message: Some(err.message), @@ -1762,7 +1764,7 @@ pub async fn fulfill_payout( .await?; } let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), + connector_payout_id: payout_response_data.connector_payout_id, status, error_code: None, error_message: None, @@ -1799,7 +1801,7 @@ pub async fn fulfill_payout( Err(err) => { let status = storage_enums::PayoutStatus::Failed; let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_data.payout_attempt.connector_payout_id.clone(), + connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, error_code: Some(err.code), error_message: Some(err.message), @@ -1896,6 +1898,8 @@ pub async fn response_handler( error_code: payout_attempt.error_code, profile_id: payout_attempt.profile_id, created: Some(payouts.created_at), + connector_transaction_id: payout_attempt.connector_payout_id, + priority: payouts.priority, attempts: None, }; Ok(services::ApplicationResponse::Json(response)) @@ -1992,6 +1996,7 @@ pub async fn payout_create_db_entries( attempt_count: 1, metadata: req.metadata.clone(), confirm: req.confirm, + priority: req.priority, ..Default::default() }; let payouts = db diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 4791ea823d..1071e5c98d 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -151,7 +151,7 @@ pub async fn construct_payout_router_data<'a, F>( let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.merchant_id.to_owned(), - customer_id: None, + customer_id: customer_details.to_owned().map(|c| c.customer_id), connector_customer: connector_customer_id, connector: connector_name.to_string(), payment_id: "".to_string(), @@ -175,6 +175,7 @@ pub async fn construct_payout_router_data<'a, F>( entity_type: payouts.entity_type.to_owned(), payout_type: payouts.payout_type, vendor_details, + priority: payouts.priority, customer_details: customer_details .to_owned() .map(|c| payments::CustomerDetails { diff --git a/crates/router/src/services/kafka/payout.rs b/crates/router/src/services/kafka/payout.rs index 517ae3de0e..9f2d388729 100644 --- a/crates/router/src/services/kafka/payout.rs +++ b/crates/router/src/services/kafka/payout.rs @@ -28,6 +28,7 @@ pub struct KafkaPayout<'a> { pub last_modified_at: OffsetDateTime, pub attempt_count: i16, pub status: storage_enums::PayoutStatus, + pub priority: Option, pub connector: Option<&'a String>, pub connector_payout_id: Option<&'a String>, @@ -63,6 +64,7 @@ impl<'a> KafkaPayout<'a> { last_modified_at: payouts.last_modified_at.assume_utc(), attempt_count: payouts.attempt_count, status: payouts.status, + priority: payouts.priority, connector: payout_attempt.connector.as_ref(), connector_payout_id: payout_attempt.connector_payout_id.as_ref(), is_eligible: payout_attempt.is_eligible, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c427f60cd1..5129ca5730 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -316,6 +316,7 @@ impl ConnectorData { Ok(name) => match name { enums::Connector::Aci => Ok(Box::new(&connector::Aci)), enums::Connector::Adyen => Ok(Box::new(&connector::Adyen)), + enums::Connector::Adyenplatform => Ok(Box::new(&connector::Adyenplatform)), enums::Connector::Airwallex => Ok(Box::new(&connector::Airwallex)), enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index a073d52375..6646684a2d 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -209,6 +209,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { Ok(match from { api_enums::Connector::Aci => Self::Aci, api_enums::Connector::Adyen => Self::Adyen, + api_enums::Connector::Adyenplatform => Self::Adyenplatform, api_enums::Connector::Airwallex => Self::Airwallex, api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, api_enums::Connector::Bambora => Self::Bambora, diff --git a/crates/router/tests/connectors/adyenplatform.rs b/crates/router/tests/connectors/adyenplatform.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/router/tests/connectors/adyenplatform.rs @@ -0,0 +1 @@ + diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index a84f3f76c7..892e1370cc 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -8,6 +8,7 @@ use test_utils::connector_auth; mod aci; mod adyen; +mod adyenplatform; mod airwallex; mod authorizedotnet; mod bambora; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index fdb4c20863..2d7b197499 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -230,3 +230,6 @@ api_key="API Key" [gpayments] api_key="API Key" + +[adyenplatform] +api_key="API Key" diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 134777dc09..a1f353f4bc 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -501,6 +501,7 @@ pub trait ConnectorActions: Connector { phone_country_code: Some("+31".to_string()), }), vendor_details: None, + priority: None, }, payment_info, ) diff --git a/crates/storage_impl/src/payouts/payouts.rs b/crates/storage_impl/src/payouts/payouts.rs index 92835dee59..c63254d72f 100644 --- a/crates/storage_impl/src/payouts/payouts.rs +++ b/crates/storage_impl/src/payouts/payouts.rs @@ -89,6 +89,7 @@ impl PayoutsInterface for KVRouterStore { status: new.status, attempt_count: new.attempt_count, confirm: new.confirm, + priority: new.priority, }; let redis_entry = kv::TypedSql { @@ -688,6 +689,7 @@ impl DataModelExt for Payouts { status: self.status, attempt_count: self.attempt_count, confirm: self.confirm, + priority: self.priority, } } @@ -714,6 +716,7 @@ impl DataModelExt for Payouts { status: storage_model.status, attempt_count: storage_model.attempt_count, confirm: storage_model.confirm, + priority: storage_model.priority, } } } @@ -743,6 +746,7 @@ impl DataModelExt for PayoutsNew { status: self.status, attempt_count: self.attempt_count, confirm: self.confirm, + priority: self.priority, } } @@ -769,6 +773,7 @@ impl DataModelExt for PayoutsNew { status: storage_model.status, attempt_count: storage_model.attempt_count, confirm: storage_model.confirm, + priority: storage_model.priority, } } } diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 7f536916d3..3ba321c174 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -9,6 +9,8 @@ pub struct ConnectorAuthentication { #[cfg(not(feature = "payouts"))] pub adyen: Option, #[cfg(feature = "payouts")] + pub adyenplatform: Option, + #[cfg(feature = "payouts")] pub adyen: Option, #[cfg(not(feature = "payouts"))] pub adyen_uk: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index e46a710054..d008ff3231 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -71,6 +71,7 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" +adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.secondary_base_url = "https://pal-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" @@ -153,6 +154,7 @@ rewards = ["cashtocode", "zen"] cards = [ "aci", "adyen", + "adyenplatform", "airwallex", "authorizedotnet", "bambora", diff --git a/migrations/2024-06-04-095858_add_priority_to_payouts/down.sql b/migrations/2024-06-04-095858_add_priority_to_payouts/down.sql new file mode 100644 index 0000000000..534969683b --- /dev/null +++ b/migrations/2024-06-04-095858_add_priority_to_payouts/down.sql @@ -0,0 +1 @@ +ALTER TABLE payouts DROP COLUMN IF EXISTS priority; \ No newline at end of file diff --git a/migrations/2024-06-04-095858_add_priority_to_payouts/up.sql b/migrations/2024-06-04-095858_add_priority_to_payouts/up.sql new file mode 100644 index 0000000000..4b2a8843ff --- /dev/null +++ b/migrations/2024-06-04-095858_add_priority_to_payouts/up.sql @@ -0,0 +1 @@ +ALTER TABLE payouts ADD COLUMN IF NOT EXISTS priority VARCHAR(32); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index cffea67133..76cbda10a1 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7463,6 +7463,7 @@ "type": "string", "description": "A connector is an integration to fulfill payments", "enum": [ + "adyenplatform", "phonypay", "fauxpay", "pretendpay", @@ -16325,11 +16326,12 @@ "type": "string", "enum": [ "adyen", - "stripe", + "adyenplatform", + "cybersource", + "ebanx", "payone", "paypal", - "ebanx", - "cybersource", + "stripe", "wise" ] }, @@ -16345,7 +16347,8 @@ "return_url", "business_country", "description", - "entity_type" + "entity_type", + "priority" ], "properties": { "payout_id": { @@ -16502,6 +16505,9 @@ "type": "string", "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", "nullable": true + }, + "priority": { + "$ref": "#/components/schemas/PayoutSendPriority" } }, "additionalProperties": false @@ -16665,6 +16671,20 @@ "example": "2022-09-10T10:11:12Z", "nullable": true }, + "connector_transaction_id": { + "type": "string", + "description": "Underlying processor's payout resource ID", + "example": "S3FC9G9M2MVFDXT5", + "nullable": true + }, + "priority": { + "allOf": [ + { + "$ref": "#/components/schemas/PayoutSendPriority" + } + ], + "nullable": true + }, "attempts": { "type": "array", "items": { @@ -16967,6 +16987,17 @@ } } }, + "PayoutSendPriority": { + "type": "string", + "enum": [ + "instant", + "fast", + "regular", + "wire", + "crossBorder", + "internal" + ] + }, "PayoutStatus": { "type": "string", "enum": [ @@ -17926,6 +17957,7 @@ "type": "string", "description": "Connectors eligible for payments routing", "enum": [ + "adyenplatform", "phonypay", "fauxpay", "pretendpay", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index dcd9c29fb6..f35d080bfe 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless gpayments helcim iatapay klarna mifinity mollie multisafepay netcetera nexinets noon nuvei opayo opennode payeezy payme payone paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1") + connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless gpayments helcim iatapay klarna mifinity mollie multisafepay netcetera nexinets noon nuvei opayo opennode payeezy payme payone paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp