diff --git a/Cargo.lock b/Cargo.lock index c38b396605..474d6d6f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4048,6 +4048,7 @@ dependencies = [ "mime", "num-traits", "openssl", + "pem", "qrcode", "quick-xml", "rand 0.8.5", @@ -5692,9 +5693,9 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64 0.22.1", "serde", diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index df042d2df8..5a8e9b214a 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -11868,6 +11868,7 @@ "nmi", "nomupay", "noon", + "nordea", "novalnet", "nuvei", "opennode", @@ -29799,6 +29800,7 @@ "nmi", "nomupay", "noon", + "nordea", "novalnet", "nuvei", "opennode", diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index c1b43bb74b..6b7fa4eae9 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -8359,6 +8359,7 @@ "nmi", "nomupay", "noon", + "nordea", "novalnet", "nuvei", "opennode", @@ -23282,6 +23283,7 @@ "nmi", "nomupay", "noon", + "nordea", "novalnet", "nuvei", "opennode", diff --git a/config/config.example.toml b/config/config.example.toml index 3fdb96eb43..2685331d0b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -812,6 +812,9 @@ paypal = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,B [pm_filters.mifinity] mifinity = { country = "BR,CN,SG,MY,DE,CH,DK,GB,ES,AD,GI,FI,FR,GR,HR,IT,JP,MX,AR,CO,CL,PE,VE,UY,PY,BO,EC,GT,HN,SV,NI,CR,PA,DO,CU,PR,NL,NO,PL,PT,SE,RU,TR,TW,HK,MO,AX,AL,DZ,AS,AO,AI,AG,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BE,BZ,BJ,BM,BT,BQ,BA,BW,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CX,CC,KM,CG,CK,CI,CW,CY,CZ,DJ,DM,EG,GQ,ER,EE,ET,FK,FO,FJ,GF,PF,TF,GA,GM,GE,GH,GL,GD,GP,GU,GG,GN,GW,GY,HT,HM,VA,IS,IN,ID,IE,IM,IL,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LI,LT,LU,MK,MG,MW,MV,ML,MT,MH,MQ,MR,MU,YT,FM,MD,MC,MN,ME,MS,MA,MZ,NA,NR,NP,NC,NZ,NE,NG,NU,NF,MP,OM,PK,PW,PS,PG,PH,PN,QA,RE,RO,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SX,SK,SI,SB,SO,ZA,GS,KR,LK,SR,SJ,SZ,TH,TL,TG,TK,TO,TT,TN,TM,TC,TV,UG,UA,AE,UZ,VU,VN,VG,VI,WF,EH,ZM", currency = "AUD,CAD,CHF,CNY,CZK,DKK,EUR,GBP,INR,JPY,NOK,NZD,PLN,RUB,SEK,ZAR,USD,EGP,UYU,UZS" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } @@ -1188,4 +1191,4 @@ hyperswitch_ai_host = "http://0.0.0.0:8000" # Hyperswitch ai workflow host [proxy_status_mapping] proxy_connector_http_status_code = false # If enabled, the http status code of the connector will be proxied in the response [list_dispute_supported_connectors] -connector_list = "worldpayvantiv" \ No newline at end of file +connector_list = "worldpayvantiv" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 4607d92000..1ae71b20dd 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -637,6 +637,9 @@ red_pagos = { country = "UY", currency = "UYU" } [pm_filters.zsl] local_bank_transfer = { country = "CN", currency = "CNY" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } apple_pay = { country = "MY", currency = "MYR" } @@ -832,4 +835,4 @@ retry_algorithm_type = "cascading" click_to_pay = {connector_list = "adyen, cybersource, trustpay"} [list_dispute_supported_connectors] -connector_list = "worldpayvantiv" \ No newline at end of file +connector_list = "worldpayvantiv" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 7a1521001c..e6d29112a0 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -103,7 +103,7 @@ nexixpay.base_url = "https://xpay.nexigroup.com/api/phoenix-0.0/psp/api/v1" nmi.base_url = "https://secure.nmi.com/" nomupay.base_url = "https://payout-api.nomupay.com" noon.base_url = "https://api.noonpayments.com/" -nordea.base_url = "https://openapi.portal.nordea.com" +nordea.base_url = "https://open.nordeaopenbanking.com" noon.key_mode = "Live" novalnet.base_url = "https://payport.novalnet.de/v2" nuvei.base_url = "https://secure.safecharge.com/" @@ -654,6 +654,8 @@ red_pagos = { country = "UY", currency = "UYU" } [pm_filters.zsl] local_bank_transfer = { country = "CN", currency = "CNY" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } @@ -843,4 +845,4 @@ click_to_pay = {connector_list = "adyen, cybersource, trustpay"} [revenue_recovery] monitoring_threshold_in_seconds = 60 -retry_algorithm_type = "cascading" \ No newline at end of file +retry_algorithm_type = "cascading" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 88e7d4115e..ccd5e00369 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -661,6 +661,9 @@ red_pagos = { country = "UY", currency = "UYU" } [pm_filters.zsl] local_bank_transfer = { country = "CN", currency = "CNY" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } apple_pay = { country = "MY", currency = "MYR" } @@ -812,7 +815,7 @@ globalpay = { long_lived_token = false, payment_method = "card", flow = "mandate outgoing_enabled = true redis_lock_expiry_seconds = 180 -[l2_l3_data_config] +[l2_l3_data_config] enabled = "true" [webhook_source_verification_call] @@ -851,4 +854,4 @@ monitoring_threshold_in_seconds = 60 retry_algorithm_type = "cascading" [list_dispute_supported_connectors] -connector_list = "worldpayvantiv" \ No newline at end of file +connector_list = "worldpayvantiv" diff --git a/config/development.toml b/config/development.toml index 9020c89ef8..76031da595 100644 --- a/config/development.toml +++ b/config/development.toml @@ -844,6 +844,9 @@ region = "" credit = { country = "US, CA", currency = "CAD,USD"} debit = { country = "US, CA", currency = "CAD,USD"} +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } apple_pay = { country = "MY", currency = "MYR" } @@ -1258,7 +1261,7 @@ dynamic_routing_enabled = false static_routing_enabled = false url = "http://localhost:8080" -[l2_l3_data_config] +[l2_l3_data_config] enabled = "true" [grpc_client.unified_connector_service] @@ -1293,4 +1296,4 @@ hyperswitch_ai_host = "http://0.0.0.0:8000" [proxy_status_mapping] proxy_connector_http_status_code = false # If enabled, the http status code of the connector will be proxied in the response [list_dispute_supported_connectors] -connector_list = "worldpayvantiv" \ No newline at end of file +connector_list = "worldpayvantiv" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c518574435..d06eb47d03 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -770,6 +770,9 @@ credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,K debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } apple_pay = { country = "MY", currency = "MYR" } @@ -1172,4 +1175,4 @@ version = "HOSTNAME" # value of HOSTNAME from deployment which tells its [proxy_status_mapping] proxy_connector_http_status_code = false # If enabled, the http status code of the connector will be proxied in the response [list_dispute_supported_connectors] -connector_list = "worldpayvantiv" \ No newline at end of file +connector_list = "worldpayvantiv" diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 50ba4dce61..3bdc756dc8 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1210,7 +1210,9 @@ pub enum ConnectorAuthType { auth_key_map: HashMap, }, CertificateAuth { + // certificate should be base64 encoded certificate: Secret, + // private_key should be base64 encoded private_key: Secret, }, #[default] diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 37d9ee4b31..9e00ff366a 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -119,7 +119,7 @@ pub enum RoutableConnectors { Nmi, Nomupay, Noon, - // Nordea, + Nordea, Novalnet, Nuvei, // Opayo, added as template code for future usage @@ -292,7 +292,7 @@ pub enum Connector { Nmi, Nomupay, Noon, - // Nordea, + Nordea, Novalnet, Nuvei, // Opayo, added as template code for future usage @@ -385,6 +385,7 @@ impl Connector { | (Self::Globalpay, _) | (Self::Jpmorgan, _) | (Self::Moneris, _) + | (Self::Nordea, _) | (Self::Paypal, _) | (Self::Payu, _) | ( @@ -478,7 +479,7 @@ impl Connector { | Self::Nexinets | Self::Nexixpay | Self::Nomupay - // | Self::Nordea + | Self::Nordea | Self::Novalnet | Self::Nuvei | Self::Opennode @@ -649,7 +650,7 @@ impl From for Connector { RoutableConnectors::Nmi => Self::Nmi, RoutableConnectors::Nomupay => Self::Nomupay, RoutableConnectors::Noon => Self::Noon, - // RoutableConnectors::Nordea => Self::Nordea, + RoutableConnectors::Nordea => Self::Nordea, RoutableConnectors::Novalnet => Self::Novalnet, RoutableConnectors::Nuvei => Self::Nuvei, RoutableConnectors::Opennode => Self::Opennode, @@ -779,7 +780,7 @@ impl TryFrom for RoutableConnectors { Connector::Nmi => Ok(Self::Nmi), Connector::Nomupay => Ok(Self::Nomupay), Connector::Noon => Ok(Self::Noon), - // Connector::Nordea => Ok(Self::Nordea), + Connector::Nordea => Ok(Self::Nordea), Connector::Novalnet => Ok(Self::Novalnet), Connector::Nuvei => Ok(Self::Nuvei), Connector::Opennode => Ok(Self::Opennode), diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 3f491d3e9a..d6a9475b17 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -110,6 +110,17 @@ pub mod date_time { now().assume_utc().format(&Iso8601::) } + /// Return the current date and time in UTC formatted as "ddd, DD MMM YYYY HH:mm:ss GMT". + pub fn now_rfc7231_http_date() -> Result { + let now_utc = OffsetDateTime::now_utc(); + // Desired format: ddd, DD MMM YYYY HH:mm:ss GMT + // Example: Fri, 23 May 2025 06:19:35 GMT + let format = time::macros::format_description!( + "[weekday repr:short], [day padding:zero] [month repr:short] [year repr:full] [hour padding:zero repr:24]:[minute padding:zero]:[second padding:zero] GMT" + ); + now_utc.format(&format) + } + impl From for &[BorrowedFormatItem<'_>] { fn from(format: DateFormat) -> Self { match format { diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 7289e6d4a9..9a5c7edb2c 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -460,6 +460,7 @@ impl ConnectorConfig { Connector::Nexixpay => Ok(connector_data.nexixpay), Connector::Prophetpay => Ok(connector_data.prophetpay), Connector::Nmi => Ok(connector_data.nmi), + Connector::Nordea => Ok(connector_data.nordea), Connector::Nomupay => Err("Use get_payout_connector_config".to_string()), Connector::Novalnet => Ok(connector_data.novalnet), Connector::Noon => Ok(connector_data.noon), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 5f38b42d4a..4646e73f71 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -6355,7 +6355,31 @@ placeholder = "Enter Acquirer Merchant ID" required = false type = "Text" - +[nordea] +[[nordea.bank_debit]] + payment_method_type = "sepa" +[nordea.connector_auth.SignatureKey] + api_key="Client Secret" + key1="Client ID" + api_secret="eIDAS Private Key" +[nordea.metadata.creditor_account_type] + name="creditor_account_type" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Type e.g. IBAN" + required=true + type="Text" +[nordea.metadata.creditor_account_value] + name="creditor_account_value" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Number" + required=true + type="Text" +[nordea.metadata.creditor_beneficiary_name] + name="creditor_beneficiary_name" + label="Creditor Account Beneficiary Name" + placeholder="Enter Beneficiary Name" + required=true + type="Text" [worldpayxml] [[worldpayxml.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 4bb725ed12..6bb5497957 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -4923,6 +4923,33 @@ placeholder = "Enter Acquirer Merchant ID" required = false type = "Text" +[nordea] +[[nordea.bank_debit]] + payment_method_type = "sepa" +[nordea.connector_auth.SignatureKey] + api_key="Client Secret" + key1="Client ID" + api_secret="eIDAS Private Key" +[nordea.metadata.creditor_account_type] + name="creditor_account_type" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Type e.g. IBAN" + required=true + type="Text" +[nordea.metadata.creditor_account_value] + name="creditor_account_value" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Number" + required=true + type="Text" +[nordea.metadata.creditor_beneficiary_name] + name="creditor_beneficiary_name" + label="Creditor Account Beneficiary Name" + placeholder="Enter Beneficiary Name" + required=true + type="Text" + + [worldpayxml] [[worldpayxml.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 3b17bfca44..37b63f8406 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -6335,6 +6335,32 @@ placeholder = "Enter Acquirer Merchant ID" required = false type = "Text" +[nordea] +[[nordea.bank_debit]] + payment_method_type = "sepa" +[nordea.connector_auth.SignatureKey] + api_key="Client Secret" + key1="Client ID" + api_secret="eIDAS Private Key" +[nordea.metadata.creditor_account_type] + name="creditor_account_type" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Type e.g. IBAN" + required=true + type="Text" +[nordea.metadata.creditor_account_value] + name="creditor_account_value" + label="Creditor Account Type" + placeholder="Enter Beneficiary Account Number" + required=true + type="Text" +[nordea.metadata.creditor_beneficiary_name] + name="creditor_beneficiary_name" + label="Creditor Account Beneficiary Name" + placeholder="Enter Beneficiary Name" + required=true + type="Text" + [worldpayxml] [[worldpayxml.credit]] diff --git a/crates/hyperswitch_connectors/Cargo.toml b/crates/hyperswitch_connectors/Cargo.toml index 63e98299c3..d06d6f299d 100644 --- a/crates/hyperswitch_connectors/Cargo.toml +++ b/crates/hyperswitch_connectors/Cargo.toml @@ -37,6 +37,7 @@ lazy_static = "1.5.0" mime = "0.3.17" num-traits = "0.2.19" openssl = {version = "0.10.70"} +pem = "3.0.5" qrcode = "0.14.1" quick-xml = { version = "0.31.0", features = ["serialize"] } rand = "0.8.5" diff --git a/crates/hyperswitch_connectors/src/connectors/nordea.rs b/crates/hyperswitch_connectors/src/connectors/nordea.rs index 9230d856f8..fb0d9e0f62 100644 --- a/crates/hyperswitch_connectors/src/connectors/nordea.rs +++ b/crates/hyperswitch_connectors/src/connectors/nordea.rs @@ -1,28 +1,39 @@ +mod requests; +mod responses; pub mod transformers; +use base64::Engine; +use common_enums::enums; use common_utils::{ + consts, date_time, errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{AccessToken, AccessTokenAuthenticationResponse, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, refunds::{Execute, RSync}, + AccessTokenAuthentication, PreProcessing, }, router_request_types::{ - AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + AccessTokenAuthenticationRequestData, AccessTokenRequestData, + PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, + PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSessionData, PaymentsSyncData, RefundsData, SetupMandateRequestData, }, - router_response_types::{PaymentsResponseData, RefundsResponseData}, + router_response_types::{ + ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData, + SupportedPaymentMethods, SupportedPaymentMethodsExt, + }, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + AccessTokenAuthenticationRouterData, PaymentsAuthorizeRouterData, + PaymentsPreProcessingRouterData, PaymentsSyncRouterData, RefreshTokenRouterData, + RefundsRouterData, }, }; use hyperswitch_interfaces::{ @@ -31,31 +42,204 @@ use hyperswitch_interfaces::{ ConnectorValidation, }, configs::Connectors, + consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::{self, AuthenticationTokenType, RefreshTokenType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; -use transformers as nordea; +use lazy_static::lazy_static; +use masking::{ExposeInterface, Mask, PeekInterface, Secret}; +use ring::{ + digest, + signature::{RsaKeyPair, RSA_PKCS1_SHA256}, +}; +use transformers::{get_error_data, NordeaAuthType}; +use url::Url; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + connectors::nordea::{ + requests::{ + NordeaOAuthExchangeRequest, NordeaOAuthRequest, NordeaPaymentsConfirmRequest, + NordeaPaymentsRequest, NordeaRouterData, + }, + responses::{ + NordeaOAuthExchangeResponse, NordeaPaymentsConfirmResponse, + NordeaPaymentsInitiateResponse, + }, + }, + constants::headers, + types::ResponseRouterData, + utils::{self, RouterData as OtherRouterData}, +}; #[derive(Clone)] pub struct Nordea { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +struct SignatureParams<'a> { + content_type: &'a str, + host: &'a str, + path: &'a str, + payload_digest: Option<&'a str>, + date: &'a str, + http_method: Method, } impl Nordea { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &StringMajorUnitForConnector, } } + + pub fn generate_digest(&self, payload: &[u8]) -> String { + let payload_digest = digest::digest(&digest::SHA256, payload); + format!("sha-256={}", consts::BASE64_ENGINE.encode(payload_digest)) + } + + pub fn generate_digest_from_request(&self, payload: &RequestContent) -> String { + let payload_bytes = match payload { + RequestContent::RawBytes(bytes) => bytes.clone(), + _ => payload.get_inner_value().expose().as_bytes().to_vec(), + }; + + self.generate_digest(&payload_bytes) + } + + fn format_private_key( + &self, + private_key_str: &str, + ) -> CustomResult { + let key = private_key_str.to_string(); + + // Check if it already has PEM headers + let pem_data = + if key.contains("BEGIN") && key.contains("END") && key.contains("PRIVATE KEY") { + key + } else { + // Remove whitespace and format with 64-char lines + let cleaned_key = key + .chars() + .filter(|c| !c.is_whitespace()) + .collect::(); + + let formatted_key = cleaned_key + .chars() + .collect::>() + .chunks(64) + .map(|chunk| chunk.iter().collect::()) + .collect::>() + .join("\n"); + + format!( + "-----BEGIN RSA PRIVATE KEY-----\n{formatted_key}\n-----END RSA PRIVATE KEY-----", + ) + }; + + Ok(pem_data) + } + + // For non-production environments, signature generation can be skipped and instead `SKIP_SIGNATURE_VALIDATION_FOR_SANDBOX` can be passed. + fn generate_signature( + &self, + auth: &NordeaAuthType, + signature_params: SignatureParams<'_>, + ) -> CustomResult { + const REQUEST_WITHOUT_CONTENT_HEADERS: &str = + "(request-target) x-nordea-originating-host x-nordea-originating-date"; + const REQUEST_WITH_CONTENT_HEADERS: &str = "(request-target) x-nordea-originating-host x-nordea-originating-date content-type digest"; + + let method_string = signature_params.http_method.to_string().to_lowercase(); + let mut normalized_string = format!( + "(request-target): {} {}\nx-nordea-originating-host: {}\nx-nordea-originating-date: {}", + method_string, signature_params.path, signature_params.host, signature_params.date + ); + + let headers = if matches!( + signature_params.http_method, + Method::Post | Method::Put | Method::Patch + ) { + let digest = signature_params.payload_digest.unwrap_or(""); + normalized_string.push_str(&format!( + "\ncontent-type: {}\ndigest: {}", + signature_params.content_type, digest + )); + REQUEST_WITH_CONTENT_HEADERS + } else { + REQUEST_WITHOUT_CONTENT_HEADERS + }; + + let signature_base64 = { + let private_key_pem = + self.format_private_key(&auth.eidas_private_key.clone().expose())?; + + let private_key_der = pem::parse(&private_key_pem).change_context( + errors::ConnectorError::InvalidConnectorConfig { + config: "eIDAS Private Key", + }, + )?; + let private_key_der_contents = private_key_der.contents(); + let key_pair = RsaKeyPair::from_der(private_key_der_contents).change_context( + errors::ConnectorError::InvalidConnectorConfig { + config: "eIDAS Private Key", + }, + )?; + + let mut signature = vec![0u8; key_pair.public().modulus_len()]; + key_pair + .sign( + &RSA_PKCS1_SHA256, + &ring::rand::SystemRandom::new(), + normalized_string.as_bytes(), + &mut signature, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + consts::BASE64_ENGINE.encode(signature) + }; + + Ok(format!( + r#"keyId="{}",algorithm="rsa-sha256",headers="{}",signature="{}""#, + auth.client_id.peek(), + headers, + signature_base64 + )) + } + + // This helper function correctly serializes a struct into the required + // non-percent-encoded form URL string. + fn get_form_urlencoded_payload( + &self, + form_data: &T, + ) -> Result, error_stack::Report> { + let json_value = serde_json::to_value(form_data) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let btree_map: std::collections::BTreeMap = + serde_json::from_value(json_value) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(btree_map + .iter() + .map(|(k, v)| { + // Remove quotes from string values for proper form encoding + let value = match v { + serde_json::Value::String(s) => s.clone(), + _ => v.to_string(), + }; + format!("{k}={value}") + }) + .collect::>() + .join("&") + .into_bytes()) + } } impl api::Payment for Nordea {} impl api::PaymentSession for Nordea {} +impl api::ConnectorAuthenticationToken for Nordea {} impl api::ConnectorAccessToken for Nordea {} impl api::MandateSetup for Nordea {} impl api::PaymentAuthorize for Nordea {} @@ -66,13 +250,15 @@ impl api::Refund for Nordea {} impl api::RefundExecute for Nordea {} impl api::RefundSync for Nordea {} impl api::PaymentToken for Nordea {} +impl api::PaymentsPreProcessing for Nordea {} impl ConnectorIntegration for Nordea { - // Not Implemented (R) } +impl ConnectorIntegration for Nordea {} + impl ConnectorCommonExt for Nordea where Self: ConnectorIntegration, @@ -80,15 +266,105 @@ where fn build_headers( &self, req: &RouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + let access_token = req + .access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; + let auth = NordeaAuthType::try_from(&req.connector_auth_type)?; + let content_type = self.get_content_type().to_string(); + let http_method = self.get_http_method(); + + // Extract host from base URL + let nordea_host = Url::parse(self.base_url(connectors)) + .change_context(errors::ConnectorError::RequestEncodingFailed)? + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(); + + let nordea_origin_date = date_time::now_rfc7231_http_date() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let full_url = self.get_url(req, connectors)?; + let url_parsed = + Url::parse(&full_url).change_context(errors::ConnectorError::RequestEncodingFailed)?; + let path = url_parsed.path(); + let path_with_query = if let Some(query) = url_parsed.query() { + format!("{path}?{query}") + } else { + path.to_string() + }; + + let mut required_headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + content_type.clone().into(), + ), + ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", access_token.token.peek()).into_masked(), + ), + ( + "X-IBM-Client-ID".to_string(), + auth.client_id.clone().expose().into_masked(), + ), + ( + "X-IBM-Client-Secret".to_string(), + auth.client_secret.clone().expose().into_masked(), + ), + ( + "X-Nordea-Originating-Date".to_string(), + nordea_origin_date.clone().into_masked(), + ), + ( + "X-Nordea-Originating-Host".to_string(), + nordea_host.clone().into_masked(), + ), + ]; + + if matches!(http_method, Method::Post | Method::Put | Method::Patch) { + let nordea_request = self.get_request_body(req, connectors)?; + + let sha256_digest = self.generate_digest_from_request(&nordea_request); + + // Add Digest header + required_headers.push(( + "Digest".to_string(), + sha256_digest.to_string().into_masked(), + )); + + let signature = self.generate_signature( + &auth, + SignatureParams { + content_type: &content_type, + host: &nordea_host, + path, + payload_digest: Some(&sha256_digest), + date: &nordea_origin_date, + http_method, + }, + )?; + + required_headers.push(("Signature".to_string(), signature.into_masked())); + } else { + // Generate signature without digest for GET requests + let signature = self.generate_signature( + &auth, + SignatureParams { + content_type: &content_type, + host: &nordea_host, + path: &path_with_query, + payload_digest: None, + date: &nordea_origin_date, + http_method, + }, + )?; + + required_headers.push(("Signature".to_string(), signature.into_masked())); + } + + Ok(required_headers) } } @@ -109,24 +385,12 @@ impl ConnectorCommon for Nordea { connectors.nordea.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = nordea::NordeaAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: nordea::NordeaErrorResponse = res + let response: responses::NordeaErrorResponse = res .response .parse_struct("NordeaErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -136,9 +400,14 @@ impl ConnectorCommon for Nordea { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: get_error_data(response.error.as_ref()) + .and_then(|failure| failure.code.clone()) + .unwrap_or(NO_ERROR_CODE.to_string()), + message: get_error_data(response.error.as_ref()) + .and_then(|failure| failure.description.clone()) + .unwrap_or(NO_ERROR_MESSAGE.to_string()), + reason: get_error_data(response.error.as_ref()) + .and_then(|failure| failure.failure_type.clone()), attempt_status: None, connector_transaction_id: None, network_decline_code: None, @@ -148,17 +417,459 @@ impl ConnectorCommon for Nordea { } } -impl ConnectorValidation for Nordea { - //TODO: implement functions when support enabled +impl ConnectorValidation for Nordea {} + +impl + ConnectorIntegration< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, + > for Nordea +{ + fn get_url( + &self, + _req: &AccessTokenAuthenticationRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}/personal/v5/authorize", + self.base_url(connectors) + )) + } + + fn get_content_type(&self) -> &'static str { + "application/json" + } + + fn get_request_body( + &self, + req: &AccessTokenAuthenticationRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = NordeaOAuthRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &AccessTokenAuthenticationRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let auth = NordeaAuthType::try_from(&req.connector_auth_type)?; + let content_type = self.common_get_content_type().to_string(); + let http_method = Method::Post; + + // Extract host from base URL + let nordea_host = Url::parse(self.base_url(connectors)) + .change_context(errors::ConnectorError::RequestEncodingFailed)? + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(); + + let nordea_origin_date = date_time::now_rfc7231_http_date() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let full_url = self.get_url(req, connectors)?; + let url_parsed = + Url::parse(&full_url).change_context(errors::ConnectorError::RequestEncodingFailed)?; + let path = url_parsed.path(); + + let request_body = self.get_request_body(req, connectors)?; + + let mut required_headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + content_type.clone().into(), + ), + ( + "X-IBM-Client-ID".to_string(), + auth.client_id.clone().expose().into_masked(), + ), + ( + "X-IBM-Client-Secret".to_string(), + auth.client_secret.clone().expose().into_masked(), + ), + ( + "X-Nordea-Originating-Date".to_string(), + nordea_origin_date.clone().into_masked(), + ), + ( + "X-Nordea-Originating-Host".to_string(), + nordea_host.clone().into_masked(), + ), + ]; + + let sha256_digest = self.generate_digest_from_request(&request_body); + + // Add Digest header + required_headers.push(( + "Digest".to_string(), + sha256_digest.to_string().into_masked(), + )); + + let signature = self.generate_signature( + &auth, + SignatureParams { + content_type: &content_type, + host: &nordea_host, + path, + payload_digest: Some(&sha256_digest), + date: &nordea_origin_date, + http_method, + }, + )?; + + required_headers.push(("Signature".to_string(), signature.into_masked())); + + let request = Some( + RequestBuilder::new() + .method(http_method) + .attach_default_headers() + .headers(required_headers) + .url(&AuthenticationTokenType::get_url(self, req, connectors)?) + .set_body(request_body) + .build(), + ); + Ok(request) + } + + fn handle_response( + &self, + data: &AccessTokenAuthenticationRouterData, + _event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + // Handle 302 redirect response + if res.status_code == 302 { + // Extract Location header + let headers = + res.headers + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "headers", + })?; + let location_header = headers + .get("Location") + .map(|value| value.to_str()) + .and_then(|location_value| location_value.ok()) + .ok_or(errors::ConnectorError::ParsingFailed)?; + + // Parse auth code from query params + let url = Url::parse(location_header) + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let code = url + .query_pairs() + .find(|(key, _)| key == "code") + .map(|(_, value)| value.to_string()) + .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "code" })?; + + // Return auth code as "token" with short expiry + Ok(RouterData { + response: Ok(AccessTokenAuthenticationResponse { + code: Secret::new(code), + expires: 60, // 60 seconds - auth code validity + }), + ..data.clone() + }) + } else { + Err( + errors::ConnectorError::UnexpectedResponseError("Expected 302 redirect".into()) + .into(), + ) + } + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } -impl ConnectorIntegration for Nordea { - //TODO: implement sessions flow +impl ConnectorIntegration for Nordea { + fn get_url( + &self, + _req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}/personal/v5/authorize/token", + self.base_url(connectors) + )) + } + + fn get_request_body( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = NordeaOAuthExchangeRequest::try_from(req)?; + let body_bytes = self.get_form_urlencoded_payload(&Box::new(connector_req))?; + Ok(RequestContent::RawBytes(body_bytes)) + } + + fn build_request( + &self, + req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + // For the OAuth token exchange request, we don't have a bearer token yet + // We're exchanging the auth code for an access token + let auth = NordeaAuthType::try_from(&req.connector_auth_type)?; + let content_type = "application/x-www-form-urlencoded".to_string(); + let http_method = Method::Post; + + // Extract host from base URL + let nordea_host = Url::parse(self.base_url(connectors)) + .change_context(errors::ConnectorError::RequestEncodingFailed)? + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(); + + let nordea_origin_date = date_time::now_rfc7231_http_date() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let full_url = self.get_url(req, connectors)?; + let url_parsed = + Url::parse(&full_url).change_context(errors::ConnectorError::RequestEncodingFailed)?; + let path = url_parsed.path(); + + let request_body = self.get_request_body(req, connectors)?; + + let mut required_headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + content_type.clone().into(), + ), + ( + "X-IBM-Client-ID".to_string(), + auth.client_id.clone().expose().into_masked(), + ), + ( + "X-IBM-Client-Secret".to_string(), + auth.client_secret.clone().expose().into_masked(), + ), + ( + "X-Nordea-Originating-Date".to_string(), + nordea_origin_date.clone().into_masked(), + ), + ( + "X-Nordea-Originating-Host".to_string(), + nordea_host.clone().into_masked(), + ), + ]; + + let sha256_digest = self.generate_digest_from_request(&request_body); + + // Add Digest header + required_headers.push(( + "Digest".to_string(), + sha256_digest.to_string().into_masked(), + )); + + let signature = self.generate_signature( + &auth, + SignatureParams { + content_type: &content_type, + host: &nordea_host, + path, + payload_digest: Some(&sha256_digest), + date: &nordea_origin_date, + http_method, + }, + )?; + + required_headers.push(("Signature".to_string(), signature.into_masked())); + + let request = Some( + RequestBuilder::new() + .method(http_method) + .attach_default_headers() + .headers(required_headers) + .url(&RefreshTokenType::get_url(self, req, connectors)?) + .set_body(request_body) + .build(), + ); + Ok(request) + } + + fn handle_response( + &self, + data: &RefreshTokenRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: NordeaOAuthExchangeResponse = res + .response + .parse_struct("NordeaOAuthExchangeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } -impl ConnectorIntegration for Nordea {} +impl ConnectorIntegration for Nordea { + fn build_request( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Nordea".to_string()) + .into(), + ) + } +} -impl ConnectorIntegration for Nordea {} +impl ConnectorIntegration + for Nordea +{ + fn get_headers( + &self, + req: &PaymentsPreProcessingRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsPreProcessingRouterData, + connectors: &Connectors, + ) -> CustomResult { + // Determine the payment endpoint based on country and currency + let country = req.get_billing_country()?; + + let currency = + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?; + + let endpoint = match (country, currency) { + (api_models::enums::CountryAlpha2::FI, api_models::enums::Currency::EUR) => { + "/personal/v5/payments/sepa-credit-transfers" + } + (api_models::enums::CountryAlpha2::DK, api_models::enums::Currency::DKK) => { + "/personal/v5/payments/domestic-credit-transfers" + } + ( + api_models::enums::CountryAlpha2::FI + | api_models::enums::CountryAlpha2::DK + | api_models::enums::CountryAlpha2::SE + | api_models::enums::CountryAlpha2::NO, + _, + ) => "/personal/v5/payments/cross-border-credit-transfers", + _ => { + return Err(errors::ConnectorError::NotSupported { + message: format!("Country {country:?} is not supported by Nordea"), + connector: "Nordea", + } + .into()) + } + }; + + Ok(format!("{}{}", self.base_url(connectors), endpoint)) + } + + fn get_request_body( + &self, + req: &PaymentsPreProcessingRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let minor_amount = + req.request + .minor_amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "minor_amount", + })?; + let currency = + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?; + + let amount = utils::convert_amount(self.amount_converter, minor_amount, currency)?; + let connector_router_data = NordeaRouterData::from((amount, req)); + let connector_req = NordeaPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsPreProcessingRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsPreProcessingRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: NordeaPaymentsInitiateResponse = res + .response + .parse_struct("NordeaPaymentsInitiateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Nordea { fn get_headers( @@ -173,12 +884,20 @@ impl ConnectorIntegration Method { + Method::Put + } + fn get_url( &self, _req: &PaymentsAuthorizeRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}{}", + self.base_url(_connectors), + "/personal/v5/payments" + )) } fn get_request_body( @@ -192,8 +911,8 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Post) + .method(types::PaymentsAuthorizeType::get_http_method(self)) .url(&types::PaymentsAuthorizeType::get_url( self, req, connectors, )?) @@ -225,12 +944,14 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: nordea::NordeaPaymentsResponse = res + let response: NordeaPaymentsConfirmResponse = res .response - .parse_struct("Nordea PaymentsAuthorizeResponse") + .parse_struct("NordeaPaymentsConfirmResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { response, data: data.clone(), @@ -260,12 +981,26 @@ impl ConnectorIntegration for Nor self.common_get_content_type() } + fn get_http_method(&self) -> Method { + Method::Get + } + fn get_url( &self, - _req: &PaymentsSyncRouterData, + req: &PaymentsSyncRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let id = req.request.connector_transaction_id.clone(); + let connector_transaction_id = id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!( + "{}{}{}", + self.base_url(_connectors), + "/personal/v5/payments/", + connector_transaction_id + )) } fn build_request( @@ -275,7 +1010,7 @@ impl ConnectorIntegration for Nor ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Get) + .method(types::PaymentsSyncType::get_http_method(self)) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) @@ -289,9 +1024,9 @@ impl ConnectorIntegration for Nor event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: nordea::NordeaPaymentsResponse = res + let response: NordeaPaymentsInitiateResponse = res .response - .parse_struct("nordea PaymentsSyncResponse") + .parse_struct("NordeaPaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -312,233 +1047,49 @@ impl ConnectorIntegration for Nor } impl ConnectorIntegration for Nordea { - fn get_headers( - &self, - req: &PaymentsCaptureRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) - } - fn build_request( &self, - req: &PaymentsCaptureRouterData, - connectors: &Connectors, + _req: &RouterData, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &PaymentsCaptureRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: nordea::NordeaPaymentsResponse = res - .response - .parse_struct("Nordea PaymentsCaptureResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) + Err(errors::ConnectorError::FlowNotSupported { + flow: "Capture".to_string(), + connector: "Nordea".to_string(), + } + .into()) } } -impl ConnectorIntegration for Nordea {} +impl ConnectorIntegration for Nordea { + fn build_request( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Payments Cancel".to_string(), + connector: "Nordea".to_string(), + } + .into()) + } +} impl ConnectorIntegration for Nordea { - fn get_headers( - &self, - req: &RefundsRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( + fn build_request( &self, _req: &RefundsRouterData, _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - req: &RefundsRouterData, - _connectors: &Connectors, - ) -> CustomResult { - let refund_amount = utils::convert_amount( - self.amount_converter, - req.request.minor_refund_amount, - req.request.currency, - )?; - - let connector_router_data = nordea::NordeaRouterData::from((refund_amount, req)); - let connector_req = nordea::NordeaRefundRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - - fn build_request( - &self, - req: &RefundsRouterData, - connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) - } - - fn handle_response( - &self, - data: &RefundsRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult, errors::ConnectorError> { - let response: nordea::RefundResponse = - res.response - .parse_struct("nordea RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) + Err(errors::ConnectorError::FlowNotSupported { + flow: "Personal API Refunds".to_string(), + connector: "Nordea".to_string(), + } + .into()) } } impl ConnectorIntegration for Nordea { - fn get_headers( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Get) - .url(&types::RefundSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &RefundSyncRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: nordea::RefundResponse = res - .response - .parse_struct("nordea RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) - } + // Default impl gets executed } #[async_trait::async_trait] @@ -565,4 +1116,54 @@ impl webhooks::IncomingWebhook for Nordea { } } -impl ConnectorSpecifications for Nordea {} +lazy_static! { + static ref NORDEA_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { + display_name: + "Nordea", + description: + "Nordea is one of the leading financial services group in the Nordics and the preferred choice for millions across the region.", + connector_type: enums::PaymentConnectorCategory::PaymentGateway, + }; + static ref NORDEA_SUPPORTED_PAYMENT_METHODS: SupportedPaymentMethods = { + let nordea_supported_capture_methods = vec![ + enums::CaptureMethod::Automatic, + enums::CaptureMethod::SequentialAutomatic, + ]; + + let mut nordea_supported_payment_methods = SupportedPaymentMethods::new(); + + nordea_supported_payment_methods.add( + enums::PaymentMethod::BankDebit, + enums::PaymentMethodType::Sepa, + PaymentMethodDetails { + mandates: common_enums::FeatureStatus::NotSupported, + // Supported only in corporate API (corporate accounts) + refunds: common_enums::FeatureStatus::NotSupported, + supported_capture_methods: nordea_supported_capture_methods.clone(), + specific_features: None, + }, + ); + + nordea_supported_payment_methods + }; + static ref NORDEA_SUPPORTED_WEBHOOK_FLOWS: Vec = Vec::new(); +} + +impl ConnectorSpecifications for Nordea { + fn get_connector_about(&self) -> Option<&'static ConnectorInfo> { + Some(&*NORDEA_CONNECTOR_INFO) + } + + fn get_supported_payment_methods(&self) -> Option<&'static SupportedPaymentMethods> { + Some(&*NORDEA_SUPPORTED_PAYMENT_METHODS) + } + + fn get_supported_webhook_flows(&self) -> Option<&'static [enums::EventClass]> { + Some(&*NORDEA_SUPPORTED_WEBHOOK_FLOWS) + } + + fn authentication_token_for_token_creation(&self) -> bool { + // Nordea requires authentication token for access token creation + true + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/nordea/requests.rs b/crates/hyperswitch_connectors/src/connectors/nordea/requests.rs new file mode 100644 index 0000000000..a30a12ce40 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/nordea/requests.rs @@ -0,0 +1,381 @@ +use common_utils::types::StringMajorUnit; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +pub struct NordeaRouterData { + pub amount: StringMajorUnit, + pub router_data: T, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NordeaOAuthRequest { + /// Country is a mandatory parameter with possible values FI, DK, NO or SE + pub country: api_models::enums::CountryAlpha2, + /// Duration of access authorization in minutes. range: 1 to 259200 minutes (180 days). + /// Duration should be left empty if the request includes PAYMENTS_SINGLE_SCA scope. + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + /// Maximum transaction history in months. Optional if ACCOUNTS_TRANSACTIONS scope is requested. Default=2 months. range: 1 to 18 months + #[serde(rename = "max_tx_history")] + #[serde(skip_serializing_if = "Option::is_none")] + pub maximum_transaction_history: Option, + /// Redirect URI you used when this application was registered with Nordea. + pub redirect_uri: String, + pub scope: Vec, + /// The OAuth2 state parameter. This is a nonce and should be used to prevent CSRF attacks. + pub state: Secret, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum GrantType { + AuthorizationCode, + RefreshToken, +} + +// To be passed in query parameters for OAuth scopes +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AccessScope { + AccountsBasic, + AccountsBalances, + AccountsDetails, + AccountsTransactions, + PaymentsMultiple, + PaymentsSingleSca, + CardsInformation, + CardsTransactions, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NordeaOAuthExchangeRequest { + /// authorization_code flow + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option>, + pub grant_type: GrantType, + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uri: Option, + /// refresh_token flow + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AccountType { + /// International bank account number + Iban, + /// National bank account number of Sweden + BbanSe, + /// National bank account number of Denmark + BbanDk, + /// National bank account number of Norway + BbanNo, + /// Bankgiro number + Bgnr, + /// Plusgiro number + Pgnr, + /// Creditor number (Giro) Denmark + GiroDk, + /// Any bank account number without any check-digit validations + BbanOther, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountNumber { + /// Type of account number + #[serde(rename = "_type")] + pub account_type: AccountType, + /// Currency of the account (Mandatory for debtor, Optional for creditor) + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option, + /// Actual account number + pub value: Secret, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct CreditorAccountReference { + /// RF or Invoice for FI Sepa payments, OCR for NO Kid payments and 01, 04, 15, 71, 73 or 75 for Danish Transfer Form payments. + #[serde(rename = "_type")] + pub creditor_reference_type: String, + /// Actual reference number + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaAddress { + /// First line of the address. e.g. Street address + pub line1: Option>, + /// Second line of the address (optional). e.g. Postal address + pub line2: Option>, + /// Third line of the address (optional). e.g. Country + pub line3: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct CreditorBank { + /// Address + pub address: Option, + /// Bank code + pub bank_code: Option, + /// Business identifier code (BIC) of the creditor bank. + /// This information is required, if the creditor account number is not in IBAN format. + #[serde(rename = "bic")] + pub business_identifier_code: Option>, + /// Country of the creditor bank. Only ISO 3166 alpha-2 codes are used. + pub country: api_models::enums::CountryAlpha2, + /// Name of the creditor bank. + #[serde(rename = "name")] + pub bank_name: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct CreditorAccount { + /// Account number + pub account: AccountNumber, + /// Creditor bank information. + pub bank: Option, + /// Country of the creditor + pub country: Option, + /// Address + pub creditor_address: Option, + /// Message for the creditor to appear on their transaction. + /// Max length: FI SEPA:140; SE:12; PGNR:25; BGNR:150; DK: 40 (Instant/Express: 140); NO: 140 + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Name of the creditor. + /// Max length: FI SEPA: 30; SE: 35; DK: Not use (Mandatory for Instant/Express payments: 70); + /// NO: 30 (mandatory for Straksbetaling/Express payments). + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option>, + /// Creditor reference number. + /// Either Reference or Message has to be passed in the Request + pub reference: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct DebitorAccount { + /// Account number + pub account: AccountNumber, + /// Own message to be on the debtor's transaction. + /// Max length 20. NB: This field is not supported for SEPA and Norwegian payments and will be ignored. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct InstructedAmount { + /// Monetary amount of the payment. Max (digits+decimals): FI SEPA: (9+2); SE:(11+2); DK:(7+2); NO:(7+2) + pub amount: StringMajorUnit, + /// Currency code according to ISO 4217. + /// NB: Possible value depends on the type of the payment. + /// For domestic payment it should be same as debtor local currency, + /// for SEPA it must be EUR, + /// for cross border it can be Currency code according to ISO 4217. + pub currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RecurrenceType { + Daily, + Weekly, + Biweekly, + MonthlySameDay, + MonthlyEom, + QuartelySameDay, + QuartelyEom, + SemiAnnuallySameDay, + SemiAnnuallyEom, + TriAnnuallySameDay, + YearlySameDay, + YearlyEom, + EveryMinuteSandboxOnly, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +#[allow(dead_code)] // This is an optional field and not having it is fine +pub enum FundsAvailabilityRequest { + True, + False, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +pub enum PaymentsUrgency { + Standard, + Express, + Sameday, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RecurringInformation { + /// Number of occurrences. Not applicable for NO (use end_date instead). Format: int32. + #[serde(skip_serializing_if = "Option::is_none")] + pub count: Option, + /// Date on which the recurrence will end. Format: YYYY-MM-DD. Applicable only for Norway. Format: date + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, + /// Repeats every interval + #[serde(skip_serializing_if = "Option::is_none")] + pub recurrence_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TppCategory { + Error, + Warning, + Info, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TppCode { + Ds0a, + Narr, + Am21, + Am04, + Tm01, + Am12, + Rc06, + Rc07, + Rc04, + Ag06, + Bg06, + Be22, + Be20, + Ff06, + Be19, + Am03, + Am11, + Ch04, + Dt01, + Ch03, + Ff08, + Ac10, + Ac02, + Ag08, + Rr09, + Rc11, + Ff10, + Rr10, + Ff05, + Ch15, + Ff04, + Ac11, + Ac03, + Ac13, + Ac14, + Ac05, + Ac06, + Rr07, + Dt03, + Am13, + Ds24, + Fr01, + Am02, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ThirdPartyMessages { + /// Category of the TPP message, INFO is further information, WARNING is something can be fixed, ERROR possibly non fixable issue + #[serde(skip_serializing_if = "Option::is_none")] + pub category: Option, + /// Additional code that is combined with the text + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + /// Additional explaining text to the TPP + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct NordeaPaymentsRequest { + /// Creditor of the payment + #[serde(rename = "creditor")] + pub creditor_account: CreditorAccount, + /// Debtor of the payment + #[serde(rename = "debtor")] + pub debitor_account: DebitorAccount, + /// Free text reference that can be provided by the PSU. + /// This identification is passed on throughout the entire end-to-end chain. + /// Only in scope for Nordea Business DK. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_to_end_identification: Option, + /// Unique identification as assigned by a partner to identify the payment. + #[serde(skip_serializing_if = "Option::is_none")] + pub external_id: Option, + /// Monetary amount + pub instructed_amount: InstructedAmount, + /// Recurring information + #[serde(skip_serializing_if = "Option::is_none")] + pub recurring: Option, + /// Use as an indicator that the supplied payment (amount, currency and debtor account) + /// should be used to check whether the funds are available for further processing - at this moment. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_availability_of_funds: Option, + /// Choose a preferred execution date (or leave blank for today's date). + /// This should be a valid bank day, and depending on the country the date will either be + /// pushed to the next valid bank day, or return an error if a non-banking day date was + /// supplied (all dates accepted in sandbox). SEPA: max +5 years from yesterday, + /// Domestic: max. +1 year from yesterday. NB: Not supported for Own transfer Non-Recurring Norway. + /// Format:date. + #[serde(skip_serializing_if = "Option::is_none")] + pub requested_execution_date: Option, + /// Additional messages for third parties + #[serde(rename = "tpp_messages")] + #[serde(skip_serializing_if = "Option::is_none")] + pub tpp_messages: Option>, + /// Urgency of the payment. NB: This field is supported for + /// DK Domestic ('standard' and 'express') + /// NO Domestic bank transfer payments ('standard'). Use 'express' for Straksbetaling (Instant payment). + /// FI Sepa ('standard' and 'express') All other payment types ignore this input. + /// For further details on urgencies and cut-offs, refer to the Nordea website. Value 'sameday' is marked as deprecated and will be removed in the future. + #[serde(skip_serializing_if = "Option::is_none")] + pub urgency: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum NordeaAuthenticationMethod { + Mta, + #[serde(rename = "CCALC (Deprecated)")] + Ccalc, + Qrt, + CardRdr, + BankidSe, + QrtSe, + BankidNo, + BankidmNo, + MtaNo, + #[serde(rename = "NEMID_2F")] + Nemid2f, + Mitid, + MtaDk, + QrtDk, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum NordeaConfirmLanguage { + Fi, + Da, + Sv, + En, + No, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct NordeaPaymentsConfirmRequest { + /// Authentication method to use for the signing of payment. + #[serde(skip_serializing_if = "Option::is_none")] + pub authentication_method: Option, + /// Language of the signing page that will be displayed to client, ISO639-1 and 639-2, default=en + #[serde(skip_serializing_if = "Option::is_none")] + pub language: Option, + pub payments_ids: Vec, + pub redirect_url: Option, + pub state: Option, +} diff --git a/crates/hyperswitch_connectors/src/connectors/nordea/responses.rs b/crates/hyperswitch_connectors/src/connectors/nordea/responses.rs new file mode 100644 index 0000000000..10693c9b43 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/nordea/responses.rs @@ -0,0 +1,261 @@ +use common_enums::CountryAlpha2; +use common_utils::types::StringMajorUnit; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use super::requests::{ + CreditorAccount, DebitorAccount, InstructedAmount, PaymentsUrgency, RecurringInformation, + ThirdPartyMessages, +}; + +// OAuth token response structure +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NordeaOAuthExchangeResponse { + pub access_token: Option>, + pub expires_in: Option, + pub refresh_token: Option>, + pub token_type: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub enum NordeaPaymentStatus { + #[default] + PendingConfirmation, + PendingSecondConfirmation, + PendingUserApproval, + OnHold, + Confirmed, + Rejected, + Paid, + InsufficientFunds, + LimitExceeded, + UserApprovalFailed, + UserApprovalTimeout, + UserApprovalCancelled, + Unknown, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaGroupHeader { + /// Response creation time. Format: date-time. + pub creation_date_time: Option, + /// HTTP code for response. Format: int32. + pub http_code: Option, + /// Original request id for correlation purposes + pub message_identification: Option, + /// Details of paginated response + pub message_pagination: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaResponseLinks { + /// Describes the nature of the link, e.g. 'details' for a link to the detailed information of a listed resource. + pub rel: Option, + /// Relative path to the linked resource + pub href: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum FeesType { + Additional, + Standard, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TransactionFee { + /// Monetary amount + pub amount: InstructedAmount, + pub description: Option, + pub excluded_from_total_fee: Option, + pub percentage: Option, + #[serde(rename = "type")] + pub fees_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct BankFee { + /// Example 'domestic_transaction' only for DK domestic payments + #[serde(rename = "_type")] + pub bank_fee_type: Option, + /// Country code according to ISO Alpha-2 + pub country_code: Option, + /// Currency code according to ISO 4217 + pub currency_code: Option, + /// Value of the fee. + pub value: Option, + pub fees: Option>, + /// Monetary amount + pub total_fee_amount: InstructedAmount, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ChargeBearer { + Shar, + Debt, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ExchangeRate { + pub base_currency: Option, + pub exchange_currency: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct MessagePagination { + /// Resource listing may return a continuationKey if there's more results available. + /// Request may be retried with the continuationKey, but otherwise same parameters, in order to get more results. + pub continuation_key: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaPaymentsInitiateResponseData { + /// Unique payment identifier assigned for new payment + #[serde(rename = "_id")] + pub payment_id: String, + /// HATEOAS inspired links: 'rel' and 'href'. Context specific link (only GET supported) + #[serde(rename = "_links")] + pub links: Option>, + /// Marked as required field in the docs, but connector does not send amount in payment_response.amount + pub amount: Option, + /// Bearer of charges. shar = The Debtor (sender of the payment) will pay all fees charged by the sending bank. + /// The Creditor (recipient of the payment) will pay all fees charged by the receiving bank. + /// debt = The Debtor (sender of the payment) will bear all of the payment transaction fees. + /// The creditor (beneficiary) will receive the full amount of the payment. + pub charge_bearer: Option, + /// Creditor of the payment + #[serde(rename = "creditor")] + pub creditor_account: CreditorAccount, + pub currency: Option, + /// Debtor of the payment + #[serde(rename = "debtor")] + pub debitor_account: Option, + /// Timestamp of payment creation. ISO 8601 format yyyy-mm-ddThh:mm:ss.fffZ. Format:date-time. + pub entry_date_time: Option, + /// Unique identification as assigned by a partner to identify the payment. + pub external_id: Option, + /// An amount the bank will charge for executing the payment + pub fee: Option, + pub indicative_exchange_rate: Option, + /// It is mentioned as `number`. It can be an integer or a decimal number. + pub rate: Option, + /// Monetary amount + pub instructed_amount: Option, + /// Indication of cross border payment to own account + pub is_own_account_transfer: Option, + /// OTP Challenge + pub otp_challenge: Option, + /// Status of the payment + pub payment_status: NordeaPaymentStatus, + /// Planned execution date will indicate the day the payment will be finalized. If the payment has been pushed due to cut-off, it will be indicated in planned execution date. Format:date. + pub planned_execution_date: Option, + /// Recurring information + pub recurring: Option, + /// Choose a preferred execution date (or leave blank for today's date). + /// This should be a valid bank day, and depending on the country the date will either be pushed to the next valid bank day, + /// or return an error if a non-banking day date was supplied (all dates accepted in sandbox). + /// SEPA: max +5 years from yesterday, Domestic: max. +1 year from yesterday. NB: Not supported for Own transfer Non-Recurring Norway. + /// Format:date. + pub requested_execution_date: Option, + /// Timestamp of payment creation. ISO 8601 format yyyy-mm-ddThh:mm:ss.fffZ Format:date-time. + pub timestamp: Option, + /// Additional messages for third parties + pub tpp_messages: Option>, + pub transaction_fee: Option>, + /// Currency that the cross border payment will be transferred in. + /// This field is only supported for cross border payments for DK. + /// If this field is not supplied then the payment will use the currency specified for the currency field of instructed_amount. + pub transfer_currency: Option, + /// Urgency of the payment. NB: This field is supported for DK Domestic ('standard' and 'express') and NO Domestic bank transfer payments ('standard' and 'express'). + /// Use 'express' for Straksbetaling (Instant payment). + /// All other payment types ignore this input. + /// For further details on urgencies and cut-offs, refer to the Nordea website. + /// Value 'sameday' is marked as deprecated and will be removed in the future. + pub urgency: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaPaymentsInitiateResponse { + /// Payment information + #[serde(rename = "response")] + pub payments_response: Option, + /// External response header + pub group_header: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaPaymentsConfirmErrorObject { + /// Error message + pub error: Option, + /// Description of the error + pub error_description: Option, + /// Payment id of the payment, the error is associated with + pub payment_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaPaymentsResponseWrapper { + pub payments: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaPaymentsConfirmResponse { + /// HATEOAS inspired links: 'rel' and 'href' + #[serde(rename = "_links")] + pub links: Option>, + /// Error description + pub errors: Option>, + /// External response header + pub group_header: Option, + /// OTP Challenge + pub otp_challenge: Option, + #[serde(rename = "response")] + pub nordea_payments_response: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaOriginalRequest { + /// Original request url + #[serde(rename = "url")] + pub nordea_url: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaFailures { + /// Failure code + pub code: Option, + /// Failure description + pub description: Option, + /// JSON path of the failing element if applicable + pub path: Option, + /// Type of the validation error, e.g. NotNull + #[serde(rename = "type")] + pub failure_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaErrorBody { + // Serde JSON because connector returns an `(item)` object in failures array object + /// More details on the occurred error: Validation error + #[serde(rename = "failures")] + pub nordea_failures: Option>, + /// Original request information + #[serde(rename = "request")] + pub nordea_request: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct NordeaErrorResponse { + /// Error response body + pub error: Option, + /// External response header + pub group_header: Option, + #[serde(rename = "httpCode")] + pub http_code: Option, + #[serde(rename = "moreInformation")] + pub more_information: Option, +} + +// Nordea does not support refunds in Private APIs. Only Corporate APIs support Refunds diff --git a/crates/hyperswitch_connectors/src/connectors/nordea/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nordea/transformers.rs index bea06d53e3..6e781e7de9 100644 --- a/crates/hyperswitch_connectors/src/connectors/nordea/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nordea/transformers.rs @@ -1,31 +1,43 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use std::collections::HashMap; + +use common_utils::{pii, request::Method, types::StringMajorUnit}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, - router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + payment_method_data::{BankDebitData, PaymentMethodData}, + router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_flow_types::{Authorize, PreProcessing}, + router_request_types::{PaymentsAuthorizeData, PaymentsPreProcessingData, ResponseId}, + router_response_types::{PaymentsResponseData, RedirectForm}, + types::{ + self, AccessTokenAuthenticationRouterData, PaymentsAuthorizeRouterData, + PaymentsPreProcessingRouterData, PaymentsSyncRouterData, + }, }; use hyperswitch_interfaces::errors; use masking::Secret; -use serde::{Deserialize, Serialize}; +use rand::distributions::DistString; +use serde::{Deserialize, Deserializer, Serialize}; use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + connectors::nordea::{ + requests::{ + AccessScope, AccountNumber, AccountType, CreditorAccount, CreditorBank, DebitorAccount, + GrantType, NordeaOAuthExchangeRequest, NordeaOAuthRequest, + NordeaPaymentsConfirmRequest, NordeaPaymentsRequest, NordeaRouterData, PaymentsUrgency, + }, + responses::{ + NordeaErrorBody, NordeaFailures, NordeaOAuthExchangeResponse, NordeaPaymentStatus, + NordeaPaymentsConfirmResponse, NordeaPaymentsInitiateResponse, + }, + }, + types::{PaymentsSyncResponseRouterData, ResponseRouterData}, + utils::{self, get_unimplemented_payment_method_error_message, RouterData as _}, }; -//TODO: Fill the struct with respective fields -pub struct NordeaRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. - pub router_data: T, -} +type Error = error_stack::Report; -impl From<(StringMinorUnit, T)> for NordeaRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(StringMajorUnit, T)> for NordeaRouterData { + fn from((amount, item): (StringMajorUnit, T)) -> Self { Self { amount, router_data: item, @@ -33,196 +45,528 @@ impl From<(StringMinorUnit, T)> for NordeaRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] -pub struct NordeaPaymentsRequest { - amount: StringMinorUnit, - card: NordeaCard, -} - -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct NordeaCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, -} - -impl TryFrom<&NordeaRouterData<&PaymentsAuthorizeRouterData>> for NordeaPaymentsRequest { - type Error = error_stack::Report; - fn try_from( - item: &NordeaRouterData<&PaymentsAuthorizeRouterData>, - ) -> Result { - match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = NordeaCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), - } - } -} - -//TODO: Fill the struct with respective fields // Auth Struct +#[derive(Debug)] pub struct NordeaAuthType { - pub(super) api_key: Secret, + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + /// PEM format private key for eIDAS signing + /// Should be base64 encoded + pub(super) eidas_private_key: Secret, } impl TryFrom<&ConnectorAuthType> for NordeaAuthType { - type Error = error_stack::Report; + type Error = Error; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + eidas_private_key: api_secret.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum NordeaPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct NordeaConnectorMetadataObject { + pub creditor_account_value: Secret, + pub creditor_account_type: String, + pub creditor_beneficiary_name: Secret, +} + +impl TryFrom<&Option> for NordeaConnectorMetadataObject { + type Error = Error; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "merchant_connector_account.metadata", + })?; + Ok(metadata) + } +} + +impl TryFrom<&AccessTokenAuthenticationRouterData> for NordeaOAuthRequest { + type Error = Error; + fn try_from(item: &AccessTokenAuthenticationRouterData) -> Result { + let country = item.get_billing_country()?; + + // Set refresh_token maximum expiry duration to 180 days (259200 / 60 = 180) + // Minimum is 1 minute + let duration = Some(259200); + let maximum_transaction_history = Some(18); + let redirect_uri = "https://hyperswitch.io".to_string(); + let scope = [ + AccessScope::AccountsBasic, + AccessScope::AccountsDetails, + AccessScope::AccountsBalances, + AccessScope::AccountsTransactions, + AccessScope::PaymentsMultiple, + ] + .to_vec(); + let state = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 15); + + Ok(Self { + country, + duration, + maximum_transaction_history, + redirect_uri, + scope, + state: state.into(), + }) + } +} + +impl TryFrom<&types::RefreshTokenRouterData> for NordeaOAuthExchangeRequest { + type Error = Error; + fn try_from(item: &types::RefreshTokenRouterData) -> Result { + let code = item + .request + .authentication_token + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "authorization_code", + })? + .code + .clone(); + let grant_type = GrantType::AuthorizationCode; + let redirect_uri = Some("https://hyperswitch.io".to_string()); + + Ok(Self { + code: Some(code), + grant_type, + redirect_uri, + refresh_token: None, // We're not using refresh_token to generate new access_token + }) + } +} + +impl TryFrom> + for RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let access_token = + item.response + .access_token + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "access_token", + })?; + + let expires_in = item.response.expires_in.unwrap_or(3600); // Default to 1 hour if not provided + + Ok(Self { + status: common_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(AccessToken { + token: access_token.clone(), + expires: expires_in, + }), + ..item.data + }) + } +} + +impl TryFrom<&str> for AccountType { + type Error = Error; + + fn try_from(value: &str) -> Result { + match value.to_uppercase().as_str() { + "IBAN" => Ok(Self::Iban), + "BBAN_SE" => Ok(Self::BbanSe), + "BBAN_DK" => Ok(Self::BbanDk), + "BBAN_NO" => Ok(Self::BbanNo), + "BGNR" => Ok(Self::Bgnr), + "PGNR" => Ok(Self::Pgnr), + "GIRO_DK" => Ok(Self::GiroDk), + "BBAN_OTHER" => Ok(Self::BbanOther), + _ => Err(errors::ConnectorError::InvalidConnectorConfig { + config: "account_type", + } + .into()), + } + } +} + +impl<'de> Deserialize<'de> for PaymentsUrgency { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?.to_lowercase(); + match s.as_str() { + "standard" => Ok(Self::Standard), + "express" => Ok(Self::Express), + "sameday" => Ok(Self::Sameday), + _ => Err(serde::de::Error::unknown_variant( + &s, + &["standard", "express", "sameday"], + )), + } + } +} + +fn get_creditor_account_from_metadata( + router_data: &PaymentsPreProcessingRouterData, +) -> Result { + let metadata: NordeaConnectorMetadataObject = + utils::to_connector_meta_from_secret(router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "merchant_connector_account.metadata", + })?; + let creditor_account = CreditorAccount { + account: AccountNumber { + account_type: AccountType::try_from(metadata.creditor_account_type.as_str()) + .unwrap_or(AccountType::Iban), + currency: router_data.request.currency, + value: metadata.creditor_account_value, + }, + country: router_data.get_optional_billing_country(), + // Merchant is the beneficiary in this case + name: Some(metadata.creditor_beneficiary_name), + message: router_data + .description + .as_ref() + .map(|desc| desc.chars().take(20).collect::()), + bank: Some(CreditorBank { + address: None, + bank_code: None, + bank_name: None, + business_identifier_code: None, + country: router_data.get_billing_country()?, + }), + creditor_address: None, + // Either Reference or Message must be supplied in the request + reference: None, + }; + Ok(creditor_account) +} + +impl TryFrom<&NordeaRouterData<&PaymentsPreProcessingRouterData>> for NordeaPaymentsRequest { + type Error = Error; + fn try_from( + item: &NordeaRouterData<&PaymentsPreProcessingRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + Some(PaymentMethodData::BankDebit(bank_debit_data)) => match bank_debit_data { + BankDebitData::SepaBankDebit { iban, .. } => { + let creditor_account = get_creditor_account_from_metadata(item.router_data)?; + let debitor_account = DebitorAccount { + account: AccountNumber { + account_type: AccountType::Iban, + currency: item.router_data.request.currency, + value: iban, + }, + message: item + .router_data + .description + .as_ref() + .map(|desc| desc.chars().take(20).collect::()), + }; + + let instructed_amount = super::requests::InstructedAmount { + amount: item.amount.clone(), + currency: item.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "amount", + }, + )?, + }; + + Ok(Self { + creditor_account, + debitor_account, + end_to_end_identification: None, + external_id: Some(item.router_data.connector_request_reference_id.clone()), + instructed_amount, + recurring: None, + request_availability_of_funds: None, + requested_execution_date: None, + tpp_messages: None, + urgency: None, + }) + } + BankDebitData::AchBankDebit { .. } + | BankDebitData::BacsBankDebit { .. } + | BankDebitData::BecsBankDebit { .. } => { + Err(errors::ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("Nordea"), + ) + .into()) + } + }, + Some(PaymentMethodData::CardRedirect(_)) + | Some(PaymentMethodData::CardDetailsForNetworkTransactionId(_)) + | Some(PaymentMethodData::Wallet(_)) + | Some(PaymentMethodData::PayLater(_)) + | Some(PaymentMethodData::BankRedirect(_)) + | Some(PaymentMethodData::BankTransfer(_)) + | Some(PaymentMethodData::Crypto(_)) + | Some(PaymentMethodData::MandatePayment) + | Some(PaymentMethodData::Reward) + | Some(PaymentMethodData::RealTimePayment(_)) + | Some(PaymentMethodData::MobilePayment(_)) + | Some(PaymentMethodData::Upi(_)) + | Some(PaymentMethodData::Voucher(_)) + | Some(PaymentMethodData::GiftCard(_)) + | Some(PaymentMethodData::OpenBanking(_)) + | Some(PaymentMethodData::CardToken(_)) + | Some(PaymentMethodData::NetworkToken(_)) + | Some(PaymentMethodData::Card(_)) + | None => { + Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()) + } + } + } +} + +impl TryFrom<&NordeaRouterData<&PaymentsAuthorizeRouterData>> for NordeaPaymentsConfirmRequest { + type Error = Error; + fn try_from( + item: &NordeaRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + let payment_ids = match &item.router_data.response { + Ok(response_data) => response_data + .get_connector_transaction_id() + .map_err(|_| errors::ConnectorError::MissingConnectorTransactionID)?, + Err(_) => return Err(errors::ConnectorError::ResponseDeserializationFailed.into()), + }; + + Ok(Self { + authentication_method: None, + language: None, + payments_ids: vec![payment_ids], + redirect_url: None, + state: None, + }) + } } impl From for common_enums::AttemptStatus { fn from(item: NordeaPaymentStatus) -> Self { match item { - NordeaPaymentStatus::Succeeded => Self::Charged, - NordeaPaymentStatus::Failed => Self::Failure, - NordeaPaymentStatus::Processing => Self::Authorizing, + NordeaPaymentStatus::Confirmed | NordeaPaymentStatus::Paid => Self::Charged, + + NordeaPaymentStatus::PendingConfirmation + | NordeaPaymentStatus::PendingSecondConfirmation => Self::ConfirmationAwaited, + NordeaPaymentStatus::PendingUserApproval => Self::AuthenticationPending, + + NordeaPaymentStatus::OnHold | NordeaPaymentStatus::Unknown => Self::Pending, + + NordeaPaymentStatus::Rejected + | NordeaPaymentStatus::InsufficientFunds + | NordeaPaymentStatus::LimitExceeded + | NordeaPaymentStatus::UserApprovalFailed + | NordeaPaymentStatus::UserApprovalTimeout + | NordeaPaymentStatus::UserApprovalCancelled => Self::Failure, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct NordeaPaymentsResponse { - status: NordeaPaymentStatus, - id: String, +pub fn get_error_data(error_response: Option<&NordeaErrorBody>) -> Option<&NordeaFailures> { + error_response + .and_then(|error| error.nordea_failures.as_ref()) + .and_then(|failures| failures.first()) } -impl TryFrom> - for RouterData +// Helper function to convert NordeaPaymentsInitiateResponse to common response data +fn convert_nordea_payment_response( + response: &NordeaPaymentsInitiateResponse, +) -> Result<(PaymentsResponseData, common_enums::AttemptStatus), Error> { + let payment_response = response + .payments_response + .as_ref() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + + let resource_id = ResponseId::ConnectorTransactionId(payment_response.payment_id.clone()); + + let response_data = PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: payment_response.external_id.clone(), + incremental_authorization_allowed: None, + charges: None, + }; + + let status = common_enums::AttemptStatus::from(payment_response.payment_status.clone()); + + Ok((response_data, status)) +} + +impl + TryFrom< + ResponseRouterData< + PreProcessing, + NordeaPaymentsInitiateResponse, + PaymentsPreProcessingData, + PaymentsResponseData, + >, + > for RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData< + PreProcessing, + NordeaPaymentsInitiateResponse, + PaymentsPreProcessingData, + PaymentsResponseData, + >, ) -> Result { + let (response, status) = convert_nordea_payment_response(&item.response)?; Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charges: None, - }), + status, + response: Ok(response), ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] -pub struct NordeaRefundRequest { - pub amount: StringMinorUnit, -} +impl + TryFrom< + ResponseRouterData< + Authorize, + NordeaPaymentsConfirmResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + > for RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + Authorize, + NordeaPaymentsConfirmResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + ) -> Result { + // First check if there are any errors in the response + if let Some(errors) = &item.response.errors { + if !errors.is_empty() { + // Get the first error for the error response + let first_error = errors + .first() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; -impl TryFrom<&NordeaRouterData<&RefundsRouterData>> for NordeaRefundRequest { - type Error = error_stack::Report; - fn try_from(item: &NordeaRouterData<&RefundsRouterData>) -> Result { - Ok(Self { - amount: item.amount.to_owned(), - }) - } -} - -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + return Ok(Self { + status: common_enums::AttemptStatus::Failure, + response: Err(ErrorResponse { + code: first_error + .error + .clone() + .unwrap_or_else(|| "UNKNOWN_ERROR".to_string()), + message: first_error + .error_description + .clone() + .unwrap_or_else(|| "Payment confirmation failed".to_string()), + reason: first_error.error_description.clone(), + status_code: item.http_code, + attempt_status: Some(common_enums::AttemptStatus::Failure), + connector_transaction_id: first_error.payment_id.clone(), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + }), + ..item.data + }); + } } - } -} -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, -} + // If no errors, proceed with normal response handling + // Check if there's a redirect link at the top level only + let redirection_data = item + .response + .links + .as_ref() + .and_then(|links| { + links.iter().find(|link| { + link.rel + .as_ref() + .map(|rel| rel == "signing") + .unwrap_or(false) + }) + }) + .and_then(|link| link.href.clone()) + .map(|redirect_url| RedirectForm::Form { + endpoint: redirect_url, + method: Method::Get, + form_fields: HashMap::new(), + }); + + let (response, status) = match &item.response.nordea_payments_response { + Some(payment_response_wrapper) => { + // Get the first payment from the payments array + let payment = payment_response_wrapper + .payments + .first() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + + let resource_id = ResponseId::ConnectorTransactionId(payment.payment_id.clone()); + + let response = Ok(PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data: Box::new(redirection_data), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: payment.external_id.clone(), + incremental_authorization_allowed: None, + charges: None, + }); + + let status = common_enums::AttemptStatus::from(payment.payment_status.clone()); + + (response, status) + } + None => { + // No payment response, but we might still have a redirect link + if let Some(redirect) = redirection_data { + let response = Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::NoResponseId, + redirection_data: Box::new(Some(redirect)), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }); + (response, common_enums::AttemptStatus::AuthenticationPending) + } else { + return Err(errors::ConnectorError::ResponseHandlingFailed.into()); + } + } + }; -impl TryFrom> for RefundsRouterData { - type Error = error_stack::Report; - fn try_from( - item: RefundsResponseRouterData, - ) -> Result { Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + status, + response, ..item.data }) } } -impl TryFrom> for RefundsRouterData { - type Error = error_stack::Report; +impl TryFrom> + for PaymentsSyncRouterData +{ + type Error = Error; fn try_from( - item: RefundsResponseRouterData, + item: PaymentsSyncResponseRouterData, ) -> Result { + let (response, status) = convert_nordea_payment_response(&item.response)?; Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + status, + response: Ok(response), ..item.data }) } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct NordeaErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, -} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 7df691d95a..484f20cf8d 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -24,6 +24,51 @@ use hyperswitch_domain_models::router_response_types::revenue_recovery::{ BillingConnectorInvoiceSyncResponse, BillingConnectorPaymentsSyncResponse, RevenueRecoveryRecordBackResponse, }; +use hyperswitch_domain_models::{ + router_data::AccessTokenAuthenticationResponse, + router_flow_types::{ + authentication::{ + Authentication, PostAuthentication, PreAuthentication, PreAuthenticationVersionCall, + }, + dispute::{Accept, Defend, Dsync, Evidence, Fetch}, + files::{Retrieve, Upload}, + mandate_revoke::MandateRevoke, + payments::{ + Approve, AuthorizeSessionToken, CalculateTax, CompleteAuthorize, + CreateConnectorCustomer, CreateOrder, IncrementalAuthorization, PostCaptureVoid, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, + UpdateMetadata, + }, + webhooks::VerifyWebhookSource, + AccessTokenAuthentication, Authenticate, AuthenticationConfirmation, + ExternalVaultCreateFlow, ExternalVaultDeleteFlow, ExternalVaultInsertFlow, + ExternalVaultRetrieveFlow, PostAuthenticate, PreAuthenticate, + }, + router_request_types::{ + authentication, + unified_authentication_service::{ + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasConfirmationRequestData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, + AcceptDisputeRequestData, AccessTokenAuthenticationRequestData, AuthorizeSessionTokenData, + CompleteAuthorizeData, ConnectorCustomerData, CreateOrderRequestData, + DefendDisputeRequestData, DisputeSyncData, FetchDisputesRequestData, + MandateRevokeRequestData, PaymentsApproveData, PaymentsCancelPostCaptureData, + PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, + PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, + PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RetrieveFileRequestData, + SdkPaymentsSessionUpdateData, SubmitEvidenceRequestData, UploadFileRequestData, + VaultRequestData, VerifyWebhookSourceRequestData, + }, + router_response_types::{ + AcceptDisputeResponse, AuthenticationResponseData, DefendDisputeResponse, + DisputeSyncResponse, FetchDisputesResponse, MandateRevokeResponseData, + PaymentsResponseData, RetrieveFileResponse, SubmitEvidenceResponse, + TaxCalculationResponseData, UploadFileResponse, VaultResponseData, + VerifyWebhookSourceResponseData, + }, +}; #[cfg(feature = "frm")] use hyperswitch_domain_models::{ router_flow_types::fraud_check::{Checkout, Fulfillment, RecordReturn, Sale, Transaction}, @@ -42,48 +87,6 @@ use hyperswitch_domain_models::{ router_request_types::PayoutsData, router_response_types::PayoutsResponseData, }; -use hyperswitch_domain_models::{ - router_flow_types::{ - authentication::{ - Authentication, PostAuthentication, PreAuthentication, PreAuthenticationVersionCall, - }, - dispute::{Accept, Defend, Dsync, Evidence, Fetch}, - files::{Retrieve, Upload}, - mandate_revoke::MandateRevoke, - payments::{ - Approve, AuthorizeSessionToken, CalculateTax, CompleteAuthorize, - CreateConnectorCustomer, CreateOrder, IncrementalAuthorization, PostCaptureVoid, - PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, - UpdateMetadata, - }, - webhooks::VerifyWebhookSource, - Authenticate, AuthenticationConfirmation, ExternalVaultCreateFlow, ExternalVaultDeleteFlow, - ExternalVaultInsertFlow, ExternalVaultRetrieveFlow, PostAuthenticate, PreAuthenticate, - }, - router_request_types::{ - authentication, - unified_authentication_service::{ - UasAuthenticationRequestData, UasAuthenticationResponseData, - UasConfirmationRequestData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, - }, - AcceptDisputeRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, - ConnectorCustomerData, CreateOrderRequestData, DefendDisputeRequestData, DisputeSyncData, - FetchDisputesRequestData, MandateRevokeRequestData, PaymentsApproveData, - PaymentsCancelPostCaptureData, PaymentsIncrementalAuthorizationData, - PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, - PaymentsRejectData, PaymentsTaxCalculationData, PaymentsUpdateMetadataData, - RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SubmitEvidenceRequestData, - UploadFileRequestData, VaultRequestData, VerifyWebhookSourceRequestData, - }, - router_response_types::{ - AcceptDisputeResponse, AuthenticationResponseData, DefendDisputeResponse, - DisputeSyncResponse, FetchDisputesResponse, MandateRevokeResponseData, - PaymentsResponseData, RetrieveFileResponse, SubmitEvidenceResponse, - TaxCalculationResponseData, UploadFileResponse, VaultResponseData, - VerifyWebhookSourceResponseData, - }, -}; #[cfg(feature = "frm")] use hyperswitch_interfaces::api::fraud_check::{ FraudCheck, FraudCheckCheckout, FraudCheckFulfillment, FraudCheckRecordReturn, FraudCheckSale, @@ -124,9 +127,10 @@ use hyperswitch_interfaces::{ ExternalVault, ExternalVaultCreate, ExternalVaultDelete, ExternalVaultInsert, ExternalVaultRetrieve, }, - ConnectorIntegration, ConnectorMandateRevoke, ConnectorRedirectResponse, - ConnectorTransactionId, UasAuthentication, UasAuthenticationConfirmation, - UasPostAuthentication, UasPreAuthentication, UnifiedAuthenticationService, + ConnectorAuthenticationToken, ConnectorIntegration, ConnectorMandateRevoke, + ConnectorRedirectResponse, ConnectorTransactionId, UasAuthentication, + UasAuthenticationConfirmation, UasPostAuthentication, UasPreAuthentication, + UnifiedAuthenticationService, }, errors::ConnectorError, }; @@ -1703,7 +1707,6 @@ default_imp_for_pre_processing_steps!( connectors::Netcetera, connectors::Nomupay, connectors::Noon, - connectors::Nordea, connectors::Novalnet, connectors::Nexinets, connectors::Opayo, @@ -7499,6 +7502,146 @@ default_imp_for_external_vault_create!( connectors::Zsl ); +macro_rules! default_imp_for_connector_authentication_token { + ($($path:ident::$connector:ident),*) => { + $( + impl ConnectorAuthenticationToken for $path::$connector {} + impl + ConnectorIntegration< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, + > for $path::$connector + {} + )* + }; +} + +default_imp_for_connector_authentication_token!( + connectors::Aci, + connectors::Adyen, + connectors::Adyenplatform, + connectors::Affirm, + connectors::Airwallex, + connectors::Amazonpay, + connectors::Archipel, + connectors::Authipay, + connectors::Authorizedotnet, + connectors::Barclaycard, + connectors::Bambora, + connectors::Bamboraapac, + connectors::Bankofamerica, + connectors::Billwerk, + connectors::Bitpay, + connectors::Bluecode, + connectors::Bluesnap, + connectors::Blackhawknetwork, + connectors::Boku, + connectors::Braintree, + connectors::Breadpay, + connectors::Cashtocode, + connectors::Celero, + connectors::Chargebee, + connectors::Checkbook, + connectors::Checkout, + connectors::Coinbase, + connectors::Coingate, + connectors::Cryptopay, + connectors::CtpMastercard, + connectors::Custombilling, + connectors::Cybersource, + connectors::Datatrans, + connectors::Deutschebank, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Dwolla, + connectors::Ebanx, + connectors::Elavon, + connectors::Facilitapay, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Fiuu, + connectors::Flexiti, + connectors::Forte, + connectors::Getnet, + connectors::Globalpay, + connectors::Globepay, + connectors::Gocardless, + connectors::Gpayments, + connectors::Helcim, + connectors::Hipay, + connectors::HyperswitchVault, + connectors::Iatapay, + connectors::Inespay, + connectors::Itaubank, + connectors::Juspaythreedsserver, + connectors::Jpmorgan, + connectors::Katapult, + connectors::Klarna, + connectors::Mpgs, + connectors::Netcetera, + connectors::Nomupay, + connectors::Nmi, + connectors::Noon, + connectors::Novalnet, + connectors::Nexinets, + connectors::Nexixpay, + connectors::Nuvei, + connectors::Opayo, + connectors::Opennode, + connectors::Payeezy, + connectors::Payload, + connectors::Paystack, + connectors::Paytm, + connectors::Payu, + connectors::Phonepe, + connectors::Paypal, + connectors::Plaid, + connectors::Powertranz, + connectors::Prophetpay, + connectors::Mifinity, + connectors::Mollie, + connectors::Moneris, + connectors::Multisafepay, + connectors::Paybox, + connectors::Payme, + connectors::Payone, + connectors::Placetopay, + connectors::Rapyd, + connectors::Razorpay, + connectors::Recurly, + connectors::Redsys, + connectors::Riskified, + connectors::Santander, + connectors::Sift, + connectors::Signifyd, + connectors::Shift4, + connectors::Silverflow, + connectors::Stax, + connectors::Stripe, + connectors::Stripebilling, + connectors::Square, + connectors::Taxjar, + connectors::Threedsecureio, + connectors::Thunes, + connectors::Tokenio, + connectors::Trustpay, + connectors::Trustpayments, + connectors::Tsys, + connectors::UnifiedAuthenticationService, + connectors::Wise, + connectors::Worldline, + connectors::Worldpay, + connectors::Worldpayvantiv, + connectors::Worldpayxml, + connectors::Wellsfargo, + connectors::Vgs, + connectors::Volt, + connectors::Xendit, + connectors::Zen, + connectors::Zsl +); + #[cfg(feature = "dummy_connector")] impl PaymentsCompleteAuthorize for connectors::DummyConnector {} #[cfg(feature = "dummy_connector")] @@ -8026,3 +8169,15 @@ impl ConnectorIntegration { } + +#[cfg(feature = "dummy_connector")] +impl ConnectorAuthenticationToken for connectors::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + ConnectorIntegration< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, + > for connectors::DummyConnector +{ +} diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 69166f3e01..046aba87b0 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -1,12 +1,13 @@ use hyperswitch_domain_models::{ - router_data::AccessToken, + router_data::{AccessToken, AccessTokenAuthenticationResponse}, router_data_v2::{ flow_common_types::{ BillingConnectorInvoiceSyncFlowData, BillingConnectorPaymentsSyncFlowData, DisputesFlowData, MandateRevokeFlowData, PaymentFlowData, RefundFlowData, RevenueRecoveryRecordBackData, WebhookSourceVerifyData, }, - AccessTokenFlowData, ExternalAuthenticationFlowData, FilesFlowData, VaultConnectorFlowData, + AccessTokenFlowData, AuthenticationTokenFlowData, ExternalAuthenticationFlowData, + FilesFlowData, VaultConnectorFlowData, }, router_flow_types::{ authentication::{ @@ -26,8 +27,8 @@ use hyperswitch_domain_models::{ BillingConnectorInvoiceSync, BillingConnectorPaymentsSync, RecoveryRecordBack, }, webhooks::VerifyWebhookSource, - AccessTokenAuth, ExternalVaultCreateFlow, ExternalVaultDeleteFlow, ExternalVaultInsertFlow, - ExternalVaultRetrieveFlow, + AccessTokenAuth, AccessTokenAuthentication, ExternalVaultCreateFlow, + ExternalVaultDeleteFlow, ExternalVaultInsertFlow, ExternalVaultRetrieveFlow, }, router_request_types::{ authentication, @@ -35,14 +36,14 @@ use hyperswitch_domain_models::{ BillingConnectorInvoiceSyncRequest, BillingConnectorPaymentsSyncRequest, RevenueRecoveryRecordBackRequest, }, - AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, - CompleteAuthorizeData, ConnectorCustomerData, CreateOrderRequestData, - DefendDisputeRequestData, DisputeSyncData, FetchDisputesRequestData, - MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsApproveData, - PaymentsAuthorizeData, PaymentsCancelData, PaymentsCancelPostCaptureData, - PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, - PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, - PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, + AcceptDisputeRequestData, AccessTokenAuthenticationRequestData, AccessTokenRequestData, + AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, + CreateOrderRequestData, DefendDisputeRequestData, DisputeSyncData, + FetchDisputesRequestData, MandateRevokeRequestData, PaymentMethodTokenizationData, + PaymentsApproveData, PaymentsAuthorizeData, PaymentsCancelData, + PaymentsCancelPostCaptureData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, + PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, + PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RefundsData, RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, SubmitEvidenceRequestData, UploadFileRequestData, VaultRequestData, VerifyWebhookSourceRequestData, @@ -118,7 +119,8 @@ use hyperswitch_interfaces::{ ExternalVaultCreateV2, ExternalVaultDeleteV2, ExternalVaultInsertV2, ExternalVaultRetrieveV2, ExternalVaultV2, }, - ConnectorAccessTokenV2, ConnectorMandateRevokeV2, ConnectorVerifyWebhookSourceV2, + ConnectorAccessTokenV2, ConnectorAuthenticationTokenV2, ConnectorMandateRevokeV2, + ConnectorVerifyWebhookSourceV2, }, connector_integration_v2::ConnectorIntegrationV2, }; @@ -524,6 +526,136 @@ default_imp_for_new_connector_integration_refund!( connectors::Zsl ); +macro_rules! default_imp_for_new_connector_integration_connector_authentication_token { + ($($path:ident::$connector:ident),*) => { + $( + impl ConnectorAuthenticationTokenV2 for $path::$connector{} + impl + ConnectorIntegrationV2 + for $path::$connector{} + )* + }; +} + +default_imp_for_new_connector_integration_connector_authentication_token!( + connectors::Vgs, + connectors::Aci, + connectors::Adyen, + connectors::Adyenplatform, + connectors::Affirm, + connectors::Airwallex, + connectors::Amazonpay, + connectors::Authipay, + connectors::Authorizedotnet, + connectors::Bambora, + connectors::Bamboraapac, + connectors::Bankofamerica, + connectors::Barclaycard, + connectors::Billwerk, + connectors::Bitpay, + connectors::Blackhawknetwork, + connectors::Bluesnap, + connectors::Boku, + connectors::Braintree, + connectors::Breadpay, + connectors::Cashtocode, + connectors::Celero, + connectors::Chargebee, + connectors::Checkbook, + connectors::Checkout, + connectors::Coinbase, + connectors::Coingate, + connectors::Cryptopay, + connectors::CtpMastercard, + connectors::Custombilling, + connectors::Cybersource, + connectors::Datatrans, + connectors::Deutschebank, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Dwolla, + connectors::Ebanx, + connectors::Elavon, + connectors::Facilitapay, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Fiuu, + connectors::Forte, + connectors::Getnet, + connectors::Globalpay, + connectors::Globepay, + connectors::Gocardless, + connectors::Gpayments, + connectors::Hipay, + connectors::Helcim, + connectors::HyperswitchVault, + connectors::Iatapay, + connectors::Inespay, + connectors::Itaubank, + connectors::Jpmorgan, + connectors::Juspaythreedsserver, + connectors::Klarna, + connectors::Nomupay, + connectors::Noon, + connectors::Nordea, + connectors::Novalnet, + connectors::Netcetera, + connectors::Nexinets, + connectors::Nexixpay, + connectors::Nmi, + connectors::Payone, + connectors::Opayo, + connectors::Opennode, + connectors::Nuvei, + connectors::Paybox, + connectors::Payeezy, + connectors::Payload, + connectors::Payme, + connectors::Paypal, + connectors::Paystack, + connectors::Payu, + connectors::Placetopay, + connectors::Plaid, + connectors::Powertranz, + connectors::Prophetpay, + connectors::Mifinity, + connectors::Mollie, + connectors::Moneris, + connectors::Multisafepay, + connectors::Rapyd, + connectors::Razorpay, + connectors::Recurly, + connectors::Redsys, + connectors::Riskified, + connectors::Santander, + connectors::Shift4, + connectors::Sift, + connectors::Silverflow, + connectors::Signifyd, + connectors::Stax, + connectors::Stripe, + connectors::Square, + connectors::Stripebilling, + connectors::Taxjar, + connectors::Threedsecureio, + connectors::Thunes, + connectors::Tokenio, + connectors::Trustpay, + connectors::Tsys, + connectors::UnifiedAuthenticationService, + connectors::Wise, + connectors::Worldline, + connectors::Volt, + connectors::Worldpay, + connectors::Worldpayvantiv, + connectors::Worldpayxml, + connectors::Wellsfargo, + connectors::Wellsfargopayout, + connectors::Xendit, + connectors::Zen, + connectors::Zsl +); + macro_rules! default_imp_for_new_connector_integration_connector_access_token { ($($path:ident::$connector:ident),*) => { $( diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 13998ed661..743c04aad7 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1696,6 +1696,7 @@ pub trait PaymentsAuthorizeRequestData { fn get_connector_mandate_id(&self) -> Result; fn get_complete_authorize_url(&self) -> Result; fn get_ip_address_as_optional(&self) -> Option>; + fn get_ip_address(&self) -> Result, Error>; fn get_optional_user_agent(&self) -> Option; fn get_original_amount(&self) -> i64; fn get_surcharge_amount(&self) -> Option; @@ -1818,6 +1819,16 @@ impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { .map(|ip| Secret::new(ip.to_string())) }) } + fn get_ip_address(&self) -> Result, Error> { + let ip_address = self + .browser_info + .clone() + .and_then(|browser_info| browser_info.ip_address); + + let val = ip_address.ok_or_else(missing_field_err("browser_info.ip_address"))?; + + Ok(Secret::new(val.to_string())) + } fn get_optional_user_agent(&self) -> Option { self.browser_info .clone() diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index f861327503..d3b6883571 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -258,6 +258,12 @@ impl ConnectorAuthType { } } +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct AccessTokenAuthenticationResponse { + pub code: Secret, + pub expires: i64, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] pub struct AccessToken { pub token: Secret, diff --git a/crates/hyperswitch_domain_models/src/router_data_v2.rs b/crates/hyperswitch_domain_models/src/router_data_v2.rs index d6498c962e..60b6070da0 100644 --- a/crates/hyperswitch_domain_models/src/router_data_v2.rs +++ b/crates/hyperswitch_domain_models/src/router_data_v2.rs @@ -8,9 +8,9 @@ pub use flow_common_types::FrmFlowData; #[cfg(feature = "payouts")] pub use flow_common_types::PayoutFlowData; pub use flow_common_types::{ - AccessTokenFlowData, DisputesFlowData, ExternalAuthenticationFlowData, FilesFlowData, - MandateRevokeFlowData, PaymentFlowData, RefundFlowData, UasFlowData, VaultConnectorFlowData, - WebhookSourceVerifyData, + AccessTokenFlowData, AuthenticationTokenFlowData, DisputesFlowData, + ExternalAuthenticationFlowData, FilesFlowData, MandateRevokeFlowData, PaymentFlowData, + RefundFlowData, UasFlowData, VaultConnectorFlowData, WebhookSourceVerifyData, }; use crate::router_data::{ConnectorAuthType, ErrorResponse}; diff --git a/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs b/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs index 5bc18dc8af..3fe6e85bda 100644 --- a/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs +++ b/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs @@ -132,6 +132,9 @@ pub struct WebhookSourceVerifyData { pub merchant_id: common_utils::id_type::MerchantId, } +#[derive(Debug, Clone)] +pub struct AuthenticationTokenFlowData {} + #[derive(Debug, Clone)] pub struct AccessTokenFlowData {} diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/access_token_auth.rs b/crates/hyperswitch_domain_models/src/router_flow_types/access_token_auth.rs index dd45ca9ca3..66c940e5ec 100644 --- a/crates/hyperswitch_domain_models/src/router_flow_types/access_token_auth.rs +++ b/crates/hyperswitch_domain_models/src/router_flow_types/access_token_auth.rs @@ -1,2 +1,5 @@ +#[derive(Clone, Debug)] +pub struct AccessTokenAuthentication; + #[derive(Clone, Debug)] pub struct AccessTokenAuth; diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 8d1bf9ffaa..142d2633fc 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -16,7 +16,7 @@ use crate::{ address, errors::api_error_response::ApiErrorResponse, mandates, payments, - router_data::{self, RouterData}, + router_data::{self, AccessTokenAuthenticationResponse, RouterData}, router_flow_types as flows, router_response_types as response_types, vault::PaymentMethodVaultingData, }; @@ -813,13 +813,29 @@ pub struct DestinationChargeRefund { pub revert_transfer: bool, } +#[derive(Debug, Clone)] +pub struct AccessTokenAuthenticationRequestData { + pub auth_creds: router_data::ConnectorAuthType, +} + +impl TryFrom for AccessTokenAuthenticationRequestData { + type Error = ApiErrorResponse; + fn try_from(connector_auth: router_data::ConnectorAuthType) -> Result { + Ok(Self { + auth_creds: connector_auth, + }) + } +} + #[derive(Debug, Clone)] pub struct AccessTokenRequestData { pub app_id: Secret, pub id: Option>, + pub authentication_token: Option, // Add more keys if required } +// This is for backward compatibility impl TryFrom for AccessTokenRequestData { type Error = ApiErrorResponse; fn try_from(connector_auth: router_data::ConnectorAuthType) -> Result { @@ -827,18 +843,22 @@ impl TryFrom for AccessTokenRequestData { router_data::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { app_id: api_key, id: None, + authentication_token: None, }), router_data::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { app_id: api_key, id: Some(key1), + authentication_token: None, }), router_data::ConnectorAuthType::SignatureKey { api_key, key1, .. } => Ok(Self { app_id: api_key, id: Some(key1), + authentication_token: None, }), router_data::ConnectorAuthType::MultiAuthKey { api_key, key1, .. } => Ok(Self { app_id: api_key, id: Some(key1), + authentication_token: None, }), _ => Err(ApiErrorResponse::InvalidDataValue { @@ -848,6 +868,25 @@ impl TryFrom for AccessTokenRequestData { } } +impl + TryFrom<( + router_data::ConnectorAuthType, + Option, + )> for AccessTokenRequestData +{ + type Error = ApiErrorResponse; + fn try_from( + (connector_auth, authentication_token): ( + router_data::ConnectorAuthType, + Option, + ), + ) -> Result { + let mut access_token_request_data = Self::try_from(connector_auth)?; + access_token_request_data.authentication_token = authentication_token; + Ok(access_token_request_data) + } +} + #[derive(Default, Debug, Clone)] pub struct AcceptDisputeRequestData { pub dispute_id: String, diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index 2dc507cf20..6036bdcca5 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -1,16 +1,16 @@ pub use diesel_models::types::OrderDetailsWithAmount; use crate::{ - router_data::{AccessToken, RouterData}, + router_data::{AccessToken, AccessTokenAuthenticationResponse, RouterData}, router_data_v2::{self, RouterDataV2}, router_flow_types::{ mandate_revoke::MandateRevoke, revenue_recovery::RecoveryRecordBack, AccessTokenAuth, - Authenticate, AuthenticationConfirmation, Authorize, AuthorizeSessionToken, - BillingConnectorInvoiceSync, BillingConnectorPaymentsSync, CalculateTax, Capture, - CompleteAuthorize, CreateConnectorCustomer, CreateOrder, Execute, IncrementalAuthorization, - PSync, PaymentMethodToken, PostAuthenticate, PostCaptureVoid, PostSessionTokens, - PreAuthenticate, PreProcessing, RSync, SdkSessionUpdate, Session, SetupMandate, - UpdateMetadata, VerifyWebhookSource, Void, + AccessTokenAuthentication, Authenticate, AuthenticationConfirmation, Authorize, + AuthorizeSessionToken, BillingConnectorInvoiceSync, BillingConnectorPaymentsSync, + CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, CreateOrder, Execute, + IncrementalAuthorization, PSync, PaymentMethodToken, PostAuthenticate, PostCaptureVoid, + PostSessionTokens, PreAuthenticate, PreProcessing, RSync, SdkSessionUpdate, Session, + SetupMandate, UpdateMetadata, VerifyWebhookSource, Void, }, router_request_types::{ revenue_recovery::{ @@ -22,12 +22,13 @@ use crate::{ UasConfirmationRequestData, UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, - AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, - ConnectorCustomerData, CreateOrderRequestData, MandateRevokeRequestData, - PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, - PaymentsCancelPostCaptureData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, - PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsSessionData, - PaymentsSyncData, PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RefundsData, + AccessTokenAuthenticationRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, + CompleteAuthorizeData, ConnectorCustomerData, CreateOrderRequestData, + MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCancelPostCaptureData, PaymentsCaptureData, + PaymentsIncrementalAuthorizationData, PaymentsPostSessionTokensData, + PaymentsPreProcessingData, PaymentsSessionData, PaymentsSyncData, + PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RefundsData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, VaultRequestData, VerifyWebhookSourceRequestData, }, @@ -67,6 +68,11 @@ pub type PaymentsCompleteAuthorizeRouterData = RouterData; pub type PaymentsTaxCalculationRouterData = RouterData; +pub type AccessTokenAuthenticationRouterData = RouterData< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, +>; pub type RefreshTokenRouterData = RouterData; pub type PaymentsPostSessionTokensRouterData = RouterData; diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs index 70d1bc2c1b..6efc23c3c3 100644 --- a/crates/hyperswitch_interfaces/src/api.rs +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -40,14 +40,17 @@ use hyperswitch_domain_models::{ connector_endpoints::Connectors, errors::api_error_response::ApiErrorResponse, payment_method_data::PaymentMethodData, - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{ + AccessToken, AccessTokenAuthenticationResponse, ConnectorAuthType, ErrorResponse, + RouterData, + }, router_data_v2::{ - flow_common_types::WebhookSourceVerifyData, AccessTokenFlowData, MandateRevokeFlowData, - UasFlowData, + flow_common_types::{AuthenticationTokenFlowData, WebhookSourceVerifyData}, + AccessTokenFlowData, MandateRevokeFlowData, UasFlowData, }, router_flow_types::{ - mandate_revoke::MandateRevoke, AccessTokenAuth, Authenticate, AuthenticationConfirmation, - PostAuthenticate, PreAuthenticate, VerifyWebhookSource, + mandate_revoke::MandateRevoke, AccessTokenAuth, AccessTokenAuthentication, Authenticate, + AuthenticationConfirmation, PostAuthenticate, PreAuthenticate, VerifyWebhookSource, }, router_request_types::{ unified_authentication_service::{ @@ -55,7 +58,8 @@ use hyperswitch_domain_models::{ UasConfirmationRequestData, UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, - AccessTokenRequestData, MandateRevokeRequestData, VerifyWebhookSourceRequestData, + AccessTokenAuthenticationRequestData, AccessTokenRequestData, MandateRevokeRequestData, + VerifyWebhookSourceRequestData, }, router_response_types::{ ConnectorInfo, MandateRevokeResponseData, PaymentMethodDetails, SupportedPaymentMethods, @@ -87,6 +91,7 @@ pub trait Connector: + ConnectorRedirectResponse + webhooks::IncomingWebhook + ConnectorAccessToken + + ConnectorAuthenticationToken + disputes::Dispute + files::FileUpload + ConnectorTransactionId @@ -109,6 +114,7 @@ impl< + Send + webhooks::IncomingWebhook + ConnectorAccessToken + + ConnectorAuthenticationToken + disputes::Dispute + files::FileUpload + ConnectorTransactionId @@ -383,6 +389,12 @@ pub trait ConnectorSpecifications { None } + /// Check if connector should make another request to create an access token + /// Connectors should override this method if they require an authentication token to create a new access token + fn authentication_token_for_token_creation(&self) -> bool { + false + } + #[cfg(not(feature = "v2"))] /// Generate connector request reference ID fn generate_connector_request_reference_id( @@ -445,6 +457,27 @@ pub trait ConnectorMandateRevokeV2: { } +/// trait ConnectorAuthenticationToken +pub trait ConnectorAuthenticationToken: + ConnectorIntegration< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, +> +{ +} + +/// trait ConnectorAuthenticationTokenV2 +pub trait ConnectorAuthenticationTokenV2: + ConnectorIntegrationV2< + AccessTokenAuthentication, + AuthenticationTokenFlowData, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, +> +{ +} + /// trait ConnectorAccessToken pub trait ConnectorAccessToken: ConnectorIntegration diff --git a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs index c40c65ac5f..66a1e29175 100644 --- a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs +++ b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs @@ -519,6 +519,14 @@ impl ConnectorSpecifications for ConnectorEnum { } } + /// Check if connector supports authentication token + fn authentication_token_for_token_creation(&self) -> bool { + match self { + Self::Old(connector) => connector.authentication_token_for_token_creation(), + Self::New(connector) => connector.authentication_token_for_token_creation(), + } + } + #[cfg(feature = "v1")] fn generate_connector_request_reference_id( &self, diff --git a/crates/hyperswitch_interfaces/src/connector_integration_v2.rs b/crates/hyperswitch_interfaces/src/connector_integration_v2.rs index d51320ebbf..999770b65c 100644 --- a/crates/hyperswitch_interfaces/src/connector_integration_v2.rs +++ b/crates/hyperswitch_interfaces/src/connector_integration_v2.rs @@ -22,6 +22,7 @@ pub trait ConnectorV2: + api::payments_v2::PaymentV2 + api::ConnectorRedirectResponse + webhooks::IncomingWebhook + + api::ConnectorAuthenticationTokenV2 + api::ConnectorAccessTokenV2 + api::disputes_v2::DisputeV2 + api::files_v2::FileUploadV2 @@ -42,6 +43,7 @@ impl< + api::ConnectorRedirectResponse + Send + webhooks::IncomingWebhook + + api::ConnectorAuthenticationTokenV2 + api::ConnectorAccessTokenV2 + api::disputes_v2::DisputeV2 + api::files_v2::FileUploadV2 diff --git a/crates/hyperswitch_interfaces/src/conversion_impls.rs b/crates/hyperswitch_interfaces/src/conversion_impls.rs index 633a7172be..d70b8e882a 100644 --- a/crates/hyperswitch_interfaces/src/conversion_impls.rs +++ b/crates/hyperswitch_interfaces/src/conversion_impls.rs @@ -9,7 +9,7 @@ use hyperswitch_domain_models::{ router_data::{self, RouterData}, router_data_v2::{ flow_common_types::{ - AccessTokenFlowData, BillingConnectorInvoiceSyncFlowData, + AccessTokenFlowData, AuthenticationTokenFlowData, BillingConnectorInvoiceSyncFlowData, BillingConnectorPaymentsSyncFlowData, DisputesFlowData, ExternalAuthenticationFlowData, FilesFlowData, MandateRevokeFlowData, PaymentFlowData, RefundFlowData, RevenueRecoveryRecordBackData, UasFlowData, VaultConnectorFlowData, @@ -91,6 +91,45 @@ fn get_default_router_data( } } +impl RouterDataConversion + for AuthenticationTokenFlowData +{ + fn from_old_router_data( + old_router_data: &RouterData, + ) -> CustomResult, ConnectorError> + where + Self: Sized, + { + let resource_common_data = Self {}; + Ok(RouterDataV2 { + flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), + resource_common_data, + connector_auth_type: old_router_data.connector_auth_type.clone(), + request: old_router_data.request.clone(), + response: old_router_data.response.clone(), + }) + } + + fn to_old_router_data( + new_router_data: RouterDataV2, + ) -> CustomResult, ConnectorError> + where + Self: Sized, + { + let Self {} = new_router_data.resource_common_data; + let request = new_router_data.request.clone(); + let response = new_router_data.response.clone(); + let router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "authentication token", + request, + response, + ); + Ok(router_data) + } +} + impl RouterDataConversion for AccessTokenFlowData { fn from_old_router_data( old_router_data: &RouterData, diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index 04053546c9..eed89fe00b 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -1,7 +1,7 @@ //! Types interface use hyperswitch_domain_models::{ - router_data::AccessToken, + router_data::{AccessToken, AccessTokenAuthenticationResponse}, router_data_v2::flow_common_types, router_flow_types::{ access_token_auth::AccessTokenAuth, @@ -24,7 +24,7 @@ use hyperswitch_domain_models::{ ExternalVaultRetrieveFlow, }, webhooks::VerifyWebhookSource, - BillingConnectorInvoiceSync, + AccessTokenAuthentication, BillingConnectorInvoiceSync, }, router_request_types::{ revenue_recovery::{ @@ -36,12 +36,12 @@ use hyperswitch_domain_models::{ UasConfirmationRequestData, UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, - AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, - CompleteAuthorizeData, ConnectorCustomerData, CreateOrderRequestData, - DefendDisputeRequestData, DisputeSyncData, FetchDisputesRequestData, - MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCancelPostCaptureData, PaymentsCaptureData, - PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, + AcceptDisputeRequestData, AccessTokenAuthenticationRequestData, AccessTokenRequestData, + AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, + CreateOrderRequestData, DefendDisputeRequestData, DisputeSyncData, + FetchDisputesRequestData, MandateRevokeRequestData, PaymentMethodTokenizationData, + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCancelPostCaptureData, + PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RefundsData, RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, @@ -193,6 +193,12 @@ pub type PayoutQuoteType = dyn ConnectorIntegration` #[cfg(feature = "payouts")] pub type PayoutSyncType = dyn ConnectorIntegration; +/// Type alias for `ConnectorIntegration` +pub type AuthenticationTokenType = dyn ConnectorIntegration< + AccessTokenAuthentication, + AccessTokenAuthenticationRequestData, + AccessTokenAuthenticationResponse, +>; /// Type alias for `ConnectorIntegration` pub type RefreshTokenType = dyn ConnectorIntegration; diff --git a/crates/payment_methods/src/configs/payment_connector_required_fields.rs b/crates/payment_methods/src/configs/payment_connector_required_fields.rs index 8e67724d68..57b6e94cbb 100644 --- a/crates/payment_methods/src/configs/payment_connector_required_fields.rs +++ b/crates/payment_methods/src/configs/payment_connector_required_fields.rs @@ -3210,6 +3210,19 @@ fn get_bank_debit_required_fields() -> HashMap { noon::transformers::NoonAuthType::try_from(self.auth_type)?; Ok(()) } - // api_enums::Connector::Nordea => { - // nordea::transformers::NordeaAuthType::try_from(self.auth_type)?; - // Ok(()) - // } + api_enums::Connector::Nordea => { + nordea::transformers::NordeaAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Novalnet => { novalnet::transformers::NovalnetAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index de9f116bb9..b31550d412 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6119,7 +6119,9 @@ where } } Some(domain::PaymentMethodData::BankDebit(_)) => { - if connector.connector_name == router_types::Connector::Gocardless { + if connector.connector_name == router_types::Connector::Gocardless + || connector.connector_name == router_types::Connector::Nordea + { router_data = router_data.preprocessing_steps(state, connector).await?; let is_error_in_response = router_data.response.is_err(); // If is_error_in_response is true, should_continue_payment should be false, we should throw the error diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index f2f7596491..cf49d65b13 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; +use hyperswitch_interfaces::api::ConnectorSpecifications; use crate::{ consts, @@ -112,10 +113,15 @@ pub async fn add_access_token< )), ); + let authentication_token = + execute_authentication_token(state, connector, router_data).await?; + let cloned_router_data = router_data.clone(); - let refresh_token_request_data = types::AccessTokenRequestData::try_from( + + let refresh_token_request_data = types::AccessTokenRequestData::try_from(( router_data.connector_auth_type.clone(), - ) + authentication_token, + )) .attach_printable( "Could not create access token request, invalid connector account credentials", )?; @@ -254,3 +260,93 @@ pub async fn refresh_connector_auth( ); Ok(access_token_router_data) } + +pub async fn execute_authentication_token< + F: Clone + 'static, + Req: Debug + Clone + 'static, + Res: Debug + Clone + 'static, +>( + state: &SessionState, + connector: &api_types::ConnectorData, + router_data: &types::RouterData, +) -> RouterResult> { + let should_create_authentication_token = connector + .connector + .authentication_token_for_token_creation(); + + if !should_create_authentication_token { + return Ok(None); + } + + let authentication_token_request_data = types::AccessTokenAuthenticationRequestData::try_from( + router_data.connector_auth_type.clone(), + ) + .attach_printable( + "Could not create authentication token request, invalid connector account credentials", + )?; + + let authentication_token_response_data: Result< + types::AccessTokenAuthenticationResponse, + types::ErrorResponse, + > = Err(types::ErrorResponse::default()); + + let auth_token_router_data = payments::helpers::router_data_type_conversion::< + _, + api_types::AccessTokenAuthentication, + _, + _, + _, + _, + >( + router_data.clone(), + authentication_token_request_data, + authentication_token_response_data, + ); + + let connector_integration: services::BoxedAuthenticationTokenConnectorIntegrationInterface< + api_types::AccessTokenAuthentication, + types::AccessTokenAuthenticationRequestData, + types::AccessTokenAuthenticationResponse, + > = connector.connector.get_connector_integration(); + + let auth_token_router_data_result = services::execute_connector_processing_step( + state, + connector_integration, + &auth_token_router_data, + payments::CallConnectorAction::Trigger, + None, + None, + ) + .await; + + let auth_token_result = match auth_token_router_data_result { + Ok(router_data) => router_data.response, + Err(connector_error) => { + // Handle timeout errors + if connector_error.current_context().is_connector_timeout() { + let error_response = 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, + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + }; + Err(error_response) + } else { + return Err(connector_error + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not get authentication token")); + } + } + }; + + let authentication_token = auth_token_result + .map_err(|_error| errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get authentication token")?; + + Ok(Some(authentication_token)) +} diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index 296b081db6..4008e0debe 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -85,8 +85,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 2aa69e6a36..a51912e566 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -248,8 +248,14 @@ impl Feature for types::PaymentsAu merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn add_session_token<'a>( diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 7f025f81c8..a452a165c7 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -103,8 +103,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/cancel_post_capture_flow.rs b/crates/router/src/core/payments/flows/cancel_post_capture_flow.rs index 67ccd612a9..5b4625c847 100644 --- a/crates/router/src/core/payments/flows/cancel_post_capture_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_post_capture_flow.rs @@ -111,8 +111,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/capture_flow.rs b/crates/router/src/core/payments/flows/capture_flow.rs index 3f2dafc0cb..61f54eb3d5 100644 --- a/crates/router/src/core/payments/flows/capture_flow.rs +++ b/crates/router/src/core/payments/flows/capture_flow.rs @@ -124,8 +124,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 1d7a7772fb..30ed347715 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -142,8 +142,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn add_payment_method_token<'a>( diff --git a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs index ef04b0354d..23a4c788e8 100644 --- a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -106,8 +106,14 @@ impl Feature, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/post_session_tokens_flow.rs b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs index b356b69513..68ecc97e8a 100644 --- a/crates/router/src/core/payments/flows/post_session_tokens_flow.rs +++ b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs @@ -105,8 +105,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index 641af1d397..93a2c1ecd4 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -168,8 +168,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/reject_flow.rs b/crates/router/src/core/payments/flows/reject_flow.rs index 2c7a9878ed..0bf9f091f3 100644 --- a/crates/router/src/core/payments/flows/reject_flow.rs +++ b/crates/router/src/core/payments/flows/reject_flow.rs @@ -84,8 +84,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 06441f2466..1f67fd4c00 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -125,8 +125,14 @@ impl Feature for types::PaymentsSessio merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn create_connector_customer<'a>( diff --git a/crates/router/src/core/payments/flows/session_update_flow.rs b/crates/router/src/core/payments/flows/session_update_flow.rs index 24b51ce1ca..a52eef0b1d 100644 --- a/crates/router/src/core/payments/flows/session_update_flow.rs +++ b/crates/router/src/core/payments/flows/session_update_flow.rs @@ -105,8 +105,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index c89f04aa69..4c1b1a445d 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -145,8 +145,14 @@ impl Feature for types::Setup merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn add_payment_method_token<'a>( diff --git a/crates/router/src/core/payments/flows/update_metadata_flow.rs b/crates/router/src/core/payments/flows/update_metadata_flow.rs index a57b6acb75..538ed55d11 100644 --- a/crates/router/src/core/payments/flows/update_metadata_flow.rs +++ b/crates/router/src/core/payments/flows/update_metadata_flow.rs @@ -104,8 +104,14 @@ impl Feature merchant_context: &domain::MerchantContext, creds_identifier: Option<&str>, ) -> RouterResult { - access_token::add_access_token(state, connector, merchant_context, self, creds_identifier) - .await + Box::pin(access_token::add_access_token( + state, + connector, + merchant_context, + self, + creds_identifier, + )) + .await } async fn build_flow_specific_connector_request( diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 0894297b74..ed44f61b4d 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -183,13 +183,13 @@ pub async fn trigger_refund_to_gateway( ) .await?; - let add_access_token_result = access_token::add_access_token( + let add_access_token_result = Box::pin(access_token::add_access_token( state, &connector, merchant_context, &router_data, creds_identifier.as_deref(), - ) + )) .await?; logger::debug!(refund_router_data=?router_data); @@ -617,13 +617,13 @@ pub async fn sync_refund_with_gateway( ) .await?; - let add_access_token_result = access_token::add_access_token( + let add_access_token_result = Box::pin(access_token::add_access_token( state, &connector, merchant_context, &router_data, creds_identifier.as_deref(), - ) + )) .await?; logger::debug!(refund_retrieve_router_data=?router_data); diff --git a/crates/router/src/core/refunds_v2.rs b/crates/router/src/core/refunds_v2.rs index ceab569885..bb4f2535ee 100644 --- a/crates/router/src/core/refunds_v2.rs +++ b/crates/router/src/core/refunds_v2.rs @@ -169,9 +169,14 @@ pub async fn trigger_refund_to_gateway( ) .await?; - let add_access_token_result = - access_token::add_access_token(state, &connector, merchant_context, &router_data, None) - .await?; + let add_access_token_result = Box::pin(access_token::add_access_token( + state, + &connector, + merchant_context, + &router_data, + None, + )) + .await?; logger::debug!(refund_router_data=?router_data); @@ -271,9 +276,14 @@ pub async fn internal_trigger_refund_to_gateway( ) .await?; - let add_access_token_result = - access_token::add_access_token(state, &connector, merchant_context, &router_data, None) - .await?; + let add_access_token_result = Box::pin(access_token::add_access_token( + state, + &connector, + merchant_context, + &router_data, + None, + )) + .await?; access_token::update_router_data_with_access_token_result( &add_access_token_result, @@ -805,9 +815,14 @@ pub async fn sync_refund_with_gateway( ) .await?; - let add_access_token_result = - access_token::add_access_token(state, &connector, merchant_context, &router_data, None) - .await?; + let add_access_token_result = Box::pin(access_token::add_access_token( + state, + &connector, + merchant_context, + &router_data, + None, + )) + .await?; logger::debug!(refund_retrieve_router_data=?router_data); @@ -886,9 +901,14 @@ pub async fn internal_sync_refund_with_gateway( ) .await?; - let add_access_token_result = - access_token::add_access_token(state, &connector, merchant_context, &router_data, None) - .await?; + let add_access_token_result = Box::pin(access_token::add_access_token( + state, + &connector, + merchant_context, + &router_data, + None, + )) + .await?; access_token::update_router_data_with_access_token_result( &add_access_token_result, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index d36cacd1a5..005f41f91c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -98,6 +98,8 @@ pub type BoxedWebhookSourceVerificationConnectorIntegrationInterface; pub type BoxedExternalAuthenticationConnectorIntegrationInterface = BoxedConnectorIntegrationInterface; +pub type BoxedAuthenticationTokenConnectorIntegrationInterface = + BoxedConnectorIntegrationInterface; pub type BoxedAccessTokenConnectorIntegrationInterface = BoxedConnectorIntegrationInterface; pub type BoxedFilesConnectorIntegrationInterface = diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 41254507c2..f735c0e586 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -47,15 +47,15 @@ use hyperswitch_domain_models::router_flow_types::{ pub use hyperswitch_domain_models::{ payment_address::PaymentAddress, router_data::{ - AccessToken, AdditionalPaymentMethodConnectorResponse, ConnectorAuthType, - ConnectorResponseData, ErrorResponse, GooglePayPaymentMethodDetails, + AccessToken, AccessTokenAuthenticationResponse, AdditionalPaymentMethodConnectorResponse, + ConnectorAuthType, ConnectorResponseData, ErrorResponse, GooglePayPaymentMethodDetails, GooglePayPredecryptDataInternal, L2L3Data, PaymentMethodBalance, PaymentMethodToken, RecurringMandatePaymentData, RouterData, }, router_data_v2::{ - AccessTokenFlowData, DisputesFlowData, ExternalAuthenticationFlowData, FilesFlowData, - MandateRevokeFlowData, PaymentFlowData, RefundFlowData, RouterDataV2, UasFlowData, - WebhookSourceVerifyData, + AccessTokenFlowData, AuthenticationTokenFlowData, DisputesFlowData, + ExternalAuthenticationFlowData, FilesFlowData, MandateRevokeFlowData, PaymentFlowData, + RefundFlowData, RouterDataV2, UasFlowData, WebhookSourceVerifyData, }, router_request_types::{ revenue_recovery::{ @@ -67,14 +67,14 @@ pub use hyperswitch_domain_models::{ UasConfirmationRequestData, UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, - AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, - BrowserInformation, ChargeRefunds, ChargeRefundsOptions, CompleteAuthorizeData, - CompleteAuthorizeRedirectResponse, ConnectorCustomerData, CreateOrderRequestData, - DefendDisputeRequestData, DestinationChargeRefund, DirectChargeRefund, DisputeSyncData, - FetchDisputesRequestData, MandateRevokeRequestData, MultipleCaptureRequestData, - PaymentMethodTokenizationData, PaymentsApproveData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCancelPostCaptureData, PaymentsCaptureData, - PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, + AcceptDisputeRequestData, AccessTokenAuthenticationRequestData, AccessTokenRequestData, + AuthorizeSessionTokenData, BrowserInformation, ChargeRefunds, ChargeRefundsOptions, + CompleteAuthorizeData, CompleteAuthorizeRedirectResponse, ConnectorCustomerData, + CreateOrderRequestData, DefendDisputeRequestData, DestinationChargeRefund, + DirectChargeRefund, DisputeSyncData, FetchDisputesRequestData, MandateRevokeRequestData, + MultipleCaptureRequestData, PaymentMethodTokenizationData, PaymentsApproveData, + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCancelPostCaptureData, + PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, PaymentsUpdateMetadataData, RefundsData, ResponseId, RetrieveFileRequestData, @@ -118,15 +118,14 @@ pub use hyperswitch_interfaces::{ }, }; +#[cfg(feature = "v2")] +use crate::core::errors; pub use crate::core::payments::CustomerDetails; #[cfg(feature = "payouts")] use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW; use crate::{ consts, - core::{ - errors::{self}, - payments::{OperationSessionGetters, PaymentData}, - }, + core::payments::{OperationSessionGetters, PaymentData}, services, types::transformers::{ForeignFrom, ForeignTryFrom}, }; @@ -1133,34 +1132,6 @@ pub struct ConnectorsList { pub connectors: Vec, } -impl ForeignTryFrom for AccessTokenRequestData { - type Error = errors::ApiErrorResponse; - fn foreign_try_from(connector_auth: ConnectorAuthType) -> Result { - match connector_auth { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - app_id: api_key, - id: None, - }), - ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { - app_id: api_key, - id: Some(key1), - }), - ConnectorAuthType::SignatureKey { api_key, key1, .. } => Ok(Self { - app_id: api_key, - id: Some(key1), - }), - ConnectorAuthType::MultiAuthKey { api_key, key1, .. } => Ok(Self { - app_id: api_key, - id: Some(key1), - }), - - _ => Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "connector_account_details", - }), - } - } -} - impl ForeignFrom<&PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { fn foreign_from(data: &PaymentsAuthorizeRouterData) -> Self { Self { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 156bb21d0b..01f872e6f3 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -43,7 +43,8 @@ use api_models::routing::{self as api_routing, RoutableConnectorChoice}; use common_enums::RoutableConnectors; use error_stack::ResultExt; pub use hyperswitch_domain_models::router_flow_types::{ - access_token_auth::AccessTokenAuth, mandate_revoke::MandateRevoke, + access_token_auth::{AccessTokenAuth, AccessTokenAuthentication}, + mandate_revoke::MandateRevoke, webhooks::VerifyWebhookSource, }; pub use hyperswitch_interfaces::{ @@ -62,7 +63,8 @@ pub use hyperswitch_interfaces::{ RevenueRecovery, RevenueRecoveryRecordBack, }, revenue_recovery_v2::RevenueRecoveryV2, - BoxedConnector, Connector, ConnectorAccessToken, ConnectorAccessTokenV2, ConnectorCommon, + BoxedConnector, Connector, ConnectorAccessToken, ConnectorAccessTokenV2, + ConnectorAuthenticationToken, ConnectorAuthenticationTokenV2, ConnectorCommon, ConnectorCommonExt, ConnectorMandateRevoke, ConnectorMandateRevokeV2, ConnectorTransactionId, ConnectorVerifyWebhookSource, ConnectorVerifyWebhookSourceV2, CurrencyUnit, diff --git a/crates/router/src/types/api/connector_mapping.rs b/crates/router/src/types/api/connector_mapping.rs index 2bd3bf3314..580ac62062 100644 --- a/crates/router/src/types/api/connector_mapping.rs +++ b/crates/router/src/types/api/connector_mapping.rs @@ -312,7 +312,9 @@ impl ConnectorData { Ok(ConnectorEnum::Old(Box::new(connector::Nomupay::new()))) } enums::Connector::Noon => Ok(ConnectorEnum::Old(Box::new(connector::Noon::new()))), - // enums::Connector::Nordea => Ok(ConnectorEnum::Old(Box::new(connector::Nordea::new()))), + enums::Connector::Nordea => { + Ok(ConnectorEnum::Old(Box::new(connector::Nordea::new()))) + } enums::Connector::Novalnet => { Ok(ConnectorEnum::Old(Box::new(connector::Novalnet::new()))) } diff --git a/crates/router/src/types/connector_transformers.rs b/crates/router/src/types/connector_transformers.rs index ddb8fc333c..4cf4bad808 100644 --- a/crates/router/src/types/connector_transformers.rs +++ b/crates/router/src/types/connector_transformers.rs @@ -99,7 +99,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Nmi => Self::Nmi, api_enums::Connector::Nomupay => Self::Nomupay, api_enums::Connector::Noon => Self::Noon, - // api_enums::Connector::Nordea => Self::Nordea, + api_enums::Connector::Nordea => Self::Nordea, api_enums::Connector::Novalnet => Self::Novalnet, api_enums::Connector::Nuvei => Self::Nuvei, api_enums::Connector::Opennode => Self::Opennode, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 625cdb5017..00784b49c1 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -289,6 +289,7 @@ api_key = "API Key" [nordea] api_key = "Client Secret" key1 = "Client ID" +api_secret = "eIDAS Private Key" [novalnet] api_key="API Key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 3829fc17eb..22a44919f8 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -81,7 +81,7 @@ pub struct ConnectorAuthentication { pub nexixpay: Option, pub nomupay: Option, pub noon: Option, - pub nordea: Option, + pub nordea: Option, pub novalnet: Option, pub nmi: Option, pub nuvei: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index adb2d4a249..41954a9f8b 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -591,6 +591,9 @@ paypal = { currency = "AUD,BRL,CAD,CHF,CNY,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,M credit = { country = "US, CA", currency = "CAD,USD" } debit = { country = "US, CA", currency = "CAD,USD" } +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } apple_pay = { country = "MY", currency = "MYR" }