diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 49970c67d596..324cabaced5e 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -4887,6 +4887,21 @@ } } }, + "AdditionalMerchantData": { + "oneOf": [ + { + "type": "object", + "required": [ + "open_banking_recipient_data" + ], + "properties": { + "open_banking_recipient_data": { + "$ref": "#/components/schemas/MerchantRecipientData" + } + } + } + ] + }, "Address": { "type": "object", "properties": { @@ -11225,6 +11240,68 @@ }, "additionalProperties": false }, + "MerchantAccountData": { + "oneOf": [ + { + "type": "object", + "required": [ + "iban" + ], + "properties": { + "iban": { + "type": "object", + "required": [ + "iban", + "name" + ], + "properties": { + "iban": { + "type": "string" + }, + "name": { + "type": "string" + }, + "connector_recipient_id": { + "type": "string", + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "bacs" + ], + "properties": { + "bacs": { + "type": "object", + "required": [ + "account_number", + "sort_code", + "name" + ], + "properties": { + "account_number": { + "type": "string" + }, + "sort_code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "connector_recipient_id": { + "type": "string", + "nullable": true + } + } + } + } + } + ] + }, "MerchantAccountDeleteResponse": { "type": "object", "required": [ @@ -11669,6 +11746,14 @@ } ], "nullable": true + }, + "additional_merchant_data": { + "allOf": [ + { + "$ref": "#/components/schemas/AdditionalMerchantData" + } + ], + "nullable": true } }, "additionalProperties": false @@ -11904,6 +11989,14 @@ }, "status": { "$ref": "#/components/schemas/ConnectorStatus" + }, + "additional_merchant_data": { + "allOf": [ + { + "$ref": "#/components/schemas/AdditionalMerchantData" + } + ], + "nullable": true } }, "additionalProperties": false @@ -12110,6 +12203,45 @@ }, "additionalProperties": false }, + "MerchantRecipientData": { + "oneOf": [ + { + "type": "object", + "required": [ + "connector_recipient_id" + ], + "properties": { + "connector_recipient_id": { + "type": "string", + "nullable": true + } + } + }, + { + "type": "object", + "required": [ + "wallet_id" + ], + "properties": { + "wallet_id": { + "type": "string", + "nullable": true + } + } + }, + { + "type": "object", + "required": [ + "account_data" + ], + "properties": { + "account_data": { + "$ref": "#/components/schemas/MerchantAccountData" + } + } + } + ] + }, "MerchantRoutingAlgorithm": { "type": "object", "description": "Routing Algorithm specific to merchants", @@ -19500,6 +19632,7 @@ "worldline", "worldpay", "zen", + "plaid", "zsl" ] }, diff --git a/config/config.example.toml b/config/config.example.toml index 9b855b1e9566..5b4c1bbd762b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -698,3 +698,6 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p [user_auth_methods] encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table + +[locker_based_open_banking_connectors] +connector_list = "" \ No newline at end of file diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index b8945db5f240..060756cf910f 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -347,3 +347,6 @@ keys = "user-agent" [saved_payment_methods] sdk_eligible_payment_methods = "card" + +[locker_based_open_banking_connectors] +connector_list = "" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index acc28a32c5f9..ce7e6df98974 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -366,3 +366,6 @@ keys = "user-agent" [saved_payment_methods] sdk_eligible_payment_methods = "card" + +[locker_based_open_banking_connectors] +connector_list = "" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index d776483ebccf..4920ca8589e6 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -370,3 +370,6 @@ keys = "user-agent" [saved_payment_methods] sdk_eligible_payment_methods = "card" + +[locker_based_open_banking_connectors] +connector_list = "" diff --git a/config/development.toml b/config/development.toml index c35de87e8084..2b7cc68e8b58 100644 --- a/config/development.toml +++ b/config/development.toml @@ -695,3 +695,6 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p [user_auth_methods] encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F" + +[locker_based_open_banking_connectors] +connector_list = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c26055f436ae..18a3189e4fd3 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -351,7 +351,7 @@ sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR" } open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "EUR,GBP,DKK,NOK,PLN,SEK,AUD,BRL" } [pm_filters.razorpay] -upi_collect = {country = "IN", currency = "INR"} +upi_collect = { country = "IN", currency = "INR" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } @@ -442,7 +442,7 @@ delay_between_retries_in_milliseconds = 500 [events.kafka] brokers = ["localhost:9092"] -fraud_check_analytics_topic= "hyperswitch-fraud-check-events" +fraud_check_analytics_topic = "hyperswitch-fraud-check-events" intent_analytics_topic = "hyperswitch-payment-intent-events" attempt_analytics_topic = "hyperswitch-payment-attempt-events" refund_analytics_topic = "hyperswitch-refund-events" @@ -519,7 +519,7 @@ enabled = false global_tenant = { schema = "public", redis_key_prefix = "" } [multitenancy.tenants] -public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} +public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" } [user_auth_methods] encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F" @@ -548,7 +548,10 @@ merchant_name = "HyperSwitch" card = "credit,debit" [payout_method_filters.adyenplatform] -sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,CZ,DE,HU,NO,PL,SE,GB,CH" , currency = "EUR,CZK,DKK,HUF,NOR,PLN,SEK,GBP,CHF" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,CZ,DE,HU,NO,PL,SE,GB,CH", currency = "EUR,CZK,DKK,HUF,NOR,PLN,SEK,GBP,CHF" } [payout_method_filters.stripe] ach = { country = "US", currency = "USD" } + +[locker_based_open_banking_connectors] +connector_list = "" diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index ec9172b7fdff..20129e4b2398 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -652,6 +652,49 @@ pub struct MerchantConnectorCreate { #[schema(value_type = Option, example = "inactive")] pub status: Option, + + /// In case the merchant needs to store any additional sensitive data + #[schema(value_type = Option)] + pub additional_merchant_data: Option, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum AdditionalMerchantData { + OpenBankingRecipientData(MerchantRecipientData), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum MerchantAccountData { + Iban { + #[schema(value_type= String)] + iban: Secret, + name: String, + #[schema(value_type= Option)] + #[serde(skip_serializing_if = "Option::is_none")] + connector_recipient_id: Option>, + }, + Bacs { + #[schema(value_type= String)] + account_number: Secret, + #[schema(value_type= String)] + sort_code: Secret, + name: String, + #[schema(value_type= Option)] + #[serde(skip_serializing_if = "Option::is_none")] + connector_recipient_id: Option>, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum MerchantRecipientData { + #[schema(value_type= Option)] + ConnectorRecipientId(Secret), + #[schema(value_type= Option)] + WalletId(Secret), + AccountData(MerchantAccountData), } // Different patterns of authentication. @@ -805,6 +848,9 @@ pub struct MerchantConnectorResponse { #[schema(value_type = ConnectorStatus, example = "inactive")] pub status: api_enums::ConnectorStatus, + + #[schema(value_type = Option)] + pub additional_merchant_data: Option, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 87d4f5e775ab..ca37332cb768 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -176,6 +176,7 @@ pub enum RoutableConnectors { Worldline, Worldpay, Zen, + Plaid, Zsl, } diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index 680e3dacc856..e01c465d1b41 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -43,6 +43,7 @@ pub struct MerchantConnectorAccount { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: storage_enums::ConnectorStatus, + pub additional_merchant_data: Option, pub connector_wallets_details: Option, } @@ -73,6 +74,7 @@ pub struct MerchantConnectorAccountNew { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, pub status: storage_enums::ConnectorStatus, + pub additional_merchant_data: Option, pub connector_wallets_details: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 8d51e378fd96..d9f5b2998624 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -707,6 +707,7 @@ diesel::table! { applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, status -> ConnectorStatus, + additional_merchant_data -> Nullable, connector_wallets_details -> Nullable, } } diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs index 820e3da42d8e..a137a6fd54df 100644 --- a/crates/kgraph_utils/benches/evaluation.rs +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -71,6 +71,7 @@ fn build_test_data( applepay_verified_domains: None, pm_auth_config: None, status: api_enums::ConnectorStatus::Inactive, + additional_merchant_data: None, }; let config = CountryCurrencyFilter { connector_configs: HashMap::new(), diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 74cc096e7309..0cbf171495be 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -759,6 +759,7 @@ mod tests { applepay_verified_domains: None, pm_auth_config: None, status: api_enums::ConnectorStatus::Inactive, + additional_merchant_data: None, }; let config_map = kgraph_types::CountryCurrencyFilter { diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 82bde980a1f7..d2e669175eb3 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -256,6 +256,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::AuthorizationStatus, api_models::enums::PaymentMethodStatus, api_models::admin::MerchantConnectorCreate, + api_models::admin::AdditionalMerchantData, + api_models::admin::MerchantRecipientData, + api_models::admin::MerchantAccountData, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, api_models::admin::FrmConfigs, diff --git a/crates/pm_auth/src/connector/plaid.rs b/crates/pm_auth/src/connector/plaid.rs index dfcfbb7eddcc..6f91b2cb1a23 100644 --- a/crates/pm_auth/src/connector/plaid.rs +++ b/crates/pm_auth/src/connector/plaid.rs @@ -15,7 +15,9 @@ use crate::{ types::{ self as auth_types, api::{ - auth_service::{self, BankAccountCredentials, ExchangeToken, LinkToken}, + auth_service::{ + self, BankAccountCredentials, ExchangeToken, LinkToken, RecipientCreate, + }, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, }, }, @@ -89,6 +91,8 @@ impl ConnectorCommon for Plaid { } impl auth_service::AuthService for Plaid {} +impl auth_service::PaymentInitiationRecipientCreate for Plaid {} +impl auth_service::PaymentInitiation for Plaid {} impl auth_service::AuthServiceLinkToken for Plaid {} impl ConnectorIntegration @@ -338,3 +342,91 @@ impl self.build_error_response(res) } } + +impl + ConnectorIntegration< + RecipientCreate, + auth_types::RecipientCreateRequest, + auth_types::RecipientCreateResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::RecipientCreateRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::RecipientCreateRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/payment_initiation/recipient/create" + )) + } + + fn get_request_body( + &self, + req: &auth_types::RecipientCreateRouterData, + ) -> errors::CustomResult { + let req_obj = plaid::PlaidRecipientCreateRequest::from(req); + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &auth_types::RecipientCreateRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentInitiationRecipientCreateType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers( + auth_types::PaymentInitiationRecipientCreateType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + auth_types::PaymentInitiationRecipientCreateType::get_request_body(self, req)?, + ) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::RecipientCreateRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidRecipientCreateResponse = res + .response + .parse_struct("PlaidRecipientCreateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(::from( + auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + )) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} diff --git a/crates/pm_auth/src/connector/plaid/transformers.rs b/crates/pm_auth/src/connector/plaid/transformers.rs index 8ce5aca7b83a..571f27f5b20b 100644 --- a/crates/pm_auth/src/connector/plaid/transformers.rs +++ b/crates/pm_auth/src/connector/plaid/transformers.rs @@ -113,6 +113,103 @@ impl TryFrom<&types::ExchangeTokenRouterData> for PlaidExchangeTokenRequest { } } +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct PlaidRecipientCreateRequest { + pub name: String, + #[serde(flatten)] + pub account_data: PlaidRecipientAccountData, + pub address: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidRecipientCreateResponse { + pub recipient_id: String, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PlaidRecipientAccountData { + Iban(Secret), + Bacs { + sort_code: Secret, + account: Secret, + }, +} + +impl From<&types::RecipientAccountData> for PlaidRecipientAccountData { + fn from(item: &types::RecipientAccountData) -> Self { + match item { + types::RecipientAccountData::Iban(iban) => Self::Iban(iban.clone()), + types::RecipientAccountData::Bacs { + sort_code, + account_number, + } => Self::Bacs { + sort_code: sort_code.clone(), + account: account_number.clone(), + }, + } + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct PlaidRecipientCreateAddress { + pub street: String, + pub city: String, + pub postal_code: String, + pub country: String, +} + +impl From<&types::RecipientCreateAddress> for PlaidRecipientCreateAddress { + fn from(item: &types::RecipientCreateAddress) -> Self { + Self { + street: item.street.clone(), + city: item.city.clone(), + postal_code: item.postal_code.clone(), + country: common_enums::CountryAlpha2::to_string(&item.country), + } + } +} + +impl From<&types::RecipientCreateRouterData> for PlaidRecipientCreateRequest { + fn from(item: &types::RecipientCreateRouterData) -> Self { + Self { + name: item.request.name.clone(), + account_data: PlaidRecipientAccountData::from(&item.request.account_data), + address: item + .request + .address + .as_ref() + .map(PlaidRecipientCreateAddress::from), + } + } +} + +impl + From< + types::ResponseRouterData< + F, + PlaidRecipientCreateResponse, + T, + types::RecipientCreateResponse, + >, + > for types::PaymentAuthRouterData +{ + fn from( + item: types::ResponseRouterData< + F, + PlaidRecipientCreateResponse, + T, + types::RecipientCreateResponse, + >, + ) -> Self { + Self { + response: Ok(types::RecipientCreateResponse { + recipient_id: item.response.recipient_id, + }), + ..item.data + } + } +} #[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub struct PlaidBankAccountCredentialsRequest { @@ -351,6 +448,7 @@ impl pub struct PlaidAuthType { pub client_id: Secret, pub secret: Secret, + pub merchant_data: Option, } impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType { @@ -360,6 +458,16 @@ impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType { types::ConnectorAuthType::BodyKey { client_id, secret } => Ok(Self { client_id: client_id.to_owned(), secret: secret.to_owned(), + merchant_data: None, + }), + types::ConnectorAuthType::OpenBankingAuth { + api_key, + key1, + merchant_data, + } => Ok(Self { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + merchant_data: Some(merchant_data.clone()), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } diff --git a/crates/pm_auth/src/types.rs b/crates/pm_auth/src/types.rs index 2537cdc6a361..7450251f157a 100644 --- a/crates/pm_auth/src/types.rs +++ b/crates/pm_auth/src/types.rs @@ -2,10 +2,11 @@ pub mod api; use std::marker::PhantomData; -use api::auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}; -use common_enums::{PaymentMethod, PaymentMethodType}; +use api::auth_service::{BankAccountCredentials, ExchangeToken, LinkToken, RecipientCreate}; +use common_enums::{CountryAlpha2, PaymentMethod, PaymentMethodType}; use common_utils::{id_type, types}; use masking::Secret; + #[derive(Debug, Clone)] pub struct PaymentAuthRouterData { pub flow: PhantomData, @@ -111,6 +112,38 @@ pub type BankDetailsRouterData = PaymentAuthRouterData< BankAccountCredentialsResponse, >; +#[derive(Debug, Clone)] +pub struct RecipientCreateRequest { + pub name: String, + pub account_data: RecipientAccountData, + pub address: Option, +} + +#[derive(Debug, Clone)] +pub struct RecipientCreateResponse { + pub recipient_id: String, +} + +#[derive(Debug, Clone)] +pub enum RecipientAccountData { + Iban(Secret), + Bacs { + sort_code: Secret, + account_number: Secret, + }, +} + +#[derive(Debug, Clone)] +pub struct RecipientCreateAddress { + pub street: String, + pub city: String, + pub postal_code: String, + pub country: CountryAlpha2, +} + +pub type RecipientCreateRouterData = + PaymentAuthRouterData; + pub type PaymentAuthLinkTokenType = dyn api::ConnectorIntegration; @@ -123,6 +156,9 @@ pub type PaymentAuthBankAccountDetailsType = dyn api::ConnectorIntegration< BankAccountCredentialsResponse, >; +pub type PaymentInitiationRecipientCreateType = + dyn api::ConnectorIntegration; + #[derive(Clone, Debug, strum::EnumString, strum::Display)] #[strum(serialize_all = "snake_case")] pub enum PaymentMethodAuthConnectors { @@ -155,12 +191,38 @@ impl ErrorResponse { } } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum MerchantAccountData { + Iban { + iban: Secret, + name: String, + }, + Bacs { + account_number: Secret, + sort_code: Secret, + name: String, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MerchantRecipientData { + ConnectorRecipientId(Secret), + WalletId(Secret), + AccountData(MerchantAccountData), +} + #[derive(Default, Debug, Clone, serde::Deserialize)] pub enum ConnectorAuthType { BodyKey { client_id: Secret, secret: Secret, }, + OpenBankingAuth { + api_key: Secret, + key1: Secret, + merchant_data: MerchantRecipientData, + }, #[default] NoKey, } diff --git a/crates/pm_auth/src/types/api.rs b/crates/pm_auth/src/types/api.rs index 3684e34ec052..d4edf1609857 100644 --- a/crates/pm_auth/src/types/api.rs +++ b/crates/pm_auth/src/types/api.rs @@ -10,7 +10,10 @@ use masking::Maskable; use crate::{ core::errors::ConnectorError, - types::{self as auth_types, api::auth_service::AuthService}, + types::{ + self as auth_types, + api::auth_service::{AuthService, PaymentInitiation}, + }, }; #[async_trait::async_trait] @@ -125,9 +128,9 @@ where } } -pub trait AuthServiceConnector: AuthService + Send + Debug {} +pub trait AuthServiceConnector: AuthService + Send + Debug + PaymentInitiation {} -impl AuthServiceConnector for T {} +impl AuthServiceConnector for T {} pub type BoxedPaymentAuthConnector = Box<&'static (dyn AuthServiceConnector + Sync)>; diff --git a/crates/pm_auth/src/types/api/auth_service.rs b/crates/pm_auth/src/types/api/auth_service.rs index 35d44970d518..498b6bec40ec 100644 --- a/crates/pm_auth/src/types/api/auth_service.rs +++ b/crates/pm_auth/src/types/api/auth_service.rs @@ -1,6 +1,7 @@ use crate::types::{ BankAccountCredentialsRequest, BankAccountCredentialsResponse, ExchangeTokenRequest, - ExchangeTokenResponse, LinkTokenRequest, LinkTokenResponse, + ExchangeTokenResponse, LinkTokenRequest, LinkTokenResponse, RecipientCreateRequest, + RecipientCreateResponse, }; pub trait AuthService: @@ -11,6 +12,8 @@ pub trait AuthService: { } +pub trait PaymentInitiation: super::ConnectorCommon + PaymentInitiationRecipientCreate {} + #[derive(Debug, Clone)] pub struct LinkToken; @@ -38,3 +41,11 @@ pub trait AuthServiceBankAccountCredentials: > { } + +#[derive(Debug, Clone)] +pub struct RecipientCreate; + +pub trait PaymentInitiationRecipientCreate: + super::ConnectorIntegration +{ +} diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 9f6c797cde8f..de53f9c189e3 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -434,5 +434,6 @@ pub(crate) async fn fetch_raw_secrets( multitenancy: conf.multitenancy, user_auth_methods, decision: conf.decision, + locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index b96784eb714c..26685d63583e 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -128,6 +128,7 @@ pub struct Settings { pub saved_payment_methods: EligiblePaymentMethods, pub user_auth_methods: SecretStateContainer, pub decision: Option, + pub locker_based_open_banking_connectors: LockerBasedRecipientConnectorList, } #[derive(Debug, Deserialize, Clone, Default)] @@ -690,6 +691,13 @@ pub struct ApplePayDecryptConifg { pub apple_pay_merchant_cert_key: Secret, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct LockerBasedRecipientConnectorList { + #[serde(deserialize_with = "deserialize_hashset")] + pub connector_list: HashSet, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct ConnectorRequestReferenceIdConfig { pub merchant_ids_send_payment_id_as_connector_request_id: HashSet, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 67b3a05adb7f..88f11fdfce89 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -7,7 +7,7 @@ use api_models::{ use common_utils::{ date_time, ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, - pii, + id_type, pii, }; #[cfg(all(feature = "keymanager_create", feature = "olap"))] use common_utils::{keymanager, types::keymanager as km_types}; @@ -15,24 +15,25 @@ use diesel_models::configs; use error_stack::{report, FutureExt, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; -use pm_auth::connector::plaid::transformers::PlaidAuthType; +use pm_auth::{connector::plaid::transformers::PlaidAuthType, types as pm_auth_types}; +use regex::Regex; use router_env::metrics::add_attributes; use uuid::Uuid; -#[cfg(all(not(feature = "v2"), feature = "olap"))] -use crate::types::transformers::ForeignFrom; use crate::{ consts, core::{ encryption::transfer_encryption_key, errors::{self, RouterResponse, RouterResult, StorageErrorExt}, + payment_methods::{cards, transformers}, payments::helpers, + pm_auth::helpers::PaymentAuthConnectorDataExt, routing::helpers as routing_helpers, utils as core_utils, }, db::StorageInterface, routes::{metrics, SessionState}, - services::{self, api as service_api, authentication}, + services::{self, api as service_api, authentication, pm_auth as payment_initiation_service}, types::{ self, api, domain::{ @@ -40,11 +41,15 @@ use crate::{ types::{self as domain_types, AsyncLift}, }, storage::{self, enums::MerchantStorageScheme}, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, }, utils::{self, OptionExt}, }; +const IBAN_MAX_LENGTH: usize = 34; +const BACS_SORT_CODE_LENGTH: usize = 6; +const BACS_MAX_ACCOUNT_NUMBER_LENGTH: usize = 8; + #[inline] pub fn create_merchant_publishable_key() -> String { format!( @@ -1107,7 +1112,9 @@ pub async fn create_payment_connector( api_enums::convert_authentication_connector(req.connector_name.to_string().as_str()); if pm_auth_connector.is_some() { - if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth + && req.connector_type != api_enums::ConnectorType::PaymentProcessor + { return Err(errors::ApiErrorResponse::InvalidRequestData { message: "Invalid connector type given".to_string(), } @@ -1172,6 +1179,30 @@ pub async fn create_payment_connector( expected_format: "auth_type and api_key".to_string(), })?; + let merchant_recipient_data = if let Some(data) = &req.additional_merchant_data { + Some( + process_open_banking_connectors( + &state, + merchant_id.as_str(), + &auth, + &req.connector_type, + &req.connector_name, + types::AdditionalMerchantData::foreign_from(data.clone()), + ) + .await?, + ) + } else { + None + } + .map(|data| { + serde_json::to_value(types::AdditionalMerchantData::OpenBankingRecipientData( + data, + )) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get MerchantRecipientData")?; + validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata)?; let frm_configs = get_frm_config_as_secret(req.frm_configs); @@ -1192,7 +1223,7 @@ pub async fn create_payment_connector( let (connector_status, disabled) = validate_status_and_disabled( req.status, req.disabled, - auth, + auth.clone(), // The validate_status_and_disabled function will use this value only // when the status can be active. So we are passing this as fallback. api_enums::ConnectorStatus::Active, @@ -1212,17 +1243,18 @@ pub async fn create_payment_connector( } } + let connector_auth = serde_json::to_value(auth) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encoding ConnectorAuthType to serde_json::Value")?; + let conn_auth = Secret::new(connector_auth); + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, connector_name: req.connector_name.to_string(), merchant_connector_id: utils::generate_id(consts::ID_LENGTH, "mca"), connector_account_details: domain_types::encrypt( - req.connector_account_details.ok_or( - errors::ApiErrorResponse::MissingRequiredField { - field_name: "connector_account_details", - }, - )?, + conn_auth, key_store.key.peek(), ) .await @@ -1256,6 +1288,17 @@ pub async fn create_payment_connector( pm_auth_config: req.pm_auth_config.clone(), status: connector_status, connector_wallets_details: helpers::get_encrypted_apple_pay_connector_wallets_details(&key_store, &req.metadata).await?, + additional_merchant_data: if let Some(mcd) = merchant_recipient_data { + Some(domain_types::encrypt( + Secret::new(mcd), + key_store.key.peek(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt additional_merchant_data")?) + } else { + None + }, }; let transaction_type = match req.connector_type { @@ -2577,3 +2620,285 @@ pub fn validate_status_and_disabled( Ok((connector_status, disabled)) } + +async fn process_open_banking_connectors( + state: &SessionState, + merchant_id: &str, + auth: &types::ConnectorAuthType, + connector_type: &api_enums::ConnectorType, + connector: &api_enums::Connector, + additional_merchant_data: types::AdditionalMerchantData, +) -> RouterResult { + let new_merchant_data = match additional_merchant_data { + types::AdditionalMerchantData::OpenBankingRecipientData(merchant_data) => { + if connector_type != &api_enums::ConnectorType::PaymentProcessor { + return Err(errors::ApiErrorResponse::InvalidConnectorConfiguration { + config: + "OpenBanking connector for Payment Initiation should be a payment processor" + .to_string(), + } + .into()); + } + match &merchant_data { + types::MerchantRecipientData::AccountData(acc_data) => { + validate_bank_account_data(acc_data)?; + + let connector_name = api_enums::Connector::to_string(connector); + + let recipient_creation_not_supported = state + .conf + .locker_based_open_banking_connectors + .connector_list + .contains(connector_name.as_str()); + + let recipient_id = if recipient_creation_not_supported { + locker_recipient_create_call(state, merchant_id, acc_data).await + } else { + connector_recipient_create_call( + state, + merchant_id, + connector_name, + auth, + acc_data, + ) + .await + } + .attach_printable("failed to get recipient_id")?; + + let conn_recipient_id = if recipient_creation_not_supported { + Some(types::RecipientIdType::LockerId(Secret::new(recipient_id))) + } else { + Some(types::RecipientIdType::ConnectorId(Secret::new( + recipient_id, + ))) + }; + + let account_data = match &acc_data { + types::MerchantAccountData::Iban { iban, name, .. } => { + types::MerchantAccountData::Iban { + iban: iban.clone(), + name: name.clone(), + connector_recipient_id: conn_recipient_id.clone(), + } + } + types::MerchantAccountData::Bacs { + account_number, + sort_code, + name, + .. + } => types::MerchantAccountData::Bacs { + account_number: account_number.clone(), + sort_code: sort_code.clone(), + name: name.clone(), + connector_recipient_id: conn_recipient_id.clone(), + }, + }; + + types::MerchantRecipientData::AccountData(account_data) + } + _ => merchant_data.clone(), + } + } + }; + + Ok(new_merchant_data) +} + +fn validate_bank_account_data(data: &types::MerchantAccountData) -> RouterResult<()> { + match data { + types::MerchantAccountData::Iban { iban, .. } => { + // IBAN check algorithm + if iban.peek().len() > IBAN_MAX_LENGTH { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "IBAN length must be up to 34 characters".to_string(), + } + .into()); + } + let pattern = Regex::new(r"^[A-Z0-9]*$") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to create regex pattern")?; + + let mut iban = iban.peek().to_string(); + + if !pattern.is_match(iban.as_str()) { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "IBAN data must be alphanumeric".to_string(), + } + .into()); + } + + // MOD check + let first_4 = iban.chars().take(4).collect::(); + iban.push_str(first_4.as_str()); + let len = iban.len(); + + let rearranged_iban = iban + .chars() + .rev() + .take(len - 4) + .collect::() + .chars() + .rev() + .collect::(); + + let mut result = String::new(); + + rearranged_iban.chars().for_each(|c| { + if c.is_ascii_uppercase() { + let digit = (u32::from(c) - u32::from('A')) + 10; + result.push_str(&format!("{:02}", digit)); + } else { + result.push(c); + } + }); + + let num = result + .parse::() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to validate IBAN")?; + + if num % 97 != 1 { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid IBAN".to_string(), + } + .into()); + } + + Ok(()) + } + types::MerchantAccountData::Bacs { + account_number, + sort_code, + .. + } => { + if account_number.peek().len() > BACS_MAX_ACCOUNT_NUMBER_LENGTH + || sort_code.peek().len() != BACS_SORT_CODE_LENGTH + { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid BACS numbers".to_string(), + } + .into()); + } + + Ok(()) + } + } +} + +async fn connector_recipient_create_call( + state: &SessionState, + merchant_id: &str, + connector_name: String, + auth: &types::ConnectorAuthType, + data: &types::MerchantAccountData, +) -> RouterResult { + let connector = pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name( + connector_name.as_str(), + )?; + + let auth = pm_auth_types::ConnectorAuthType::foreign_try_from(auth.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while converting ConnectorAuthType")?; + + let connector_integration: pm_auth_types::api::BoxedConnectorIntegration< + '_, + pm_auth_types::api::auth_service::RecipientCreate, + pm_auth_types::RecipientCreateRequest, + pm_auth_types::RecipientCreateResponse, + > = connector.connector.get_connector_integration(); + + let req = match data { + types::MerchantAccountData::Iban { iban, name, .. } => { + pm_auth_types::RecipientCreateRequest { + name: name.clone(), + account_data: pm_auth_types::RecipientAccountData::Iban(iban.clone()), + address: None, + } + } + types::MerchantAccountData::Bacs { + account_number, + sort_code, + name, + .. + } => pm_auth_types::RecipientCreateRequest { + name: name.clone(), + account_data: pm_auth_types::RecipientAccountData::Bacs { + sort_code: sort_code.clone(), + account_number: account_number.clone(), + }, + address: None, + }, + }; + + let router_data = pm_auth_types::RecipientCreateRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_id.to_owned()), + connector: Some(connector_name), + request: req, + response: Err(pm_auth_types::ErrorResponse { + status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), + reason: None, + }), + connector_http_status_code: None, + connector_auth_type: auth, + }; + + let resp = payment_initiation_service::execute_connector_processing_step( + state, + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling recipient create connector api")?; + + let recipient_create_resp = + resp.response + .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let recipient_id = recipient_create_resp.recipient_id; + + Ok(recipient_id) +} + +async fn locker_recipient_create_call( + state: &SessionState, + merchant_id: &str, + data: &types::MerchantAccountData, +) -> RouterResult { + let enc_data = serde_json::to_string(data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert to MerchantAccountData json to String")?; + + let cust_id = id_type::CustomerId::from(merchant_id.to_string().into()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert to CustomerId")?; + + let payload = transformers::StoreLockerReq::LockerGeneric(transformers::StoreGenericReq { + merchant_id, + merchant_customer_id: cust_id.clone(), + enc_data, + ttl: state.conf.locker.ttl_for_storage_in_secs, + }); + + let store_resp = cards::call_to_locker_hs( + state, + &payload, + &cust_id, + api_enums::LockerChoice::HyperswitchCardVault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encrypt merchant bank account data")?; + + Ok(store_resp.card_reference) +} diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index d3393b5b1f78..6eea962b9e58 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -209,6 +209,7 @@ impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType { Ok::(Self { client_id: api_key.to_owned(), secret: key1.to_owned(), + merchant_data: None, }) } _ => Err(errors::ConnectorError::FailedToObtainAuthType), diff --git a/crates/router/src/core/pm_auth/transformers.rs b/crates/router/src/core/pm_auth/transformers.rs index 8a1369c2e02f..d516cab62fe7 100644 --- a/crates/router/src/core/pm_auth/transformers.rs +++ b/crates/router/src/core/pm_auth/transformers.rs @@ -2,6 +2,36 @@ use pm_auth::types::{self as pm_auth_types}; use crate::{core::errors, types, types::transformers::ForeignTryFrom}; +impl From for pm_auth_types::MerchantAccountData { + fn from(from: types::MerchantAccountData) -> Self { + match from { + types::MerchantAccountData::Iban { iban, name, .. } => Self::Iban { iban, name }, + types::MerchantAccountData::Bacs { + account_number, + sort_code, + name, + .. + } => Self::Bacs { + account_number, + sort_code, + name, + }, + } + } +} + +impl From for pm_auth_types::MerchantRecipientData { + fn from(value: types::MerchantRecipientData) -> Self { + match value { + types::MerchantRecipientData::ConnectorRecipientId(id) => { + Self::ConnectorRecipientId(id) + } + types::MerchantRecipientData::WalletId(id) => Self::WalletId(id), + types::MerchantRecipientData::AccountData(data) => Self::AccountData(data.into()), + } + } +} + impl ForeignTryFrom for pm_auth_types::ConnectorAuthType { type Error = errors::ConnectorError; fn foreign_try_from(auth_type: types::ConnectorAuthType) -> Result { diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index a176db265495..567d16ee269b 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -440,7 +440,7 @@ impl MerchantConnectorAccountInterface for Store { #[cfg(feature = "accounts_cache")] // Redact all caches as any of might be used because of backwards compatibility - cache::publish_and_redact_multiple( + Box::pin(cache::publish_and_redact_multiple( self, [ cache::CacheKind::Accounts( @@ -454,7 +454,7 @@ impl MerchantConnectorAccountInterface for Store { ), ], || update, - ) + )) .await .map_err(|error| { // Returning `DatabaseConnectionError` after logging the actual error because @@ -784,6 +784,7 @@ impl MerchantConnectorAccountInterface for MockDb { pm_auth_config: t.pm_auth_config, status: t.status, connector_wallets_details: t.connector_wallets_details.map(Encryption::from), + additional_merchant_data: t.additional_merchant_data.map(|data| data.into()), }; accounts.push(account.clone()); account @@ -991,6 +992,7 @@ mod merchant_connector_account_cache_tests { .await .unwrap(), ), + additional_merchant_data: None, }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c457ca59df80..cc735612ac3b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -12,7 +12,7 @@ pub mod domain; #[cfg(feature = "frm")] pub mod fraud_check; pub mod pm_auth; - +use masking::Secret; pub mod storage; pub mod transformers; use std::marker::PhantomData; @@ -606,6 +606,150 @@ pub struct ResponseRouterData { pub http_code: u16, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum RecipientIdType { + ConnectorId(Secret), + LockerId(Secret), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MerchantAccountData { + Iban { + iban: Secret, + name: String, + connector_recipient_id: Option, + }, + Bacs { + account_number: Secret, + sort_code: Secret, + name: String, + connector_recipient_id: Option, + }, +} + +impl ForeignFrom for api_models::admin::MerchantAccountData { + fn foreign_from(from: MerchantAccountData) -> Self { + match from { + MerchantAccountData::Iban { + iban, + name, + connector_recipient_id, + } => Self::Iban { + iban, + name, + connector_recipient_id: match connector_recipient_id { + Some(RecipientIdType::ConnectorId(id)) => Some(id.clone()), + _ => None, + }, + }, + MerchantAccountData::Bacs { + account_number, + sort_code, + name, + connector_recipient_id, + } => Self::Bacs { + account_number, + sort_code, + name, + connector_recipient_id: match connector_recipient_id { + Some(RecipientIdType::ConnectorId(id)) => Some(id.clone()), + _ => None, + }, + }, + } + } +} + +impl From for MerchantAccountData { + fn from(from: api_models::admin::MerchantAccountData) -> Self { + match from { + api_models::admin::MerchantAccountData::Iban { + iban, + name, + connector_recipient_id, + } => Self::Iban { + iban, + name, + connector_recipient_id: connector_recipient_id.map(RecipientIdType::ConnectorId), + }, + api_models::admin::MerchantAccountData::Bacs { + account_number, + sort_code, + name, + connector_recipient_id, + } => Self::Bacs { + account_number, + sort_code, + name, + connector_recipient_id: connector_recipient_id.map(RecipientIdType::ConnectorId), + }, + } + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MerchantRecipientData { + ConnectorRecipientId(Secret), + WalletId(Secret), + AccountData(MerchantAccountData), +} + +impl ForeignFrom for api_models::admin::MerchantRecipientData { + fn foreign_from(value: MerchantRecipientData) -> Self { + match value { + MerchantRecipientData::ConnectorRecipientId(id) => Self::ConnectorRecipientId(id), + MerchantRecipientData::WalletId(id) => Self::WalletId(id), + MerchantRecipientData::AccountData(data) => { + Self::AccountData(api_models::admin::MerchantAccountData::foreign_from(data)) + } + } + } +} + +impl From for MerchantRecipientData { + fn from(value: api_models::admin::MerchantRecipientData) -> Self { + match value { + api_models::admin::MerchantRecipientData::ConnectorRecipientId(id) => { + Self::ConnectorRecipientId(id) + } + api_models::admin::MerchantRecipientData::WalletId(id) => Self::WalletId(id), + api_models::admin::MerchantRecipientData::AccountData(data) => { + Self::AccountData(data.into()) + } + } + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AdditionalMerchantData { + OpenBankingRecipientData(MerchantRecipientData), +} + +impl ForeignFrom for AdditionalMerchantData { + fn foreign_from(value: api_models::admin::AdditionalMerchantData) -> Self { + match value { + api_models::admin::AdditionalMerchantData::OpenBankingRecipientData(data) => { + Self::OpenBankingRecipientData(MerchantRecipientData::from(data)) + } + } + } +} + +impl ForeignFrom for api_models::admin::AdditionalMerchantData { + fn foreign_from(value: AdditionalMerchantData) -> Self { + match value { + AdditionalMerchantData::OpenBankingRecipientData(data) => { + Self::OpenBankingRecipientData( + api_models::admin::MerchantRecipientData::foreign_from(data), + ) + } + } + } +} + impl ForeignFrom for ConnectorAuthType { fn foreign_from(value: api_models::admin::ConnectorAuthType) -> Self { match value { diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 6c2f6a06e1ed..2cf091eda12e 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -40,6 +40,7 @@ pub struct MerchantConnectorAccount { pub pm_auth_config: Option, pub status: enums::ConnectorStatus, pub connector_wallets_details: Option>>, + pub additional_merchant_data: Option>>, } #[derive(Debug)] @@ -101,6 +102,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { pm_auth_config: self.pm_auth_config, status: self.status, connector_wallets_details: self.connector_wallets_details.map(Encryption::from), + additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), }, ) } @@ -148,6 +150,17 @@ impl behaviour::Conversion for MerchantConnectorAccount { .change_context(ValidationError::InvalidValue { message: "Failed while decrypting connector wallets details".to_string(), })?, + additional_merchant_data: if let Some(data) = other.additional_merchant_data { + Some( + Encryptable::decrypt(data, key.peek(), GcmAes256) + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting additional_merchant_data".to_string(), + })?, + ) + } else { + None + }, }) } @@ -177,6 +190,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { pm_auth_config: self.pm_auth_config, status: self.status, connector_wallets_details: self.connector_wallets_details.map(Encryption::from), + additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), }) } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index b06599769f4d..d1f01ac8ef76 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -26,6 +26,7 @@ use crate::{ }, services::authentication::get_header_value_by_key, types::{ + self as router_types, api::{self as api_types, routing as routing_types}, storage, }, @@ -977,6 +978,19 @@ impl TryFrom for api_models::admin::MerchantCo applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, status: item.status, + additional_merchant_data: item + .additional_merchant_data + .map(|data| { + let data = data.into_inner(); + serde_json::Value::parse_value::( + data.expose(), + "AdditionalMerchantData", + ) + .attach_printable("Unable to deserialize additional_merchant_data") + .change_context(errors::ApiErrorResponse::InternalServerError) + }) + .transpose()? + .map(api_models::admin::AdditionalMerchantData::foreign_from), }) } } diff --git a/migrations/2024-04-12-100925_mca_additional_merchant_data/down.sql b/migrations/2024-04-12-100925_mca_additional_merchant_data/down.sql new file mode 100644 index 000000000000..7614b52872d2 --- /dev/null +++ b/migrations/2024-04-12-100925_mca_additional_merchant_data/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS additional_merchant_data; \ No newline at end of file diff --git a/migrations/2024-04-12-100925_mca_additional_merchant_data/up.sql b/migrations/2024-04-12-100925_mca_additional_merchant_data/up.sql new file mode 100644 index 000000000000..4b48796785e6 --- /dev/null +++ b/migrations/2024-04-12-100925_mca_additional_merchant_data/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE merchant_connector_account ADD COLUMN IF NOT EXISTS additional_merchant_data BYTEA DEFAULT NULL; \ No newline at end of file