From 95f2e0b8c51bfe116241fc486069e10e578a5ff8 Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 5 Nov 2024 19:04:59 +0530 Subject: [PATCH 1/5] feat(nix): add support for running external services through services-flake (#6377) --- .gitignore | 7 ++++++- docs/try_local_system.md | 43 ++++++++++++++++++++++++++++++++++++++++ flake.lock | 40 +++++++++++++++++++++++++++++++++---- flake.nix | 38 +++++++++++++++++++++++++++++++++-- 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 1aa3faf2c1d1..1209263db3c8 100644 --- a/.gitignore +++ b/.gitignore @@ -263,4 +263,9 @@ result* node_modules/ # cypress credentials -creds.json \ No newline at end of file +creds.json + +/.direnv + +# Nix services data +/data \ No newline at end of file diff --git a/docs/try_local_system.md b/docs/try_local_system.md index a9c5e81138c3..07756463e740 100644 --- a/docs/try_local_system.md +++ b/docs/try_local_system.md @@ -15,6 +15,10 @@ Check the Table Of Contents to jump to the relevant section. - [Run hyperswitch using Docker Compose](#run-hyperswitch-using-docker-compose) - [Running additional services](#running-additional-services) - [Set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose) +- [Set up a Nix development environment](#set-up-a-nix-development-environment) + - [Install Nix](#install-nix) + - [Using external services through Nix](#using-external-services-through-nix) + - [Develop in a Nix environment (coming soon)](#develop-in-a-nix-environment-coming-soon) - [Set up a Rust environment and other dependencies](#set-up-a-rust-environment-and-other-dependencies) - [Set up dependencies on Ubuntu-based systems](#set-up-dependencies-on-ubuntu-based-systems) - [Set up dependencies on Windows (Ubuntu on WSL2)](#set-up-dependencies-on-windows-ubuntu-on-wsl2) @@ -166,6 +170,43 @@ Once the services have been confirmed to be up and running, you can proceed with If the command returned a `200 OK` status code, proceed with [trying out our APIs](#try-out-our-apis). +## Set up a Nix development environment + +A Nix development environment simplifies the setup of required project dependencies. This is available for MacOS, Linux and WSL2 users. + +### Install nix + +We recommend that you install Nix using [the DetSys nix-installer][detsys-nixos-installer], which automatically enables flakes. + +As an **optional** next step, if you are interested in using Nix to manage your dotfiles and local packages, you can setup [nixos-unified-template][nixos-unified-template-repo]. + +### Using external services through Nix + +Once Nix is installed, you can use it to manage external services via `flakes`. More services will be added soon. + +- Run below command in hyperswitch directory + + ```shell + nix run .#ext-services + ``` + +This will start the following services using `process-compose` +- PostgreSQL + - Creates database and an user to be used by the application +- Redis + +### Develop in a Nix environment (coming soon) + +Nix development environment ensures all the required project dependencies, including both the tools and services are readily available, eliminating the need for manual setup. + +Run below command in hyperswitch directory + + ```shell + nix develop + ``` + +**NOTE:** This is a work in progress, and only a selected commands are available at the moment. Look in `flake.nix` (hyperswitch-shell) for a full list of packages. + ## Set up a Rust environment and other dependencies If you are using `nix`, please skip the setup dependencies step and jump to @@ -681,3 +722,5 @@ To explore more of our APIs, please check the remaining folders in the [refunds-create]: https://www.postman.com/hyperswitch/workspace/hyperswitch-development/request/25176162-4d1315c6-ac61-4411-8f7d-15d4e4e736a1 [refunds-retrieve]: https://www.postman.com/hyperswitch/workspace/hyperswitch-development/request/25176162-137d6260-24f7-4752-9e69-26b61b83df0d [connector-specific-details]: https://docs.google.com/spreadsheets/d/e/2PACX-1vQWHLza9m5iO4Ol-tEBx22_Nnq8Mb3ISCWI53nrinIGLK8eHYmHGnvXFXUXEut8AFyGyI9DipsYaBLG/pubhtml?gid=748960791&single=true +[detsys-nixos-installer]: https://nixos.asia/en/install +[nixos-unified-template-repo]: https://github.com/juspay/nixos-unified-template#on-non-nixos diff --git a/flake.lock b/flake.lock index 6f9551084751..6bdae435765b 100644 --- a/flake.lock +++ b/flake.lock @@ -107,11 +107,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1676569297, - "narHash": "sha256-2n4C4H3/U+3YbDrQB6xIw7AaLdFISCCFwOkcETAigqU=", + "lastModified": 1728888510, + "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ac1f5b72a9e95873d1de0233fddcb56f99884b37", + "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c", "type": "github" }, "original": { @@ -137,12 +137,29 @@ "type": "github" } }, + "process-compose-flake": { + "locked": { + "lastModified": 1728868941, + "narHash": "sha256-yEMzxZfy+EE9gSqn++SyZeAVHXYupFT8Wyf99Z/CXXU=", + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "rev": "29301aec92d73c9b075fcfd06a6fb18665bfe6b5", + "type": "github" + }, + "original": { + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "type": "github" + } + }, "root": { "inputs": { "cargo2nix": "cargo2nix", "flake-parts": "flake-parts", "nixpkgs": "nixpkgs_2", - "rust-overlay": "rust-overlay_2" + "process-compose-flake": "process-compose-flake", + "rust-overlay": "rust-overlay_2", + "services-flake": "services-flake" } }, "rust-overlay": { @@ -187,6 +204,21 @@ "repo": "rust-overlay", "type": "github" } + }, + "services-flake": { + "locked": { + "lastModified": 1728811751, + "narHash": "sha256-IrwycNtt6jxJGCi+QJ8Bbzt9flg0vNeGLAR0KBbj4a8=", + "owner": "juspay", + "repo": "services-flake", + "rev": "e9f663036f3b1b1a12b0f136628ef93a8be92443", + "type": "github" + }, + "original": { + "owner": "juspay", + "repo": "services-flake", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ad3de7e660b1..9516d0c81a7a 100644 --- a/flake.nix +++ b/flake.nix @@ -8,10 +8,14 @@ # TODO: Move away from these to https://github.com/juspay/rust-flake cargo2nix.url = "github:cargo2nix/cargo2nix/release-0.11.0"; rust-overlay.url = "github:oxalica/rust-overlay"; + + process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; + services-flake.url = "github:juspay/services-flake"; }; outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.process-compose-flake.flakeModule ]; systems = inputs.nixpkgs.lib.systems.flakeExposed; perSystem = { self', pkgs, lib, system, ... }: let @@ -27,10 +31,10 @@ devShells.default = pkgs.mkShell { name = "hyperswitch-shell"; packages = with pkgs; [ + just + nixd openssl pkg-config - exa - fd rust-bin.stable.${rustVersion}.default ] ++ lib.optionals stdenv.isDarwin [ # arch might have issue finding these libs. @@ -38,6 +42,36 @@ frameworks.Foundation ]; }; + + /* For running external services + - Redis + - Postgres + */ + process-compose."ext-services" = + let + developmentToml = lib.importTOML ./config/development.toml; + databaseName = developmentToml.master_database.dbname; + databaseUser = developmentToml.master_database.username; + databasePass = developmentToml.master_database.password; + in + { + imports = [ inputs.services-flake.processComposeModules.default ]; + services.redis."r1".enable = true; + /* Postgres + - Create an user and grant all privileges + - Create a database + */ + services.postgres."p1" = { + enable = true; + initialScript = { + before = "CREATE USER ${databaseUser} WITH PASSWORD '${databasePass}' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;"; + after = "GRANT ALL PRIVILEGES ON DATABASE ${databaseName} to ${databaseUser};"; + }; + initialDatabases = [ + { name = databaseName; } + ]; + }; + }; }; }; } From 6b66cccd02c2589bb2dad38b46f4da7e1455ca0b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:13:11 +0530 Subject: [PATCH 2/5] feat(users): Add `force_two_factor_auth` environment variable (#6466) --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 3 ++- config/deployments/production.toml | 3 ++- config/deployments/sandbox.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/api_models/src/user.rs | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/core/user.rs | 8 ++++++-- loadtest/config/development.toml | 1 + 10 files changed, 17 insertions(+), 4 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index b1189b51faee..519429123e1d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -396,6 +396,7 @@ password_validity_in_days = 90 # Number of days after which password shoul two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should be done again if doing update/change from inside totp_issuer_name = "Hyperswitch" # Name of the issuer for TOTP base_url = "" # Base url used for user specific redirects and emails +force_two_factor_auth = false # Whether to force two factor authentication for all users #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 5228f435fcd9..529506f53ca2 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -139,6 +139,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Integ" base_url = "https://integ.hyperswitch.io" +force_two_factor_auth = false [frm] enabled = true @@ -395,4 +396,4 @@ connector_list = "" card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] -connector_list = "cybersource" \ No newline at end of file +connector_list = "cybersource" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index ed2430b266a5..61a465a8fb7e 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -146,6 +146,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Production" base_url = "https://live.hyperswitch.io" +force_two_factor_auth = false [frm] enabled = false @@ -409,4 +410,4 @@ connector_list = "" card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] -connector_list = "cybersource" \ No newline at end of file +connector_list = "cybersource" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 3c39f31faecf..fb7f8c2e67cf 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -146,6 +146,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Sandbox" base_url = "https://app.hyperswitch.io" +force_two_factor_auth = false [frm] enabled = true diff --git a/config/development.toml b/config/development.toml index ca4dcb9529ec..085245562836 100644 --- a/config/development.toml +++ b/config/development.toml @@ -318,6 +318,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Dev" base_url = "http://localhost:8080" +force_two_factor_auth = false [bank_config.eps] stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index b84c3dacd9c3..37d621665bd7 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -56,6 +56,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch" base_url = "http://localhost:8080" +force_two_factor_auth = false [locker] host = "" diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 089089038b8a..089426c68ba6 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -211,6 +211,7 @@ pub struct TwoFactorAuthStatusResponseWithAttempts { #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct TwoFactorStatus { pub status: Option, + pub is_skippable: bool, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 61e026ae2c57..f675aad11a72 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -556,6 +556,7 @@ pub struct UserSettings { pub two_factor_auth_expiry_in_secs: i64, pub totp_issuer_name: String, pub base_url: String, + pub force_two_factor_auth: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 822c29b21d99..35b26926ed2b 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1319,7 +1319,7 @@ pub async fn list_user_roles_details( )) .await .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to construct proifle map")? + .attach_printable("Failed to construct profile map")? .into_iter() .map(|profile| (profile.get_id().to_owned(), profile.profile_name)) .collect::>(); @@ -1927,7 +1927,7 @@ pub async fn terminate_two_factor_auth( .change_context(UserErrors::InternalServerError)? .into(); - if !skip_two_factor_auth { + if state.conf.user.force_two_factor_auth || !skip_two_factor_auth { if !tfa_utils::check_totp_in_redis(&state, &user_token.user_id).await? && !tfa_utils::check_recovery_code_in_redis(&state, &user_token.user_id).await? { @@ -1997,9 +1997,12 @@ pub async fn check_two_factor_auth_status_with_attempts( .await .change_context(UserErrors::InternalServerError)? .into(); + + let is_skippable = state.conf.user.force_two_factor_auth.not(); if user_from_db.get_totp_status() == TotpStatus::NotSet { return Ok(ApplicationResponse::Json(user_api::TwoFactorStatus { status: None, + is_skippable, })); }; @@ -2018,6 +2021,7 @@ pub async fn check_two_factor_auth_status_with_attempts( totp, recovery_code, }), + is_skippable, })) } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index be7d71314ac7..b0e2cac9b269 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -35,6 +35,7 @@ jwt_secret = "secret" password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch" +force_two_factor_auth = false [locker] host = "" From b048e39b5c4213752da7765834915cca6bf776f6 Mon Sep 17 00:00:00 2001 From: Sayak Bhattacharya Date: Tue, 5 Nov 2024 20:46:26 +0530 Subject: [PATCH 3/5] Feat(connector): [JP MORGAN] Added Template code for cards integration (#6467) Co-authored-by: Sayak Bhattacharya Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/connector_enums.rs | 2 + crates/common_enums/src/connector_enums.rs | 1 + .../hyperswitch_connectors/src/connectors.rs | 9 +- .../src/connectors/jpmorgan.rs | 568 ++++++++++++++++++ .../src/connectors/jpmorgan/transformers.rs | 228 +++++++ .../src/default_implementations.rs | 31 + .../src/default_implementations_v2.rs | 22 + crates/hyperswitch_interfaces/src/configs.rs | 1 + crates/router/src/connector.rs | 14 +- .../connector_integration_v2_impls.rs | 3 + crates/router/src/core/payments/flows.rs | 4 + crates/router/src/types/api.rs | 1 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/jpmorgan.rs | 427 +++++++++++++ crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 2 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 25 files changed, 1316 insertions(+), 12 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/jpmorgan.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs create mode 100644 crates/router/tests/connectors/jpmorgan.rs diff --git a/config/config.example.toml b/config/config.example.toml index 519429123e1d..3a4a2c271b01 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -220,6 +220,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 529506f53ca2..ebc1e36d1591 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -62,6 +62,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 61a465a8fb7e..ef1ee42b9e67 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -66,6 +66,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://iata-pay.iata.org/api/v1" itaubank.base_url = "https://secure.api.itau/" +jpmorgan.base_url = "https://api-ms.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.klarna.com/" mifinity.base_url = "https://secure.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index fb7f8c2e67cf..a09dba1a9a66 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -66,6 +66,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/development.toml b/config/development.toml index 085245562836..46d6378f0832 100644 --- a/config/development.toml +++ b/config/development.toml @@ -130,6 +130,7 @@ cards = [ "helcim", "iatapay", "itaubank", + "jpmorgan", "mollie", "multisafepay", "netcetera", @@ -231,6 +232,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 37d621665bd7..5a2e7249fda4 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -150,6 +150,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -245,6 +246,7 @@ cards = [ "helcim", "iatapay", "itaubank", + "jpmorgan", "mollie", "multisafepay", "netcetera", diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 5803800f4df0..68fd73cd0de0 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -84,6 +84,7 @@ pub enum Connector { Helcim, Iatapay, Itaubank, + //Jpmorgan, Klarna, Mifinity, Mollie, @@ -221,6 +222,7 @@ impl Connector { | Self::Helcim | Self::Iatapay | Self::Itaubank + //| Self::Jpmorgan | Self::Klarna | Self::Mifinity | Self::Mollie diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index a5f6d0fe356c..386b4c35a4b7 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -80,6 +80,7 @@ pub enum RoutableConnectors { Helcim, Iatapay, Itaubank, + //Jpmorgan, Klarna, Mifinity, Mollie, diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index bd9efa57bc60..fb29417eaa08 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -15,6 +15,7 @@ pub mod fiuu; pub mod forte; pub mod globepay; pub mod helcim; +pub mod jpmorgan; pub mod mollie; pub mod multisafepay; pub mod nexinets; @@ -41,8 +42,8 @@ pub use self::{ cashtocode::Cashtocode, coinbase::Coinbase, cryptopay::Cryptopay, deutschebank::Deutschebank, digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, fiserv::Fiserv, fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, helcim::Helcim, - mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nexixpay::Nexixpay, - novalnet::Novalnet, payeezy::Payeezy, payu::Payu, powertranz::Powertranz, razorpay::Razorpay, - shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, tsys::Tsys, - volt::Volt, worldline::Worldline, worldpay::Worldpay, zen::Zen, zsl::Zsl, + jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, + nexixpay::Nexixpay, novalnet::Novalnet, payeezy::Payeezy, payu::Payu, powertranz::Powertranz, + razorpay::Razorpay, shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, + tsys::Tsys, volt::Volt, worldline::Worldline, worldpay::Worldpay, zen::Zen, zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs new file mode 100644 index 000000000000..6095a53ad00b --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs @@ -0,0 +1,568 @@ +pub mod transformers; + +use common_utils::{ + errors::CustomResult, + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, + types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_flow_types::{ + access_token_auth::AccessTokenAuth, + payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + refunds::{Execute, RSync}, + }, + router_request_types::{ + AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + RefundsData, SetupMandateRequestData, + }, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, +}; +// +use hyperswitch_interfaces::{ + api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, + configs::Connectors, + errors, + events::connector_api_logs::ConnectorEvent, + types::{self, Response}, + webhooks, +}; +use masking::{ExposeInterface, Mask}; +use transformers as jpmorgan; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Jpmorgan { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Jpmorgan { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Jpmorgan {} +impl api::PaymentSession for Jpmorgan {} +impl api::ConnectorAccessToken for Jpmorgan {} +impl api::MandateSetup for Jpmorgan {} +impl api::PaymentAuthorize for Jpmorgan {} +impl api::PaymentSync for Jpmorgan {} +impl api::PaymentCapture for Jpmorgan {} +impl api::PaymentVoid for Jpmorgan {} +impl api::Refund for Jpmorgan {} +impl api::RefundExecute for Jpmorgan {} +impl api::RefundSync for Jpmorgan {} +impl api::PaymentToken for Jpmorgan {} + +impl ConnectorIntegration + for Jpmorgan +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Jpmorgan +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _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) + } +} + +impl ConnectorCommon for Jpmorgan { + fn id(&self) -> &'static str { + "jpmorgan" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + //todo!() + // TODO! Check connector documentation, on which unit they are processing the currency. + // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, + // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.jpmorgan.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = jpmorgan::JpmorganAuthType::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: jpmorgan::JpmorganErrorResponse = res + .response + .parse_struct("JpmorganErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Jpmorgan { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Jpmorgan { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Jpmorgan {} + +impl ConnectorIntegration + for Jpmorgan +{ +} + +impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &PaymentsAuthorizeRouterData, + 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: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("Jpmorgan PaymentsAuthorizeResponse") + .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 Jpmorgan { + fn get_headers( + &self, + req: &PaymentsSyncRouterData, + 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: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("jpmorgan PaymentsSyncResponse") + .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 Jpmorgan { + 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, + ) -> 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: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("Jpmorgan 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) + } +} + +impl ConnectorIntegration for Jpmorgan {} + +impl ConnectorIntegration for Jpmorgan { + 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( + &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 = jpmorgan::JpmorganRouterData::from((refund_amount, req)); + let connector_req = jpmorgan::JpmorganRefundRequest::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: jpmorgan::RefundResponse = res + .response + .parse_struct("jpmorgan 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) + } +} + +impl ConnectorIntegration for Jpmorgan { + 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: jpmorgan::RefundResponse = res + .response + .parse_struct("jpmorgan 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) + } +} + +#[async_trait::async_trait] +impl webhooks::IncomingWebhook for Jpmorgan { + fn get_webhook_object_reference_id( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs new file mode 100644 index 000000000000..90b058697051 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs @@ -0,0 +1,228 @@ +use common_enums::enums; +use common_utils::types::StringMinorUnit; +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}, +}; +use hyperswitch_interfaces::errors; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + types::{RefundsResponseRouterData, ResponseRouterData}, + utils::PaymentsAuthorizeRequestData, +}; + +//TODO: Fill the struct with respective fields +pub struct JpmorganRouterData { + pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl From<(StringMinorUnit, T)> for JpmorganRouterData { + fn from((amount, item): (StringMinorUnit, T)) -> Self { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Self { + amount, + router_data: item, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct JpmorganPaymentsRequest { + amount: StringMinorUnit, + card: JpmorganCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct JpmorganCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &JpmorganRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = JpmorganCard { + 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 +pub struct JpmorganAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for JpmorganAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.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 JpmorganPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: JpmorganPaymentStatus) -> Self { + match item { + JpmorganPaymentStatus::Succeeded => Self::Charged, + JpmorganPaymentStatus::Failed => Self::Failure, + JpmorganPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct JpmorganPaymentsResponse { + status: JpmorganPaymentStatus, + id: String, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + 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, + charge_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct JpmorganRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &JpmorganRouterData<&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 + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +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), + }), + ..item.data + }) + } +} + +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), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct JpmorganErrorResponse { + 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 31071bead445..fe866d2af7c9 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -106,6 +106,7 @@ default_imp_for_authorize_session_token!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -159,6 +160,7 @@ default_imp_for_calculate_tax!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Mollie, connectors::Multisafepay, connectors::Nexinets, @@ -209,6 +211,7 @@ default_imp_for_session_update!( connectors::Fiservemea, connectors::Forte, connectors::Helcim, + connectors::Jpmorgan, connectors::Razorpay, connectors::Shift4, connectors::Stax, @@ -264,6 +267,7 @@ default_imp_for_post_session_tokens!( connectors::Fiservemea, connectors::Forte, connectors::Helcim, + connectors::Jpmorgan, connectors::Razorpay, connectors::Shift4, connectors::Stax, @@ -319,6 +323,7 @@ default_imp_for_complete_authorize!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Multisafepay, connectors::Novalnet, connectors::Nexinets, @@ -369,6 +374,7 @@ default_imp_for_incremental_authorization!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -424,6 +430,7 @@ default_imp_for_create_customer!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Mollie, connectors::Multisafepay, connectors::Novalnet, @@ -478,6 +485,7 @@ default_imp_for_connector_redirect_response!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Multisafepay, connectors::Nexinets, connectors::Nexixpay, @@ -528,6 +536,7 @@ default_imp_for_pre_processing_steps!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Payeezy, @@ -581,6 +590,7 @@ default_imp_for_post_processing_steps!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -636,6 +646,7 @@ default_imp_for_approve!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -691,6 +702,7 @@ default_imp_for_reject!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -746,6 +758,7 @@ default_imp_for_webhook_source_verification!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -802,6 +815,7 @@ default_imp_for_accept_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -857,6 +871,7 @@ default_imp_for_submit_evidence!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -911,6 +926,7 @@ default_imp_for_defend_dispute!( connectors::Fiuu, connectors::Forte, connectors::Globepay, + connectors::Jpmorgan, connectors::Helcim, connectors::Novalnet, connectors::Nexinets, @@ -976,6 +992,7 @@ default_imp_for_file_upload!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1033,6 +1050,7 @@ default_imp_for_payouts_create!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1090,6 +1108,7 @@ default_imp_for_payouts_retrieve!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1147,6 +1166,7 @@ default_imp_for_payouts_eligibility!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1204,6 +1224,7 @@ default_imp_for_payouts_fulfill!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1261,6 +1282,7 @@ default_imp_for_payouts_cancel!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1318,6 +1340,7 @@ default_imp_for_payouts_quote!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1375,6 +1398,7 @@ default_imp_for_payouts_recipient!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1432,6 +1456,7 @@ default_imp_for_payouts_recipient_account!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1489,6 +1514,7 @@ default_imp_for_frm_sale!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1546,6 +1572,7 @@ default_imp_for_frm_checkout!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1603,6 +1630,7 @@ default_imp_for_frm_transaction!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1660,6 +1688,7 @@ default_imp_for_frm_fulfillment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1717,6 +1746,7 @@ default_imp_for_frm_record_return!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1771,6 +1801,7 @@ default_imp_for_revoking_mandates!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 04af32989338..47cf6b9c1e40 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -222,6 +222,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -278,6 +279,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -329,6 +331,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -386,6 +389,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -442,6 +446,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -498,6 +503,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -564,6 +570,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -622,6 +629,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -680,6 +688,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -738,6 +747,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -796,6 +806,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -854,6 +865,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -912,6 +924,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -970,6 +983,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1028,6 +1042,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1084,6 +1099,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1142,6 +1158,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1200,6 +1217,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1258,6 +1276,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1316,6 +1335,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1374,6 +1394,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, @@ -1429,6 +1450,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Jpmorgan, connectors::Novalnet, connectors::Nexinets, connectors::Nexixpay, diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index fd09251699be..2c631849446d 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -47,6 +47,7 @@ pub struct Connectors { pub helcim: ConnectorParams, pub iatapay: ConnectorParams, pub itaubank: ConnectorParams, + pub jpmorgan: ConnectorParams, pub klarna: ConnectorParams, pub mifinity: ConnectorParams, pub mollie: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 899e5f2b15c2..c35856354de0 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -50,13 +50,13 @@ pub use hyperswitch_connectors::connectors::{ cryptopay, cryptopay::Cryptopay, deutschebank, deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, fiuu::Fiuu, forte, forte::Forte, - globepay, globepay::Globepay, helcim, helcim::Helcim, mollie, mollie::Mollie, multisafepay, - multisafepay::Multisafepay, nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, - novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, payu::Payu, powertranz, - powertranz::Powertranz, razorpay, razorpay::Razorpay, shift4, shift4::Shift4, square, - square::Square, stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, thunes::Thunes, tsys, - tsys::Tsys, volt, volt::Volt, worldline, worldline::Worldline, worldpay, worldpay::Worldpay, - zen, zen::Zen, zsl, zsl::Zsl, + globepay, globepay::Globepay, helcim, helcim::Helcim, jpmorgan, jpmorgan::Jpmorgan, mollie, + mollie::Mollie, multisafepay, multisafepay::Multisafepay, nexinets, nexinets::Nexinets, + nexixpay, nexixpay::Nexixpay, novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, + payu::Payu, powertranz, powertranz::Powertranz, razorpay, razorpay::Razorpay, shift4, + shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, + thunes::Thunes, tsys, tsys::Tsys, volt, volt::Volt, worldline, worldline::Worldline, worldpay, + worldpay::Worldpay, zen, zen::Zen, zsl, zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index 6ec9cd7c9321..10ed62c70c53 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1142,6 +1142,7 @@ default_imp_for_new_connector_integration_payouts!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, @@ -1786,6 +1787,7 @@ default_imp_for_new_connector_integration_frm!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, @@ -2278,6 +2280,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 5597ef413899..b28f39a51de1 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -482,6 +482,7 @@ default_imp_for_connector_request_id!( connector::Gpayments, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, @@ -1006,6 +1007,7 @@ default_imp_for_payouts!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, @@ -1801,6 +1803,7 @@ default_imp_for_fraud_check!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, @@ -2461,6 +2464,7 @@ default_imp_for_connector_authentication!( connector::Helcim, connector::Iatapay, connector::Itaubank, + connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Mollie, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 428506d6c8ab..fbc55dce4445 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -429,6 +429,7 @@ impl ConnectorData { Ok(ConnectorEnum::Old(Box::new(connector::Iatapay::new()))) } enums::Connector::Itaubank => { + //enums::Connector::Jpmorgan => Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan))), Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) } enums::Connector::Klarna => { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 8e5f0944997c..bc72431bd9c8 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -281,6 +281,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Helcim => Self::Helcim, api_enums::Connector::Iatapay => Self::Iatapay, api_enums::Connector::Itaubank => Self::Itaubank, + //api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, api_enums::Connector::Mifinity => Self::Mifinity, api_enums::Connector::Mollie => Self::Mollie, diff --git a/crates/router/tests/connectors/jpmorgan.rs b/crates/router/tests/connectors/jpmorgan.rs new file mode 100644 index 000000000000..9e364a3bbb61 --- /dev/null +++ b/crates/router/tests/connectors/jpmorgan.rs @@ -0,0 +1,427 @@ +use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData}; +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct JpmorganTest; +impl JpmorganTest { + #[allow(dead_code)] + fn new() -> Self { + Self + } +} +impl ConnectorActions for JpmorganTest {} +impl utils::Connector for JpmorganTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Jpmorgan; + utils::construct_connector_data_old( + Box::new(Jpmorgan::new()), + types::Connector::Plaid, + api::GetToken::Connector, + None, + ) + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .jpmorgan + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "jpmorgan".to_string() + } +} + +static CONNECTOR: JpmorganTest = JpmorganTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenarios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 24d1de432f95..c2a0d75411be 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -43,6 +43,7 @@ mod gpayments; mod helcim; mod iatapay; mod itaubank; +mod jpmorgan; mod mifinity; mod mollie; mod multisafepay; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 6ec2ec5a1dcf..caaafcdb6cf5 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -284,6 +284,8 @@ api_secret = "Client Key" [thunes] api_key="API Key" +[jpmorgan] +api_key="API Key" [elavon] api_key="API Key" \ No newline at end of file diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 08aa09599ca4..7508c76048c8 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -49,6 +49,7 @@ pub struct ConnectorAuthentication { pub helcim: Option, pub iatapay: Option, pub itaubank: Option, + pub jpmorgan: Option, pub mifinity: Option, pub mollie: Option, pub multisafepay: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index b0e2cac9b269..723498f7e7a1 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -116,6 +116,7 @@ gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayment helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" +jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -211,6 +212,7 @@ cards = [ "helcim", "iatapay", "itaubank", + "jpmorgan", "mollie", "multisafepay", "netcetera", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index f7609ff92922..7e19844ad4d2 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank klarna mifinity mollie multisafepay netcetera nexinets nexixpay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay zsl "$1") + connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res="$(echo ${sorted[@]})" sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From ae4df051cc4b6445946da91795eebbf8d5890815 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:20:42 +0000 Subject: [PATCH 4/5] chore(version): 2024.11.06.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee4d40834bf..8d5eb5d0517b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.06.0 + +### Features + +- **config:** Update vector config ([#6365](https://github.com/juspay/hyperswitch/pull/6365)) ([`2919db8`](https://github.com/juspay/hyperswitch/commit/2919db874bd84372663228f2531ba18338e039c0)) +- **connector:** + - [ELAVON] Template PR ([#6309](https://github.com/juspay/hyperswitch/pull/6309)) ([`b481e5c`](https://github.com/juspay/hyperswitch/commit/b481e5cb8ffe417591a2fb917f37ba72667f2fcd)) + - [Paypal] implement vaulting for paypal wallet and cards while purchasing ([#5323](https://github.com/juspay/hyperswitch/pull/5323)) ([`22ba2db`](https://github.com/juspay/hyperswitch/commit/22ba2dbb2870471315d688147b3b53c432ce15dc)) + - [JP MORGAN] Added Template code for cards integration ([#6467](https://github.com/juspay/hyperswitch/pull/6467)) ([`b048e39`](https://github.com/juspay/hyperswitch/commit/b048e39b5c4213752da7765834915cca6bf776f6)) +- **db:** Implement `MerchantAccountInteraface` for `Mockdb` ([#6283](https://github.com/juspay/hyperswitch/pull/6283)) ([`5f493a5`](https://github.com/juspay/hyperswitch/commit/5f493a5166aa0a0a29f9aed538cad03def657c22)) +- **nix:** Add support for running external services through services-flake ([#6377](https://github.com/juspay/hyperswitch/pull/6377)) ([`95f2e0b`](https://github.com/juspay/hyperswitch/commit/95f2e0b8c51bfe116241fc486069e10e578a5ff8)) +- **users:** Add `force_two_factor_auth` environment variable ([#6466](https://github.com/juspay/hyperswitch/pull/6466)) ([`6b66ccc`](https://github.com/juspay/hyperswitch/commit/6b66cccd02c2589bb2dad38b46f4da7e1455ca0b)) + +### Bug Fixes + +- **connector:** + - Expiration Year Incorrectly Populated as YYYY Format in Paybox Mandates ([#6474](https://github.com/juspay/hyperswitch/pull/6474)) ([`e457ccd`](https://github.com/juspay/hyperswitch/commit/e457ccd91e60d5168e0a3283dfa325097f455076)) + - [Cybersource] remove newline in billing address with space ([#6478](https://github.com/juspay/hyperswitch/pull/6478)) ([`7f1d345`](https://github.com/juspay/hyperswitch/commit/7f1d34571f72f63b8bb52aff995ad093e3b6d856)) +- **refunds:** Remove to schema from refund aggregate response and exclude it from open api documentation ([#6405](https://github.com/juspay/hyperswitch/pull/6405)) ([`449c9cf`](https://github.com/juspay/hyperswitch/commit/449c9cfe557b3540e4ad25e48e012b531eb232fd)) +- Replace deprecated backticks with $(...) for command substitution ([#6337](https://github.com/juspay/hyperswitch/pull/6337)) ([`1c92f58`](https://github.com/juspay/hyperswitch/commit/1c92f5843009db42778f94bc9fd915b411a93f76)) +- Lazy connection pools for dynamic routing service ([#6437](https://github.com/juspay/hyperswitch/pull/6437)) ([`71d9933`](https://github.com/juspay/hyperswitch/commit/71d99332204ddfbb3cf305c7d3bc8840d508bf47)) + +**Full Changelog:** [`2024.11.05.0...2024.11.06.0`](https://github.com/juspay/hyperswitch/compare/2024.11.05.0...2024.11.06.0) + +- - - + ## 2024.11.05.0 ### Features From 01c5216fdd6f1d841082868cccea6054b64e9e07 Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Wed, 6 Nov 2024 14:46:16 +0530 Subject: [PATCH 5/5] feat(analytics): implement currency conversion to power multi-currency aggregation (#6418) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 2 + crates/analytics/Cargo.toml | 2 + crates/analytics/src/errors.rs | 8 ++ .../src/payment_intents/accumulator.rs | 29 +++++- crates/analytics/src/payment_intents/core.rs | 94 ++++++++++++++++++- .../payment_processed_amount.rs | 7 +- .../smart_retried_amount.rs | 6 +- .../metrics/smart_retried_amount.rs | 7 +- crates/analytics/src/payments/accumulator.rs | 15 ++- crates/analytics/src/payments/core.rs | 50 +++++++++- .../metrics/payment_processed_amount.rs | 6 ++ .../payment_processed_amount.rs | 8 ++ crates/api_models/src/analytics.rs | 6 ++ .../src/analytics/payment_intents.rs | 4 + crates/api_models/src/analytics/payments.rs | 2 + crates/router/src/analytics.rs | 23 +++-- crates/router/src/core/currency.rs | 18 ++++ crates/router/src/utils/currency.rs | 10 +- 18 files changed, 273 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 760d2865ec2d..d232fcbf024c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "bigdecimal", "common_enums", "common_utils", + "currency_conversion", "diesel_models", "error-stack", "futures 0.3.30", @@ -363,6 +364,7 @@ dependencies = [ "opensearch", "reqwest 0.11.27", "router_env", + "rust_decimal", "serde", "serde_json", "sqlx", diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index 6cf886896c3c..34732c55e4e7 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -21,6 +21,7 @@ hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces" masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } #Third Party dependencies actix-web = "4.5.1" @@ -34,6 +35,7 @@ futures = "0.3.30" once_cell = "1.19.0" opensearch = { version = "2.2.0", features = ["aws-auth"] } reqwest = { version = "0.11.27", features = ["serde_json"] } +rust_decimal = "1.35" serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.115" sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "bigdecimal"] } diff --git a/crates/analytics/src/errors.rs b/crates/analytics/src/errors.rs index 0e39a4ddd928..d7b15a6db115 100644 --- a/crates/analytics/src/errors.rs +++ b/crates/analytics/src/errors.rs @@ -12,6 +12,8 @@ pub enum AnalyticsError { UnknownError, #[error("Access Forbidden Analytics Error")] AccessForbiddenError, + #[error("Failed to fetch currency exchange rate")] + ForexFetchFailed, } impl ErrorSwitch for AnalyticsError { @@ -32,6 +34,12 @@ impl ErrorSwitch for AnalyticsError { Self::AccessForbiddenError => { ApiErrorResponse::Unauthorized(ApiError::new("IR", 0, "Access Forbidden", None)) } + Self::ForexFetchFailed => ApiErrorResponse::InternalServerError(ApiError::new( + "HE", + 0, + "Failed to fetch currency exchange rate", + None, + )), } } } diff --git a/crates/analytics/src/payment_intents/accumulator.rs b/crates/analytics/src/payment_intents/accumulator.rs index cbb8335cea01..ef3cd3129c48 100644 --- a/crates/analytics/src/payment_intents/accumulator.rs +++ b/crates/analytics/src/payment_intents/accumulator.rs @@ -86,7 +86,7 @@ impl PaymentIntentMetricAccumulator for CountAccumulator { } impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator { - type MetricOutput = (Option, Option); + type MetricOutput = (Option, Option, Option, Option); #[inline] fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) { self.amount = match ( @@ -117,7 +117,7 @@ impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator { .amount_without_retries .and_then(|i| u64::try_from(i).ok()) .or(Some(0)); - (with_retries, without_retries) + (with_retries, without_retries, Some(0), Some(0)) } } @@ -185,7 +185,14 @@ impl PaymentIntentMetricAccumulator for PaymentsSuccessRateAccumulator { } impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator { - type MetricOutput = (Option, Option, Option, Option); + type MetricOutput = ( + Option, + Option, + Option, + Option, + Option, + Option, + ); #[inline] fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) { self.total_with_retries = match ( @@ -235,6 +242,8 @@ impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator { count_with_retries, total_without_retries, count_without_retries, + Some(0), + Some(0), ) } } @@ -301,13 +310,19 @@ impl PaymentIntentMetricsAccumulator { payments_success_rate, payments_success_rate_without_smart_retries, ) = self.payments_success_rate.collect(); - let (smart_retried_amount, smart_retried_amount_without_smart_retries) = - self.smart_retried_amount.collect(); + let ( + smart_retried_amount, + smart_retried_amount_without_smart_retries, + smart_retried_amount_in_usd, + smart_retried_amount_without_smart_retries_in_usd, + ) = self.smart_retried_amount.collect(); let ( payment_processed_amount, payment_processed_count, payment_processed_amount_without_smart_retries, payment_processed_count_without_smart_retries, + payment_processed_amount_in_usd, + payment_processed_amount_without_smart_retries_in_usd, ) = self.payment_processed_amount.collect(); let ( payments_success_rate_distribution_without_smart_retries, @@ -317,7 +332,9 @@ impl PaymentIntentMetricsAccumulator { successful_smart_retries: self.successful_smart_retries.collect(), total_smart_retries: self.total_smart_retries.collect(), smart_retried_amount, + smart_retried_amount_in_usd, smart_retried_amount_without_smart_retries, + smart_retried_amount_without_smart_retries_in_usd, payment_intent_count: self.payment_intent_count.collect(), successful_payments, successful_payments_without_smart_retries, @@ -330,6 +347,8 @@ impl PaymentIntentMetricsAccumulator { payment_processed_count_without_smart_retries, payments_success_rate_distribution_without_smart_retries, payments_failure_rate_distribution_without_smart_retries, + payment_processed_amount_in_usd, + payment_processed_amount_without_smart_retries_in_usd, } } } diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 3e8915c60a2a..7ea8e9007f7b 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -10,8 +10,10 @@ use api_models::analytics::{ PaymentIntentFiltersResponse, PaymentIntentsAnalyticsMetadata, PaymentIntentsMetricsResponse, SankeyResponse, }; -use common_enums::IntentStatus; +use bigdecimal::ToPrimitive; +use common_enums::{Currency, IntentStatus}; use common_utils::{errors::CustomResult, types::TimeRange}; +use currency_conversion::{conversion::convert, types::ExchangeRates}; use error_stack::ResultExt; use router_env::{ instrument, logger, @@ -120,6 +122,7 @@ pub async fn get_sankey( #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, + ex_rates: &ExchangeRates, auth: &AuthInfo, req: GetPaymentIntentMetricRequest, ) -> AnalyticsResult> { @@ -227,16 +230,20 @@ pub async fn get_metrics( let mut success = 0; let mut success_without_smart_retries = 0; let mut total_smart_retried_amount = 0; + let mut total_smart_retried_amount_in_usd = 0; let mut total_smart_retried_amount_without_smart_retries = 0; + let mut total_smart_retried_amount_without_smart_retries_in_usd = 0; let mut total = 0; let mut total_payment_processed_amount = 0; + let mut total_payment_processed_amount_in_usd = 0; let mut total_payment_processed_count = 0; let mut total_payment_processed_amount_without_smart_retries = 0; + let mut total_payment_processed_amount_without_smart_retries_in_usd = 0; let mut total_payment_processed_count_without_smart_retries = 0; let query_data: Vec = metrics_accumulator .into_iter() .map(|(id, val)| { - let collected_values = val.collect(); + let mut collected_values = val.collect(); if let Some(success_count) = collected_values.successful_payments { success += success_count; } @@ -248,20 +255,95 @@ pub async fn get_metrics( total += total_count; } if let Some(retried_amount) = collected_values.smart_retried_amount { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(retried_amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.smart_retried_amount_in_usd = amount_in_usd; total_smart_retried_amount += retried_amount; + total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0); } if let Some(retried_amount) = collected_values.smart_retried_amount_without_smart_retries { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(retried_amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd; total_smart_retried_amount_without_smart_retries += retried_amount; + total_smart_retried_amount_without_smart_retries_in_usd += + amount_in_usd.unwrap_or(0); } if let Some(amount) = collected_values.payment_processed_amount { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.payment_processed_amount_in_usd = amount_in_usd; + total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); total_payment_processed_amount += amount; } if let Some(count) = collected_values.payment_processed_count { total_payment_processed_count += count; } if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.payment_processed_amount_without_smart_retries_in_usd = + amount_in_usd; + total_payment_processed_amount_without_smart_retries_in_usd += + amount_in_usd.unwrap_or(0); total_payment_processed_amount_without_smart_retries += amount; } if let Some(count) = collected_values.payment_processed_count_without_smart_retries { @@ -294,6 +376,14 @@ pub async fn get_metrics( total_payment_processed_amount_without_smart_retries: Some( total_payment_processed_amount_without_smart_retries, ), + total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd), + total_smart_retried_amount_without_smart_retries_in_usd: Some( + total_smart_retried_amount_without_smart_retries_in_usd, + ), + total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), + total_payment_processed_amount_without_smart_retries_in_usd: Some( + total_payment_processed_amount_without_smart_retries_in_usd, + ), total_payment_processed_count: Some(total_payment_processed_count), total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs index e77722450630..01d580534834 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs @@ -61,7 +61,7 @@ where query_builder .add_select_column("attempt_count == 1 as first_attempt") .switch()?; - + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Sum { field: "amount", @@ -101,7 +101,10 @@ where .add_group_by_clause("attempt_count") .attach_printable("Error grouping by attempt_count") .switch()?; - + query_builder + .add_group_by_clause("currency") + .attach_printable("Error grouping by currency") + .switch()?; if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs index 6d36aca5172b..cf7af6e11e7e 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs @@ -63,6 +63,7 @@ where .add_select_column("attempt_count == 1 as first_attempt") .switch()?; + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -102,7 +103,10 @@ where .add_group_by_clause("first_attempt") .attach_printable("Error grouping by first_attempt") .switch()?; - + query_builder + .add_group_by_clause("currency") + .attach_printable("Error grouping by first_attempt") + .switch()?; if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index b23fcafdee08..9497dc89f42c 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -62,7 +62,7 @@ where query_builder .add_select_column("attempt_count == 1 as first_attempt") .switch()?; - + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -102,7 +102,10 @@ where .add_group_by_clause("first_attempt") .attach_printable("Error grouping by first_attempt") .switch()?; - + query_builder + .add_group_by_clause("currency") + .attach_printable("Error grouping by currency") + .switch()?; if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs index 4388b2071fee..651eeb0bcfe7 100644 --- a/crates/analytics/src/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -272,7 +272,14 @@ impl PaymentMetricAccumulator for CountAccumulator { } impl PaymentMetricAccumulator for ProcessedAmountAccumulator { - type MetricOutput = (Option, Option, Option, Option); + type MetricOutput = ( + Option, + Option, + Option, + Option, + Option, + Option, + ); #[inline] fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { self.total_with_retries = match ( @@ -322,6 +329,8 @@ impl PaymentMetricAccumulator for ProcessedAmountAccumulator { count_with_retries, total_without_retries, count_without_retries, + Some(0), + Some(0), ) } } @@ -378,6 +387,8 @@ impl PaymentMetricsAccumulator { payment_processed_count, payment_processed_amount_without_smart_retries, payment_processed_count_without_smart_retries, + payment_processed_amount_usd, + payment_processed_amount_without_smart_retries_usd, ) = self.processed_amount.collect(); let ( payments_success_rate_distribution, @@ -406,6 +417,8 @@ impl PaymentMetricsAccumulator { payments_failure_rate_distribution_without_smart_retries, failure_reason_count, failure_reason_count_without_smart_retries, + payment_processed_amount_usd, + payment_processed_amount_without_smart_retries_usd, } } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs index 59ae549b2839..bcd009270dc1 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -9,7 +9,10 @@ use api_models::analytics::{ FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, PaymentFiltersResponse, PaymentsAnalyticsMetadata, PaymentsMetricsResponse, }; +use bigdecimal::ToPrimitive; +use common_enums::Currency; use common_utils::errors::CustomResult; +use currency_conversion::{conversion::convert, types::ExchangeRates}; use error_stack::ResultExt; use router_env::{ instrument, logger, @@ -46,6 +49,7 @@ pub enum TaskType { #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, + ex_rates: &ExchangeRates, auth: &AuthInfo, req: GetPaymentMetricRequest, ) -> AnalyticsResult> { @@ -224,18 +228,57 @@ pub async fn get_metrics( let mut total_payment_processed_count_without_smart_retries = 0; let mut total_failure_reasons_count = 0; let mut total_failure_reasons_count_without_smart_retries = 0; + let mut total_payment_processed_amount_usd = 0; + let mut total_payment_processed_amount_without_smart_retries_usd = 0; let query_data: Vec = metrics_accumulator .into_iter() .map(|(id, val)| { - let collected_values = val.collect(); + let mut collected_values = val.collect(); if let Some(amount) = collected_values.payment_processed_amount { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.payment_processed_amount_usd = amount_in_usd; total_payment_processed_amount += amount; + total_payment_processed_amount_usd += amount_in_usd.unwrap_or(0); } if let Some(count) = collected_values.payment_processed_count { total_payment_processed_count += count; } if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.payment_processed_amount_without_smart_retries_usd = amount_in_usd; total_payment_processed_amount_without_smart_retries += amount; + total_payment_processed_amount_without_smart_retries_usd += + amount_in_usd.unwrap_or(0); } if let Some(count) = collected_values.payment_processed_count_without_smart_retries { total_payment_processed_count_without_smart_retries += count; @@ -252,14 +295,17 @@ pub async fn get_metrics( } }) .collect(); - Ok(PaymentsMetricsResponse { query_data, meta_data: [PaymentsAnalyticsMetadata { total_payment_processed_amount: Some(total_payment_processed_amount), + total_payment_processed_amount_usd: Some(total_payment_processed_amount_usd), total_payment_processed_amount_without_smart_retries: Some( total_payment_processed_amount_without_smart_retries, ), + total_payment_processed_amount_without_smart_retries_usd: Some( + total_payment_processed_amount_without_smart_retries_usd, + ), total_payment_processed_count: Some(total_payment_processed_count), total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries, diff --git a/crates/analytics/src/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs index b8b3868803c6..fa54c1730416 100644 --- a/crates/analytics/src/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -50,6 +50,7 @@ where alias: Some("total"), }) .switch()?; + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -79,6 +80,11 @@ where .switch()?; } + query_builder + .add_group_by_clause("currency") + .attach_printable("Error grouping by currency") + .switch()?; + if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs index 9bc554eaae71..a315b2fc4c82 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs @@ -57,6 +57,8 @@ where query_builder.add_select_column("first_attempt").switch()?; + query_builder.add_select_column("currency").switch()?; + query_builder .add_select_column(Aggregate::Sum { field: "amount", @@ -95,6 +97,12 @@ where .add_group_by_clause("first_attempt") .attach_printable("Error grouping by first_attempt") .switch()?; + + query_builder + .add_group_by_clause("currency") + .attach_printable("Error grouping by currency") + .switch()?; + if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index b95404080b03..8d63bc3096ca 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -203,7 +203,9 @@ pub struct AnalyticsMetadata { #[derive(Debug, serde::Serialize)] pub struct PaymentsAnalyticsMetadata { pub total_payment_processed_amount: Option, + pub total_payment_processed_amount_usd: Option, pub total_payment_processed_amount_without_smart_retries: Option, + pub total_payment_processed_amount_without_smart_retries_usd: Option, pub total_payment_processed_count: Option, pub total_payment_processed_count_without_smart_retries: Option, pub total_failure_reasons_count: Option, @@ -218,6 +220,10 @@ pub struct PaymentIntentsAnalyticsMetadata { pub total_smart_retried_amount_without_smart_retries: Option, pub total_payment_processed_amount: Option, pub total_payment_processed_amount_without_smart_retries: Option, + pub total_smart_retried_amount_in_usd: Option, + pub total_smart_retried_amount_without_smart_retries_in_usd: Option, + pub total_payment_processed_amount_in_usd: Option, + pub total_payment_processed_amount_without_smart_retries_in_usd: Option, pub total_payment_processed_count: Option, pub total_payment_processed_count_without_smart_retries: Option, } diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 60662f2e90af..3ac3c09d35f6 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -161,7 +161,9 @@ pub struct PaymentIntentMetricsBucketValue { pub successful_smart_retries: Option, pub total_smart_retries: Option, pub smart_retried_amount: Option, + pub smart_retried_amount_in_usd: Option, pub smart_retried_amount_without_smart_retries: Option, + pub smart_retried_amount_without_smart_retries_in_usd: Option, pub payment_intent_count: Option, pub successful_payments: Option, pub successful_payments_without_smart_retries: Option, @@ -169,8 +171,10 @@ pub struct PaymentIntentMetricsBucketValue { pub payments_success_rate: Option, pub payments_success_rate_without_smart_retries: Option, pub payment_processed_amount: Option, + pub payment_processed_amount_in_usd: Option, pub payment_processed_count: Option, pub payment_processed_amount_without_smart_retries: Option, + pub payment_processed_amount_without_smart_retries_in_usd: Option, pub payment_processed_count_without_smart_retries: Option, pub payments_success_rate_distribution_without_smart_retries: Option, pub payments_failure_rate_distribution_without_smart_retries: Option, diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 1120ab092d75..1faba79eb378 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -271,8 +271,10 @@ pub struct PaymentMetricsBucketValue { pub payment_count: Option, pub payment_success_count: Option, pub payment_processed_amount: Option, + pub payment_processed_amount_usd: Option, pub payment_processed_count: Option, pub payment_processed_amount_without_smart_retries: Option, + pub payment_processed_amount_without_smart_retries_usd: Option, pub payment_processed_count_without_smart_retries: Option, pub avg_ticket_size: Option, pub payment_error_message: Option>, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 150931e9c8a7..aba1c2e2efaa 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -32,7 +32,10 @@ pub mod routes { use crate::{ consts::opensearch::SEARCH_INDEXES, - core::{api_locking, errors::user::UserErrors, verification::utils}, + core::{ + api_locking, currency::get_forex_exchange_rates, errors::user::UserErrors, + verification::utils, + }, db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload}, routes::AppState, services::{ @@ -397,7 +400,8 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - analytics::payments::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -435,7 +439,8 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - analytics::payments::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -480,7 +485,8 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - analytics::payments::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -520,7 +526,8 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - analytics::payment_intents::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -558,7 +565,8 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - analytics::payment_intents::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -603,7 +611,8 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - analytics::payment_intents::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs index 96d75098271b..912484b014a7 100644 --- a/crates/router/src/core/currency.rs +++ b/crates/router/src/core/currency.rs @@ -1,4 +1,6 @@ +use analytics::errors::AnalyticsError; use common_utils::errors::CustomResult; +use currency_conversion::types::ExchangeRates; use error_stack::ResultExt; use crate::{ @@ -46,3 +48,19 @@ pub async fn convert_forex( .change_context(ApiErrorResponse::InternalServerError)?, )) } + +pub async fn get_forex_exchange_rates( + state: SessionState, +) -> CustomResult { + let forex_api = state.conf.forex_api.get_inner(); + let rates = get_forex_rates( + &state, + forex_api.call_delay, + forex_api.local_fetch_retry_delay, + forex_api.local_fetch_retry_count, + ) + .await + .change_context(AnalyticsError::ForexFetchFailed)?; + + Ok((*rates.data).clone()) +} diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index dcfe0347d6fa..2173478ab673 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -26,7 +26,7 @@ const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD"; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct FxExchangeRatesCacheEntry { - data: Arc, + pub data: Arc, timestamp: i64, } @@ -421,7 +421,13 @@ pub async fn fallback_fetch_forex_rates( conversions.insert(enum_curr, currency_factors); } None => { - logger::error!("Rates for {} not received from API", &enum_curr); + if enum_curr == enums::Currency::USD { + let currency_factors = + CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + conversions.insert(enum_curr, currency_factors); + } else { + logger::error!("Rates for {} not received from API", &enum_curr); + } } }; }