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/CHANGELOG.md b/CHANGELOG.md index dee4d40834bf..21fc88e5e6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.08.0 + +### Features + +- **payments:** Add audit events for PaymentCreate update ([#6427](https://github.com/juspay/hyperswitch/pull/6427)) ([`063fe90`](https://github.com/juspay/hyperswitch/commit/063fe904c66c9af3d7ce0a82ad712eac69e41786)) + +**Full Changelog:** [`2024.11.07.1...2024.11.08.0`](https://github.com/juspay/hyperswitch/compare/2024.11.07.1...2024.11.08.0) + +- - - + +## 2024.11.07.1 + +### Bug Fixes + +- **users:** Add force rotate password on first login for non-email flow ([#6483](https://github.com/juspay/hyperswitch/pull/6483)) ([`b43033c`](https://github.com/juspay/hyperswitch/commit/b43033c2d9530d291651326cd987476e4924132b)) + +### Refactors + +- **connector:** Added amount conversion framework to Wise. ([#6469](https://github.com/juspay/hyperswitch/pull/6469)) ([`1ba3d84`](https://github.com/juspay/hyperswitch/commit/1ba3d84df1e93d2286db1a262c4a67b3861b90c0)) + +**Full Changelog:** [`2024.11.07.0...2024.11.07.1`](https://github.com/juspay/hyperswitch/compare/2024.11.07.0...2024.11.07.1) + +- - - + +## 2024.11.07.0 + +### Features + +- **analytics:** Implement currency conversion to power multi-currency aggregation ([#6418](https://github.com/juspay/hyperswitch/pull/6418)) ([`01c5216`](https://github.com/juspay/hyperswitch/commit/01c5216fdd6f1d841082868cccea6054b64e9e07)) + +### Bug Fixes + +- **core:** PMD Not Getting Populated for Saved Card Transactions ([#6497](https://github.com/juspay/hyperswitch/pull/6497)) ([`b8b2060`](https://github.com/juspay/hyperswitch/commit/b8b206057c5b464420a6d115a1116ef5cc695bf7)) + +**Full Changelog:** [`2024.11.06.0...2024.11.07.0`](https://github.com/juspay/hyperswitch/compare/2024.11.06.0...2024.11.07.0) + +- - - + +## 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 diff --git a/Cargo.lock b/Cargo.lock index b13fbbf148db..7c9cb19d5c98 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", @@ -2048,6 +2050,7 @@ name = "common_enums" version = "0.1.0" dependencies = [ "diesel", + "masking", "router_derive", "serde", "serde_json", diff --git a/config/config.example.toml b/config/config.example.toml index b1189b51faee..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/" @@ -396,6 +397,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..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/" @@ -139,6 +140,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 +397,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..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/" @@ -146,6 +147,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 +411,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..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/" @@ -146,6 +147,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..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/" @@ -318,6 +320,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..5a2e7249fda4 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 = "" @@ -149,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/" @@ -244,6 +246,7 @@ cards = [ "helcim", "iatapay", "itaubank", + "jpmorgan", "mollie", "multisafepay", "netcetera", 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/opensearch.rs b/crates/analytics/src/opensearch.rs index a6e6c486ebe1..84a2b9db3d4e 100644 --- a/crates/analytics/src/opensearch.rs +++ b/crates/analytics/src/opensearch.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use api_models::{ analytics::search::SearchIndex, errors::types::{ApiError, ApiErrorResponse}, @@ -456,7 +458,8 @@ pub struct OpenSearchQueryBuilder { pub count: Option, pub filters: Vec<(String, Vec)>, pub time_range: Option, - pub search_params: Vec, + search_params: Vec, + case_sensitive_fields: HashSet<&'static str>, } impl OpenSearchQueryBuilder { @@ -469,6 +472,12 @@ impl OpenSearchQueryBuilder { count: Default::default(), filters: Default::default(), time_range: Default::default(), + case_sensitive_fields: HashSet::from([ + "customer_email.keyword", + "search_tags.keyword", + "card_last_4.keyword", + "payment_id.keyword", + ]), } } @@ -490,48 +499,16 @@ impl OpenSearchQueryBuilder { pub fn get_status_field(&self, index: &SearchIndex) -> &str { match index { - SearchIndex::Refunds => "refund_status.keyword", - SearchIndex::Disputes => "dispute_status.keyword", + SearchIndex::Refunds | SearchIndex::SessionizerRefunds => "refund_status.keyword", + SearchIndex::Disputes | SearchIndex::SessionizerDisputes => "dispute_status.keyword", _ => "status.keyword", } } - pub fn replace_status_field(&self, filters: &[Value], index: &SearchIndex) -> Vec { - filters - .iter() - .map(|filter| { - if let Some(terms) = filter.get("terms").and_then(|v| v.as_object()) { - let mut new_filter = filter.clone(); - if let Some(new_terms) = - new_filter.get_mut("terms").and_then(|v| v.as_object_mut()) - { - let key = "status.keyword"; - if let Some(status_terms) = terms.get(key) { - new_terms.remove(key); - new_terms.insert( - self.get_status_field(index).to_string(), - status_terms.clone(), - ); - } - } - new_filter - } else { - filter.clone() - } - }) - .collect() - } - - /// # Panics - /// - /// This function will panic if: - /// - /// * The structure of the JSON query is not as expected (e.g., missing keys or incorrect types). - /// - /// Ensure that the input data and the structure of the query are valid and correctly handled. - pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult> { - let mut query_obj = Map::new(); - let mut bool_obj = Map::new(); + pub fn build_filter_array( + &self, + case_sensitive_filters: Vec<&(String, Vec)>, + ) -> Vec { let mut filter_array = Vec::new(); filter_array.push(json!({ @@ -542,13 +519,12 @@ impl OpenSearchQueryBuilder { } })); - let mut filters = self - .filters - .iter() + let case_sensitive_json_filters = case_sensitive_filters + .into_iter() .map(|(k, v)| json!({"terms": {k: v}})) .collect::>(); - filter_array.append(&mut filters); + filter_array.extend(case_sensitive_json_filters); if let Some(ref time_range) = self.time_range { let range = json!(time_range); @@ -559,8 +535,72 @@ impl OpenSearchQueryBuilder { })); } - let should_array = self - .search_params + filter_array + } + + pub fn build_case_insensitive_filters( + &self, + mut payload: Value, + case_insensitive_filters: &[&(String, Vec)], + auth_array: Vec, + index: &SearchIndex, + ) -> Value { + let mut must_array = case_insensitive_filters + .iter() + .map(|(k, v)| { + let key = if *k == "status.keyword" { + self.get_status_field(index).to_string() + } else { + k.clone() + }; + json!({ + "bool": { + "must": [ + { + "bool": { + "should": v.iter().map(|value| { + json!({ + "term": { + format!("{}", key): { + "value": value, + "case_insensitive": true + } + } + }) + }).collect::>(), + "minimum_should_match": 1 + } + } + ] + } + }) + }) + .collect::>(); + + must_array.push(json!({ "bool": { + "must": [ + { + "bool": { + "should": auth_array, + "minimum_should_match": 1 + } + } + ] + }})); + + if let Some(query) = payload.get_mut("query") { + if let Some(bool_obj) = query.get_mut("bool") { + if let Some(bool_map) = bool_obj.as_object_mut() { + bool_map.insert("must".to_string(), Value::Array(must_array)); + } + } + } + + payload + } + + pub fn build_auth_array(&self) -> Vec { + self.search_params .iter() .map(|user_level| match user_level { AuthInfo::OrgLevel { org_id } => { @@ -579,11 +619,17 @@ impl OpenSearchQueryBuilder { }) } AuthInfo::MerchantLevel { - org_id: _, + org_id, merchant_ids, } => { let must_clauses = vec![ - // TODO: Add org_id field to the filters + json!({ + "term": { + "organization_id.keyword": { + "value": org_id + } + } + }), json!({ "terms": { "merchant_id.keyword": merchant_ids @@ -598,12 +644,18 @@ impl OpenSearchQueryBuilder { }) } AuthInfo::ProfileLevel { - org_id: _, + org_id, merchant_id, profile_ids, } => { let must_clauses = vec![ - // TODO: Add org_id field to the filters + json!({ + "term": { + "organization_id.keyword": { + "value": org_id + } + } + }), json!({ "term": { "merchant_id.keyword": { @@ -625,55 +677,60 @@ impl OpenSearchQueryBuilder { }) } }) - .collect::>(); + .collect::>() + } + + /// # Panics + /// + /// This function will panic if: + /// + /// * The structure of the JSON query is not as expected (e.g., missing keys or incorrect types). + /// + /// Ensure that the input data and the structure of the query are valid and correctly handled. + pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult> { + let mut query_obj = Map::new(); + let mut bool_obj = Map::new(); + + let (case_sensitive_filters, case_insensitive_filters): (Vec<_>, Vec<_>) = self + .filters + .iter() + .partition(|(k, _)| self.case_sensitive_fields.contains(k.as_str())); + + let filter_array = self.build_filter_array(case_sensitive_filters); if !filter_array.is_empty() { bool_obj.insert("filter".to_string(), Value::Array(filter_array)); } + + let should_array = self.build_auth_array(); + if !bool_obj.is_empty() { query_obj.insert("bool".to_string(), Value::Object(bool_obj)); } - let mut query = Map::new(); - query.insert("query".to_string(), Value::Object(query_obj)); + let mut sort_obj = Map::new(); + sort_obj.insert( + "@timestamp".to_string(), + json!({ + "order": "desc" + }), + ); Ok(indexes .iter() .map(|index| { - let updated_query = query - .get("query") - .and_then(|q| q.get("bool")) - .and_then(|b| b.get("filter")) - .and_then(|f| f.as_array()) - .map(|filters| self.replace_status_field(filters, index)) - .unwrap_or_default(); - let mut final_bool_obj = Map::new(); - if !updated_query.is_empty() { - final_bool_obj.insert("filter".to_string(), Value::Array(updated_query)); - } - if !should_array.is_empty() { - final_bool_obj.insert("should".to_string(), Value::Array(should_array.clone())); - final_bool_obj - .insert("minimum_should_match".to_string(), Value::Number(1.into())); - } - let mut final_query = Map::new(); - if !final_bool_obj.is_empty() { - final_query.insert("bool".to_string(), Value::Object(final_bool_obj)); - } - - let mut sort_obj = Map::new(); - sort_obj.insert( - "@timestamp".to_string(), - json!({ - "order": "desc" - }), - ); - let payload = json!({ - "query": Value::Object(final_query), + let mut payload = json!({ + "query": query_obj.clone(), "sort": [ - Value::Object(sort_obj) + Value::Object(sort_obj.clone()) ] }); + payload = self.build_case_insensitive_filters( + payload, + &case_insensitive_filters, + should_array.clone(), + index, + ); payload }) .collect::>()) 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/analytics/src/search.rs b/crates/analytics/src/search.rs index f53b07b12321..9be0200030d2 100644 --- a/crates/analytics/src/search.rs +++ b/crates/analytics/src/search.rs @@ -190,7 +190,17 @@ pub async fn search_results( search_params: Vec, ) -> CustomResult { let search_req = req.search_req; - + if search_req.query.trim().is_empty() + && search_req + .filters + .as_ref() + .map_or(true, |filters| filters.is_all_none()) + { + return Err(OpenSearchError::BadRequestError( + "Both query and filters are empty".to_string(), + ) + .into()); + } let mut query_builder = OpenSearchQueryBuilder::new( OpenSearchQuery::Search(req.index), search_req.query, 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/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/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0ab86c8e35ce..3e95fb9f2315 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5,6 +5,9 @@ use std::{ }; pub mod additional_info; use cards::CardNumber; +use common_enums::ProductType; +#[cfg(feature = "v2")] +use common_utils::id_type::GlobalPaymentId; use common_utils::{ consts::default_payments_list_limit, crypto, @@ -374,7 +377,7 @@ pub struct PaymentsIntentResponse { /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent #[schema(value_type = Option>)] - pub allowed_payment_method_types: Option, + pub allowed_payment_method_types: Option>, /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] @@ -386,7 +389,7 @@ pub struct PaymentsIntentResponse { /// Additional data that might be required by hyperswitch based on the requested features by the merchants. #[schema(value_type = Option)] - pub feature_metadata: Option, + pub feature_metadata: Option, /// Whether to generate the payment link for this payment or not (if applicable) #[schema(value_type = EnablePaymentLinkRequest)] @@ -5107,18 +5110,6 @@ pub struct OrderDetailsWithAmount { impl masking::SerializableSecret for OrderDetailsWithAmount {} -#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum ProductType { - #[default] - Physical, - Digital, - Travel, - Ride, - Event, - Accommodation, -} - #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct OrderDetails { /// Name of the product that is being purchased 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/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index da03b530eb8c..92fc2f02066b 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -22,6 +22,7 @@ utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order # First party crates router_derive = { version = "0.1.0", path = "../router_derive" } +masking = { version = "0.1.0", path = "../masking" } [dev-dependencies] serde_json = "1.0.115" 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/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 917030c1e80e..4d2e117a8809 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,5 +1,7 @@ +mod payments; use std::num::{ParseFloatError, TryFromIntError}; +pub use payments::ProductType; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -1575,6 +1577,8 @@ pub enum PaymentMethodType { OpenBankingPIS, } +impl masking::SerializableSecret for PaymentMethodType {} + /// Indicates the type of payment method. Eg: 'card', 'wallet', etc. #[derive( Clone, diff --git a/crates/common_enums/src/enums/payments.rs b/crates/common_enums/src/enums/payments.rs new file mode 100644 index 000000000000..895303bab4f5 --- /dev/null +++ b/crates/common_enums/src/enums/payments.rs @@ -0,0 +1,14 @@ +use serde; +use utoipa::ToSchema; + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProductType { + #[default] + Physical, + Digital, + Travel, + Ride, + Event, + Accommodation, +} diff --git a/crates/common_utils/src/hashing.rs b/crates/common_utils/src/hashing.rs index d08cd9f0868a..0982ca537881 100644 --- a/crates/common_utils/src/hashing.rs +++ b/crates/common_utils/src/hashing.rs @@ -1,7 +1,7 @@ use masking::{PeekInterface, Secret, Strategy}; use serde::{Deserialize, Serialize, Serializer}; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, PartialEq, Debug, Deserialize)] /// Represents a hashed string using blake3's hashing strategy. pub struct HashedString>(Secret); diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 598035524a72..c7a3818d5fea 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -41,6 +41,7 @@ pub mod refund; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; +pub mod types; pub mod unified_translations; #[allow(unused_qualifications)] diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 210b12b7aadc..883e23930977 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -9,6 +9,8 @@ use crate::enums as storage_enums; use crate::schema::payment_intent; #[cfg(feature = "v2")] use crate::schema_v2::payment_intent; +#[cfg(feature = "v2")] +use crate::types::{FeatureMetadata, OrderDetailsWithAmount}; #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Selectable)] @@ -32,11 +34,11 @@ pub struct PaymentIntent { pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, pub active_attempt_id: Option, - #[diesel(deserialize_as = super::OptionalDieselArray)] - pub order_details: Option>, + #[diesel(deserialize_as = super::OptionalDieselArray>)] + pub order_details: Option>>, pub allowed_payment_method_types: Option, pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: common_utils::id_type::ProfileId, pub payment_link_id: Option, @@ -249,11 +251,11 @@ pub struct PaymentIntentNew { pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, pub active_attempt_id: Option, - #[diesel(deserialize_as = super::OptionalDieselArray)] - pub order_details: Option>, + #[diesel(deserialize_as = super::OptionalDieselArray>)] + pub order_details: Option>>, pub allowed_payment_method_types: Option, pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: common_utils::id_type::ProfileId, pub payment_link_id: Option, diff --git a/crates/diesel_models/src/types.rs b/crates/diesel_models/src/types.rs new file mode 100644 index 000000000000..94f1fa8d2664 --- /dev/null +++ b/crates/diesel_models/src/types.rs @@ -0,0 +1,57 @@ +use common_utils::{hashing::HashedString, pii, types::MinorUnit}; +use diesel::{ + sql_types::{Json, Jsonb}, + AsExpression, FromSqlRow, +}; +use masking::{Secret, WithType}; +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromSqlRow, AsExpression)] +#[diesel(sql_type = Jsonb)] +pub struct OrderDetailsWithAmount { + /// Name of the product that is being purchased + pub product_name: String, + /// The quantity of the product to be purchased + pub quantity: u16, + /// the amount per quantity of product + pub amount: MinorUnit, + // Does the order includes shipping + pub requires_shipping: Option, + /// The image URL of the product + pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Sub category of the product that is being purchased + pub sub_category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, + /// The tax code for the product + pub product_tax_code: Option, +} + +impl masking::SerializableSecret for OrderDetailsWithAmount {} + +common_utils::impl_to_sql_from_sql_json!(OrderDetailsWithAmount); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)] +#[diesel(sql_type = Json)] +pub struct FeatureMetadata { + /// Redirection response coming in request as metadata field only for redirection scenarios + pub redirect_response: Option, + // TODO: Convert this to hashedstrings to avoid PII sensitive data + /// Additional tags to be used for global search + pub search_tags: Option>>, +} +impl masking::SerializableSecret for FeatureMetadata {} +common_utils::impl_to_sql_from_sql_json!(FeatureMetadata); + +#[derive(Default, Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +pub struct RedirectResponse { + pub param: Option>, + pub json_payload: Option, +} +impl masking::SerializableSecret for RedirectResponse {} +common_utils::impl_to_sql_from_sql_json!(RedirectResponse); 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_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index a0daab47a24e..110917226fe7 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use api_models::payments::{self, Address, AddressDetails, OrderDetailsWithAmount, PhoneDetails}; +use api_models::payments::{self, Address, AddressDetails, PhoneDetails}; use base64::Engine; use common_enums::{ enums, @@ -26,6 +26,7 @@ use hyperswitch_domain_models::{ PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSyncData, RefundsData, ResponseId, SetupMandateRequestData, }, + types::OrderDetailsWithAmount, }; use hyperswitch_interfaces::{api, consts, errors, types::Response}; use image::Luma; diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index af6c8e65c848..02795481ad9c 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -30,6 +30,12 @@ pub trait PayoutAttemptInterface {} #[cfg(not(feature = "payouts"))] pub trait PayoutsInterface {} +use api_models::payments::{ + FeatureMetadata as ApiFeatureMetadata, OrderDetailsWithAmount as ApiOrderDetailsWithAmount, + RedirectResponse as ApiRedirectResponse, +}; +use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount, RedirectResponse}; + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub enum RemoteStorageObject { ForeignID(String), @@ -60,6 +66,116 @@ use std::fmt::Debug; pub trait ApiModelToDieselModelConvertor { /// Convert from a foreign type to the current type fn convert_from(from: F) -> Self; + fn convert_back(self) -> F; +} + +impl ApiModelToDieselModelConvertor for FeatureMetadata { + fn convert_from(from: ApiFeatureMetadata) -> Self { + let ApiFeatureMetadata { + redirect_response, + search_tags, + } = from; + Self { + redirect_response: redirect_response.map(RedirectResponse::convert_from), + search_tags, + } + } + + fn convert_back(self) -> ApiFeatureMetadata { + let Self { + redirect_response, + search_tags, + } = self; + ApiFeatureMetadata { + redirect_response: redirect_response + .map(|redirect_response| redirect_response.convert_back()), + search_tags, + } + } +} + +impl ApiModelToDieselModelConvertor for RedirectResponse { + fn convert_from(from: ApiRedirectResponse) -> Self { + let ApiRedirectResponse { + param, + json_payload, + } = from; + Self { + param, + json_payload, + } + } + + fn convert_back(self) -> ApiRedirectResponse { + let Self { + param, + json_payload, + } = self; + ApiRedirectResponse { + param, + json_payload, + } + } +} + +impl ApiModelToDieselModelConvertor for OrderDetailsWithAmount { + fn convert_from(from: ApiOrderDetailsWithAmount) -> Self { + let ApiOrderDetailsWithAmount { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } = from; + Self { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } + } + + fn convert_back(self) -> ApiOrderDetailsWithAmount { + let Self { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } = self; + ApiOrderDetailsWithAmount { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } + } } #[cfg(feature = "v2")] @@ -86,6 +202,31 @@ impl ApiModelToDieselModelConvertor }), } } + fn convert_back(self) -> api_models::admin::PaymentLinkConfigRequest { + let Self { + theme, + logo, + seller_name, + sdk_layout, + display_sdk_only, + enabled_saved_payment_method, + transaction_details, + } = self; + api_models::admin::PaymentLinkConfigRequest { + theme, + logo, + seller_name, + sdk_layout, + display_sdk_only, + enabled_saved_payment_method, + transaction_details: transaction_details.map(|transaction_details| { + transaction_details + .into_iter() + .map(|transaction_detail| transaction_detail.convert_back()) + .collect() + }), + } + } } #[cfg(feature = "v2")] @@ -101,6 +242,19 @@ impl ApiModelToDieselModelConvertor api_models::admin::PaymentLinkTransactionDetails { + let Self { + key, + value, + ui_configuration, + } = self; + api_models::admin::PaymentLinkTransactionDetails { + key, + value, + ui_configuration: ui_configuration + .map(|ui_configuration| ui_configuration.convert_back()), + } + } } #[cfg(feature = "v2")] @@ -114,6 +268,18 @@ impl ApiModelToDieselModelConvertor api_models::admin::TransactionDetailsUiConfiguration { + let Self { + position, + is_key_bold, + is_value_bold, + } = self; + api_models::admin::TransactionDetailsUiConfiguration { + position, + is_key_bold, + is_value_bold, + } + } } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index cd41458dfbf5..a21788e1bb0a 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -3,8 +3,6 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] use api_models::payments::Address; -#[cfg(feature = "v2")] -use api_models::payments::OrderDetailsWithAmount; use common_utils::{ self, crypto::Encryptable, @@ -26,6 +24,8 @@ pub mod payment_attempt; pub mod payment_intent; use common_enums as storage_enums; +#[cfg(feature = "v2")] +use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; use self::payment_attempt::PaymentAttempt; use crate::RemoteStorageObject; @@ -278,10 +278,10 @@ pub struct PaymentIntent { pub order_details: Option>>, /// This is the list of payment method types that are allowed for the payment intent. /// This field allows the merchant to restrict the payment methods that can be used for the payment intent. - pub allowed_payment_method_types: Option, + pub allowed_payment_method_types: Option>, /// This metadata contains details about pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, /// Number of attempts that have been made for the order pub attempt_count: i16, /// The profile id for the payment. @@ -381,20 +381,14 @@ impl PaymentIntent { billing_address: Option>>, shipping_address: Option>>, ) -> CustomResult { - let allowed_payment_method_types = request - .get_allowed_payment_method_types_as_value() - .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting allowed payment method types as value")?; let connector_metadata = request .get_connector_metadata_as_value() .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Error getting connector metadata as value")?; - let feature_metadata = request - .get_feature_metadata_as_value() - .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting feature metadata as value")?; let request_incremental_authorization = Self::get_request_incremental_authorization_value(&request)?; + let allowed_payment_method_types = request.allowed_payment_method_types; + let session_expiry = common_utils::date_time::now().saturating_add(time::Duration::seconds( request.session_expiry.map(i64::from).unwrap_or( @@ -404,9 +398,12 @@ impl PaymentIntent { ), )); let client_secret = payment_id.generate_client_secret(); - let order_details = request - .order_details - .map(|order_details| order_details.into_iter().map(Secret::new).collect()); + let order_details = request.order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(OrderDetailsWithAmount::convert_from(order_detail))) + .collect() + }); Ok(Self { id: payment_id.clone(), merchant_id: merchant_account.get_id().clone(), @@ -428,7 +425,7 @@ impl PaymentIntent { order_details, allowed_payment_method_types, connector_metadata, - feature_metadata, + feature_metadata: request.feature_metadata.map(FeatureMetadata::convert_from), // Attempt count is 0 in create intent as no attempt is made yet attempt_count: 0, profile_id: profile.get_id().clone(), diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index c9f7a5e2ae52..3b26ff94e547 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -14,6 +14,8 @@ use common_utils::{ MinorUnit, }, }; +#[cfg(feature = "v2")] +use diesel_models::types::OrderDetailsWithAmount; use diesel_models::{ PaymentIntent as DieselPaymentIntent, PaymentIntentNew as DieselPaymentIntentNew, }; @@ -33,6 +35,7 @@ use crate::{ type_encryption::{crypto_operation, CryptoOperation}, RemoteStorageObject, }; + #[async_trait::async_trait] pub trait PaymentIntentInterface { async fn update_payment_intent( @@ -1180,18 +1183,22 @@ impl behaviour::Conversion for PaymentIntent { setup_future_usage: Some(setup_future_usage), client_secret, active_attempt_id: active_attempt.map(|attempt| attempt.get_id()), - order_details: order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| order_detail.encode_to_value().map(Secret::new)) - .collect::, _>>() + order_details: order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(order_detail.expose())) + .collect::>() + }), + allowed_payment_method_types: allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types + .encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize allowed_payment_method_types".to_string(), + }) }) - .transpose() - .change_context(ValidationError::InvalidValue { - message: "invalid value found for order_details".to_string(), - })?, - allowed_payment_method_types, + .transpose()? + .map(Secret::new), connector_metadata, feature_metadata, attempt_count, @@ -1290,7 +1297,13 @@ impl behaviour::Conversion for PaymentIntent { .transpose() .change_context(common_utils::errors::CryptoError::DecodingFailed) .attach_printable("Error while deserializing Address")?; - + let allowed_payment_method_types = storage_model + .allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types.parse_value("Vec") + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed)?; Ok::>(Self { merchant_id: storage_model.merchant_id, status: storage_model.status, @@ -1309,19 +1322,13 @@ impl behaviour::Conversion for PaymentIntent { active_attempt: storage_model .active_attempt_id .map(RemoteStorageObject::ForeignID), - order_details: storage_model - .order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| { - order_detail.expose().parse_value("OrderDetailsWithAmount") - }) - .collect::, _>>() - }) - .transpose() - .change_context(common_utils::errors::CryptoError::DecodingFailed)?, - allowed_payment_method_types: storage_model.allowed_payment_method_types, + order_details: storage_model.order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(order_detail.expose())) + .collect::>() + }), + allowed_payment_method_types, connector_metadata: storage_model.connector_metadata, feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, @@ -1389,19 +1396,18 @@ impl behaviour::Conversion for PaymentIntent { setup_future_usage: Some(self.setup_future_usage), client_secret: self.client_secret, active_attempt_id: self.active_attempt.map(|attempt| attempt.get_id()), - order_details: self - .order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| order_detail.encode_to_value().map(Secret::new)) - .collect::, _>>() + order_details: self.order_details, + allowed_payment_method_types: self + .allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types + .encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize allowed_payment_method_types".to_string(), + }) }) - .transpose() - .change_context(ValidationError::InvalidValue { - message: "Invalid value found for ".to_string(), - })?, - allowed_payment_method_types: self.allowed_payment_method_types, + .transpose()? + .map(Secret::new), connector_metadata: self.connector_metadata, feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 344eb632f4ba..c645d0b4b532 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -7,7 +7,7 @@ use common_utils::{ id_type, pii, types::{self as common_types, MinorUnit}, }; -use diesel_models::enums as storage_enums; +use diesel_models::{enums as storage_enums, types::OrderDetailsWithAmount}; use error_stack::ResultExt; use masking::Secret; use serde::Serialize; @@ -49,7 +49,7 @@ pub struct PaymentsAuthorizeData { pub customer_acceptance: Option, pub setup_mandate_details: Option, pub browser_info: Option, - pub order_details: Option>, + pub order_details: Option>, pub order_category: Option, pub session_token: Option, pub enrolled_for_3ds: bool, @@ -288,7 +288,7 @@ pub struct PaymentsPreProcessingData { pub payment_method_type: Option, pub setup_mandate_details: Option, pub capture_method: Option, - pub order_details: Option>, + pub order_details: Option>, pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, @@ -823,7 +823,7 @@ pub struct PaymentsSessionData { pub currency: common_enums::Currency, pub country: Option, pub surcharge_details: Option, - pub order_details: Option>, + pub order_details: Option>, pub email: Option, // Minor Unit amount for amount frame work pub minor_amount: MinorUnit, @@ -834,7 +834,7 @@ pub struct PaymentsTaxCalculationData { pub amount: MinorUnit, pub currency: storage_enums::Currency, pub shipping_cost: Option, - pub order_details: Option>, + pub order_details: Option>, pub shipping_address: Address, } diff --git a/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs b/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs index 30a87c5c13d2..4d7f4bfdf495 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs @@ -4,6 +4,7 @@ use common_utils::{ events::{ApiEventMetric, ApiEventsType}, pii::Email, }; +use diesel_models::types::OrderDetailsWithAmount; use masking::Secret; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -12,7 +13,7 @@ use crate::router_request_types; #[derive(Debug, Clone)] pub struct FraudCheckSaleData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub email: Option, } @@ -20,7 +21,7 @@ pub struct FraudCheckSaleData { #[derive(Debug, Clone)] pub struct FraudCheckCheckoutData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub browser_info: Option, pub payment_method_data: Option, @@ -31,7 +32,7 @@ pub struct FraudCheckCheckoutData { #[derive(Debug, Clone)] pub struct FraudCheckTransactionData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub payment_method: Option, pub error_code: Option, diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index a629f8e6392a..201a073dd3e3 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -1,3 +1,5 @@ +pub use diesel_models::types::OrderDetailsWithAmount; + use crate::{ router_data::{AccessToken, RouterData}, router_flow_types::{ 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/masking/src/strategy.rs b/crates/masking/src/strategy.rs index eb705ca490a7..b497cc3ed4f4 100644 --- a/crates/masking/src/strategy.rs +++ b/crates/masking/src/strategy.rs @@ -8,7 +8,7 @@ pub trait Strategy { /// Debug with type #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum WithType {} impl Strategy for WithType { diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 75d1bb9f2751..c4a57830ae94 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -400,7 +400,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ApplePayShippingContactFields, api_models::payments::ApplePayAddressParameters, api_models::payments::AmountInfo, - api_models::payments::ProductType, + api_models::enums::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 40f694b80a23..2201e3c49282 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -187,6 +187,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerResponse, api_models::admin::AcceptedCountries, api_models::admin::AcceptedCurrencies, + api_models::enums::ProductType, api_models::enums::PaymentType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, @@ -339,7 +340,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ApplePayShippingContactFields, api_models::payments::ApplePayAddressParameters, api_models::payments::AmountInfo, - api_models::payments::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 150931e9c8a7..ed25725358a0 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::{ @@ -47,6 +50,11 @@ pub mod routes { pub struct Analytics; impl Analytics { + #[cfg(feature = "v2")] + pub fn server(_state: AppState) -> Scope { + todo!() + } + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/analytics") .app_data(web::Data::new(state)) @@ -397,7 +405,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 +444,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) }, @@ -447,6 +457,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. @@ -480,7 +491,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 +532,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 +571,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) }, @@ -570,6 +584,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetPaymentIntentMetricRequest` element. @@ -603,7 +618,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) }, @@ -693,6 +709,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. @@ -946,6 +963,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_payment_filters( state: web::Data, req: actix_web::HttpRequest, @@ -1067,6 +1085,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_refund_filters( state: web::Data, req: actix_web::HttpRequest, @@ -1250,6 +1269,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_merchant_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1300,6 +1320,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_org_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1348,6 +1369,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1402,7 +1424,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_merchant_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1452,7 +1474,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_org_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1501,6 +1523,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1556,6 +1579,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_merchant_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -1605,7 +1629,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_org_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -1654,6 +1678,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -2064,6 +2089,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_dispute_filters( state: web::Data, req: actix_web::HttpRequest, @@ -2167,6 +2193,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetDisputeMetricRequest` element. @@ -2310,6 +2337,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_sankey( state: web::Data, req: actix_web::HttpRequest, 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/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/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 95ac2a67a533..bba9fc2f8568 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1763,8 +1763,7 @@ pub fn get_address_info( } fn get_line_items(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Vec { - let order_details: Option> = - item.router_data.request.order_details.clone(); + let order_details = item.router_data.request.order_details.clone(); match order_details { Some(od) => od .iter() diff --git a/crates/router/src/connector/paybox/transformers.rs b/crates/router/src/connector/paybox/transformers.rs index 4e1270f419c8..27096c2a63b5 100644 --- a/crates/router/src/connector/paybox/transformers.rs +++ b/crates/router/src/connector/paybox/transformers.rs @@ -1109,7 +1109,16 @@ impl TryFrom<&PayboxRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for |data| Some(data.clone()), ), customer_id: match item.router_data.request.is_mandate_payment() { - true => Some(Secret::new(item.router_data.payment_id.clone())), + true => { + let reference_id = item + .router_data + .connector_mandate_request_reference_id + .clone() + .ok_or_else(|| errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_request_reference_id", + })?; + Some(Secret::new(reference_id)) + } false => None, }, }) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index d2d38855dafc..cfa183e9e833 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -113,7 +113,7 @@ pub struct LineItem { price: i64, quantity: i32, title: String, - product_type: Option, + product_type: Option, requires_shipping: Option, product_id: Option, category: Option, diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index eed0e9937b25..348ca3d099a6 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -153,7 +153,7 @@ impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { item_is_digital: order_detail .product_type .as_ref() - .map(|product| (product == &api_models::payments::ProductType::Digital)), + .map(|product| (product == &common_enums::ProductType::Digital)), }) .collect::>(); let metadata: SignifydFrmMetadata = item @@ -390,7 +390,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ item_is_digital: order_detail .product_type .as_ref() - .map(|product| (product == &api_models::payments::ProductType::Digital)), + .map(|product| (product == &common_enums::ProductType::Digital)), }) .collect::>(); let metadata: SignifydFrmMetadata = item diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 76def480cffe..a9a5a01eef63 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -7,7 +7,7 @@ use std::{ use api_models::payouts::{self, PayoutVendorAccountDetails}; use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, - payments::{self, OrderDetailsWithAmount}, + payments, }; use base64::Engine; use common_utils::{ @@ -18,7 +18,7 @@ use common_utils::{ pii::{self, Email, IpAddress}, types::{AmountConvertor, MinorUnit}, }; -use diesel_models::enums; +use diesel_models::{enums, types::OrderDetailsWithAmount}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ mandates, diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 8123ae7ec796..9f41651b2f92 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -1,8 +1,8 @@ pub mod transformers; -use std::fmt::Debug; #[cfg(feature = "payouts")] use common_utils::request::RequestContent; +use common_utils::types::{AmountConvertor, MinorUnit, MinorUnitForConnector}; use error_stack::{report, ResultExt}; #[cfg(feature = "payouts")] use masking::PeekInterface; @@ -10,6 +10,7 @@ use masking::PeekInterface; use router_env::{instrument, tracing}; use self::transformers as wise; +use super::utils::convert_amount; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -27,8 +28,18 @@ use crate::{ utils::BytesExt, }; -#[derive(Debug, Clone)] -pub struct Wise; +#[derive(Clone)] +pub struct Wise { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Wise { + pub fn new() -> &'static Self { + &Self { + amount_converter: &MinorUnitForConnector, + } + } +} impl ConnectorCommonExt for Wise where @@ -362,7 +373,13 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.source_currency, + )?; + let connector_router_data = wise::WiseRouterData::from((amount, req)); + let connector_req = wise::WisePayoutQuoteRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } @@ -441,7 +458,13 @@ impl req: &types::PayoutsRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.source_currency, + )?; + let connector_router_data = wise::WiseRouterData::from((amount, req)); + let connector_req = wise::WiseRecipientCreateRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index fe82ffa59547..94849f2b1ca7 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -2,6 +2,7 @@ use api_models::payouts::PayoutMethodData; #[cfg(feature = "payouts")] use common_utils::pii::Email; +use common_utils::types::MinorUnit; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -16,6 +17,20 @@ use crate::{ }, }; use crate::{core::errors, types}; +#[derive(Debug, Serialize)] +pub struct WiseRouterData { + pub amount: MinorUnit, + pub router_data: T, +} + +impl From<(MinorUnit, T)> for WiseRouterData { + fn from((amount, router_data): (MinorUnit, T)) -> Self { + Self { + amount, + router_data, + } + } +} pub struct WiseAuthType { pub(super) api_key: Secret, @@ -156,8 +171,8 @@ pub struct WiseRecipientCreateResponse { pub struct WisePayoutQuoteRequest { source_currency: String, target_currency: String, - source_amount: Option, - target_amount: Option, + source_amount: Option, + target_amount: Option, pay_out: WisePayOutOption, } @@ -348,9 +363,12 @@ fn get_payout_bank_details( // Payouts recipient create request transform #[cfg(feature = "payouts")] -impl TryFrom<&types::PayoutsRouterData> for WiseRecipientCreateRequest { +impl TryFrom<&WiseRouterData<&types::PayoutsRouterData>> for WiseRecipientCreateRequest { type Error = Error; - fn try_from(item: &types::PayoutsRouterData) -> Result { + fn try_from( + item_data: &WiseRouterData<&types::PayoutsRouterData>, + ) -> Result { + let item = item_data.router_data; let request = item.request.to_owned(); let customer_details = request.customer_details.to_owned(); let payout_method_data = item.get_payout_method_data()?; @@ -420,14 +438,17 @@ impl TryFrom // Payouts quote request transform #[cfg(feature = "payouts")] -impl TryFrom<&types::PayoutsRouterData> for WisePayoutQuoteRequest { +impl TryFrom<&WiseRouterData<&types::PayoutsRouterData>> for WisePayoutQuoteRequest { type Error = Error; - fn try_from(item: &types::PayoutsRouterData) -> Result { + fn try_from( + item_data: &WiseRouterData<&types::PayoutsRouterData>, + ) -> Result { + let item = item_data.router_data; let request = item.request.to_owned(); let payout_type = request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Bank => Ok(Self { - source_amount: Some(request.amount), + source_amount: Some(item_data.amount), source_currency: request.source_currency.to_string(), target_amount: None, target_currency: request.destination_currency.to_string(), 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/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index 3f5988777fd5..2aa486fdb3c7 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -7,8 +7,11 @@ use api_models::{ use common_enums::FrmSuggestion; use common_utils::pii::SecretSerdeValue; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; -pub use hyperswitch_domain_models::router_request_types::fraud_check::{ - Address, Destination, FrmFulfillmentRequest, FulfillmentStatus, Fulfillments, Product, +pub use hyperswitch_domain_models::{ + router_request_types::fraud_check::{ + Address, Destination, FrmFulfillmentRequest, FulfillmentStatus, Fulfillments, Product, + }, + types::OrderDetailsWithAmount, }; use masking::Serialize; use serde::Deserialize; @@ -54,7 +57,7 @@ pub struct FrmData { pub fraud_check: FraudCheck, pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, - pub order_details: Option>, + pub order_details: Option>, pub refund: Option, pub frm_metadata: Option, } @@ -79,7 +82,7 @@ pub struct PaymentToFrmData { pub merchant_account: MerchantAccount, pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, - pub order_details: Option>, + pub order_details: Option>, pub frm_metadata: Option, } 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/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index fe5e7a7e72f3..2bc18122b227 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -12,6 +12,7 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, payments::{helpers, operations, PaymentData}, }, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -213,7 +214,7 @@ impl UpdateTracker, api::PaymentsCaptureRequest> for async fn update_trackers<'b>( &'b self, state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -257,6 +258,11 @@ impl UpdateTracker, api::PaymentsCaptureRequest> for ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentApprove)) + .with(payment_data.to_event()) + .emit(); Ok((Box::new(self), payment_data)) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 1bd3ddce7351..635b3c24cf21 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -576,41 +576,6 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_method_info, } = mandate_details; - let additional_pm_data_from_locker = if let Some(ref pm) = payment_method_info { - let card_detail_from_locker: Option = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| { - v.parse_value("PaymentMethodsData") - .map_err(|err| { - router_env::logger::info!( - "PaymentMethodsData deserialization failed: {:?}", - err - ) - }) - .ok() - }) - .and_then(|pmd| match pmd { - PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), - _ => None, - }); - card_detail_from_locker.map(|card_details| { - let additional_data = card_details.into(); - api_models::payments::AdditionalPaymentData::Card(Box::new(additional_data)) - }) - } else { - None - }; - payment_attempt.payment_method_data = additional_pm_data_from_locker - .as_ref() - .map(Encode::encode_to_value) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode additional pm data")?; - - payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); - payment_attempt.payment_method_type = payment_method_type .or(payment_attempt.payment_method_type) .or(payment_method_info @@ -650,6 +615,41 @@ impl GetTracker, api::PaymentsRequest> for Pa } else { (None, payment_method_info) }; + let additional_pm_data_from_locker = if let Some(ref pm) = payment_method_info { + let card_detail_from_locker: Option = pm + .payment_method_data + .clone() + .map(|x| x.into_inner().expose()) + .and_then(|v| { + v.parse_value("PaymentMethodsData") + .map_err(|err| { + router_env::logger::info!( + "PaymentMethodsData deserialization failed: {:?}", + err + ) + }) + .ok() + }) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), + _ => None, + }); + card_detail_from_locker.map(|card_details| { + let additional_data = card_details.into(); + api_models::payments::AdditionalPaymentData::Card(Box::new(additional_data)) + }) + } else { + None + }; + payment_attempt.payment_method_data = additional_pm_data_from_locker + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode additional pm data")?; + + payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); + // The operation merges mandate data from both request and payment_attempt let setup_mandate = mandate_data.map(|mut sm| { sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index d4057c523bc6..c65e58ea2d48 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -42,6 +42,7 @@ use crate::{ utils as core_utils, }, db::StorageInterface, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -818,7 +819,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen async fn update_trackers<'b>( &'b self, state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut payment_data: PaymentData, customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -923,6 +924,11 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentCreate)) + .with(payment_data.to_event()) + .emit(); // payment_data.mandate_id = response.and_then(|router_data| router_data.request.mandate_id); Ok(( diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 38b475590fdb..45d0f75ccec8 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -18,6 +18,8 @@ use diesel_models::{ use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::PaymentConfirmData; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; use masking::{ExposeInterface, Maskable, PeekInterface, Secret}; use router_env::{instrument, metrics::add_attributes, tracing}; @@ -822,13 +824,16 @@ where order_details: payment_intent.order_details.clone().map(|order_details| { order_details .into_iter() - .map(|order_detail| order_detail.expose()) + .map(|order_detail| order_detail.expose().convert_back()) .collect() }), allowed_payment_method_types: payment_intent.allowed_payment_method_types.clone(), metadata: payment_intent.metadata.clone(), connector_metadata: payment_intent.connector_metadata.clone(), - feature_metadata: payment_intent.feature_metadata.clone(), + feature_metadata: payment_intent + .feature_metadata + .clone() + .map(|feature_metadata| feature_metadata.convert_back()), payment_link_enabled: payment_intent.enable_payment_link.clone(), payment_link_config: payment_intent .payment_link_config 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/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index abd602760fae..fea112bc3b8b 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,11 +1,8 @@ use std::{collections::HashSet, marker::PhantomData, str::FromStr}; +use api_models::enums::{DisputeStage, DisputeStatus}; #[cfg(feature = "payouts")] use api_models::payouts::PayoutVendorAccountDetails; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payments::OrderDetailsWithAmount, -}; use common_enums::{IntentStatus, RequestIncrementalAuthorization}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; @@ -17,7 +14,7 @@ use common_utils::{ use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ merchant_connector_account::MerchantConnectorAccount, payment_address::PaymentAddress, - router_data::ErrorResponse, + router_data::ErrorResponse, types::OrderDetailsWithAmount, }; #[cfg(feature = "payouts")] use masking::{ExposeInterface, PeekInterface}; diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8ca0b2937666..cf73bd5b111b 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -73,7 +73,7 @@ use crate::{ unified_translations::UnifiedTranslationsInterface, CommonStorageInterface, GlobalStorageInterface, MasterKeyInterface, StorageInterface, }, - services::{authentication, kafka::KafkaProducer, Store}, + services::{kafka::KafkaProducer, Store}, types::{domain, storage, AccessToken}, }; @@ -1007,7 +1007,8 @@ impl MerchantAccountInterface for KafkaStore { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { self.diesel_store .find_merchant_account_by_publishable_key(state, publishable_key) .await diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 398445038922..6eb355e4434f 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -13,7 +13,6 @@ use crate::{ connection, core::errors::{self, CustomResult}, db::merchant_key_store::MerchantKeyStoreInterface, - services::authentication, types::{ domain::{ self, @@ -68,7 +67,7 @@ where &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult; + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError>; #[cfg(feature = "olap")] async fn list_merchant_accounts_by_organization_id( @@ -229,7 +228,8 @@ impl MerchantAccountInterface for Store { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { let fetch_by_pub_key_func = || async { let conn = connection::pg_connection_read(self).await?; @@ -261,20 +261,15 @@ impl MerchantAccountInterface for Store { &self.get_master_key().to_vec().into(), ) .await?; - - Ok(authentication::AuthenticationData { - merchant_account: merchant_account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - - key_store, - profile_id: None, - }) + let domain_merchant_account = merchant_account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?; + Ok((domain_merchant_account, key_store)) } #[cfg(feature = "olap")] @@ -578,7 +573,8 @@ impl MerchantAccountInterface for MockDb { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { let accounts = self.merchant_accounts.lock().await; let account = accounts .iter() @@ -599,20 +595,16 @@ impl MerchantAccountInterface for MockDb { &self.get_master_key().to_vec().into(), ) .await?; - Ok(authentication::AuthenticationData { - merchant_account: account - .clone() - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - - key_store, - profile_id: None, - }) + let merchant_account = account + .clone() + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?; + Ok((merchant_account, key_store)) } async fn update_all_merchant_account( diff --git a/crates/router/src/events/audit_events.rs b/crates/router/src/events/audit_events.rs index 9b7a688f7eba..9fbd754b43c0 100644 --- a/crates/router/src/events/audit_events.rs +++ b/crates/router/src/events/audit_events.rs @@ -27,6 +27,8 @@ pub enum AuditEventType { capture_amount: Option, multiple_capture_count: Option, }, + PaymentApprove, + PaymentCreate, } #[derive(Debug, Clone, Serialize)] @@ -65,6 +67,8 @@ impl Event for AuditEvent { AuditEventType::RefundSuccess => "refund_success", AuditEventType::RefundFail => "refund_fail", AuditEventType::PaymentCancelled { .. } => "payment_cancelled", + AuditEventType::PaymentApprove { .. } => "payment_approve", + AuditEventType::PaymentCreate { .. } => "payment_create", }; format!( "{event_type}-{}", diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 0996838b4cfa..64a0630c4013 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -357,7 +357,7 @@ pub async fn connector_create( state, &req, payload, - |state, auth_data, req, _| { + |state, auth_data: auth::AuthenticationData, req, _| { create_connector( state, req, @@ -554,6 +554,8 @@ pub async fn connector_list( ) .await } + +#[cfg(all(feature = "v1", feature = "olap"))] /// Merchant Connector - List /// /// List Merchant Connector Details for the merchant diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ae099a36e444..5ad8f3864418 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -46,7 +46,7 @@ use super::pm_auth; use super::poll; #[cfg(feature = "olap")] use super::routing; -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(all(feature = "oltp", feature = "v1"))] use super::webhooks::*; @@ -66,6 +66,8 @@ pub use crate::analytics::opensearch::OpenSearchClient; use crate::analytics::AnalyticsProvider; #[cfg(feature = "partial-auth")] use crate::errors::RouterResult; +#[cfg(feature = "v1")] +use crate::routes::cards_info::card_iin_info; #[cfg(all(feature = "frm", feature = "oltp"))] use crate::routes::fraud_check as frm_routes; #[cfg(all(feature = "recon", feature = "olap"))] @@ -74,7 +76,6 @@ pub use crate::{ configs::settings, db::{CommonStorageInterface, GlobalStorageInterface, StorageImpl, StorageInterface}, events::EventsHandler, - routes::cards_info::card_iin_info, services::{get_cache_store, get_store}, }; use crate::{ @@ -670,6 +671,7 @@ pub struct Forex; #[cfg(any(feature = "olap", feature = "oltp"))] impl Forex { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/forex") .app_data(web::Data::new(state.clone())) @@ -679,6 +681,10 @@ impl Forex { web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), ) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } #[cfg(feature = "olap")] @@ -1055,6 +1061,11 @@ pub struct Payouts; #[cfg(feature = "payouts")] impl Payouts { + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payouts").app_data(web::Data::new(state)); route = route.service(web::resource("/create").route(web::post().to(payouts_create))); @@ -1563,11 +1574,16 @@ impl Disputes { pub struct Cards; impl Cards { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/cards") .app_data(web::Data::new(state)) .service(web::resource("/{bin}").route(web::get().to(card_iin_info))) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Files; @@ -1628,6 +1644,7 @@ pub struct PayoutLink; #[cfg(feature = "payouts")] impl PayoutLink { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payout_link").app_data(web::Data::new(state)); route = route.service( @@ -1635,6 +1652,10 @@ impl PayoutLink { ); route } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Profile; @@ -1750,6 +1771,7 @@ pub struct ProfileNew; #[cfg(feature = "olap")] impl ProfileNew { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/account/{account_id}/profile") .app_data(web::Data::new(state)) @@ -1760,6 +1782,10 @@ impl ProfileNew { web::resource("/connectors").route(web::get().to(admin::connector_list_profile)), ) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Gsm; diff --git a/crates/router/src/routes/cards_info.rs b/crates/router/src/routes/cards_info.rs index 889b6e0ec401..1fe2d6db34dc 100644 --- a/crates/router/src/routes/cards_info.rs +++ b/crates/router/src/routes/cards_info.rs @@ -7,6 +7,7 @@ use crate::{ services::{api, authentication as auth}, }; +#[cfg(feature = "v1")] /// Cards Info - Retrieve /// /// Retrieve the card information given the card bin diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs index 4d800ddf1a5e..b3969509bf13 100644 --- a/crates/router/src/routes/currency.rs +++ b/crates/router/src/routes/currency.rs @@ -7,6 +7,7 @@ use crate::{ services::{api, authentication as auth}, }; +#[cfg(feature = "v1")] pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::RetrieveForexFlow; Box::pin(api::server_wrap( @@ -25,6 +26,7 @@ pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> Htt .await } +#[cfg(feature = "v1")] pub async fn convert_forex( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index 5577bb96ef01..1935b8cd00ec 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -13,6 +13,7 @@ use crate::{ types::api::disputes as dispute_types, }; +#[cfg(feature = "v1")] /// Disputes - Retrieve Dispute #[utoipa::path( get, @@ -109,6 +110,7 @@ pub async fn retrieve_disputes_list( .await } +#[cfg(feature = "v1")] /// Disputes - List Disputes for The Given Business Profiles #[utoipa::path( get, @@ -200,6 +202,7 @@ pub async fn get_disputes_filters(state: web::Data, req: HttpRequest) .await } +#[cfg(feature = "v1")] /// Disputes - Disputes Filters Profile #[utoipa::path( get, @@ -241,6 +244,7 @@ pub async fn get_disputes_filters_profile( .await } +#[cfg(feature = "v1")] /// Disputes - Accept Dispute #[utoipa::path( get, @@ -291,6 +295,8 @@ pub async fn accept_dispute( )) .await } + +#[cfg(feature = "v1")] /// Disputes - Submit Dispute Evidence #[utoipa::path( post, @@ -336,6 +342,7 @@ pub async fn submit_dispute_evidence( )) .await } +#[cfg(feature = "v1")] /// Disputes - Attach Evidence to Dispute /// /// To attach an evidence file to dispute @@ -390,6 +397,7 @@ pub async fn attach_dispute_evidence( .await } +#[cfg(feature = "v1")] /// Disputes - Retrieve Dispute #[utoipa::path( get, @@ -506,6 +514,7 @@ pub async fn get_disputes_aggregate( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::DisputesAggregate))] pub async fn get_disputes_aggregate_profile( state: web::Data, diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 2ebb1176aeaa..e68a7a63b338 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -12,6 +12,7 @@ use crate::{ types::api::files, }; +#[cfg(feature = "v1")] /// Files - Create /// /// To create a file @@ -56,6 +57,8 @@ pub async fn files_create( )) .await } + +#[cfg(feature = "v1")] /// Files - Delete /// /// To delete a file @@ -100,6 +103,8 @@ pub async fn files_delete( )) .await } + +#[cfg(feature = "v1")] /// Files - Retrieve /// /// To retrieve a file diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 4df4a1ada087..2fcfdbdc937f 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -85,7 +85,7 @@ pub async fn create_payment_method_api( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, _| async move { + |state, auth: auth::AuthenticationData, req, _| async move { Box::pin(create_payment_method( &state, req, @@ -114,7 +114,7 @@ pub async fn create_payment_method_intent_api( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, _| async move { + |state, auth: auth::AuthenticationData, req, _| async move { Box::pin(payment_method_intent_create( &state, req, @@ -196,7 +196,7 @@ pub async fn payment_method_update_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, req, _| { + |state, auth: auth::AuthenticationData, req, _| { update_payment_method( state, auth.merchant_account, @@ -229,7 +229,7 @@ pub async fn payment_method_retrieve_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, pm, _| { + |state, auth: auth::AuthenticationData, pm, _| { retrieve_payment_method(state, pm, auth.key_store, auth.merchant_account) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -256,7 +256,7 @@ pub async fn payment_method_delete_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, pm, _| { + |state, auth: auth::AuthenticationData, pm, _| { delete_payment_method(state, pm, auth.key_store, auth.merchant_account) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -728,6 +728,7 @@ pub async fn initiate_pm_collect_link_flow( .await } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] /// Generate a form link for collecting payment methods for a customer #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodCollectLink))] pub async fn render_pm_collect_link( @@ -863,6 +864,7 @@ pub async fn payment_method_delete_api( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::ListCountriesCurrencies))] pub async fn list_countries_currencies_for_connector_payment_method( state: web::Data, diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 62cb4a0b79b1..ba785fbee8e6 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -123,7 +123,7 @@ pub async fn payments_create_intent( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, req_state| { + |state, auth: auth::AuthenticationData, req, req_state| { payments::payments_intent_core::< api_types::PaymentCreateIntent, payment_types::PaymentsIntentResponse, @@ -183,7 +183,7 @@ pub async fn payments_get_intent( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, req, req_state| { + |state, auth: auth::AuthenticationData, req, req_state| { payments::payments_intent_core::< api_types::PaymentGetIntent, payment_types::PaymentsIntentResponse, @@ -2103,7 +2103,7 @@ pub async fn payment_confirm_intent( state, &req, internal_payload, - |state, auth: auth::AuthenticationDataV2, req, req_state| async { + |state, auth: auth::AuthenticationData, req, req_state| async { let payment_id = req.global_payment_id; let request = req.payload; diff --git a/crates/router/src/routes/payout_link.rs b/crates/router/src/routes/payout_link.rs index 87157435721c..25528b21ed85 100644 --- a/crates/router/src/routes/payout_link.rs +++ b/crates/router/src/routes/payout_link.rs @@ -12,6 +12,7 @@ use crate::{ }, AppState, }; +#[cfg(feature = "v1")] pub async fn render_payout_link( state: web::Data, req: actix_web::HttpRequest, diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 62d16c4c4d56..2329a48ef2bc 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -49,6 +49,8 @@ pub async fn payouts_create( )) .await } + +#[cfg(all(feature = "v1", feature = "payouts"))] /// Payouts - Retrieve #[instrument(skip_all, fields(flow = ?Flow::PayoutsRetrieve))] pub async fn payouts_retrieve( @@ -245,7 +247,7 @@ pub async fn payouts_list( } /// Payouts - List Profile -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsList))] pub async fn payouts_list_profile( state: web::Data, @@ -323,7 +325,7 @@ pub async fn payouts_list_by_filter( } /// Payouts - Filtered list -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsList))] pub async fn payouts_list_by_filter_profile( state: web::Data, @@ -394,7 +396,7 @@ pub async fn payouts_list_available_filters_for_merchant( } /// Payouts - Available filters for Profile -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsFilter))] pub async fn payouts_list_available_filters_for_profile( state: web::Data, diff --git a/crates/router/src/routes/profiles.rs b/crates/router/src/routes/profiles.rs index cbb7957987fc..6c2e5407cff1 100644 --- a/crates/router/src/routes/profiles.rs +++ b/crates/router/src/routes/profiles.rs @@ -245,6 +245,7 @@ pub async fn profiles_list( .await } +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::ProfileList))] pub async fn profiles_list_at_profile_level( state: web::Data, @@ -337,6 +338,7 @@ pub async fn toggle_extended_card_info( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsList))] pub async fn payment_connector_list_profile( state: web::Data, diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index f3a589524a16..29c759425566 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -15,7 +15,7 @@ use crate::{ routes::AppState, services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_create_config( state: web::Data, @@ -56,6 +56,48 @@ pub async fn routing_create_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all)] +pub async fn routing_create_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + transaction_type: &enums::TransactionType, +) -> impl Responder { + let flow = Flow::RoutingCreateConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, payload, _| { + routing::create_routing_algorithm_under_profile( + state, + auth.merchant_account, + auth.key_store, + Some(auth.profile.get_id().clone()), + payload, + transaction_type, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::RoutingWrite, + minimum_entity_level: EntityType::Profile, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileRoutingWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_link_config( @@ -146,7 +188,7 @@ pub async fn routing_link_config( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_retrieve_config( state: web::Data, @@ -186,6 +228,47 @@ pub async fn routing_retrieve_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all)] +pub async fn routing_retrieve_config( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let algorithm_id = path.into_inner(); + let flow = Flow::RoutingRetrieveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + algorithm_id, + |state, auth: auth::AuthenticationData, algorithm_id, _| { + routing::retrieve_routing_algorithm_from_algorithm_id( + state, + auth.merchant_account, + auth.key_store, + Some(auth.profile.get_id().clone()), + algorithm_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::RoutingRead, + minimum_entity_level: EntityType::Profile, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileRoutingRead, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn list_routing_configs( @@ -226,7 +309,7 @@ pub async fn list_routing_configs( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn list_routing_configs_for_profile( state: web::Data, diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 56ad42947c24..d89f3c65cbe6 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -8,6 +8,7 @@ use crate::{ services::{api, authentication as auth, authorization::permissions::Permission}, }; +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::Verification))] pub async fn apple_pay_merchant_registration( state: web::Data, diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs index b8e089f0660e..76e10652a313 100644 --- a/crates/router/src/routes/verify_connector.rs +++ b/crates/router/src/routes/verify_connector.rs @@ -8,6 +8,7 @@ use crate::{ services::{self, authentication as auth, authorization::permissions::Permission}, }; +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::VerifyPaymentConnector))] pub async fn payment_connector_verify( state: web::Data, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 3e5d92926a5e..857f96eca5de 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -19,8 +19,10 @@ use router_env::logger; use serde::Serialize; use self::blacklist::BlackList; +#[cfg(all(feature = "partial-auth", feature = "v1"))] +use self::detached::ExtractedPayload; #[cfg(feature = "partial-auth")] -use self::detached::{ExtractedPayload, GetAuthType}; +use self::detached::GetAuthType; use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; @@ -30,7 +32,7 @@ use crate::configs::Settings; use crate::consts; #[cfg(feature = "olap")] use crate::core::errors::UserResult; -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] use crate::core::metrics; use crate::{ core::{ @@ -51,6 +53,7 @@ pub mod decision; #[cfg(feature = "partial-auth")] mod detached; +#[cfg(feature = "v1")] #[derive(Clone, Debug)] pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, @@ -60,7 +63,7 @@ pub struct AuthenticationData { #[cfg(feature = "v2")] #[derive(Clone, Debug)] -pub struct AuthenticationDataV2 { +pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, pub key_store: domain::MerchantKeyStore, pub profile: domain::Profile, @@ -277,6 +280,7 @@ impl AuthInfo for () { } } +#[cfg(feature = "v1")] impl AuthInfo for AuthenticationData { fn get_merchant_id(&self) -> Option<&id_type::MerchantId> { Some(self.merchant_account.get_id()) @@ -284,7 +288,7 @@ impl AuthInfo for AuthenticationData { } #[cfg(feature = "v2")] -impl AuthInfo for AuthenticationDataV2 { +impl AuthInfo for AuthenticationData { fn get_merchant_id(&self) -> Option<&id_type::MerchantId> { Some(self.merchant_account.get_id()) } @@ -369,7 +373,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for ApiKeyAuth +impl AuthenticateAndFetch for ApiKeyAuth where A: SessionStateInfo + Sync, { @@ -377,7 +381,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let api_key = get_api_key(request_headers) .change_context(errors::ApiErrorResponse::Unauthorized)? .trim(); @@ -429,7 +433,12 @@ where let profile = state .store() - .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &stored_api_key.merchant_id, + &profile_id, + ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; @@ -443,7 +452,7 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; - let auth = AuthenticationDataV2 { + let auth = AuthenticationData { merchant_account: merchant, key_store, profile, @@ -458,6 +467,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for ApiKeyAuth where @@ -555,7 +565,7 @@ where } } -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] #[async_trait] impl AuthenticateAndFetch for HeaderAuth where @@ -642,10 +652,10 @@ where #[cfg(all(feature = "partial-auth", feature = "v2"))] #[async_trait] -impl AuthenticateAndFetch for HeaderAuth +impl AuthenticateAndFetch for HeaderAuth where A: SessionStateInfo + Sync, - I: AuthenticateAndFetch + I: AuthenticateAndFetch + AuthenticateAndFetch + GetAuthType + Sync @@ -655,7 +665,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let (auth_data, auth_type): (AuthenticationData, AuthenticationType) = self .0 .authenticate_and_fetch(request_headers, state) @@ -667,14 +677,15 @@ where let key_manager_state = &(&state.session_state()).into(); let profile = state .store() - .find_business_profile_by_profile_id( + .find_business_profile_by_merchant_id_profile_id( key_manager_state, &auth_data.key_store, + auth_data.merchant_account.get_id(), &profile_id, ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; - let auth_data_v2 = AuthenticationDataV2 { + let auth_data_v2 = AuthenticationData { merchant_account: auth_data.merchant_account, key_store: auth_data.key_store, profile, @@ -683,7 +694,7 @@ where } } -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] async fn construct_authentication_data( state: &A, merchant_id: &id_type::MerchantId, @@ -870,6 +881,7 @@ where #[derive(Debug)] pub struct AdminApiAuthWithMerchantIdFromRoute(pub id_type::MerchantId); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromRoute where @@ -895,27 +907,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant account for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -930,6 +928,64 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = self.0.clone(); + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + /// A helper struct to extract headers from the request pub(crate) struct HeaderMapStruct<'a> { headers: &'a HeaderMap, @@ -999,6 +1055,7 @@ impl<'a> HeaderMapStruct<'a> { #[derive(Debug)] pub struct AdminApiAuthWithMerchantIdFromHeader; +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromHeader where @@ -1025,27 +1082,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::MerchantAccountNotFound) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant account for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -1059,9 +1102,69 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + #[derive(Debug)] pub struct EphemeralKeyAuth; +// #[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -1088,6 +1191,7 @@ where #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for MerchantIdAuth where @@ -1107,26 +1211,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &self.0, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -1142,6 +1233,61 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for MerchantIdAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let key_manager_state = &(&state.session_state()).into(); + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &self.0, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &self.0, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &self.0, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantId { + merchant_id: auth.merchant_account.get_id().clone(), + }, + )) + } +} + #[derive(Debug)] pub struct PublishableKeyAuth; @@ -1152,6 +1298,7 @@ impl GetAuthType for PublishableKeyAuth { } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for PublishableKeyAuth where @@ -1169,19 +1316,16 @@ where .store() .find_merchant_account_by_publishable_key(key_manager_state, publishable_key) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - }) - .map(|auth| { + .to_not_found_response(errors::ApiErrorResponse::Unauthorized) + .map(|(merchant_account, key_store)| { + let merchant_id = merchant_account.get_id().clone(); ( - auth.clone(), - AuthenticationType::PublishableKey { - merchant_id: auth.merchant_account.get_id().clone(), + AuthenticationData { + merchant_account, + key_store, + profile_id: None, }, + AuthenticationType::PublishableKey { merchant_id }, ) }) } @@ -1189,7 +1333,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for PublishableKeyAuth +impl AuthenticateAndFetch for PublishableKeyAuth where A: SessionStateInfo + Sync, { @@ -1197,43 +1341,34 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let publishable_key = get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; let key_manager_state = &(&state.session_state()).into(); - let authentication_data = state + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let (merchant_account, key_store) = state .store() .find_merchant_account_by_publishable_key(key_manager_state, publishable_key) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - })?; - - let profile_id = HeaderMapStruct::new(request_headers) - .get_id_type_from_header::(headers::X_PROFILE_ID)?; - + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant_id = merchant_account.get_id().clone(); let profile = state .store() - .find_business_profile_by_profile_id( + .find_business_profile_by_merchant_id_profile_id( key_manager_state, - &authentication_data.key_store, + &key_store, + &merchant_id, &profile_id, ) .await - .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { - id: profile_id.get_string_repr().to_owned(), - })?; - - let merchant_id = authentication_data.merchant_account.get_id().clone(); - + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; Ok(( - AuthenticationDataV2 { - merchant_account: authentication_data.merchant_account, - key_store: authentication_data.key_store, + AuthenticationData { + merchant_account, + key_store, profile, }, AuthenticationType::PublishableKey { merchant_id }, @@ -1445,6 +1580,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantFromHeader where @@ -1459,7 +1595,6 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } - let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1511,6 +1646,86 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let merchant_id_from_header = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + + // Check if token has access to MerchantId that has been requested through headers + if payload.merchant_id != merchant_id_from_header { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let key_manager_state = &(&state.session_state()).into(); + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + + Ok(( + auth, + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + #[async_trait] impl AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute where @@ -1543,6 +1758,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantFromRoute where @@ -1602,12 +1818,86 @@ where )) } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + if payload.merchant_id != self.merchant_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(payload.user_id), + }, + )) + } +} pub struct JWTAuthMerchantAndProfileFromRoute { pub merchant_id: id_type::MerchantId, pub profile_id: id_type::ProfileId, pub required_permission: Permission, } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantAndProfileFromRoute where @@ -1682,6 +1972,7 @@ pub struct JWTAuthProfileFromRoute { pub required_permission: Permission, } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthProfileFromRoute where @@ -1759,6 +2050,76 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthProfileFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(payload.user_id), + }, + )) + } +} + pub async fn parse_jwt_payload(headers: &HeaderMap, state: &A) -> RouterResult where T: serde::de::DeserializeOwned, @@ -1777,6 +2138,7 @@ where decode_jwt(&token, state).await } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -1835,7 +2197,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for JWTAuth +impl AuthenticateAndFetch for JWTAuth where A: SessionStateInfo + Sync, { @@ -1843,7 +2205,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); @@ -1869,7 +2231,12 @@ where let profile = state .store() - .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state @@ -1883,7 +2250,7 @@ where .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) .attach_printable("Failed to fetch merchant account for the merchant id")?; let merchant_id = merchant.get_id().clone(); - let auth = AuthenticationDataV2 { + let auth = AuthenticationData { merchant_account: merchant, key_store, profile, @@ -1900,6 +2267,7 @@ where pub type AuthenticationDataWithUserId = (AuthenticationData, String); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -2009,6 +2377,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for DashboardNoPermissionAuth where diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 428506d6c8ab..361fd93d80b0 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 => { @@ -483,7 +484,7 @@ impl ConnectorData { enums::Connector::Stripe => { Ok(ConnectorEnum::Old(Box::new(connector::Stripe::new()))) } - enums::Connector::Wise => Ok(ConnectorEnum::Old(Box::new(&connector::Wise))), + enums::Connector::Wise => Ok(ConnectorEnum::Old(Box::new(connector::Wise::new()))), enums::Connector::Worldline => { Ok(ConnectorEnum::Old(Box::new(&connector::Worldline))) } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 8331dff95c1b..c48fe3320f3d 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1,4 +1,8 @@ -use std::{collections::HashSet, ops, str::FromStr}; +use std::{ + collections::HashSet, + ops::{Deref, Not}, + str::FromStr, +}; use api_models::{ admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, @@ -153,7 +157,7 @@ impl TryFrom for UserEmail { } } -impl ops::Deref for UserEmail { +impl Deref for UserEmail { type Target = Secret; fn deref(&self) -> &Self::Target { @@ -565,10 +569,24 @@ pub struct NewUser { user_id: String, name: UserName, email: UserEmail, - password: Option, + password: Option, new_merchant: NewUserMerchant, } +#[derive(Clone)] +pub struct NewUserPassword { + password: UserPassword, + is_temporary: bool, +} + +impl Deref for NewUserPassword { + type Target = UserPassword; + + fn deref(&self) -> &Self::Target { + &self.password + } +} + impl NewUser { pub fn get_user_id(&self) -> String { self.user_id.clone() @@ -587,7 +605,9 @@ impl NewUser { } pub fn get_password(&self) -> Option { - self.password.clone() + self.password + .as_ref() + .map(|password| password.deref().clone()) } pub async fn insert_user_in_db( @@ -697,7 +717,9 @@ impl TryFrom for storage_user::UserNew { totp_status: TotpStatus::NotSet, totp_secret: None, totp_recovery_codes: None, - last_password_modified_at: value.password.is_some().then_some(now), + last_password_modified_at: value + .password + .and_then(|password_inner| password_inner.is_temporary.not().then_some(now)), }) } } @@ -708,7 +730,10 @@ impl TryFrom for NewUser { fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { let email = value.email.clone().try_into()?; let name = UserName::new(value.name.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let user_id = uuid::Uuid::new_v4().to_string(); let new_merchant = NewUserMerchant::try_from(value)?; @@ -729,7 +754,10 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::try_from(value.email.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { @@ -770,7 +798,10 @@ impl TryFrom<(user_api::CreateInternalUserRequest, id_type::OrganizationId)> for let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::new(value.name.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let new_merchant = NewUserMerchant::try_from((value, org_id))?; Ok(Self { @@ -789,16 +820,21 @@ impl TryFrom for NewUser { fn try_from(value: UserMerchantCreateRequestWithToken) -> Result { let user = value.0.clone(); let new_merchant = NewUserMerchant::try_from(value)?; + let password = user + .0 + .password + .map(UserPassword::new_password_without_validation) + .transpose()? + .map(|password| NewUserPassword { + password, + is_temporary: false, + }); Ok(Self { user_id: user.0.user_id, name: UserName::new(user.0.name)?, email: user.0.email.clone().try_into()?, - password: user - .0 - .password - .map(UserPassword::new_password_without_validation) - .transpose()?, + password, new_merchant, }) } @@ -810,8 +846,10 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.0.email.clone().try_into()?; let name = UserName::new(value.0.name.clone())?; - let password = cfg!(not(feature = "email")) - .then_some(UserPassword::new(password::get_temp_password())?); + let password = cfg!(not(feature = "email")).then_some(NewUserPassword { + password: UserPassword::new(password::get_temp_password())?, + is_temporary: true, + }); let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { 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/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); + } } }; } 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/payme.rs b/crates/router/tests/connectors/payme.rs index 3b4cf5195f5e..dbc3795e7f15 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -1,7 +1,8 @@ use std::str::FromStr; -use api_models::payments::{Address, AddressDetails, OrderDetailsWithAmount}; +use api_models::payments::{Address, AddressDetails}; use common_utils::{pii::Email, types::MinorUnit}; +use diesel_models::types::OrderDetailsWithAmount; use masking::Secret; use router::types::{self, domain, storage::enums, PaymentAddress}; 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/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index 761678046701..984a43d48a76 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -28,7 +28,7 @@ impl utils::Connector for WiseTest { fn get_payout_data(&self) -> Option { use router::connector::Wise; Some(utils::construct_connector_data_old( - Box::new(&Wise), + Box::new(Wise::new()), types::Connector::Wise, api::GetToken::Connector, None, diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index da83bdc7d415..de60c5e8d86a 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -1,8 +1,8 @@ use std::str::FromStr; -use api_models::payments::OrderDetailsWithAmount; use cards::CardNumber; use common_utils::{pii::Email, types::MinorUnit}; +use hyperswitch_domain_models::types::OrderDetailsWithAmount; use masking::Secret; use router::types::{self, domain, storage::enums}; 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/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; } + ]; + }; + }; }; }; } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index be7d71314ac7..723498f7e7a1 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 = "" @@ -115,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/" @@ -210,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