From 5283d16bc6868c1b3a9988a337930a6d067d01bd Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Tue, 5 Dec 2023 00:24:16 +0530 Subject: [PATCH] feat(pm_auth): Migrate pm auth apis (#3051) Co-authored-by: Chethan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 2 + config/development.toml | 110 ++-- crates/api_models/Cargo.toml | 2 + crates/api_models/src/enums.rs | 25 + crates/api_models/src/lib.rs | 1 + crates/api_models/src/pm_auth.rs | 57 ++ crates/pm_auth/src/connector/plaid.rs | 87 ++- .../src/connector/plaid/transformers.rs | 183 +++++- crates/pm_auth/src/consts.rs | 7 +- crates/pm_auth/src/core/errors.rs | 2 + crates/pm_auth/src/types.rs | 38 +- crates/pm_auth/src/types/api.rs | 2 +- crates/pm_auth/src/types/api/auth_service.rs | 5 +- crates/router/Cargo.toml | 2 +- crates/router/src/configs/settings.rs | 7 + crates/router/src/core/admin.rs | 148 ++++- .../router/src/core/payment_methods/cards.rs | 105 +++- crates/router/src/core/pm_auth.rs | 592 +++++++++++++++++- crates/router/src/core/pm_auth/helpers.rs | 33 + .../router/src/core/pm_auth/transformers.rs | 18 + crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/pm_auth.rs | 73 +++ crates/router/src/services.rs | 1 + crates/router/src/services/authentication.rs | 12 + crates/router/src/services/pm_auth.rs | 95 +++ crates/router/src/types.rs | 1 + crates/router/src/types/pm_auth.rs | 38 ++ crates/router_env/src/logger/types.rs | 4 + 30 files changed, 1553 insertions(+), 106 deletions(-) create mode 100644 crates/api_models/src/pm_auth.rs create mode 100644 crates/router/src/core/pm_auth/helpers.rs create mode 100644 crates/router/src/core/pm_auth/transformers.rs create mode 100644 crates/router/src/routes/pm_auth.rs create mode 100644 crates/router/src/services/pm_auth.rs create mode 100644 crates/router/src/types/pm_auth.rs diff --git a/Cargo.lock b/Cargo.lock index f57b8a770462..1d581698e5a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,8 @@ dependencies = [ "common_utils", "error-stack", "euclid", + "frunk", + "frunk_core", "masking", "mime", "reqwest", diff --git a/config/development.toml b/config/development.toml index fa5fddb0d60a..192e974c0a70 100644 --- a/config/development.toml +++ b/config/development.toml @@ -243,7 +243,7 @@ adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24, stripe = { banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" } [bank_config.open_banking_uk] -adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled"} +adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" } [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" @@ -292,45 +292,45 @@ giropay = { country = "DE", currency = "EUR" } eps = { country = "AT", currency = "EUR" } sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } ideal = { country = "NL", currency = "EUR" } -blik = {country = "PL", currency = "PLN"} -trustly = {country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} -online_banking_czech_republic = {country = "CZ", currency = "EUR,CZK"} -online_banking_finland = {country = "FI", currency = "EUR"} -online_banking_poland = {country = "PL", currency = "PLN"} -online_banking_slovakia = {country = "SK", currency = "EUR,CZK"} -bancontact_card = {country = "BE", currency = "EUR"} -ach = {country = "US", currency = "USD"} -bacs = {country = "UK", currency = "GBP"} -sepa = {country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR"} -ali_pay_hk = {country = "HK", currency = "HKD"} -bizum = {country = "ES", currency = "EUR"} -go_pay = {country = "ID", currency = "IDR"} -kakao_pay = {country = "KR", currency = "KRW"} -momo = {country = "VN", currency = "VND"} -gcash = {country = "PH", currency = "PHP"} -online_banking_fpx = {country = "MY", currency = "MYR"} -online_banking_thailand = {country = "TH", currency = "THB"} -touch_n_go = {country = "MY", currency = "MYR"} -atome = {country = "MY,SG", currency = "MYR,SGD"} -swish = {country = "SE", currency = "SEK"} -permata_bank_transfer = {country = "ID", currency = "IDR"} -bca_bank_transfer = {country = "ID", currency = "IDR"} -bni_va = {country = "ID", currency = "IDR"} -bri_va = {country = "ID", currency = "IDR"} -cimb_va = {country = "ID", currency = "IDR"} -danamon_va = {country = "ID", currency = "IDR"} -mandiri_va = {country = "ID", currency = "IDR"} -alfamart = {country = "ID", currency = "IDR"} -indomaret = {country = "ID", currency = "IDR"} -open_banking_uk = {country = "GB", currency = "GBP"} -oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} -seven_eleven = {country = "JP", currency = "JPY"} -lawson = {country = "JP", currency = "JPY"} -mini_stop = {country = "JP", currency = "JPY"} -family_mart = {country = "JP", currency = "JPY"} -seicomart = {country = "JP", currency = "JPY"} -pay_easy = {country = "JP", currency = "JPY"} +blik = { country = "PL", currency = "PLN" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +bancontact_card = { country = "BE", currency = "EUR" } +ach = { country = "US", currency = "USD" } +bacs = { country = "UK", currency = "GBP" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +ali_pay_hk = { country = "HK", currency = "HKD" } +bizum = { country = "ES", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +momo = { country = "VN", currency = "VND" } +gcash = { country = "PH", currency = "PHP" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_thailand = { country = "TH", currency = "THB" } +touch_n_go = { country = "MY", currency = "MYR" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +swish = { country = "SE", currency = "SEK" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } [pm_filters.braintree] paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } @@ -398,17 +398,17 @@ debit = { currency = "USD" } stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } -mollie = {long_lived_token = false, payment_method = "card"} -square = {long_lived_token = false, payment_method = "card"} +mollie = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } -payme = {long_lived_token = false, payment_method = "card"} -gocardless = {long_lived_token = true, payment_method = "bank_debit"} +payme = { long_lived_token = false, payment_method = "card" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } [temp_locker_enable_config] -stripe = {payment_method = "bank_transfer"} -nuvei = {payment_method = "card"} -shift4 = {payment_method = "card"} -bluesnap = {payment_method = "card"} +stripe = { payment_method = "bank_transfer" } +nuvei = { payment_method = "card" } +shift4 = { payment_method = "card" } +bluesnap = { payment_method = "card" } [connector_customer] connector_list = "gocardless,stax,stripe" @@ -447,9 +447,9 @@ wallet.apple_pay = { connector_list = "stripe,adyen" } wallet.paypal = { connector_list = "adyen" } card.credit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } card.debit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } -bank_debit.ach = { connector_list = "gocardless"} -bank_debit.becs = { connector_list = "gocardless"} -bank_debit.sepa = { connector_list = "gocardless"} +bank_debit.ach = { connector_list = "gocardless" } +bank_debit.becs = { connector_list = "gocardless" } +bank_debit.sepa = { connector_list = "gocardless" } [connector_request_reference_id_config] merchant_ids_send_payment_id_as_connector_request_id = [] @@ -469,8 +469,12 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" + [lock_settings] -redis_lock_expiry_seconds = 180 # 3 * 60 seconds +redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 [kv_config] @@ -504,4 +508,4 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 -queue_strategy = "Fifo" \ No newline at end of file +queue_strategy = "Fifo" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index cb2e243745de..8f018f082ca6 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -29,6 +29,8 @@ strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } +frunk = "0.4.1" +frunk_core = "0.4.1" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 535be4dfb159..ed0cd461113d 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + pub use common_enums::*; use utoipa::ToSchema; @@ -470,3 +472,26 @@ pub enum LockerChoice { Basilisk, Tartarus, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PmAuthConnectors { + Plaid, +} + +pub fn convert_pm_auth_connector(connector_name: &str) -> Option { + PmAuthConnectors::from_str(connector_name).ok() +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 056888839a54..5d902c9c5ee6 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -22,6 +22,7 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +pub mod pm_auth; pub mod refunds; pub mod routing; pub mod surcharge_decision_configs; diff --git a/crates/api_models/src/pm_auth.rs b/crates/api_models/src/pm_auth.rs new file mode 100644 index 000000000000..7044bd8d3352 --- /dev/null +++ b/crates/api_models/src/pm_auth.rs @@ -0,0 +1,57 @@ +use common_enums::{PaymentMethod, PaymentMethodType}; +use common_utils::{ + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, +}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub struct LinkTokenCreateRequest { + pub language: Option, // optional language field to be passed + pub client_secret: Option, // client secret to be passed in req body + pub payment_id: String, // payment_id to be passed in req body for redis pm_auth connector name fetch + pub payment_method: PaymentMethod, // payment_method to be used for filtering pm_auth connector + pub payment_method_type: PaymentMethodType, // payment_method_type to be used for filtering pm_auth connector +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct LinkTokenCreateResponse { + pub link_token: String, // link_token received in response + pub connector: String, // pm_auth connector name in response +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] + +pub struct ExchangeTokenCreateRequest { + pub public_token: String, + pub client_secret: Option, + pub payment_id: String, + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ExchangeTokenCreateResponse { + pub access_token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConfig { + pub enabled_payment_methods: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConnectorChoice { + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, + pub connector_name: String, + pub mca_id: String, +} + +impl_misc_api_event_type!( + LinkTokenCreateRequest, + LinkTokenCreateResponse, + ExchangeTokenCreateRequest, + ExchangeTokenCreateResponse +); diff --git a/crates/pm_auth/src/connector/plaid.rs b/crates/pm_auth/src/connector/plaid.rs index 86953315afe8..d25aba881d2d 100644 --- a/crates/pm_auth/src/connector/plaid.rs +++ b/crates/pm_auth/src/connector/plaid.rs @@ -15,7 +15,7 @@ use crate::{ types::{ self as auth_types, api::{ - auth_service::{self, ExchangeToken, LinkToken}, + auth_service::{self, BankAccountCredentials, ExchangeToken, LinkToken}, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, }, }, @@ -266,3 +266,88 @@ impl self.build_error_response(res) } } + +impl auth_service::AuthServiceBankAccountCredentials for Plaid {} + +impl + ConnectorIntegration< + BankAccountCredentials, + auth_types::BankAccountCredentialsRequest, + auth_types::BankAccountCredentialsResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::BankDetailsRouterData, + 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::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/auth/get")) + } + + fn get_request_body( + &self, + req: &auth_types::BankDetailsRouterData, + ) -> errors::CustomResult, errors::ConnectorError> { + let req_obj = plaid::PlaidBankAccountCredentialsRequest::try_from(req)?; + let plaid_req = RequestBody::log_and_get_request_body( + &req_obj, + Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(plaid_req)) + } + + fn build_request( + &self, + req: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthBankAccountDetailsType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthBankAccountDetailsType::get_headers( + self, req, connectors, + )?) + .body(auth_types::PaymentAuthBankAccountDetailsType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::BankDetailsRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidBankAccountCredentialsResponse = res + .response + .parse_struct("PlaidBankAccountCredentialsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_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 370be89fd111..5e1ad67aead0 100644 --- a/crates/pm_auth/src/connector/plaid/transformers.rs +++ b/crates/pm_auth/src/connector/plaid/transformers.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; + +use common_enums::PaymentMethodType; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -23,26 +26,18 @@ impl TryFrom<&types::LinkTokenRouterData> for PlaidLinkTokenRequest { type Error = error_stack::Report; fn try_from(item: &types::LinkTokenRouterData) -> Result { Ok(Self { - client_name: item.request.client_name.clone().ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "client_name", - }, - )?, + client_name: item.request.client_name.clone(), country_codes: item.request.country_codes.clone().ok_or( errors::ConnectorError::MissingRequiredField { field_name: "country_codes", }, )?, - language: item.request.language.clone().ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "language", - }, - )?, + language: item.request.language.clone().unwrap_or("en".to_string()), products: vec!["auth".to_string()], user: User { client_user_id: item.request.user_info.clone().ok_or( errors::ConnectorError::MissingRequiredField { - field_name: "user.client_user_id", + field_name: "country_codes", }, )?, }, @@ -53,8 +48,6 @@ impl TryFrom<&types::LinkTokenRouterData> for PlaidLinkTokenRequest { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub struct PlaidLinkTokenResponse { - expiration: String, - request_id: String, link_token: String, } @@ -68,9 +61,7 @@ impl ) -> Result { Ok(Self { response: Ok(types::LinkTokenResponse { - expiration: Some(item.response.expiration), - request_id: Some(item.response.request_id), - link_token: Some(item.response.link_token), + link_token: item.response.link_token, }), ..item.data }) @@ -87,7 +78,6 @@ pub struct PlaidExchangeTokenRequest { pub struct PlaidExchangeTokenResponse { pub access_token: String, - pub request_id: String, } impl @@ -106,8 +96,7 @@ impl ) -> Result { Ok(Self { response: Ok(types::ExchangeTokenResponse { - access_token: Some(item.response.access_token), - request_id: Some(item.response.request_id), + access_token: item.response.access_token, }), ..item.data }) @@ -123,6 +112,160 @@ impl TryFrom<&types::ExchangeTokenRouterData> for PlaidExchangeTokenRequest { } } +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidBankAccountCredentialsRequest { + access_token: String, + options: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsResponse { + pub accounts: Vec, + pub numbers: PlaidBankAccountCredentialsNumbers, + // pub item: PlaidBankAccountCredentialsItem, + pub request_id: String, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct BankAccountCredentialsOptions { + account_ids: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsAccounts { + pub account_id: String, + pub name: String, + pub subtype: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBalances { + pub available: Option, + pub current: Option, + pub limit: Option, + pub iso_currency_code: Option, + pub unofficial_currency_code: Option, + pub last_updated_datetime: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsNumbers { + pub ach: Vec, + pub eft: Vec, + pub international: Vec, + pub bacs: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsItem { + pub item_id: String, + pub institution_id: Option, + pub webhook: Option, + pub error: Option, +} +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsACH { + pub account_id: String, + pub account: String, + pub routing: String, + pub wire_routing: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsEFT { + pub account_id: String, + pub account: String, + pub institution: String, + pub branch: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsInternational { + pub account_id: String, + pub iban: String, + pub bic: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBacs { + pub account_id: String, + pub account: String, + pub sort_code: String, +} + +impl TryFrom<&types::BankDetailsRouterData> for PlaidBankAccountCredentialsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::BankDetailsRouterData) -> Result { + Ok(Self { + access_token: item.request.access_token.clone(), + options: item.request.optional_ids.as_ref().map(|bank_account_ids| { + BankAccountCredentialsOptions { + account_ids: bank_account_ids.ids.clone(), + } + }), + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + > for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + ) -> Result { + let (account_numbers, accounts_info) = (item.response.numbers, item.response.accounts); + let mut bank_account_vec = Vec::new(); + let mut id_to_suptype = HashMap::new(); + + accounts_info.into_iter().for_each(|acc| { + id_to_suptype.insert(acc.account_id, (acc.subtype, acc.name)); + }); + + account_numbers.ach.into_iter().for_each(|ach| { + let (acc_type, acc_name) = + if let Some((_type, name)) = id_to_suptype.get(&ach.account_id) { + (_type.to_owned(), Some(name.clone())) + } else { + (None, None) + }; + + let bank_details_new = types::BankAccountDetails { + account_name: acc_name, + account_number: ach.account, + routing_number: ach.routing, + payment_method_type: PaymentMethodType::Ach, + account_id: ach.account_id, + account_type: acc_type, + }; + + bank_account_vec.push(bank_details_new); + }); + + Ok(Self { + response: Ok(types::BankAccountCredentialsResponse { + credentials: bank_account_vec, + }), + ..item.data + }) + } +} pub struct PlaidAuthType { pub client_id: Secret, pub secret: Secret, @@ -141,7 +284,7 @@ impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType { } } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct PlaidErrorResponse { pub display_message: Option, diff --git a/crates/pm_auth/src/consts.rs b/crates/pm_auth/src/consts.rs index f3e535c6d7b0..dac3485ec8fc 100644 --- a/crates/pm_auth/src/consts.rs +++ b/crates/pm_auth/src/consts.rs @@ -1,2 +1,5 @@ -pub(crate) const NO_ERROR_CODE: &str = "No error code"; -pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; +pub const REQUEST_TIME_OUT: u64 = 30; // will timeout after the mentioned limit +pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; // timeout error code +pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; // error message for timed out request +pub const NO_ERROR_CODE: &str = "No error code"; +pub const NO_ERROR_MESSAGE: &str = "No error message"; diff --git a/crates/pm_auth/src/core/errors.rs b/crates/pm_auth/src/core/errors.rs index 597455ecbf51..31b178a6276f 100644 --- a/crates/pm_auth/src/core/errors.rs +++ b/crates/pm_auth/src/core/errors.rs @@ -4,6 +4,8 @@ pub enum ConnectorError { FailedToObtainAuthType, #[error("Missing required field: {field_name}")] MissingRequiredField { field_name: &'static str }, + #[error("Failed to execute a processing step: {0:?}")] + ProcessingStepFailed(Option), #[error("Failed to deserialize connector response")] ResponseDeserializationFailed, #[error("Failed to encode connector request")] diff --git a/crates/pm_auth/src/types.rs b/crates/pm_auth/src/types.rs index 1cccb11cee0f..6f5875247f1f 100644 --- a/crates/pm_auth/src/types.rs +++ b/crates/pm_auth/src/types.rs @@ -3,6 +3,7 @@ pub mod api; use std::marker::PhantomData; use api::auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}; +use common_enums::PaymentMethodType; use masking::Secret; #[derive(Debug, Clone)] pub struct PaymentAuthRouterData { @@ -12,11 +13,12 @@ pub struct PaymentAuthRouterData { pub request: Request, pub response: Result, pub connector_auth_type: ConnectorAuthType, + pub connector_http_status_code: Option, } #[derive(Debug, Clone)] pub struct LinkTokenRequest { - pub client_name: Option, + pub client_name: String, pub country_codes: Option>, pub language: Option, pub user_info: Option, @@ -24,9 +26,7 @@ pub struct LinkTokenRequest { #[derive(Debug, Clone)] pub struct LinkTokenResponse { - pub expiration: Option, - pub link_token: Option, - pub request_id: Option, + pub link_token: String, } pub type LinkTokenRouterData = @@ -39,8 +39,15 @@ pub struct ExchangeTokenRequest { #[derive(Debug, Clone)] pub struct ExchangeTokenResponse { - pub access_token: Option, - pub request_id: Option, + pub access_token: String, +} + +impl From for api_models::pm_auth::ExchangeTokenCreateResponse { + fn from(value: ExchangeTokenResponse) -> Self { + Self { + access_token: value.access_token, + } + } } pub type ExchangeTokenRouterData = @@ -49,6 +56,12 @@ pub type ExchangeTokenRouterData = #[derive(Debug, Clone)] pub struct BankAccountCredentialsRequest { pub access_token: String, + pub optional_ids: Option, +} + +#[derive(Debug, Clone)] +pub struct BankAccountOptionalIDs { + pub ids: Vec, } #[derive(Debug, Clone)] @@ -58,10 +71,12 @@ pub struct BankAccountCredentialsResponse { #[derive(Debug, Clone)] pub struct BankAccountDetails { - pub account_id: String, - pub account_name: String, + pub account_name: Option, pub account_number: String, pub routing_number: String, + pub payment_method_type: PaymentMethodType, + pub account_id: String, + pub account_type: Option, } pub type BankDetailsRouterData = PaymentAuthRouterData< @@ -82,7 +97,7 @@ pub type PaymentAuthBankAccountDetailsType = dyn self::api::ConnectorIntegration BankAccountCredentialsResponse, >; -#[derive(Clone, Debug, strum::EnumString)] +#[derive(Clone, Debug, strum::EnumString, strum::Display)] #[strum(serialize_all = "snake_case")] pub enum PaymentMethodAuthConnectors { Plaid, @@ -130,3 +145,8 @@ pub struct Response { pub response: bytes::Bytes, pub status_code: u16, } + +#[derive(serde::Deserialize, Clone)] +pub struct AuthServiceQueryParam { + pub client_secret: Option, +} diff --git a/crates/pm_auth/src/types/api.rs b/crates/pm_auth/src/types/api.rs index 6f083dc42bdd..2416d0fee1de 100644 --- a/crates/pm_auth/src/types/api.rs +++ b/crates/pm_auth/src/types/api.rs @@ -129,7 +129,7 @@ pub trait AuthServiceConnector: AuthService + Send + Debug {} impl AuthServiceConnector for T {} -type BoxedPaymentAuthConnector = Box<&'static (dyn AuthServiceConnector + Sync)>; +pub type BoxedPaymentAuthConnector = Box<&'static (dyn AuthServiceConnector + Sync)>; #[derive(Clone, Debug)] pub struct PaymentAuthConnectorData { diff --git a/crates/pm_auth/src/types/api/auth_service.rs b/crates/pm_auth/src/types/api/auth_service.rs index 8cee37b4b9b7..35d44970d518 100644 --- a/crates/pm_auth/src/types/api/auth_service.rs +++ b/crates/pm_auth/src/types/api/auth_service.rs @@ -4,7 +4,10 @@ use crate::types::{ }; pub trait AuthService: - super::ConnectorCommon + AuthServiceLinkToken + AuthServiceExchangeToken + super::ConnectorCommon + + AuthServiceLinkToken + + AuthServiceExchangeToken + + AuthServiceBankAccountCredentials { } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 2d40ffec6aa7..cbf1309d9958 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -110,10 +110,10 @@ currency_conversion = { version = "0.1.0", path = "../currency_conversion" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } +pm_auth = { version = "0.1.0", path = "../pm_auth", package = "pm_auth" } external_services = { version = "0.1.0", path = "../external_services" } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } -pm_auth = { version = "0.1.0", path = "../pm_auth" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f2d962b0abee..e7374254571b 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -100,6 +100,7 @@ pub struct Settings { pub required_fields: RequiredFields, pub delayed_session_response: DelayedSessionConfig, pub webhook_source_verification_call: WebhookSourceVerificationCall, + pub payment_method_auth: PaymentMethodAuth, pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig, #[cfg(feature = "payouts")] pub payouts: Payouts, @@ -144,6 +145,12 @@ pub struct ForexApi { pub redis_lock_timeout: u64, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PaymentMethodAuth { + pub redis_expiry: i64, + pub pm_auth_key: String, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct DefaultExchangeRates { pub base_currency: String, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 6d564473efe4..a0c8d208b874 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,7 +10,7 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; -use error_stack::{report, FutureExt, ResultExt}; +use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; use pm_auth::connector::plaid::transformers::PlaidAuthType; @@ -763,7 +763,7 @@ pub async fn create_payment_connector( ) .await?; - let routable_connector = + let mut routable_connector = api_enums::RoutableConnectors::from_str(&req.connector_name.to_string()).ok(); let business_profile = state @@ -774,6 +774,30 @@ pub async fn create_payment_connector( id: profile_id.to_owned(), })?; + let pm_auth_connector = + api_enums::convert_pm_auth_connector(req.connector_name.to_string().as_str()); + + let is_unroutable_connector = if pm_auth_connector.is_some() { + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector type given".to_string(), + }) + .into_report(); + } + true + } else { + let routable_connector_option = req + .connector_name + .to_string() + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector name given".to_string(), + })?; + routable_connector = Some(routable_connector_option); + false + }; + // If connector label is not passed in the request, generate one let connector_label = req .connector_label @@ -878,6 +902,20 @@ pub async fn create_payment_connector( api_enums::ConnectorStatus::Active, )?; + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + &*state.clone().store, + merchant_id.clone().as_str(), + &key_store, + merchant_account, + &Some(profile_id.clone()), + ) + .await?; + } + } + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -949,7 +987,7 @@ pub async fn create_payment_connector( #[cfg(feature = "connector_choice_mca_id")] merchant_connector_id: Some(mca.merchant_connector_id.clone()), #[cfg(not(feature = "connector_choice_mca_id"))] - sub_label: req.business_sub_label, + sub_label: req.business_sub_label.clone(), }; if !default_routing_config.contains(&choice) { @@ -957,7 +995,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, merchant_id, - default_routing_config, + default_routing_config.clone(), ) .await?; } @@ -966,7 +1004,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, &profile_id.clone(), - default_routing_config_for_profile, + default_routing_config_for_profile.clone(), ) .await?; } @@ -981,10 +1019,94 @@ pub async fn create_payment_connector( ], ); + match is_unroutable_connector { + true => (), + false => { + if let Some(routable_connector_val) = routable_connector { + let choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: routable_connector_val, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Some(mca.merchant_connector_id.clone()), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: req.business_sub_label.clone(), + }; + + if !default_routing_config.contains(&choice) { + default_routing_config.push(choice.clone()); + routing_helpers::update_merchant_default_config( + &*state.clone().store, + merchant_id, + default_routing_config, + ) + .await?; + } + + if !default_routing_config_for_profile.contains(&choice) { + default_routing_config_for_profile.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + &profile_id, + default_routing_config_for_profile, + ) + .await?; + } + } + } + }; + let mca_response = mca.try_into()?; Ok(service_api::ApplicationResponse::Json(mca_response)) } +async fn validate_pm_auth( + val: serde_json::Value, + db: &dyn StorageInterface, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + profile_id: &Option, +) -> RouterResponse<()> { + let config = serde_json::from_value::(val) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "invalid data received for payment method auth config".to_string(), + }) + .attach_printable("Failed to deserialize Payment Method Auth config")?; + + let all_mcas = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + for conn_choice in config.enabled_payment_methods { + let pm_auth_mca = all_mcas + .clone() + .into_iter() + .find(|mca| mca.merchant_connector_id == conn_choice.mca_id) + .ok_or(errors::ApiErrorResponse::GenericNotFoundError { + message: "connector account not found".to_string(), + }) + .into_report()?; + + if &pm_auth_mca.profile_id != profile_id { + return Err(errors::ApiErrorResponse::GenericNotFoundError { + message: "pm auth profile_id differs from connector profile_id".to_string(), + }) + .into_report(); + } + } + + Ok(services::ApplicationResponse::StatusOk) +} + pub async fn retrieve_payment_connector( state: AppState, merchant_id: String, @@ -1067,7 +1189,7 @@ pub async fn update_payment_connector( .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - let _merchant_account = db + let merchant_account = db .find_merchant_account_by_merchant_id(merchant_id, &key_store) .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; @@ -1107,6 +1229,20 @@ pub async fn update_payment_connector( let (connector_status, disabled) = validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + db, + merchant_id, + &key_store, + merchant_account, + &mca.profile_id, + ) + .await?; + } + } + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 545733e298ab..2d171da7fa6c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -13,6 +13,7 @@ use api_models::{ ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, + pm_auth::PaymentMethodAuthConfig, surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ @@ -26,6 +27,8 @@ use masking::Secret; use router_env::{instrument, tracing}; use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; +#[cfg(not(feature = "connector_choice_mca_id"))] +use crate::core::utils::get_connector_label; use crate::{ configs::settings, core::{ @@ -1079,9 +1082,9 @@ pub async fn list_payment_methods( logger::debug!(mca_before_filtering=?filtered_mcas); let mut response: Vec = vec![]; - for mca in filtered_mcas { - let payment_methods = match mca.payment_methods_enabled { - Some(pm) => pm, + for mca in &filtered_mcas { + let payment_methods = match &mca.payment_methods_enabled { + Some(pm) => pm.clone(), None => continue, }; @@ -1092,13 +1095,15 @@ pub async fn list_payment_methods( payment_intent.as_ref(), payment_attempt.as_ref(), billing_address.as_ref(), - mca.connector_name, + mca.connector_name.clone(), pm_config_mapping, &state.conf.mandates.supported_payment_methods, ) .await?; } + let mut pmt_to_auth_connector = HashMap::new(); + if let Some((payment_attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent.as_ref()) { @@ -1202,6 +1207,84 @@ pub async fn list_payment_methods( pre_routing_results.insert(pm_type, routable_choice); } + let redis_conn = db + .get_redis_conn() + .map_err(|redis_error| logger::error!(?redis_error)) + .ok(); + + let mut val = Vec::new(); + + for (payment_method_type, routable_connector_choice) in &pre_routing_results { + #[cfg(not(feature = "connector_choice_mca_id"))] + let connector_label = get_connector_label( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + routable_connector_choice.sub_label.as_ref(), + #[cfg(feature = "connector_choice_mca_id")] + None, + routable_connector_choice.connector.to_string().as_str(), + ); + #[cfg(not(feature = "connector_choice_mca_id"))] + let matched_mca = filtered_mcas + .iter() + .find(|m| connector_label == m.connector_label); + + #[cfg(feature = "connector_choice_mca_id")] + let matched_mca = filtered_mcas.iter().find(|m| { + routable_connector_choice.merchant_connector_id.as_ref() + == Some(&m.merchant_connector_id) + }); + + if let Some(m) = matched_mca { + let pm_auth_config = m + .pm_auth_config + .as_ref() + .map(|config| { + serde_json::from_value::(config.clone()) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + let matched_config = match pm_auth_config { + Some(config) => { + let internal_config = config + .enabled_payment_methods + .iter() + .find(|config| config.payment_method_type == *payment_method_type) + .cloned(); + + internal_config + } + None => None, + }; + + if let Some(config) = matched_config { + pmt_to_auth_connector + .insert(*payment_method_type, config.connector_name.clone()); + val.push(config); + } + } + } + + let pm_auth_key = format!("pm_auth_{}", payment_intent.payment_id); + let redis_expiry = state.conf.payment_method_auth.redis_expiry; + + if let Some(rc) = redis_conn { + rc.serialize_and_set_key_with_expiry(pm_auth_key.as_str(), val, redis_expiry) + .await + .attach_printable("Failed to store pm auth data in redis") + .unwrap_or_else(|err| { + logger::error!(error=?err); + }) + }; + routing_info.pre_routing_results = Some(pre_routing_results); let encoded = utils::Encode::::encode_to_value(&routing_info) @@ -1459,7 +1542,9 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1494,7 +1579,9 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1524,7 +1611,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1557,7 +1644,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1590,7 +1677,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 72d90f612f93..800bef87d628 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -1,10 +1,188 @@ -use pm_auth::connector::plaid::transformers::PlaidAuthType; +use std::collections::HashMap; + +use api_models::{enums, payment_methods}; +use hex; +pub mod helpers; +pub mod transformers; + +use common_utils::{ + consts, + crypto::{HmacSha256, SignMessage}, + ext_traits::AsyncExt, + generate_id, +}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +pub use external_services::kms; +use helpers::PaymentAuthConnectorDataExt; +use masking::{ExposeInterface, PeekInterface}; +use pm_auth::{ + connector::plaid::transformers::PlaidAuthType, + types::{ + self as pm_auth_types, + api::{ + auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}, + BoxedConnectorIntegration, PaymentAuthConnectorData, + }, + }, +}; use crate::{ - core::errors, - types::{self, transformers::ForeignTryFrom}, + core::{ + errors::{self, ApiErrorResponse, RouterResponse, RouterResult}, + payment_methods::cards, + payments::helpers as oss_helpers, + }, + db::StorageInterface, + logger, + routes::AppState, + services::{ + pm_auth::{self as pm_auth_services}, + ApplicationResponse, + }, + types::{ + self, + domain::{self, types::decrypt}, + storage, + transformers::ForeignTryFrom, + }, }; +pub async fn create_link_token( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::LinkTokenCreateRequest, +) -> RouterResponse { + let db = &*state.store; + + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .into_iter() + .find(|config| { + config.payment_method == payload.payment_method + && config.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "connector name not found".to_string(), + }) + .into_report()?; + + let connector_name = selected_config.connector_name.as_str(); + + let connector = PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + let connector_integration: BoxedConnectorIntegration< + '_, + LinkToken, + pm_auth_types::LinkTokenRequest, + pm_auth_types::LinkTokenResponse, + > = connector.connector.get_connector_integration(); + + let payment_intent = oss_helpers::verify_payment_intent_time_and_client_secret( + &*state.store, + &merchant_account, + payload.client_secret, + ) + .await?; + + let billing_country = payment_intent + .as_ref() + .async_map(|pi| async { + oss_helpers::get_address_by_id( + &*state.store, + pi.billing_address_id.clone(), + &key_store, + pi.payment_id.clone(), + merchant_account.merchant_id.clone(), + merchant_account.storage_scheme, + ) + .await + }) + .await + .transpose()? + .flatten() + .and_then(|address| address.country) + .map(|country| country.to_string()); + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &selected_config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account)?; + + let router_data = pm_auth_types::LinkTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id), + connector: Some(connector_name.to_string()), + request: pm_auth_types::LinkTokenRequest { + client_name: "HyperSwitch".to_string(), + country_codes: Some(vec![billing_country.ok_or( + errors::ApiErrorResponse::MissingRequiredField { + field_name: "billing_country", + }, + )?]), + language: payload.language, + user_info: payment_intent.and_then(|pi| pi.customer_id), + }, + response: Ok(pm_auth_types::LinkTokenResponse { + link_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let connector_resp = pm_auth_services::execute_connector_processing_step( + state.as_ref(), + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling link token creation connector api")?; + + let link_token_resp = + connector_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let response = api_models::pm_auth::LinkTokenCreateResponse { + link_token: link_token_resp.link_token, + connector: connector.connector_name.to_string(), + }; + + Ok(ApplicationResponse::Json(response)) +} + impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType { type Error = errors::ConnectorError; @@ -20,3 +198,411 @@ impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType { } } } + +pub async fn exchange_token_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResponse<()> { + let db = &*state.store; + + let config = get_selected_config_from_redis(db, &payload).await?; + + let connector_name = config.connector_name.as_str(); + + let connector = + pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account.clone())?; + + let access_token = get_access_token_from_exchange_api( + &connector, + connector_name, + &payload, + &auth_type, + &state, + ) + .await?; + + let bank_account_details_resp = get_bank_account_creds( + connector, + &merchant_account, + connector_name, + &access_token, + auth_type, + &state, + None, + ) + .await?; + + Box::pin(store_bank_details_in_payment_methods( + key_store, + payload, + merchant_account, + state, + bank_account_details_resp, + (connector_name, access_token), + merchant_connector_account.merchant_connector_id, + )) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +async fn store_bank_details_in_payment_methods( + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, + merchant_account: domain::MerchantAccount, + state: AppState, + bank_account_details_resp: pm_auth_types::BankAccountCredentialsResponse, + connector_details: (&str, String), + mca_id: String, +) -> RouterResult<()> { + let key = key_store.key.get_inner().peek(); + let db = &*state.clone().store; + let (connector_name, access_token) = connector_details; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payload.payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + let customer_id = payment_intent + .customer_id + .ok_or(ApiErrorResponse::CustomerNotFound)?; + + let payment_methods = db + .find_payment_method_by_customer_id_merchant_id_list( + &customer_id, + &merchant_account.merchant_id, + ) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + let mut hash_to_payment_method: HashMap< + String, + ( + storage::PaymentMethod, + payment_methods::PaymentMethodDataBankCreds, + ), + > = HashMap::new(); + + for pm in payment_methods { + if pm.payment_method == enums::PaymentMethod::BankDebit { + let bank_details_pm_data = decrypt::( + pm.payment_method_data.clone(), + key, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank account details")? + .map(|x| x.into_inner().expose()) + .map(|v| { + serde_json::from_value::(v) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + .and_then(|pmd| match pmd { + payment_methods::PaymentMethodsData::BankDetails(bank_creds) => Some(bank_creds), + _ => None, + }) + .ok_or(ApiErrorResponse::InternalServerError)?; + + hash_to_payment_method.insert( + bank_details_pm_data.hash.clone(), + (pm, bank_details_pm_data), + ); + } + } + + #[cfg(feature = "kms")] + let pm_auth_key = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(state.conf.payment_method_auth.pm_auth_key.clone()) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + #[cfg(not(feature = "kms"))] + let pm_auth_key = state.conf.payment_method_auth.pm_auth_key.clone(); + + let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = + Vec::new(); + let mut new_entries: Vec = Vec::new(); + + for creds in bank_account_details_resp.credentials { + let hash_string = format!("{}-{}", creds.account_number, creds.routing_number); + let generated_hash = hex::encode( + HmacSha256::sign_message(&HmacSha256, pm_auth_key.as_bytes(), hash_string.as_bytes()) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to sign the message")?, + ); + + let contains_account = hash_to_payment_method.get(&generated_hash); + let mut pmd = payment_methods::PaymentMethodDataBankCreds { + mask: creds + .account_number + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::(), + hash: generated_hash, + account_type: creds.account_type, + account_name: creds.account_name, + payment_method_type: creds.payment_method_type, + connector_details: vec![payment_methods::BankAccountConnectorDetails { + connector: connector_name.to_string(), + mca_id: mca_id.clone(), + access_token: payment_methods::BankAccountAccessCreds::AccessToken( + access_token.clone(), + ), + account_id: creds.account_id, + }], + }; + + if let Some((pm, details)) = contains_account { + pmd.connector_details.extend( + details + .connector_details + .clone() + .into_iter() + .filter(|conn| conn.mca_id != mca_id), + ); + + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { + payment_method_data: Some(encrypted_data), + }; + + update_entries.push((pm.clone(), pm_update)); + } else { + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let pm_new = storage::PaymentMethodNew { + customer_id: customer_id.clone(), + merchant_id: merchant_account.merchant_id.clone(), + payment_method_id: pm_id, + payment_method: enums::PaymentMethod::BankDebit, + payment_method_type: Some(creds.payment_method_type), + payment_method_issuer: None, + scheme: None, + metadata: None, + payment_method_data: Some(encrypted_data), + ..storage::PaymentMethodNew::default() + }; + + new_entries.push(pm_new); + }; + } + + store_in_db(update_entries, new_entries, db).await?; + + Ok(()) +} + +async fn store_in_db( + update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)>, + new_entries: Vec, + db: &dyn StorageInterface, +) -> RouterResult<()> { + let update_entries_futures = update_entries + .into_iter() + .map(|(pm, pm_update)| db.update_payment_method(pm, pm_update)) + .collect::>(); + + let new_entries_futures = new_entries + .into_iter() + .map(|pm_new| db.insert_payment_method(pm_new)) + .collect::>(); + + let update_futures = futures::future::join_all(update_entries_futures); + let new_futures = futures::future::join_all(new_entries_futures); + + let (update, new) = tokio::join!(update_futures, new_futures); + + let _ = update + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + let _ = new + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + Ok(()) +} + +async fn get_bank_account_creds( + connector: PaymentAuthConnectorData, + merchant_account: &domain::MerchantAccount, + connector_name: &str, + access_token: &str, + auth_type: pm_auth_types::ConnectorAuthType, + state: &AppState, + bank_account_id: Option, +) -> RouterResult { + let connector_integration_bank_details: BoxedConnectorIntegration< + '_, + BankAccountCredentials, + pm_auth_types::BankAccountCredentialsRequest, + pm_auth_types::BankAccountCredentialsResponse, + > = connector.connector.get_connector_integration(); + + let router_data_bank_details = pm_auth_types::BankDetailsRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id.clone()), + connector: Some(connector_name.to_string()), + request: pm_auth_types::BankAccountCredentialsRequest { + access_token: access_token.to_string(), + optional_ids: bank_account_id + .map(|id| pm_auth_types::BankAccountOptionalIDs { ids: vec![id] }), + }, + response: Ok(pm_auth_types::BankAccountCredentialsResponse { + credentials: Vec::new(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let bank_details_resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration_bank_details, + &router_data_bank_details, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling bank account details connector api")?; + + let bank_account_details_resp = + bank_details_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + Ok(bank_account_details_resp) +} + +async fn get_access_token_from_exchange_api( + connector: &PaymentAuthConnectorData, + connector_name: &str, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, + auth_type: &pm_auth_types::ConnectorAuthType, + state: &AppState, +) -> RouterResult { + let connector_integration: BoxedConnectorIntegration< + '_, + ExchangeToken, + pm_auth_types::ExchangeTokenRequest, + pm_auth_types::ExchangeTokenResponse, + > = connector.connector.get_connector_integration(); + + let router_data = pm_auth_types::ExchangeTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: None, + connector: Some(connector_name.to_string()), + request: pm_auth_types::ExchangeTokenRequest { + public_token: payload.public_token.clone(), + }, + response: Ok(pm_auth_types::ExchangeTokenResponse { + access_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type.clone(), + }; + + let resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling exchange token connector api")?; + + let exchange_token_resp = + resp.response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let access_token = exchange_token_resp.access_token; + Ok(access_token) +} + +async fn get_selected_config_from_redis( + db: &dyn StorageInterface, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResult { + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .iter() + .find(|conf| { + conf.payment_method == payload.payment_method + && conf.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "connector name not found".to_string(), + }) + .into_report()? + .clone(); + + Ok(selected_config) +} diff --git a/crates/router/src/core/pm_auth/helpers.rs b/crates/router/src/core/pm_auth/helpers.rs new file mode 100644 index 000000000000..43d30705a803 --- /dev/null +++ b/crates/router/src/core/pm_auth/helpers.rs @@ -0,0 +1,33 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::{IntoReport, ResultExt}; +use pm_auth::types::{self as pm_auth_types, api::BoxedPaymentAuthConnector}; + +use crate::{ + core::errors::{self, ApiErrorResponse}, + types::{self, domain, transformers::ForeignTryFrom}, +}; + +pub trait PaymentAuthConnectorDataExt { + fn get_connector_by_name(name: &str) -> errors::CustomResult + where + Self: Sized; + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult; +} + +pub fn get_connector_auth_type( + merchant_connector_account: domain::MerchantConnectorAccount, +) -> errors::CustomResult { + let auth_type: types::ConnectorAuthType = merchant_connector_account + .connector_account_details + .parse_value("ConnectorAuthType") + .change_context(ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + pm_auth_types::ConnectorAuthType::foreign_try_from(auth_type) + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while converting ConnectorAuthType") +} diff --git a/crates/router/src/core/pm_auth/transformers.rs b/crates/router/src/core/pm_auth/transformers.rs new file mode 100644 index 000000000000..8a1369c2e02f --- /dev/null +++ b/crates/router/src/core/pm_auth/transformers.rs @@ -0,0 +1,18 @@ +use pm_auth::types::{self as pm_auth_types}; + +use crate::{core::errors, types, types::transformers::ForeignTryFrom}; + +impl ForeignTryFrom for pm_auth_types::ConnectorAuthType { + type Error = errors::ConnectorError; + fn foreign_try_from(auth_type: types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => { + Ok::(Self::BodyKey { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType), + } + } +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index b19ef5d7016b..767bd2dba8c8 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -36,6 +36,8 @@ pub mod verify_connector; pub mod webhooks; pub mod locker_migration; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod pm_auth; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(any(feature = "olap", feature = "oltp"))] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a145f3e7e5d7..1344cf44ae90 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -20,6 +20,8 @@ use super::currency; use super::dummy_connector::*; #[cfg(feature = "payouts")] use super::payouts::*; +#[cfg(feature = "oltp")] +use super::pm_auth; #[cfg(feature = "olap")] use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] @@ -532,6 +534,8 @@ impl PaymentMethods { .route(web::post().to(payment_method_update_api)) .route(web::delete().to(payment_method_delete_api)), ) + .service(web::resource("/auth/link").route(web::post().to(pm_auth::link_token_create))) + .service(web::resource("/auth/exchange").route(web::post().to(pm_auth::exchange_token))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6aa2bbad0b15..45c4fe18ac7c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -13,6 +13,7 @@ pub enum ApiIdentifier { Ephemeral, Mandates, PaymentMethods, + PaymentMethodAuth, Payouts, Disputes, CardsInfo, @@ -85,6 +86,8 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsDelete | Flow::ValidatePaymentMethod => Self::PaymentMethods, + Flow::PmAuthLinkTokenCreate | Flow::PmAuthExchangeToken => Self::PaymentMethodAuth, + Flow::PaymentsCreate | Flow::PaymentsRetrieve | Flow::PaymentsUpdate diff --git a/crates/router/src/routes/pm_auth.rs b/crates/router/src/routes/pm_auth.rs new file mode 100644 index 000000000000..cfadd787c310 --- /dev/null +++ b/crates/router/src/routes/pm_auth.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models as api_types; +use router_env::{instrument, tracing, types::Flow}; + +use crate::{core::api_locking, routes::AppState, services::api as oss_api}; + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthLinkTokenCreate))] +pub async fn link_token_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthLinkTokenCreate; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::create_link_token( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthExchangeToken))] +pub async fn exchange_token( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthExchangeToken; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::exchange_token_core( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index e46612b95dfc..57f3b802bd5d 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -6,6 +6,7 @@ pub mod encryption; pub mod jwt; pub mod kafka; pub mod logger; +pub mod pm_auth; #[cfg(feature = "email")] pub mod email; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 8a0cd7c729e9..b48465ebd174 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -641,6 +641,18 @@ impl ClientSecretFetch for api_models::payments::RetrievePaymentLinkRequest { } } +impl ClientSecretFetch for api_models::pm_auth::LinkTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + +impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( diff --git a/crates/router/src/services/pm_auth.rs b/crates/router/src/services/pm_auth.rs new file mode 100644 index 000000000000..7487b12663b1 --- /dev/null +++ b/crates/router/src/services/pm_auth.rs @@ -0,0 +1,95 @@ +use pm_auth::{ + consts, + core::errors::ConnectorError, + types::{self as pm_auth_types, api::BoxedConnectorIntegration, PaymentAuthRouterData}, +}; + +use crate::{ + core::errors::{self}, + logger, + routes::AppState, + services::{self}, +}; + +pub async fn execute_connector_processing_step< + 'b, + 'a, + T: 'static, + Req: Clone + 'static, + Resp: Clone + 'static, +>( + state: &'b AppState, + connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>, + req: &'b PaymentAuthRouterData, + connector: &pm_auth_types::PaymentMethodAuthConnectors, +) -> errors::CustomResult, ConnectorError> +where + T: Clone, + Req: Clone, + Resp: Clone, +{ + let mut router_data = req.clone(); + + let connector_request = connector_integration.build_request(req, connector)?; + + match connector_request { + Some(request) => { + logger::debug!(connector_request=?request); + let response = services::api::call_connector_api(state, request).await; + logger::debug!(connector_response=?response); + match response { + Ok(body) => { + let response = match body { + Ok(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + let connector_http_status_code = Some(body.status_code); + let mut data = + connector_integration.handle_response(&router_data, body)?; + data.connector_http_status_code = connector_http_status_code; + + data + } + Err(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + router_data.connector_http_status_code = Some(body.status_code); + + let error = match body.status_code { + 500..=511 => connector_integration.get_5xx_error_response(body)?, + _ => connector_integration.get_error_response(body)?, + }; + + router_data.response = Err(error); + + router_data + } + }; + Ok(response) + } + Err(error) => { + if error.current_context().is_upstream_timeout() { + let error_response = pm_auth_types::ErrorResponse { + code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), + message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), + reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), + status_code: 504, + }; + router_data.response = Err(error_response); + router_data.connector_http_status_code = Some(504); + Ok(router_data) + } else { + Err(error.change_context(ConnectorError::ProcessingStepFailed(None))) + } + } + } + } + None => Ok(router_data), + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c267a54cc57b..f31297ab8f7d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -8,6 +8,7 @@ pub mod api; pub mod domain; +pub mod pm_auth; pub mod storage; pub mod transformers; diff --git a/crates/router/src/types/pm_auth.rs b/crates/router/src/types/pm_auth.rs new file mode 100644 index 000000000000..e2d08c6afeac --- /dev/null +++ b/crates/router/src/types/pm_auth.rs @@ -0,0 +1,38 @@ +use std::str::FromStr; + +use error_stack::{IntoReport, ResultExt}; +use pm_auth::{ + connector::plaid, + types::{ + self as pm_auth_types, + api::{BoxedPaymentAuthConnector, PaymentAuthConnectorData}, + }, +}; + +use crate::core::{ + errors::{self, ApiErrorResponse}, + pm_auth::helpers::PaymentAuthConnectorDataExt, +}; + +impl PaymentAuthConnectorDataExt for PaymentAuthConnectorData { + fn get_connector_by_name(name: &str) -> errors::CustomResult { + let connector_name = pm_auth_types::PaymentMethodAuthConnectors::from_str(name) + .into_report() + .change_context(ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name.clone())?; + Ok(Self { + connector, + connector_name, + }) + } + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + match connector_name { + pm_auth_types::PaymentMethodAuthConnectors::Plaid => Ok(Box::new(&plaid::Plaid)), + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index f54a5a82baaf..fa06a3941505 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -287,6 +287,10 @@ pub enum Flow { UserMerchantAccountList, /// Get users for merchant account GetUserDetails, + /// PaymentMethodAuth Link token create + PmAuthLinkTokenCreate, + /// PaymentMethodAuth Exchange token create + PmAuthExchangeToken, } ///