diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 496b8cb8d7..a5db864f18 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -7278,6 +7278,17 @@ "$ref": "#/components/schemas/WalletAdditionalData" } } + }, + { + "type": "object", + "required": [ + "BankRedirect" + ], + "properties": { + "BankRedirect": { + "$ref": "#/components/schemas/BankRedirectAdditionalData" + } + } } ], "description": "Masked payout method details for storing in db" @@ -9489,6 +9500,29 @@ "nationale_nederlanden" ] }, + "BankRedirect": { + "oneOf": [ + { + "type": "object", + "required": [ + "interac" + ], + "properties": { + "interac": { + "$ref": "#/components/schemas/Interac" + } + } + } + ] + }, + "BankRedirectAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/InteracAdditionalData" + } + ], + "description": "Masked payout method details for wallet payout method" + }, "BankRedirectBilling": { "type": "object", "required": [ @@ -17170,6 +17204,31 @@ "expired" ] }, + "Interac": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "description": "Customer email linked with interac account", + "example": "john.doe@example.com" + } + } + }, + "InteracAdditionalData": { + "type": "object", + "description": "Masked payout method details for interac bank redirect payout method", + "properties": { + "email": { + "type": "string", + "description": "Email linked with interac account", + "example": "john.doe@example.com", + "nullable": true + } + } + }, "IssuerData": { "type": "object", "description": "Represents data about the issuer used in the 3DS decision rule.", @@ -26853,6 +26912,7 @@ "cybersource", "ebanx", "gigadat", + "loonio", "nomupay", "nuvei", "payone", @@ -27469,6 +27529,17 @@ "$ref": "#/components/schemas/Wallet" } } + }, + { + "type": "object", + "required": [ + "bank_redirect" + ], + "properties": { + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirect" + } + } } ], "description": "The payout method information required for carrying out a payout" @@ -27507,6 +27578,17 @@ "$ref": "#/components/schemas/WalletAdditionalData" } } + }, + { + "type": "object", + "required": [ + "bank_redirect" + ], + "properties": { + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirectAdditionalData" + } + } } ], "description": "The payout method information for response" @@ -27587,7 +27669,8 @@ "enum": [ "card", "bank", - "wallet" + "wallet", + "bank_redirect" ] }, "PayoutUpdateRequest": { diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 1bbeb5f04c..1064d6dd3f 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -4085,6 +4085,17 @@ "$ref": "#/components/schemas/WalletAdditionalData" } } + }, + { + "type": "object", + "required": [ + "BankRedirect" + ], + "properties": { + "BankRedirect": { + "$ref": "#/components/schemas/BankRedirectAdditionalData" + } + } } ], "description": "Masked payout method details for storing in db" @@ -5951,6 +5962,29 @@ "nationale_nederlanden" ] }, + "BankRedirect": { + "oneOf": [ + { + "type": "object", + "required": [ + "interac" + ], + "properties": { + "interac": { + "$ref": "#/components/schemas/Interac" + } + } + } + ] + }, + "BankRedirectAdditionalData": { + "oneOf": [ + { + "$ref": "#/components/schemas/InteracAdditionalData" + } + ], + "description": "Masked payout method details for wallet payout method" + }, "BankRedirectBilling": { "type": "object", "required": [ @@ -13348,6 +13382,31 @@ "expired" ] }, + "Interac": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "description": "Customer email linked with interac account", + "example": "john.doe@example.com" + } + } + }, + "InteracAdditionalData": { + "type": "object", + "description": "Masked payout method details for interac bank redirect payout method", + "properties": { + "email": { + "type": "string", + "description": "Email linked with interac account", + "example": "john.doe@example.com", + "nullable": true + } + } + }, "JCSVoucherData": { "type": "object", "properties": { @@ -20790,6 +20849,7 @@ "cybersource", "ebanx", "gigadat", + "loonio", "nomupay", "nuvei", "payone", @@ -21609,6 +21669,17 @@ "$ref": "#/components/schemas/Wallet" } } + }, + { + "type": "object", + "required": [ + "bank_redirect" + ], + "properties": { + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirect" + } + } } ], "description": "The payout method information required for carrying out a payout" @@ -21647,6 +21718,17 @@ "$ref": "#/components/schemas/WalletAdditionalData" } } + }, + { + "type": "object", + "required": [ + "bank_redirect" + ], + "properties": { + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirectAdditionalData" + } + } } ], "description": "The payout method information for response" @@ -21764,7 +21846,8 @@ "enum": [ "card", "bank", - "wallet" + "wallet", + "bank_redirect" ] }, "Paypal": { diff --git a/config/config.example.toml b/config/config.example.toml index d27a00b372..967f76556c 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1035,6 +1035,9 @@ debit = { country = "AT,BE,BG,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GR,HR,HU,IE,IS,IT,LI,LT [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [payment_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 04e42d43ad..ce7892b7b7 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -787,6 +787,9 @@ debit = { country = "AT,BE,BG,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GR,HR,HU,IE,IS,IT,LI,LT [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [pm_filters.inespay] sepa = { country = "ES", currency = "EUR"} diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 3372854f34..5bff73c9f7 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -857,6 +857,9 @@ debit = { country = "AT,BE,BG,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GR,HR,HU,IE,IS,IT,LI,LT [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [temp_locker_enable_config] bluesnap.payment_method = "card" nuvei.payment_method = "card" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 62c6699e44..94b7d5d385 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -863,6 +863,9 @@ apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [temp_locker_enable_config] bluesnap.payment_method = "card" nuvei.payment_method = "card" diff --git a/config/development.toml b/config/development.toml index b34ec0cab5..5f94dd041e 100644 --- a/config/development.toml +++ b/config/development.toml @@ -1184,6 +1184,9 @@ debit = { country = "AT,BE,BG,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GR,HR,HU,IE,IS,IT,LI,LT [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [payment_link] sdk_url = "http://localhost:9050/HyperLoader.js" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index f4f7472c61..999f7e67df 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -1195,6 +1195,9 @@ debit = { country = "AT,BE,BG,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GR,HR,HU,IE,IS,IT,LI,LT [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } +[payout_method_filters.loonio] +interac = { currency = "CAD" } + [locker_based_open_banking_connectors] connector_list = "tokenio" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index de46df2373..45ce4f46d0 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -49,6 +49,7 @@ pub enum PayoutConnectors { Cybersource, Ebanx, Gigadat, + Loonio, Nomupay, Nuvei, Payone, @@ -77,6 +78,7 @@ impl From for RoutableConnectors { PayoutConnectors::Cybersource => Self::Cybersource, PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Gigadat => Self::Gigadat, + PayoutConnectors::Loonio => Self::Loonio, PayoutConnectors::Nomupay => Self::Nomupay, PayoutConnectors::Nuvei => Self::Nuvei, PayoutConnectors::Payone => Self::Payone, @@ -96,6 +98,7 @@ impl From for Connector { PayoutConnectors::Cybersource => Self::Cybersource, PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Gigadat => Self::Gigadat, + PayoutConnectors::Loonio => Self::Loonio, PayoutConnectors::Nomupay => Self::Nomupay, PayoutConnectors::Nuvei => Self::Nuvei, PayoutConnectors::Payone => Self::Payone, @@ -115,6 +118,7 @@ impl TryFrom for PayoutConnectors { Connector::Adyenplatform => Ok(Self::Adyenplatform), Connector::Cybersource => Ok(Self::Cybersource), Connector::Ebanx => Ok(Self::Ebanx), + Connector::Loonio => Ok(Self::Loonio), Connector::Nuvei => Ok(Self::Nuvei), Connector::Nomupay => Ok(Self::Nomupay), Connector::Payone => Ok(Self::Payone), diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index d61fc5bbc0..03b5380416 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -242,6 +242,7 @@ pub enum PayoutMethodData { Card(CardPayout), Bank(Bank), Wallet(Wallet), + BankRedirect(BankRedirect), } impl Default for PayoutMethodData { @@ -378,6 +379,19 @@ pub enum Wallet { Venmo(Venmo), } +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum BankRedirect { + Interac(Interac), +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct Interac { + /// Customer email linked with interac account + #[schema(value_type = String, example = "john.doe@example.com")] + pub email: Email, +} + #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct Paypal { /// Email linked with paypal account @@ -602,6 +616,8 @@ pub enum PayoutMethodDataResponse { Bank(Box), #[schema(value_type = WalletAdditionalData)] Wallet(Box), + #[schema(value_type = BankRedirectAdditionalData)] + BankRedirect(Box), } #[derive( @@ -988,6 +1004,18 @@ impl From for payout_method_utils::WalletAdditionalData { } } +impl From for payout_method_utils::BankRedirectAdditionalData { + fn from(bank_redirect: BankRedirect) -> Self { + match bank_redirect { + BankRedirect::Interac(Interac { email }) => { + Self::Interac(Box::new(payout_method_utils::InteracAdditionalData { + email: Some(ForeignFrom::foreign_from(email)), + })) + } + } + } +} + impl From for PayoutMethodDataResponse { fn from(additional_data: payout_method_utils::AdditionalPayoutMethodData) -> Self { match additional_data { @@ -1000,6 +1028,9 @@ impl From for PayoutMethodDataR payout_method_utils::AdditionalPayoutMethodData::Wallet(wallet_data) => { Self::Wallet(wallet_data) } + payout_method_utils::AdditionalPayoutMethodData::BankRedirect(bank_redirect) => { + Self::BankRedirect(bank_redirect) + } } } } diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 3a92de5350..2b225e09a5 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -374,6 +374,7 @@ impl Connector { | (_, Some(PayoutType::Card)) | (Self::Adyenplatform, _) | (Self::Nomupay, _) + | (Self::Loonio, _) ) } #[cfg(feature = "payouts")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index b9053b46bc..a6b9acd7e3 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -7865,6 +7865,7 @@ pub enum PayoutType { Card, Bank, Wallet, + BankRedirect, } /// Type of entity to whom the payout is being carried out to, select from the given list of options diff --git a/crates/common_utils/src/payout_method_utils.rs b/crates/common_utils/src/payout_method_utils.rs index 3a2a9e4c38..94f23c179d 100644 --- a/crates/common_utils/src/payout_method_utils.rs +++ b/crates/common_utils/src/payout_method_utils.rs @@ -23,6 +23,8 @@ pub enum AdditionalPayoutMethodData { Bank(Box), /// Additional data for wallet payout method Wallet(Box), + /// Additional data for Bank Redirect payout method + BankRedirect(Box), } crate::impl_to_sql_from_sql_json!(AdditionalPayoutMethodData); @@ -238,3 +240,25 @@ pub struct VenmoAdditionalData { #[schema(value_type = Option, example = "******* 3349")] pub telephone_number: Option, } + +/// Masked payout method details for wallet payout method +#[derive( + Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +pub enum BankRedirectAdditionalData { + /// Additional data for interac bank redirect payout method + Interac(Box), +} + +/// Masked payout method details for interac bank redirect payout method +#[derive( + Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct InteracAdditionalData { + /// Email linked with interac account + #[schema(value_type = Option, example = "john.doe@example.com")] + pub email: Option, +} diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index be428b1ef1..e92311b46c 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -299,6 +299,8 @@ pub struct ConnectorConfig { pub jpmorgan: Option, pub klarna: Option, pub loonio: Option, + #[cfg(feature = "payouts")] + pub loonio_payout: Option, pub mifinity: Option, pub mollie: Option, pub moneris: Option, @@ -403,6 +405,7 @@ impl ConnectorConfig { PayoutConnectors::Cybersource => Ok(connector_data.cybersource_payout), PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout), PayoutConnectors::Gigadat => Ok(connector_data.gigadat_payout), + PayoutConnectors::Loonio => Ok(connector_data.loonio_payout), PayoutConnectors::Nomupay => Ok(connector_data.nomupay_payout), PayoutConnectors::Nuvei => Ok(connector_data.nuvei_payout), PayoutConnectors::Payone => Ok(connector_data.payone_payout), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index a15dda841a..3f59af3e24 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -7302,6 +7302,13 @@ key1 = "Merchant Token" [[loonio.bank_redirect]] payment_method_type = "interac" +[loonio_payout] +[loonio_payout.connector_auth.BodyKey] +api_key = "Merchant ID" +key1 = "Merchant Token" +[[loonio_payout.bank_redirect]] +payment_method_type = "interac" + [tesouro] [[tesouro.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 450f0bf90f..d860c4828c 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -6039,6 +6039,13 @@ key1 = "Merchant Token" [[loonio.bank_redirect]] payment_method_type = "interac" +[loonio_payout] +[loonio_payout.connector_auth.BodyKey] +api_key = "Merchant ID" +key1 = "Merchant Token" +[[loonio_payout.bank_redirect]] +payment_method_type = "interac" + [tesouro] [[tesouro.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index cb6995fb11..cc6d82b276 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -7280,6 +7280,12 @@ key1 = "Merchant Token" [[loonio.bank_redirect]] payment_method_type = "interac" +[loonio_payout] +[loonio_payout.connector_auth.BodyKey] +api_key = "Merchant ID" +key1 = "Merchant Token" +[[loonio_payout.bank_redirect]] +payment_method_type = "interac" [tesouro] [[tesouro.credit]] diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index 22fb997530..0664fc27ec 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -166,4 +166,5 @@ pub enum PayoutType { Card, BankTransfer, Wallet, + BankRedirect, } diff --git a/crates/hyperswitch_connectors/src/connectors/adyen.rs b/crates/hyperswitch_connectors/src/connectors/adyen.rs index a07986ba18..a155d7addd 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen.rs @@ -1593,15 +1593,19 @@ impl ConnectorIntegration for Adyen &req.connector_meta_data, )?; let payout_type = req.request.get_payout_type()?; + let path_segment = match payout_type { + enums::PayoutType::Bank | enums::PayoutType::Wallet => "confirmThirdParty", + enums::PayoutType::Card => "payout", + enums::PayoutType::BankRedirect => { + return Err(errors::ConnectorError::NotImplemented( + "bank redirect payouts not supoorted by adyen".to_string(), + ) + .into()) + } + }; Ok(format!( "{}pal/servlet/Payout/{}/{}", - endpoint, - ADYEN_API_VERSION, - match payout_type { - enums::PayoutType::Bank | enums::PayoutType::Wallet => - "confirmThirdParty".to_string(), - enums::PayoutType::Card => "payout".to_string(), - } + endpoint, ADYEN_API_VERSION, path_segment )) } @@ -1627,7 +1631,9 @@ impl ConnectorIntegration for Adyen let mut api_key = vec![( headers::X_API_KEY.to_string(), match payout_type { - enums::PayoutType::Bank | enums::PayoutType::Wallet => { + enums::PayoutType::Bank + | enums::PayoutType::Wallet + | enums::PayoutType::BankRedirect => { auth.review_key.unwrap_or(auth.api_key).into_masked() } enums::PayoutType::Card => auth.api_key.into_masked(), diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index 957653b3fb..e455882151 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -5742,6 +5742,10 @@ impl TryFrom<&AdyenRouterData<&PayoutsRouterData>> for AdyenPayoutCreateRe .transpose()?, }) } + PayoutMethodData::BankRedirect(_) => Err(errors::ConnectorError::NotSupported { + message: "Bank redirect payout creation is not supported".to_string(), + connector: "Adyen", + })?, } } } @@ -5755,7 +5759,9 @@ impl TryFrom<&AdyenRouterData<&PayoutsRouterData>> for AdyenPayoutFulfillR let payout_type = item.router_data.request.get_payout_type()?; let merchant_account = auth_type.merchant_account; match payout_type { - storage_enums::PayoutType::Bank | storage_enums::PayoutType::Wallet => { + storage_enums::PayoutType::Bank + | storage_enums::PayoutType::Wallet + | storage_enums::PayoutType::BankRedirect => { Ok(Self::GenericFulfillRequest(PayoutFulfillGenericRequest { merchant_account, original_reference: item diff --git a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs index 2f49d29756..fd4b249ee3 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs @@ -458,9 +458,11 @@ impl TryFrom> let request = &raw_payment.item.router_data.request; match raw_payment.raw_payout_method_data { - payouts::PayoutMethodData::Wallet(_) => Err(ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Adyenplatform"), - ))?, + payouts::PayoutMethodData::Wallet(_) | payouts::PayoutMethodData::BankRedirect(_) => { + Err(ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Adyenplatform"), + ))? + } payouts::PayoutMethodData::Card(c) => { let card_holder: AdyenAccountHolder = (raw_payment.item.router_data, &c).try_into()?; @@ -661,10 +663,12 @@ impl TryFrom for AdyenPayoutMethod { match payout_type { enums::PayoutType::Bank => Ok(Self::Bank), enums::PayoutType::Card => Ok(Self::Card), - enums::PayoutType::Wallet => Err(report!(ConnectorError::NotSupported { - message: "Card or wallet payouts".to_string(), - connector: "Adyenplatform", - })), + enums::PayoutType::Wallet | enums::PayoutType::BankRedirect => { + Err(report!(ConnectorError::NotSupported { + message: "Bakredirect or wallet payouts".to_string(), + connector: "Adyenplatform", + })) + } } } } diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index cbd97ccf84..eaf6304ef2 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -4975,12 +4975,12 @@ impl TryFrom<&CybersourceRouterData<&PayoutsRouterData>> payment_information, }) } - enums::PayoutType::Bank | enums::PayoutType::Wallet => { - Err(errors::ConnectorError::NotSupported { - message: "PayoutType is not supported".to_string(), - connector: "Cybersource", - })? - } + enums::PayoutType::Bank + | enums::PayoutType::Wallet + | enums::PayoutType::BankRedirect => Err(errors::ConnectorError::NotSupported { + message: "PayoutType is not supported".to_string(), + connector: "Cybersource", + })?, } } } @@ -5034,12 +5034,12 @@ impl TryFrom for PaymentInformation { }; Ok(Self::Cards(Box::new(CardPaymentInformation { card }))) } - PayoutMethodData::Bank(_) | PayoutMethodData::Wallet(_) => { - Err(errors::ConnectorError::NotSupported { - message: "PayoutMethod is not supported".to_string(), - connector: "Cybersource", - })? - } + PayoutMethodData::Bank(_) + | PayoutMethodData::Wallet(_) + | PayoutMethodData::BankRedirect(_) => Err(errors::ConnectorError::NotSupported { + message: "PayoutMethod is not supported".to_string(), + connector: "Cybersource", + })?, } } } diff --git a/crates/hyperswitch_connectors/src/connectors/ebanx/transformers.rs b/crates/hyperswitch_connectors/src/connectors/ebanx/transformers.rs index 82e3791256..42caad6c2f 100644 --- a/crates/hyperswitch_connectors/src/connectors/ebanx/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/ebanx/transformers.rs @@ -139,12 +139,13 @@ impl TryFrom<&EbanxRouterData<&PayoutsRouterData>> for EbanxPayoutCrea payee, }) } - PayoutMethodData::Card(_) | PayoutMethodData::Bank(_) | PayoutMethodData::Wallet(_) => { - Err(ConnectorError::NotSupported { - message: "Payment Method Not Supported".to_string(), - connector: "Ebanx", - })? - } + PayoutMethodData::Card(_) + | PayoutMethodData::Bank(_) + | PayoutMethodData::Wallet(_) + | PayoutMethodData::BankRedirect(_) => Err(ConnectorError::NotSupported { + message: "Payment Method Not Supported".to_string(), + connector: "Ebanx", + })?, } } } @@ -245,10 +246,12 @@ impl TryFrom<&EbanxRouterData<&PayoutsRouterData>> for EbanxPayoutFulfillR .to_owned() .ok_or(ConnectorError::MissingRequiredField { field_name: "uid" })?, }), - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotSupported { - message: "Payout Method Not Supported".to_string(), - connector: "Ebanx", - })?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotSupported { + message: "Payout Method Not Supported".to_string(), + connector: "Ebanx", + })? + } } } } @@ -334,10 +337,12 @@ impl TryFrom<&PayoutsRouterData> for EbanxPayoutCancelRequest { .to_owned() .ok_or(ConnectorError::MissingRequiredField { field_name: "uid" })?, }), - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotSupported { - message: "Payout Method Not Supported".to_string(), - connector: "Ebanx", - })?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotSupported { + message: "Payout Method Not Supported".to_string(), + connector: "Ebanx", + })? + } } } } diff --git a/crates/hyperswitch_connectors/src/connectors/loonio.rs b/crates/hyperswitch_connectors/src/connectors/loonio.rs index 1de6f4236d..ba4f80cc9b 100644 --- a/crates/hyperswitch_connectors/src/connectors/loonio.rs +++ b/crates/hyperswitch_connectors/src/connectors/loonio.rs @@ -31,6 +31,13 @@ use hyperswitch_domain_models::{ RefundSyncRouterData, RefundsRouterData, }, }; +#[cfg(feature = "payouts")] +use hyperswitch_domain_models::{ + router_flow_types::{PoFulfill, PoSync}, + types::{PayoutsData, PayoutsResponseData, PayoutsRouterData}, +}; +#[cfg(feature = "payouts")] +use hyperswitch_interfaces::types::{PayoutFulfillType, PayoutSyncType}; use hyperswitch_interfaces::{ api::{ self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications, @@ -73,6 +80,11 @@ impl api::Refund for Loonio {} impl api::RefundExecute for Loonio {} impl api::RefundSync for Loonio {} impl api::PaymentToken for Loonio {} +impl api::Payouts for Loonio {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Loonio {} +#[cfg(feature = "payouts")] +impl api::PayoutSync for Loonio {} impl ConnectorIntegration for Loonio @@ -587,6 +599,164 @@ impl ConnectorIntegration for Loonio { } } +#[cfg(feature = "payouts")] +impl ConnectorIntegration for Loonio { + fn get_headers( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}api/v1/transactions/outgoing/send_to_interac", + self.base_url(connectors), + )) + } + + fn get_request_body( + &self, + req: &PayoutsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.destination_currency, + )?; + + let connector_router_data = loonio::LoonioRouterData::from((amount, req)); + let connector_req = loonio::LoonioPayoutFulfillRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&PayoutFulfillType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(PayoutFulfillType::get_headers(self, req, connectors)?) + .set_body(PayoutFulfillType::get_request_body(self, req, connectors)?) + .build(); + + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &PayoutsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: loonio::LoonioPayoutFulfillResponse = res + .response + .parse_struct("LoonioPayoutFulfillResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +#[cfg(feature = "payouts")] +impl ConnectorIntegration for Loonio { + fn get_url( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult { + let transfer_id = req.request.connector_payout_id.to_owned().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "transaction_id", + }, + )?; + Ok(format!( + "{}api/v1/transactions/{}/details", + connectors.loonio.base_url, transfer_id + )) + } + + fn get_headers( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn build_request( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Get) + .url(&PayoutSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(PayoutSyncType::get_headers(self, req, connectors)?) + .build(); + + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &PayoutsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: loonio::LoonioPayoutSyncResponse = res + .response + .parse_struct("LoonioPayoutSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + #[async_trait::async_trait] impl webhooks::IncomingWebhook for Loonio { async fn verify_webhook_source( diff --git a/crates/hyperswitch_connectors/src/connectors/loonio/transformers.rs b/crates/hyperswitch_connectors/src/connectors/loonio/transformers.rs index f1c594145b..85bbe1ab31 100644 --- a/crates/hyperswitch_connectors/src/connectors/loonio/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/loonio/transformers.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +#[cfg(feature = "payouts")] +use api_models::payouts::{BankRedirect, PayoutMethodData}; use api_models::webhooks; use common_enums::{enums, Currency}; use common_utils::{id_type, pii::Email, request::Method, types::FloatMajorUnit}; @@ -11,10 +13,17 @@ use hyperswitch_domain_models::{ router_response_types::{PaymentsResponseData, RedirectForm, RefundsResponseData}, types::{PaymentsAuthorizeRouterData, RefundsRouterData}, }; +#[cfg(feature = "payouts")] +use hyperswitch_domain_models::{ + router_flow_types::PoFulfill, router_response_types::PayoutsResponseData, + types::PayoutsRouterData, +}; use hyperswitch_interfaces::errors; use masking::Secret; use serde::{Deserialize, Serialize}; +#[cfg(feature = "payouts")] +use crate::types::PayoutsResponseRouterData; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, utils::{self, PaymentsAuthorizeRequestData, RouterData as _}, @@ -417,3 +426,146 @@ impl From<&LoonioWebhookEventCode> for enums::AttemptStatus { } } } + +// Payout Structures +#[cfg(feature = "payouts")] +#[derive(Debug, Serialize)] +pub struct LoonioPayoutFulfillRequest { + pub currency_code: Currency, + pub customer_profile: LoonioCustomerProfile, + pub amount: FloatMajorUnit, + pub customer_id: id_type::CustomerId, + pub transaction_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub webhook_url: Option, +} + +#[cfg(feature = "payouts")] +impl TryFrom<&LoonioRouterData<&PayoutsRouterData>> for LoonioPayoutFulfillRequest { + type Error = error_stack::Report; + fn try_from( + item: &LoonioRouterData<&PayoutsRouterData>, + ) -> Result { + match item.router_data.get_payout_method_data()? { + PayoutMethodData::BankRedirect(BankRedirect::Interac(interac_data)) => { + let customer_profile = LoonioCustomerProfile { + first_name: item.router_data.get_billing_first_name()?, + last_name: item.router_data.get_billing_last_name()?, + email: interac_data.email, + }; + + Ok(Self { + currency_code: item.router_data.request.destination_currency, + customer_profile, + amount: item.amount, + customer_id: item.router_data.get_customer_id()?, + transaction_id: item.router_data.connector_request_reference_id.clone(), + webhook_url: item.router_data.request.webhook_url.clone(), + }) + } + PayoutMethodData::Card(_) | PayoutMethodData::Bank(_) | PayoutMethodData::Wallet(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Payment Method Not Supported".to_string(), + connector: "Loonio", + })? + } + } + } +} + +#[cfg(feature = "payouts")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoonioPayoutFulfillResponse { + pub id: i64, + pub api_transaction_id: String, + #[serde(rename = "type")] + pub transaction_type: String, + pub state: LoonioPayoutStatus, +} + +#[cfg(feature = "payouts")] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum LoonioPayoutStatus { + Created, + Prepared, + Pending, + Settled, + Available, + Rejected, + Abandoned, + ConnectedAbandoned, + ConnectedInsufficientFunds, + Failed, + Nsf, + Returned, + Rollback, +} + +#[cfg(feature = "payouts")] +impl From for enums::PayoutStatus { + fn from(item: LoonioPayoutStatus) -> Self { + match item { + LoonioPayoutStatus::Created | LoonioPayoutStatus::Prepared => Self::Initiated, + LoonioPayoutStatus::Pending => Self::Pending, + LoonioPayoutStatus::Settled | LoonioPayoutStatus::Available => Self::Success, + LoonioPayoutStatus::Rejected + | LoonioPayoutStatus::Abandoned + | LoonioPayoutStatus::ConnectedAbandoned + | LoonioPayoutStatus::ConnectedInsufficientFunds + | LoonioPayoutStatus::Failed + | LoonioPayoutStatus::Nsf + | LoonioPayoutStatus::Returned + | LoonioPayoutStatus::Rollback => Self::Failed, + } + } +} + +#[cfg(feature = "payouts")] +impl TryFrom> + for PayoutsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: PayoutsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PayoutsResponseData { + status: Some(enums::PayoutStatus::from(item.response.state)), + connector_payout_id: Some(item.response.api_transaction_id), + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + error_code: None, + error_message: None, + }), + ..item.data + }) + } +} + +#[cfg(feature = "payouts")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoonioPayoutSyncResponse { + pub transaction_id: String, + pub state: LoonioPayoutStatus, +} + +#[cfg(feature = "payouts")] +impl TryFrom> for PayoutsRouterData { + type Error = error_stack::Report; + fn try_from( + item: PayoutsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PayoutsResponseData { + status: Some(enums::PayoutStatus::from(item.response.state)), + connector_payout_id: Some(item.response.transaction_id.to_string()), + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + error_code: None, + error_message: None, + }), + ..item.data + }) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs index 8fe123f5e3..2d7b826e41 100644 --- a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs @@ -2458,7 +2458,8 @@ impl TryFrom for NuveiPayoutCardData { expiration_year: card_data.expiry_year, }), api_models::payouts::PayoutMethodData::Bank(_) - | api_models::payouts::PayoutMethodData::Wallet(_) => { + | api_models::payouts::PayoutMethodData::Wallet(_) + | api_models::payouts::PayoutMethodData::BankRedirect(_) => { Err(errors::ConnectorError::NotImplemented( "Selected Payout Method is not implemented for Nuvei".to_string(), ) diff --git a/crates/hyperswitch_connectors/src/connectors/payone/transformers.rs b/crates/hyperswitch_connectors/src/connectors/payone/transformers.rs index 25c0320a5e..b51bb3d389 100644 --- a/crates/hyperswitch_connectors/src/connectors/payone/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/payone/transformers.rs @@ -139,41 +139,45 @@ impl TryFrom>> for PayonePayoutFu amount: item.amount, currency_code: item.router_data.request.destination_currency.to_string(), }; - let card_payout_method_specific_input = - match item.router_data.get_payout_method_data()? { - PayoutMethodData::Card(card_data) => CardPayoutMethodSpecificInput { - card: Card { - card_number: card_data.card_number.clone(), - card_holder_name: card_data - .card_holder_name - .clone() - .get_required_value("card_holder_name") - .change_context(ConnectorError::MissingRequiredField { - field_name: "payout_method_data.card.holder_name", - })?, - expiry_date: card_data - .get_card_expiry_month_year_2_digit_with_delimiter( - "".to_string(), - )?, - }, - payment_product_id: PaymentProductId::try_from( - card_data.get_card_issuer()?, - )?, + let card_payout_method_specific_input = match item + .router_data + .get_payout_method_data()? + { + PayoutMethodData::Card(card_data) => CardPayoutMethodSpecificInput { + card: Card { + card_number: card_data.card_number.clone(), + card_holder_name: card_data + .card_holder_name + .clone() + .get_required_value("card_holder_name") + .change_context(ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card.holder_name", + })?, + expiry_date: card_data + .get_card_expiry_month_year_2_digit_with_delimiter( + "".to_string(), + )?, }, - PayoutMethodData::Bank(_) | PayoutMethodData::Wallet(_) => { - Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Payone"), - ))? - } - }; + payment_product_id: PaymentProductId::try_from( + card_data.get_card_issuer()?, + )?, + }, + PayoutMethodData::Bank(_) + | PayoutMethodData::Wallet(_) + | PayoutMethodData::BankRedirect(_) => Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Payone"), + ))?, + }; Ok(Self { amount_of_money, card_payout_method_specific_input, }) } - PayoutType::Wallet | PayoutType::Bank => Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Payone"), - ))?, + PayoutType::Wallet | PayoutType::Bank | PayoutType::BankRedirect => { + Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Payone"), + ))? + } } } } diff --git a/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs b/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs index 321aad7655..fc3f6bfdc1 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs @@ -445,6 +445,13 @@ impl TryFrom<&PayoutsRouterData> for StripeConnectRecipientAccountCreateRe } .into()) } + api_models::payouts::PayoutMethodData::BankRedirect(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Payouts via BankRedirect are not supported".to_string(), + connector: "stripe", + } + .into()) + } } } } diff --git a/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs b/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs index a965597645..3a3d9ecc72 100644 --- a/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs @@ -413,9 +413,11 @@ impl TryFrom<&WiseRouterData<&PayoutsRouterData>> for WiseRecipientCreateR }?; let payout_type = request.get_payout_type()?; match payout_type { - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Wise"), - ))?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Wise"), + ))? + } PayoutType::Bank => { let account_holder_name = customer_details .ok_or(ConnectorError::MissingRequiredField { @@ -478,9 +480,11 @@ impl TryFrom<&WiseRouterData<&PayoutsRouterData>> for WisePayoutQuoteReque target_currency: request.destination_currency.to_string(), pay_out: WisePayOutOption::default(), }), - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Wise"), - ))?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Wise"), + ))? + } } } } @@ -536,9 +540,11 @@ impl TryFrom<&PayoutsRouterData> for WisePayoutCreateRequest { details: wise_transfer_details, }) } - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Wise"), - ))?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Wise"), + ))? + } } } } @@ -580,9 +586,11 @@ impl TryFrom<&PayoutsRouterData> for WisePayoutFulfillRequest { PayoutType::Bank => Ok(Self { fund_type: FundType::default(), }), - PayoutType::Card | PayoutType::Wallet => Err(ConnectorError::NotImplemented( - get_unimplemented_payment_method_error_message("Wise"), - ))?, + PayoutType::Card | PayoutType::Wallet | PayoutType::BankRedirect => { + Err(ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Wise"), + ))? + } } } } diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index be8aee1441..c4b6ceaf3c 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -3865,7 +3865,6 @@ default_imp_for_payouts!( connectors::Juspaythreedsserver, connectors::Katapult, connectors::Klarna, - connectors::Loonio, connectors::Mifinity, connectors::Mollie, connectors::Moneris, @@ -4155,7 +4154,6 @@ default_imp_for_payouts_retrieve!( connectors::Juspaythreedsserver, connectors::Katapult, connectors::Klarna, - connectors::Loonio, connectors::Netcetera, connectors::Nmi, connectors::Noon, @@ -4447,7 +4445,6 @@ default_imp_for_payouts_fulfill!( connectors::Juspaythreedsserver, connectors::Katapult, connectors::Klarna, - connectors::Loonio, connectors::Netcetera, connectors::Noon, connectors::Nordea, diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 6ef0b64125..b87d57370f 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -232,11 +232,13 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::CardAdditionalData, common_utils::payout_method_utils::BankAdditionalData, common_utils::payout_method_utils::WalletAdditionalData, + common_utils::payout_method_utils::BankRedirectAdditionalData, common_utils::payout_method_utils::AchBankTransferAdditionalData, common_utils::payout_method_utils::BacsBankTransferAdditionalData, common_utils::payout_method_utils::SepaBankTransferAdditionalData, common_utils::payout_method_utils::PixBankTransferAdditionalData, common_utils::payout_method_utils::PaypalAdditionalData, + common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, @@ -661,6 +663,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::CardPayout, api_models::payouts::Wallet, api_models::payouts::Paypal, + api_models::payouts::BankRedirect, + api_models::payouts::Interac, api_models::payouts::Venmo, api_models::payouts::AchBankTransfer, api_models::payouts::BacsBankTransfer, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index f4c1944931..c4b67a2cd5 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -174,11 +174,13 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::CardAdditionalData, common_utils::payout_method_utils::BankAdditionalData, common_utils::payout_method_utils::WalletAdditionalData, + common_utils::payout_method_utils::BankRedirectAdditionalData, common_utils::payout_method_utils::AchBankTransferAdditionalData, common_utils::payout_method_utils::BacsBankTransferAdditionalData, common_utils::payout_method_utils::SepaBankTransferAdditionalData, common_utils::payout_method_utils::PixBankTransferAdditionalData, common_utils::payout_method_utils::PaypalAdditionalData, + common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, @@ -630,6 +632,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::CardPayout, api_models::payouts::Wallet, api_models::payouts::Paypal, + api_models::payouts::BankRedirect, + api_models::payouts::Interac, api_models::payouts::Venmo, api_models::payouts::AchBankTransfer, api_models::payouts::BacsBankTransfer, diff --git a/crates/router/src/configs/defaults/payout_required_fields.rs b/crates/router/src/configs/defaults/payout_required_fields.rs index 24c13ec539..56e975d81c 100644 --- a/crates/router/src/configs/defaults/payout_required_fields.rs +++ b/crates/router/src/configs/defaults/payout_required_fields.rs @@ -244,6 +244,24 @@ fn get_connector_payment_method_type_fields( ) } + // Bank Redirect + PaymentMethodType::Interac => { + common_fields.extend(get_interac_fields()); + ( + payment_method_type, + ConnectorFields { + fields: HashMap::from([( + connector.into(), + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: common_fields, + }, + )]), + }, + ) + } + _ => ( payment_method_type, ConnectorFields { @@ -375,6 +393,38 @@ fn get_paypal_fields() -> HashMap { )]) } +fn get_interac_fields() -> HashMap { + HashMap::from([ + ( + "payout_method_data.bank_redirect.interac.email".to_string(), + RequiredFieldInfo { + required_field: "payout_method_data.bank_redirect.interac.email".to_string(), + display_name: "email".to_string(), + field_type: FieldType::Text, + value: None, + }, + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_address_first_name".to_string(), + field_type: FieldType::Text, + 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, + }, + ), + ]) +} + fn get_countries_for_connector(connector: PayoutConnectors) -> Vec { match connector { PayoutConnectors::Adyenplatform => vec![ diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index af091faedc..8941dea672 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -4803,6 +4803,12 @@ pub async fn get_bank_from_hs_locker( message: "Expected bank details, found wallet details instead".to_string(), } .into()), + api::PayoutMethodData::BankRedirect(_) => { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Expected bank details, found bank redirect details instead".to_string(), + } + .into()) + } } } diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 98888520d5..1b99c1ab33 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -824,6 +824,7 @@ pub enum VaultPayoutMethod { Card(String), Bank(String), Wallet(String), + BankRedirect(String), } #[cfg(feature = "payouts")] @@ -836,6 +837,9 @@ impl Vaultable for api::PayoutMethodData { Self::Card(card) => VaultPayoutMethod::Card(card.get_value1(customer_id)?), Self::Bank(bank) => VaultPayoutMethod::Bank(bank.get_value1(customer_id)?), Self::Wallet(wallet) => VaultPayoutMethod::Wallet(wallet.get_value1(customer_id)?), + Self::BankRedirect(bank_redirect) => { + VaultPayoutMethod::BankRedirect(bank_redirect.get_value1(customer_id)?) + } }; value1 @@ -852,6 +856,9 @@ impl Vaultable for api::PayoutMethodData { Self::Card(card) => VaultPayoutMethod::Card(card.get_value2(customer_id)?), Self::Bank(bank) => VaultPayoutMethod::Bank(bank.get_value2(customer_id)?), Self::Wallet(wallet) => VaultPayoutMethod::Wallet(wallet.get_value2(customer_id)?), + Self::BankRedirect(bank_redirect) => { + VaultPayoutMethod::BankRedirect(bank_redirect.get_value2(customer_id)?) + } }; value2 @@ -887,12 +894,95 @@ impl Vaultable for api::PayoutMethodData { let (wallet, supp_data) = api::WalletPayout::from_values(mvalue1, mvalue2)?; Ok((Self::Wallet(wallet), supp_data)) } + ( + VaultPayoutMethod::BankRedirect(mvalue1), + VaultPayoutMethod::BankRedirect(mvalue2), + ) => { + let (bank_redirect, supp_data) = + api::BankRedirectPayout::from_values(mvalue1, mvalue2)?; + Ok((Self::BankRedirect(bank_redirect), supp_data)) + } _ => Err(errors::VaultError::PayoutMethodNotSupported) .attach_printable("Payout method not supported"), } } } +#[cfg(feature = "payouts")] +impl Vaultable for api::BankRedirectPayout { + fn get_value1( + &self, + _customer_id: Option, + ) -> CustomResult { + let value1 = match self { + Self::Interac(interac_data) => TokenizedBankRedirectSensitiveValues { + email: interac_data.email.clone(), + bank_redirect_type: PaymentMethodType::Interac, + }, + }; + + value1 + .encode_to_string_of_json() + .change_context(errors::VaultError::RequestEncodingFailed) + .attach_printable( + "Failed to encode bank redirect data - TokenizedBankRedirectSensitiveValues", + ) + } + + fn get_value2( + &self, + customer_id: Option, + ) -> CustomResult { + let value2 = TokenizedBankRedirectInsensitiveValues { customer_id }; + + value2 + .encode_to_string_of_json() + .change_context(errors::VaultError::RequestEncodingFailed) + .attach_printable("Failed to encode wallet data value2") + } + + fn from_values( + value1: String, + value2: String, + ) -> CustomResult<(Self, SupplementaryVaultData), errors::VaultError> { + let value1: TokenizedBankRedirectSensitiveValues = value1 + .parse_struct("TokenizedBankRedirectSensitiveValues") + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Could not deserialize into wallet data value1")?; + + let value2: TokenizedBankRedirectInsensitiveValues = value2 + .parse_struct("TokenizedBankRedirectInsensitiveValues") + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Could not deserialize into wallet data value2")?; + + let bank_redirect = match value1.bank_redirect_type { + PaymentMethodType::Interac => Self::Interac(api_models::payouts::Interac { + email: value1.email, + }), + _ => Err(errors::VaultError::PayoutMethodNotSupported) + .attach_printable("Payout method not supported")?, + }; + + let supp_data = SupplementaryVaultData { + customer_id: value2.customer_id, + payment_method_id: None, + }; + + Ok((bank_redirect, supp_data)) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenizedBankRedirectSensitiveValues { + pub email: Email, + pub bank_redirect_type: PaymentMethodType, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenizedBankRedirectInsensitiveValues { + pub customer_id: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MockTokenizeDBValue { pub value1: String, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 00bcc7a26f..e2d6cf65e8 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -377,7 +377,8 @@ pub async fn save_payout_data_to_locker( Some(wallet.to_owned()), api_enums::PaymentMethodType::foreign_from(wallet), ), - payouts::PayoutMethodData::Card(_) => { + payouts::PayoutMethodData::Card(_) + | payouts::PayoutMethodData::BankRedirect(_) => { Err(errors::ApiErrorResponse::InternalServerError)? } } @@ -1533,6 +1534,11 @@ pub async fn get_additional_payout_data( Box::new(wallet_data.to_owned().into()), )) } + api::PayoutMethodData::BankRedirect(bank_redirect_data) => { + Some(payout_additional::AdditionalPayoutMethodData::BankRedirect( + Box::new(bank_redirect_data.to_owned().into()), + )) + } } } diff --git a/crates/router/src/types/api/payouts.rs b/crates/router/src/types/api/payouts.rs index b5a6ac24ca..2652c14ab8 100644 --- a/crates/router/src/types/api/payouts.rs +++ b/crates/router/src/types/api/payouts.rs @@ -1,10 +1,11 @@ pub use api_models::payouts::{ - AchBankTransfer, BacsBankTransfer, Bank as BankPayout, CardPayout, PaymentMethodTypeInfo, - PayoutActionRequest, PayoutAttemptResponse, PayoutCreateRequest, PayoutCreateResponse, - PayoutEnabledPaymentMethodsInfo, PayoutLinkResponse, PayoutListConstraints, - PayoutListFilterConstraints, PayoutListFilters, PayoutListResponse, PayoutMethodData, - PayoutMethodDataResponse, PayoutRequest, PayoutRetrieveBody, PayoutRetrieveRequest, - PixBankTransfer, RequiredFieldsOverrideRequest, SepaBankTransfer, Wallet as WalletPayout, + AchBankTransfer, BacsBankTransfer, Bank as BankPayout, BankRedirect as BankRedirectPayout, + CardPayout, PaymentMethodTypeInfo, PayoutActionRequest, PayoutAttemptResponse, + PayoutCreateRequest, PayoutCreateResponse, PayoutEnabledPaymentMethodsInfo, PayoutLinkResponse, + PayoutListConstraints, PayoutListFilterConstraints, PayoutListFilters, PayoutListResponse, + PayoutMethodData, PayoutMethodDataResponse, PayoutRequest, PayoutRetrieveBody, + PayoutRetrieveRequest, PixBankTransfer, RequiredFieldsOverrideRequest, SepaBankTransfer, + Wallet as WalletPayout, }; pub use hyperswitch_domain_models::router_flow_types::payouts::{ PoCancel, PoCreate, PoEligibility, PoFulfill, PoQuote, PoRecipient, PoRecipientAccount, PoSync, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 3eea8b1006..430fceb3bd 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1195,6 +1195,9 @@ impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentM api_models::payouts::PayoutMethodData::Bank(bank) => Self::foreign_from(bank), api_models::payouts::PayoutMethodData::Card(_) => Self::Debit, api_models::payouts::PayoutMethodData::Wallet(wallet) => Self::foreign_from(wallet), + api_models::payouts::PayoutMethodData::BankRedirect(bank_redirect) => { + Self::foreign_from(bank_redirect) + } } } } @@ -1221,6 +1224,15 @@ impl ForeignFrom<&api_models::payouts::Wallet> for api_enums::PaymentMethodType } } +#[cfg(feature = "payouts")] +impl ForeignFrom<&api_models::payouts::BankRedirect> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::BankRedirect) -> Self { + match value { + api_models::payouts::BankRedirect::Interac(_) => Self::Interac, + } + } +} + #[cfg(feature = "payouts")] impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentMethod { fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { @@ -1228,6 +1240,7 @@ impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentM api_models::payouts::PayoutMethodData::Bank(_) => Self::BankTransfer, api_models::payouts::PayoutMethodData::Card(_) => Self::Card, api_models::payouts::PayoutMethodData::Wallet(_) => Self::Wallet, + api_models::payouts::PayoutMethodData::BankRedirect(_) => Self::BankRedirect, } } } @@ -1239,6 +1252,7 @@ impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_models::enums:: api_models::payouts::PayoutMethodData::Bank(_) => Self::Bank, api_models::payouts::PayoutMethodData::Card(_) => Self::Card, api_models::payouts::PayoutMethodData::Wallet(_) => Self::Wallet, + api_models::payouts::PayoutMethodData::BankRedirect(_) => Self::BankRedirect, } } } @@ -1250,6 +1264,7 @@ impl ForeignFrom for api_enums::PaymentMethod { api_models::enums::PayoutType::Bank => Self::BankTransfer, api_models::enums::PayoutType::Card => Self::Card, api_models::enums::PayoutType::Wallet => Self::Wallet, + api_models::enums::PayoutType::BankRedirect => Self::BankRedirect, } } } diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 9a85d50105..fa5638d7b4 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -128,6 +128,16 @@ impl AdyenTest { paypal_id: None, }), )), + enums::PayoutType::BankRedirect => { + Some(types::api::PayoutMethodData::BankRedirect( + types::api::payouts::BankRedirectPayout::Interac( + api_models::payouts::Interac { + email: Email::from_str("EmailUsedForPayPalAccount@example.com") + .ok()?, + }, + ), + )) + } }, ..Default::default() }) diff --git a/migrations/2025-10-07-100547-0000_add_bank_redirect_in/down.sql b/migrations/2025-10-07-100547-0000_add_bank_redirect_in/down.sql new file mode 100644 index 0000000000..c7c9cbeb40 --- /dev/null +++ b/migrations/2025-10-07-100547-0000_add_bank_redirect_in/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2025-10-07-100547-0000_add_bank_redirect_in/up.sql b/migrations/2025-10-07-100547-0000_add_bank_redirect_in/up.sql new file mode 100644 index 0000000000..b409b7dd40 --- /dev/null +++ b/migrations/2025-10-07-100547-0000_add_bank_redirect_in/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "PayoutType" ADD VALUE IF NOT EXISTS 'bank_redirect'; \ No newline at end of file