From 53b8fefbc2f8b58d1be7e9f35ca8b7e44e327bfb Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Fri, 3 Nov 2023 00:50:16 +0530 Subject: [PATCH 01/57] ci(postman): Fix collection throwing 404 (#2759) --- postman/collection-dir/stripe/MerchantAccounts/.meta.json | 1 + 1 file changed, 1 insertion(+) diff --git a/postman/collection-dir/stripe/MerchantAccounts/.meta.json b/postman/collection-dir/stripe/MerchantAccounts/.meta.json index ef71d120e462..02ea600d2eb8 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/.meta.json +++ b/postman/collection-dir/stripe/MerchantAccounts/.meta.json @@ -2,6 +2,7 @@ "childrenOrder": [ "Merchant Account - Create", "Merchant Account - Retrieve", + "Merchant Account - List", "Merchant Account - Update" ] } From c0a5e7b7d945095053606e35c9bb23a06090c4e3 Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:01:47 +0530 Subject: [PATCH 02/57] feat(analytics): analytics APIs (#2676) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- Cargo.lock | 281 ++++++++- config/config.example.toml | 14 +- config/docker_compose.toml | 11 + crates/api_models/src/analytics.rs | 137 +++++ crates/api_models/src/analytics/payments.rs | 176 ++++++ crates/api_models/src/analytics/refunds.rs | 177 ++++++ crates/api_models/src/lib.rs | 1 + crates/common_utils/src/custom_serde.rs | 48 ++ crates/router/Cargo.toml | 3 + crates/router/src/analytics.rs | 123 ++++ crates/router/src/analytics/core.rs | 96 ++++ crates/router/src/analytics/errors.rs | 32 ++ crates/router/src/analytics/metrics.rs | 9 + .../router/src/analytics/metrics/request.rs | 60 ++ crates/router/src/analytics/payments.rs | 13 + .../src/analytics/payments/accumulator.rs | 150 +++++ crates/router/src/analytics/payments/core.rs | 129 +++++ .../router/src/analytics/payments/filters.rs | 58 ++ .../router/src/analytics/payments/metrics.rs | 137 +++++ .../payments/metrics/avg_ticket_size.rs | 126 +++++ .../payments/metrics/payment_count.rs | 117 ++++ .../metrics/payment_processed_amount.rs | 128 +++++ .../payments/metrics/payment_success_count.rs | 127 +++++ .../payments/metrics/success_rate.rs | 123 ++++ crates/router/src/analytics/payments/types.rs | 46 ++ crates/router/src/analytics/query.rs | 533 ++++++++++++++++++ crates/router/src/analytics/refunds.rs | 10 + .../src/analytics/refunds/accumulator.rs | 110 ++++ crates/router/src/analytics/refunds/core.rs | 104 ++++ .../router/src/analytics/refunds/filters.rs | 59 ++ .../router/src/analytics/refunds/metrics.rs | 126 +++++ .../analytics/refunds/metrics/refund_count.rs | 116 ++++ .../metrics/refund_processed_amount.rs | 122 ++++ .../refunds/metrics/refund_success_count.rs | 122 ++++ .../refunds/metrics/refund_success_rate.rs | 117 ++++ crates/router/src/analytics/refunds/types.rs | 41 ++ crates/router/src/analytics/routes.rs | 145 +++++ crates/router/src/analytics/sqlx.rs | 386 +++++++++++++ crates/router/src/analytics/types.rs | 114 ++++ crates/router/src/analytics/utils.rs | 22 + crates/router/src/configs/settings.rs | 4 + crates/router/src/lib.rs | 3 + crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 12 + crates/router_env/src/lib.rs | 19 +- crates/router_env/src/metrics.rs | 19 + loadtest/config/development.toml | 12 + 47 files changed, 4507 insertions(+), 13 deletions(-) create mode 100644 crates/api_models/src/analytics.rs create mode 100644 crates/api_models/src/analytics/payments.rs create mode 100644 crates/api_models/src/analytics/refunds.rs create mode 100644 crates/router/src/analytics.rs create mode 100644 crates/router/src/analytics/core.rs create mode 100644 crates/router/src/analytics/errors.rs create mode 100644 crates/router/src/analytics/metrics.rs create mode 100644 crates/router/src/analytics/metrics/request.rs create mode 100644 crates/router/src/analytics/payments.rs create mode 100644 crates/router/src/analytics/payments/accumulator.rs create mode 100644 crates/router/src/analytics/payments/core.rs create mode 100644 crates/router/src/analytics/payments/filters.rs create mode 100644 crates/router/src/analytics/payments/metrics.rs create mode 100644 crates/router/src/analytics/payments/metrics/avg_ticket_size.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_count.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_processed_amount.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_success_count.rs create mode 100644 crates/router/src/analytics/payments/metrics/success_rate.rs create mode 100644 crates/router/src/analytics/payments/types.rs create mode 100644 crates/router/src/analytics/query.rs create mode 100644 crates/router/src/analytics/refunds.rs create mode 100644 crates/router/src/analytics/refunds/accumulator.rs create mode 100644 crates/router/src/analytics/refunds/core.rs create mode 100644 crates/router/src/analytics/refunds/filters.rs create mode 100644 crates/router/src/analytics/refunds/metrics.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_count.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_count.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_rate.rs create mode 100644 crates/router/src/analytics/refunds/types.rs create mode 100644 crates/router/src/analytics/routes.rs create mode 100644 crates/router/src/analytics/sqlx.rs create mode 100644 crates/router/src/analytics/types.rs create mode 100644 crates/router/src/analytics/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 665703f3d505..3ad36999acdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "smallvec", "tokio", @@ -361,6 +361,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -583,6 +589,15 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -1132,10 +1147,21 @@ dependencies = [ "async-trait", "futures-channel", "futures-util", - "parking_lot", + "parking_lot 0.12.1", "tokio", ] +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1591,6 +1617,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + [[package]] name = "crc16" version = "0.4.0" @@ -1649,6 +1690,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1748,7 +1799,7 @@ dependencies = [ "hashbrown 0.14.1", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.8", ] [[package]] @@ -1971,6 +2022,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "drainer" version = "0.1.0" @@ -2137,6 +2194,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "flate2" version = "1.0.27" @@ -2202,7 +2265,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot", + "parking_lot 0.12.1", "rand 0.8.5", "redis-protocol", "semver", @@ -2309,6 +2372,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -2511,12 +2585,28 @@ name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.1", +] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -2530,6 +2620,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3213,7 +3312,7 @@ dependencies = [ "crossbeam-utils", "futures-util", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "quanta", "rustc_version", "scheduled-thread-pool", @@ -3522,6 +3621,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -3529,7 +3639,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -3917,7 +4041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot", + "parking_lot 0.12.1", "scheduled-thread-pool", ] @@ -4234,10 +4358,12 @@ dependencies = [ "aws-sdk-s3", "base64 0.21.4", "bb8", + "bigdecimal", "blake3", "bytes", "cards", "clap", + "common_enums", "common_utils", "config", "data_models", @@ -4286,6 +4412,7 @@ dependencies = [ "sha-1 0.9.8", "signal-hook", "signal-hook-tokio", + "sqlx", "storage_impl", "strum 0.24.1", "tera", @@ -4559,7 +4686,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -4786,7 +4913,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot", + "parking_lot 0.12.1", "serial_test_derive", ] @@ -4979,6 +5106,111 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +dependencies = [ + "ahash 0.7.6", + "atoi", + "base64 0.13.1", + "bigdecimal", + "bitflags 1.3.2", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dirs", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "hkdf", + "hmac", + "indexmap 1.9.3", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "paste", + "percent-encoding", + "rand 0.8.5", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "time", + "tokio-stream", + "url", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +dependencies = [ + "dotenvy", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +dependencies = [ + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + [[package]] name = "storage_impl" version = "0.1.0" @@ -5023,6 +5255,17 @@ dependencies = [ "regex", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" @@ -5257,7 +5500,7 @@ dependencies = [ "futures", "http", "log", - "parking_lot", + "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", @@ -5375,7 +5618,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.4", @@ -5804,6 +6047,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unidecode" version = "0.3.0" @@ -6094,6 +6343,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/config/config.example.toml b/config/config.example.toml index 59083d6c71d3..5943c05e6106 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -433,10 +433,22 @@ apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm - [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" +# Analytics configuration. +[analytics] +source = "sqlx" # The Analytics source/strategy to be used + +[analytics.sqlx] +username = "db_user" # Analytics DB Username +password = "db_pass" # Analytics DB Password +host = "localhost" # Analytics DB Host +port = 5432 # Analytics DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds + # Config for KV setup [kv_config] # TTL for KV in seconds diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 20ca175ceb84..4e630cd46f89 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -317,5 +317,16 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 +[analytics] +source = "sqlx" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "pg" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 + [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs new file mode 100644 index 000000000000..3d94cf21fd26 --- /dev/null +++ b/crates/api_models/src/analytics.rs @@ -0,0 +1,137 @@ +use std::collections::HashSet; + +use time::PrimitiveDateTime; + +use self::{ + payments::{PaymentDimensions, PaymentMetrics}, + refunds::{RefundDimensions, RefundMetrics}, +}; + +pub mod payments; +pub mod refunds; + +#[derive(Debug, serde::Serialize)] +pub struct NameDescription { + pub name: String, + pub desc: String, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInfoResponse { + pub metrics: Vec, + pub download_dimensions: Option>, + pub dimensions: Vec, +} + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct TimeRange { + #[serde(with = "common_utils::custom_serde::iso8601")] + pub start_time: PrimitiveDateTime, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub end_time: Option, +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +pub struct TimeSeries { + pub granularity: Granularity, +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +pub enum Granularity { + #[serde(rename = "G_ONEMIN")] + OneMin, + #[serde(rename = "G_FIVEMIN")] + FiveMin, + #[serde(rename = "G_FIFTEENMIN")] + FifteenMin, + #[serde(rename = "G_THIRTYMIN")] + ThirtyMin, + #[serde(rename = "G_ONEHOUR")] + OneHour, + #[serde(rename = "G_ONEDAY")] + OneDay, +} + +#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: payments::PaymentFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} + +#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRefundMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: refunds::RefundFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} + +#[derive(Debug, serde::Serialize)] +pub struct AnalyticsMetadata { + pub current_time_range: TimeRange, +} + +#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FilterValue { + pub dimension: PaymentDimensions, + pub values: Vec, +} + +#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRefundFilterRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RefundFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RefundFilterValue { + pub dimension: RefundDimensions, + pub values: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MetricsResponse { + pub query_data: Vec, + pub meta_data: [AnalyticsMetadata; 1], +} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs new file mode 100644 index 000000000000..5e1d673736e5 --- /dev/null +++ b/crates/api_models/src/analytics/payments.rs @@ -0,0 +1,176 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; + +use super::{NameDescription, TimeRange}; +use crate::enums::Connector; + +#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +pub struct PaymentFilters { + #[serde(default)] + pub currency: Vec, + #[serde(default)] + pub status: Vec, + #[serde(default)] + pub connector: Vec, + #[serde(default)] + pub auth_type: Vec, + #[serde(default)] + pub payment_method: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PaymentDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + Connector, + PaymentMethod, + Currency, + #[strum(serialize = "authentication_type")] + #[serde(rename = "authentication_type")] + AuthType, + #[strum(serialize = "status")] + #[serde(rename = "status")] + PaymentStatus, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentMetrics { + PaymentSuccessRate, + PaymentCount, + PaymentSuccessCount, + PaymentProcessedAmount, + AvgTicketSize, +} + +pub mod metric_behaviour { + pub struct PaymentSuccessRate; + pub struct PaymentCount; + pub struct PaymentSuccessCount; + pub struct PaymentProcessedAmount; + pub struct AvgTicketSize; +} + +impl From for NameDescription { + fn from(value: PaymentMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: PaymentDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct PaymentMetricsBucketIdentifier { + pub currency: Option, + pub status: Option, + pub connector: Option, + #[serde(rename = "authentication_type")] + pub auth_type: Option, + pub payment_method: Option, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + // Coz FE sucks + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl PaymentMetricsBucketIdentifier { + pub fn new( + currency: Option, + status: Option, + connector: Option, + auth_type: Option, + payment_method: Option, + normalized_time_range: TimeRange, + ) -> Self { + Self { + currency, + status, + connector, + auth_type, + payment_method, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for PaymentMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.currency.hash(state); + self.status.map(|i| i.to_string()).hash(state); + self.connector.hash(state); + self.auth_type.map(|i| i.to_string()).hash(state); + self.payment_method.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for PaymentMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct PaymentMetricsBucketValue { + pub payment_success_rate: Option, + pub payment_count: Option, + pub payment_success_count: Option, + pub payment_processed_amount: Option, + pub avg_ticket_size: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: PaymentMetricsBucketValue, + #[serde(flatten)] + pub dimensions: PaymentMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs new file mode 100644 index 000000000000..1ee05db41f20 --- /dev/null +++ b/crates/api_models/src/analytics/refunds.rs @@ -0,0 +1,177 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use common_enums::enums::{Currency, RefundStatus}; + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +// TODO RefundType common_enums need to mapped to storage_model +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RefundType { + InstantRefund, + #[default] + RegularRefund, + RetryRefund, +} + +use super::{NameDescription, TimeRange}; +#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +pub struct RefundFilters { + #[serde(default)] + pub currency: Vec, + #[serde(default)] + pub refund_status: Vec, + #[serde(default)] + pub connector: Vec, + #[serde(default)] + pub refund_type: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RefundDimensions { + Currency, + RefundStatus, + Connector, + RefundType, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RefundMetrics { + RefundSuccessRate, + RefundCount, + RefundSuccessCount, + RefundProcessedAmount, +} + +pub mod metric_behaviour { + pub struct RefundSuccessRate; + pub struct RefundCount; + pub struct RefundSuccessCount; + pub struct RefundProcessedAmount; +} + +impl From for NameDescription { + fn from(value: RefundMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: RefundDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct RefundMetricsBucketIdentifier { + pub currency: Option, + pub refund_status: Option, + pub connector: Option, + pub refund_type: Option, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl Hash for RefundMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.currency.hash(state); + self.refund_status.map(|i| i.to_string()).hash(state); + self.connector.hash(state); + self.refund_type.hash(state); + self.time_bucket.hash(state); + } +} +impl PartialEq for RefundMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +impl RefundMetricsBucketIdentifier { + pub fn new( + currency: Option, + refund_status: Option, + connector: Option, + refund_type: Option, + normalized_time_range: TimeRange, + ) -> Self { + Self { + currency, + refund_status, + connector, + refund_type, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +#[derive(Debug, serde::Serialize)] +pub struct RefundMetricsBucketValue { + pub refund_success_rate: Option, + pub refund_count: Option, + pub refund_success_count: Option, + pub refund_processed_amount: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct RefundMetricsBucketResponse { + #[serde(flatten)] + pub values: RefundMetricsBucketValue, + #[serde(flatten)] + pub dimensions: RefundMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index dab1b46adbad..b71645e2d163 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] pub mod admin; +pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index d64abe38e5b0..edbfa143a667 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -170,3 +170,51 @@ pub mod json_string { serde_json::from_str(&j).map_err(de::Error::custom) } } + +/// Use a custom ISO 8601 format when serializing and deserializing +/// [`PrimitiveDateTime`][PrimitiveDateTime]. +/// +/// [PrimitiveDateTime]: ::time::PrimitiveDateTime +pub mod iso8601custom { + + use serde::{ser::Error as _, Deserializer, Serialize, Serializer}; + use time::{ + format_description::well_known::{ + iso8601::{Config, EncodedConfig, TimePrecision}, + Iso8601, + }, + serde::iso8601, + PrimitiveDateTime, UtcOffset, + }; + + const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT + .set_time_precision(TimePrecision::Second { + decimal_digits: None, + }) + .encode(); + + /// Serialize a [`PrimitiveDateTime`] using the well-known ISO 8601 format. + pub fn serialize(date_time: &PrimitiveDateTime, serializer: S) -> Result + where + S: Serializer, + { + date_time + .assume_utc() + .format(&Iso8601::) + .map_err(S::Error::custom)? + .replace('T', " ") + .replace('Z', "") + .serialize(serializer) + } + + /// Deserialize an [`PrimitiveDateTime`] from its ISO 8601 representation. + pub fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + iso8601::deserialize(deserializer).map(|offset_date_time| { + let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC); + PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time()) + }) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 81b23314ffb8..0349a6b9d4c1 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -41,6 +41,7 @@ aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } base64 = "0.21.2" bb8 = "0.8" +bigdecimal = "0.3.1" blake3 = "1.3.3" bytes = "1.4.0" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } @@ -78,6 +79,7 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" signal-hook = "0.3.15" strum = { version = "0.24.1", features = ["derive"] } +sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } @@ -95,6 +97,7 @@ digest = "0.9" api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } cards = { version = "0.1.0", path = "../cards" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } +common_enums = { version = "0.1.0", path = "../common_enums"} external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs new file mode 100644 index 000000000000..fbb848ea963d --- /dev/null +++ b/crates/router/src/analytics.rs @@ -0,0 +1,123 @@ +mod core; +mod errors; +pub mod metrics; +mod payments; +mod query; +mod refunds; +pub mod routes; + +mod sqlx; +mod types; +mod utils; + +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use router_env::{instrument, tracing}; + +use self::{ + payments::metrics::{PaymentMetric, PaymentMetricRow}, + refunds::metrics::{RefundMetric, RefundMetricRow}, + sqlx::SqlxClient, +}; +use crate::configs::settings::Database; + +#[derive(Clone, Debug)] +pub enum AnalyticsProvider { + Sqlx(SqlxClient), +} + +impl AnalyticsProvider { + #[instrument(skip_all)] + pub async fn get_payment_metrics( + &self, + metric: &PaymentMetrics, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_refund_metrics( + &self, + metric: &RefundMetrics, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } + + pub async fn from_conf( + config: &AnalyticsConfig, + #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, + ) -> Self { + match config { + AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( + SqlxClient::from_conf( + sqlx, + #[cfg(feature = "kms")] + kms_client, + ) + .await, + ), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum AnalyticsConfig { + Sqlx { sqlx: Database }, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self::Sqlx { + sqlx: Database::default(), + } + } +} diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs new file mode 100644 index 000000000000..bf124a6c0e85 --- /dev/null +++ b/crates/router/src/analytics/core.rs @@ -0,0 +1,96 @@ +use api_models::analytics::{ + payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, + GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, + RefundFiltersResponse, +}; +use error_stack::ResultExt; + +use super::{ + errors::{self, AnalyticsError}, + payments::filters::{get_payment_filter_for_dimension, FilterRow}, + refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, + types::AnalyticsDomain, + utils, AnalyticsProvider, +}; +use crate::{services::ApplicationResponse, types::domain}; + +pub type AnalyticsApiResponse = errors::AnalyticsResult>; + +pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { + let info = match domain { + AnalyticsDomain::Payments => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, + AnalyticsDomain::Refunds => GetInfoResponse { + metrics: utils::get_refund_metrics_info(), + download_dimensions: None, + dimensions: utils::get_refund_dimensions(), + }, + }; + Ok(ApplicationResponse::Json(info)) +} + +pub async fn payment_filters_core( + pool: AnalyticsProvider, + req: GetPaymentFiltersRequest, + merchant: domain::MerchantAccount, +) -> AnalyticsApiResponse { + let mut res = PaymentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool.clone() { + AnalyticsProvider::Sqlx(pool) => { + get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: FilterRow| match dim { + PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentDimensions::Connector => fil.connector, + PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentMethod => fil.payment_method, + }) + .collect::>(); + res.query_data.push(FilterValue { + dimension: dim, + values, + }) + } + + Ok(ApplicationResponse::Json(res)) +} + +pub async fn refund_filter_core( + pool: AnalyticsProvider, + req: GetRefundFilterRequest, + merchant: domain::MerchantAccount, +) -> AnalyticsApiResponse { + let mut res = RefundFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool.clone() { + AnalyticsProvider::Sqlx(pool) => { + get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: RefundFilterRow| match dim { + RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), + RefundDimensions::Connector => fil.connector, + RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), + }) + .collect::>(); + res.query_data.push(RefundFilterValue { + dimension: dim, + values, + }) + } + Ok(ApplicationResponse::Json(res)) +} diff --git a/crates/router/src/analytics/errors.rs b/crates/router/src/analytics/errors.rs new file mode 100644 index 000000000000..da0b2f239cd7 --- /dev/null +++ b/crates/router/src/analytics/errors.rs @@ -0,0 +1,32 @@ +use api_models::errors::types::{ApiError, ApiErrorResponse}; +use common_utils::errors::{CustomResult, ErrorSwitch}; + +pub type AnalyticsResult = CustomResult; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum AnalyticsError { + #[allow(dead_code)] + #[error("Not implemented: {0}")] + NotImplemented(&'static str), + #[error("Unknown Analytics Error")] + UnknownError, +} + +impl ErrorSwitch for AnalyticsError { + fn switch(&self) -> ApiErrorResponse { + match self { + Self::NotImplemented(feature) => ApiErrorResponse::NotImplemented(ApiError::new( + "IR", + 0, + format!("{feature} is not implemented."), + None, + )), + Self::UnknownError => ApiErrorResponse::InternalServerError(ApiError::new( + "HE", + 0, + "Something went wrong", + None, + )), + } + } +} diff --git a/crates/router/src/analytics/metrics.rs b/crates/router/src/analytics/metrics.rs new file mode 100644 index 000000000000..6222315a8c06 --- /dev/null +++ b/crates/router/src/analytics/metrics.rs @@ -0,0 +1,9 @@ +use router_env::{global_meter, histogram_metric, histogram_metric_u64, metrics_context}; + +metrics_context!(CONTEXT); +global_meter!(GLOBAL_METER, "ROUTER_API"); + +histogram_metric!(METRIC_FETCH_TIME, GLOBAL_METER); +histogram_metric_u64!(BUCKETS_FETCHED, GLOBAL_METER); + +pub mod request; diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/router/src/analytics/metrics/request.rs new file mode 100644 index 000000000000..b7c202f2db25 --- /dev/null +++ b/crates/router/src/analytics/metrics/request.rs @@ -0,0 +1,60 @@ +pub fn add_attributes>( + key: &'static str, + value: T, +) -> router_env::opentelemetry::KeyValue { + router_env::opentelemetry::KeyValue::new(key, value) +} + +#[inline] +pub async fn record_operation_time( + future: F, + metric: &once_cell::sync::Lazy>, + metric_name: &api_models::analytics::payments::PaymentMetrics, + source: &crate::analytics::AnalyticsProvider, +) -> R +where + F: futures::Future, +{ + let (result, time) = time_future(future).await; + let attributes = &[ + add_attributes("metric_name", metric_name.to_string()), + add_attributes( + "source", + match source { + crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", + }, + ), + ]; + let value = time.as_secs_f64(); + metric.record(&super::CONTEXT, value, attributes); + + router_env::logger::debug!("Attributes: {:?}, Time: {}", attributes, value); + result +} + +use std::time; + +#[inline] +pub async fn time_future(future: F) -> (R, time::Duration) +where + F: futures::Future, +{ + let start = time::Instant::now(); + let result = future.await; + let time_spent = start.elapsed(); + (result, time_spent) +} + +#[macro_export] +macro_rules! histogram_metric { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); + }; + ($name:ident, $meter:ident, $description:literal) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); + }; +} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs new file mode 100644 index 000000000000..527bf75a3c72 --- /dev/null +++ b/crates/router/src/analytics/payments.rs @@ -0,0 +1,13 @@ +pub mod accumulator; +mod core; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; + +pub trait PaymentAnalytics: + metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics +{ +} + +pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/router/src/analytics/payments/accumulator.rs new file mode 100644 index 000000000000..5eebd0974693 --- /dev/null +++ b/crates/router/src/analytics/payments/accumulator.rs @@ -0,0 +1,150 @@ +use api_models::analytics::payments::PaymentMetricsBucketValue; +use common_enums::enums as storage_enums; +use router_env::logger; + +use super::metrics::PaymentMetricRow; + +#[derive(Debug, Default)] +pub struct PaymentMetricsAccumulator { + pub payment_success_rate: SuccessRateAccumulator, + pub payment_count: CountAccumulator, + pub payment_success: CountAccumulator, + pub processed_amount: SumAccumulator, + pub avg_ticket_size: AverageAccumulator, +} + +#[derive(Debug, Default)] +pub struct SuccessRateAccumulator { + pub success: i64, + pub total: i64, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option, +} + +#[derive(Debug, Default)] +pub struct AverageAccumulator { + pub total: u32, + pub count: u32, +} + +pub trait PaymentMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl PaymentMetricAccumulator for SuccessRateAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + if let Some(ref status) = metrics.status { + if status.as_ref() == &storage_enums::AttemptStatus::Charged { + self.success += metrics.count.unwrap_or_default(); + } + }; + self.total += metrics.count.unwrap_or_default(); + } + + fn collect(self) -> Self::MetricOutput { + if self.total <= 0 { + None + } else { + Some( + f64::from(u32::try_from(self.success).ok()?) * 100.0 + / f64::from(u32::try_from(self.total).ok()?), + ) + } + } +} + +impl PaymentMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl PaymentMetricAccumulator for SumAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + self.total = match ( + self.total, + metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + u64::try_from(self.total.unwrap_or(0)).ok() + } +} + +impl PaymentMetricAccumulator for AverageAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + let total = metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_u32); + let count = metrics.count.and_then(|total| u32::try_from(total).ok()); + + match (total, count) { + (Some(total), Some(count)) => { + self.total += total; + self.count += count; + } + _ => { + logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); + } + } + } + + fn collect(self) -> Self::MetricOutput { + if self.count == 0 { + None + } else { + Some(f64::from(self.total) / f64::from(self.count)) + } + } +} + +impl PaymentMetricsAccumulator { + pub fn collect(self) -> PaymentMetricsBucketValue { + PaymentMetricsBucketValue { + payment_success_rate: self.payment_success_rate.collect(), + payment_count: self.payment_count.collect(), + payment_success_count: self.payment_success.collect(), + payment_processed_amount: self.processed_amount.collect(), + avg_ticket_size: self.avg_ticket_size.collect(), + } + } +} diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs new file mode 100644 index 000000000000..23eca8879a70 --- /dev/null +++ b/crates/router/src/analytics/payments/core.rs @@ -0,0 +1,129 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, + AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::PaymentMetricsAccumulator; +use crate::{ + analytics::{ + core::AnalyticsApiResponse, errors::AnalyticsError, metrics, + payments::PaymentMetricAccumulator, AnalyticsProvider, + }, + services::ApplicationResponse, + types::domain, +}; + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: AnalyticsProvider, + merchant_account: domain::MerchantAccount, + req: GetPaymentMetricRequest, +) -> AnalyticsApiResponse> { + let mut metrics_accumulator: HashMap< + PaymentMetricsBucketIdentifier, + PaymentMetricsAccumulator, + > = HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id = merchant_account.merchant_id.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_query", + payment_metric = metric_type.as_ref() + ); + set.spawn( + async move { + let data = pool + .get_payment_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes( + "source", + match pool { + crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", + }, + ), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + PaymentMetrics::PaymentSuccessRate => metrics_builder + .payment_success_rate + .add_metrics_bucket(&value), + PaymentMetrics::PaymentCount => { + metrics_builder.payment_count.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + PaymentMetrics::AvgTicketSize => { + metrics_builder.avg_ticket_size.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(ApplicationResponse::Json(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + })) +} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/router/src/analytics/payments/filters.rs new file mode 100644 index 000000000000..f009aaa76329 --- /dev/null +++ b/crates/router/src/analytics/payments/filters.rs @@ -0,0 +1,58 @@ +use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; +use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, + LoadRow, + }, +}; + +pub trait PaymentFilterAnalytics: LoadRow {} + +pub async fn get_payment_filter_for_dimension( + dimension: PaymentDimensions, + merchant: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + PaymentFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +pub struct FilterRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, +} diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/router/src/analytics/payments/metrics.rs new file mode 100644 index 000000000000..f492e5bd4df9 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics.rs @@ -0,0 +1,137 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +mod avg_ticket_size; +mod payment_count; +mod payment_processed_amount; +mod payment_success_count; +mod success_rate; + +use avg_ticket_size::AvgTicketSize; +use payment_count::PaymentCount; +use payment_processed_amount::PaymentProcessedAmount; +use payment_success_count::PaymentSuccessCount; +use success_rate::PaymentSuccessRate; + +#[derive(Debug, PartialEq, Eq)] +pub struct PaymentMetricRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub total: Option, + pub count: Option, + pub start_bucket: Option, + pub end_bucket: Option, +} + +pub trait PaymentMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait PaymentMetric +where + T: AnalyticsDataSource + PaymentMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl PaymentMetric for PaymentMetrics +where + T: AnalyticsDataSource + PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentSuccessRate => { + PaymentSuccessRate + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentCount => { + PaymentCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentSuccessCount => { + PaymentSuccessCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentProcessedAmount => { + PaymentProcessedAmount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AvgTicketSize => { + AvgTicketSize + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs new file mode 100644 index 000000000000..2230d870e68a --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::{PaymentMetric, PaymentMetricRow}; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AvgTicketSize; + +#[async_trait::async_trait] +impl PaymentMetric for AvgTicketSize +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/router/src/analytics/payments/metrics/payment_count.rs new file mode 100644 index 000000000000..661cec3dac36 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_count.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs new file mode 100644 index 000000000000..2ec0c6f18f9c --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs @@ -0,0 +1,128 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentProcessedAmount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentProcessedAmount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/router/src/analytics/payments/metrics/payment_success_count.rs new file mode 100644 index 000000000000..8245fe7aeb88 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_success_count.rs @@ -0,0 +1,127 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentSuccessCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/router/src/analytics/payments/metrics/success_rate.rs new file mode 100644 index 000000000000..c63956d4b157 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/success_rate.rs @@ -0,0 +1,123 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessRate; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentSuccessRate +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(PaymentDimensions::PaymentStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/types.rs b/crates/router/src/analytics/payments/types.rs new file mode 100644 index 000000000000..fdfbedef383d --- /dev/null +++ b/crates/router/src/analytics/payments/types.rs @@ -0,0 +1,46 @@ +use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; +use error_stack::ResultExt; + +use crate::analytics::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for PaymentFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + + if !self.status.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::PaymentStatus, &self.status) + .attach_printable("Error adding payment status filter")?; + } + + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + + if !self.auth_type.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::AuthType, &self.auth_type) + .attach_printable("Error adding auth type filter")?; + } + + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) + .attach_printable("Error adding payment method filter")?; + } + Ok(()) + } +} diff --git a/crates/router/src/analytics/query.rs b/crates/router/src/analytics/query.rs new file mode 100644 index 000000000000..b1f621d8153d --- /dev/null +++ b/crates/router/src/analytics/query.rs @@ -0,0 +1,533 @@ +#![allow(dead_code)] +use std::marker::PhantomData; + +use api_models::{ + analytics::{ + self as analytics_api, + payments::PaymentDimensions, + refunds::{RefundDimensions, RefundType}, + Granularity, + }, + enums::Connector, + refunds::RefundStatus, +}; +use common_enums::{ + enums as storage_enums, + enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, +}; +use common_utils::errors::{CustomResult, ParsingError}; +use error_stack::{IntoReport, ResultExt}; +use router_env::logger; + +use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; +use crate::analytics::types::QueryExecutionError; +pub type QueryResult = error_stack::Result; +pub trait QueryFilter +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; +} + +pub trait GroupByClause +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_group_by_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; +} + +pub trait SeriesBucket { + type SeriesType; + type GranularityLevel; + + fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel; + + fn get_bucket_size(&self) -> u8; + + fn clip_to_start( + &self, + value: Self::SeriesType, + ) -> error_stack::Result; + + fn clip_to_end( + &self, + value: Self::SeriesType, + ) -> error_stack::Result; +} + +impl QueryFilter for analytics_api::TimeRange +where + T: AnalyticsDataSource, + time::PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + builder.add_custom_filter_clause("created_at", self.start_time, FilterTypes::Gte)?; + if let Some(end) = self.end_time { + builder.add_custom_filter_clause("created_at", end, FilterTypes::Lte)?; + } + Ok(()) + } +} + +impl GroupByClause for Granularity { + fn set_group_by_clause( + &self, + builder: &mut QueryBuilder, + ) -> QueryResult<()> { + let trunc_scale = self.get_lowest_common_granularity_level(); + + let granularity_bucket_scale = match self { + Self::OneMin => None, + Self::FiveMin | Self::FifteenMin | Self::ThirtyMin => Some("minute"), + Self::OneHour | Self::OneDay => None, + }; + + let granularity_divisor = self.get_bucket_size(); + + builder + .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) + .attach_printable("Error adding time prune group by")?; + if let Some(scale) = granularity_bucket_scale { + builder + .add_group_by_clause(format!( + "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" + )) + .attach_printable("Error adding time binning group by")?; + } + Ok(()) + } +} + +#[derive(strum::Display)] +#[strum(serialize_all = "lowercase")] +pub enum TimeGranularityLevel { + Minute, + Hour, + Day, +} + +impl SeriesBucket for Granularity { + type SeriesType = time::PrimitiveDateTime; + + type GranularityLevel = TimeGranularityLevel; + + fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel { + match self { + Self::OneMin => TimeGranularityLevel::Minute, + Self::FiveMin | Self::FifteenMin | Self::ThirtyMin | Self::OneHour => { + TimeGranularityLevel::Hour + } + Self::OneDay => TimeGranularityLevel::Day, + } + } + + fn get_bucket_size(&self) -> u8 { + match self { + Self::OneMin => 60, + Self::FiveMin => 5, + Self::FifteenMin => 15, + Self::ThirtyMin => 30, + Self::OneHour => 60, + Self::OneDay => 24, + } + } + + fn clip_to_start( + &self, + value: Self::SeriesType, + ) -> error_stack::Result { + let clip_start = |value: u8, modulo: u8| -> u8 { value - value % modulo }; + + let clipped_time = match ( + self.get_lowest_common_granularity_level(), + self.get_bucket_size(), + ) { + (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT + .replace_second(clip_start(value.second(), i)) + .and_then(|t| t.replace_minute(value.minute())) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT + .replace_minute(clip_start(value.minute(), i)) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Day, i) => { + time::Time::MIDNIGHT.replace_hour(clip_start(value.hour(), i)) + } + } + .into_report() + .change_context(PostProcessingError::BucketClipping)?; + + Ok(value.replace_time(clipped_time)) + } + + fn clip_to_end( + &self, + value: Self::SeriesType, + ) -> error_stack::Result { + let clip_end = |value: u8, modulo: u8| -> u8 { value + modulo - 1 - value % modulo }; + + let clipped_time = match ( + self.get_lowest_common_granularity_level(), + self.get_bucket_size(), + ) { + (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT + .replace_second(clip_end(value.second(), i)) + .and_then(|t| t.replace_minute(value.minute())) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT + .replace_minute(clip_end(value.minute(), i)) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Day, i) => { + time::Time::MIDNIGHT.replace_hour(clip_end(value.hour(), i)) + } + } + .into_report() + .change_context(PostProcessingError::BucketClipping) + .attach_printable_lazy(|| format!("Bucket Clip Error: {value}"))?; + + Ok(value.replace_time(clipped_time)) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum QueryBuildingError { + #[allow(dead_code)] + #[error("Not Implemented: {0}")] + NotImplemented(String), + #[error("Failed to Serialize to SQL")] + SqlSerializeError, + #[error("Failed to build sql query: {0}")] + InvalidQuery(&'static str), +} + +#[derive(thiserror::Error, Debug)] +pub enum PostProcessingError { + #[error("Error Clipping values to bucket sizes")] + BucketClipping, +} + +#[derive(Debug)] +pub enum Aggregate { + Count { + field: Option, + alias: Option<&'static str>, + }, + Sum { + field: R, + alias: Option<&'static str>, + }, + Min { + field: R, + alias: Option<&'static str>, + }, + Max { + field: R, + alias: Option<&'static str>, + }, +} + +#[derive(Debug)] +pub struct QueryBuilder +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + columns: Vec, + filters: Vec<(String, FilterTypes, String)>, + group_by: Vec, + having: Option>, + table: AnalyticsCollection, + distinct: bool, + db_type: PhantomData, +} + +pub trait ToSql { + fn to_sql(&self) -> error_stack::Result; +} + +/// Implement `ToSql` on arrays of types that impl `ToString`. +macro_rules! impl_to_sql_for_to_string { + ($($type:ty),+) => { + $( + impl ToSql for $type { + fn to_sql(&self) -> error_stack::Result { + Ok(self.to_string()) + } + } + )+ + }; +} + +impl_to_sql_for_to_string!( + String, + &str, + &PaymentDimensions, + &RefundDimensions, + PaymentDimensions, + RefundDimensions, + PaymentMethod, + AuthenticationType, + Connector, + AttemptStatus, + RefundStatus, + storage_enums::RefundStatus, + Currency, + RefundType, + &String, + &bool, + &u64 +); + +#[allow(dead_code)] +#[derive(Debug)] +pub enum FilterTypes { + Equal, + EqualBool, + In, + Gte, + Lte, + Gt, +} + +impl QueryBuilder +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + pub fn new(table: AnalyticsCollection) -> Self { + Self { + columns: Default::default(), + filters: Default::default(), + group_by: Default::default(), + having: Default::default(), + table, + distinct: Default::default(), + db_type: Default::default(), + } + } + + pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { + self.columns.push( + column + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing select column")?, + ); + Ok(()) + } + + pub fn set_distinct(&mut self) { + self.distinct = true + } + + pub fn add_filter_clause( + &mut self, + key: impl ToSql, + value: impl ToSql, + ) -> QueryResult<()> { + self.add_custom_filter_clause(key, value, FilterTypes::Equal) + } + + pub fn add_bool_filter_clause( + &mut self, + key: impl ToSql, + value: impl ToSql, + ) -> QueryResult<()> { + self.add_custom_filter_clause(key, value, FilterTypes::EqualBool) + } + + pub fn add_custom_filter_clause( + &mut self, + lhs: impl ToSql, + rhs: impl ToSql, + comparison: FilterTypes, + ) -> QueryResult<()> { + self.filters.push(( + lhs.to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing filter key")?, + comparison, + rhs.to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing filter value")?, + )); + Ok(()) + } + + pub fn add_filter_in_range_clause( + &mut self, + key: impl ToSql, + values: &[impl ToSql], + ) -> QueryResult<()> { + let list = values + .iter() + .map(|i| { + // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection + i.to_sql().map(|s| { + let trimmed_str = s.replace(' ', ""); + format!("'{trimmed_str}'") + }) + }) + .collect::, ParsingError>>() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing range filter value")? + .join(", "); + self.add_custom_filter_clause(key, list, FilterTypes::In) + } + + pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { + self.group_by.push( + column + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing group by field")?, + ); + Ok(()) + } + + pub fn add_granularity_in_mins(&mut self, granularity: &Granularity) -> QueryResult<()> { + let interval = match granularity { + Granularity::OneMin => "1", + Granularity::FiveMin => "5", + Granularity::FifteenMin => "15", + Granularity::ThirtyMin => "30", + Granularity::OneHour => "60", + Granularity::OneDay => "1440", + }; + let _ = self.add_select_column(format!( + "toStartOfInterval(created_at, INTERVAL {interval} MINUTE) as time_bucket" + )); + Ok(()) + } + + fn get_filter_clause(&self) -> String { + self.filters + .iter() + .map(|(l, op, r)| match op { + FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::Equal => format!("{l} = '{r}'"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= '{r}'"), + FilterTypes::Gt => format!("{l} > {r}"), + FilterTypes::Lte => format!("{l} <= '{r}'"), + }) + .collect::>() + .join(" AND ") + } + + fn get_select_clause(&self) -> String { + self.columns.join(", ") + } + + fn get_group_by_clause(&self) -> String { + self.group_by.join(", ") + } + + #[allow(dead_code)] + pub fn add_having_clause( + &mut self, + aggregate: Aggregate, + filter_type: FilterTypes, + value: impl ToSql, + ) -> QueryResult<()> + where + Aggregate: ToSql, + { + let aggregate = aggregate + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing having aggregate")?; + let value = value + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing having value")?; + let entry = (aggregate, filter_type, value); + if let Some(having) = &mut self.having { + having.push(entry); + } else { + self.having = Some(vec![entry]); + } + Ok(()) + } + + pub fn get_filter_type_clause(&self) -> Option { + self.having.as_ref().map(|vec| { + vec.iter() + .map(|(l, op, r)| match op { + FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= {r}"), + FilterTypes::Lte => format!("{l} < {r}"), + FilterTypes::Gt => format!("{l} > {r}"), + }) + .collect::>() + .join(" AND ") + }) + } + + pub fn build_query(&mut self) -> QueryResult + where + Aggregate<&'static str>: ToSql, + { + if self.columns.is_empty() { + Err(QueryBuildingError::InvalidQuery( + "No select fields provided", + )) + .into_report()?; + } + let mut query = String::from("SELECT "); + + if self.distinct { + query.push_str("DISTINCT "); + } + + query.push_str(&self.get_select_clause()); + + query.push_str(" FROM "); + + query.push_str( + &self + .table + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing table value")?, + ); + + if !self.filters.is_empty() { + query.push_str(" WHERE "); + query.push_str(&self.get_filter_clause()); + } + + if !self.group_by.is_empty() { + query.push_str(" GROUP BY "); + query.push_str(&self.get_group_by_clause()); + } + + if self.having.is_some() { + if let Some(condition) = self.get_filter_type_clause() { + query.push_str(" HAVING "); + query.push_str(condition.as_str()); + } + } + Ok(query) + } + + pub async fn execute_query( + &mut self, + store: &P, + ) -> CustomResult, QueryExecutionError>, QueryBuildingError> + where + P: LoadRow, + Aggregate<&'static str>: ToSql, + { + let query = self + .build_query() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Failed to execute query")?; + logger::debug!(?query); + Ok(store.load_results(query.as_str()).await) + } +} diff --git a/crates/router/src/analytics/refunds.rs b/crates/router/src/analytics/refunds.rs new file mode 100644 index 000000000000..a8b52effe76d --- /dev/null +++ b/crates/router/src/analytics/refunds.rs @@ -0,0 +1,10 @@ +pub mod accumulator; +mod core; + +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; + +pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} +pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/router/src/analytics/refunds/accumulator.rs new file mode 100644 index 000000000000..3d0c0e659f6c --- /dev/null +++ b/crates/router/src/analytics/refunds/accumulator.rs @@ -0,0 +1,110 @@ +use api_models::analytics::refunds::RefundMetricsBucketValue; +use common_enums::enums as storage_enums; + +use super::metrics::RefundMetricRow; +#[derive(Debug, Default)] +pub struct RefundMetricsAccumulator { + pub refund_success_rate: SuccessRateAccumulator, + pub refund_count: CountAccumulator, + pub refund_success: CountAccumulator, + pub processed_amount: SumAccumulator, +} + +#[derive(Debug, Default)] +pub struct SuccessRateAccumulator { + pub success: i64, + pub total: i64, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option, +} + +pub trait RefundMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl RefundMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl RefundMetricAccumulator for SumAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + self.total = match ( + self.total, + metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.total.and_then(|i| u64::try_from(i).ok()) + } +} + +impl RefundMetricAccumulator for SuccessRateAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + if let Some(ref refund_status) = metrics.refund_status { + if refund_status.as_ref() == &storage_enums::RefundStatus::Success { + self.success += metrics.count.unwrap_or_default(); + } + }; + self.total += metrics.count.unwrap_or_default(); + } + + fn collect(self) -> Self::MetricOutput { + if self.total <= 0 { + None + } else { + Some( + f64::from(u32::try_from(self.success).ok()?) * 100.0 + / f64::from(u32::try_from(self.total).ok()?), + ) + } + } +} + +impl RefundMetricsAccumulator { + pub fn collect(self) -> RefundMetricsBucketValue { + RefundMetricsBucketValue { + refund_success_rate: self.refund_success_rate.collect(), + refund_count: self.refund_count.collect(), + refund_success_count: self.refund_success.collect(), + refund_processed_amount: self.processed_amount.collect(), + } + } +} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs new file mode 100644 index 000000000000..4c2d2c394181 --- /dev/null +++ b/crates/router/src/analytics/refunds/core.rs @@ -0,0 +1,104 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, + AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, +}; + +use super::RefundMetricsAccumulator; +use crate::{ + analytics::{ + core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, + AnalyticsProvider, + }, + services::ApplicationResponse, + types::domain, +}; + +pub async fn get_metrics( + pool: AnalyticsProvider, + merchant_account: domain::MerchantAccount, + req: GetRefundMetricRequest, +) -> AnalyticsApiResponse> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id = merchant_account.merchant_id.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_refund_query", + refund_metric = metric_type.as_ref() + ); + set.spawn( + async move { + let data = pool + .get_refund_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + for (id, value) in data? { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + RefundMetrics::RefundSuccessRate => metrics_builder + .refund_success_rate + .add_metrics_bucket(&value), + RefundMetrics::RefundCount => { + metrics_builder.refund_count.add_metrics_bucket(&value) + } + RefundMetrics::RefundSuccessCount => { + metrics_builder.refund_success.add_metrics_bucket(&value) + } + RefundMetrics::RefundProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| RefundMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(ApplicationResponse::Json(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + })) +} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/router/src/analytics/refunds/filters.rs new file mode 100644 index 000000000000..6b45e9194fad --- /dev/null +++ b/crates/router/src/analytics/refunds/filters.rs @@ -0,0 +1,59 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundType}, + Granularity, TimeRange, +}; +use common_enums::enums::{Currency, RefundStatus}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, + LoadRow, + }, +}; +pub trait RefundFilterAnalytics: LoadRow {} + +pub async fn get_refund_filter_for_dimension( + dimension: RefundDimensions, + merchant: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + RefundFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +pub struct RefundFilterRow { + pub currency: Option>, + pub refund_status: Option>, + pub connector: Option, + pub refund_type: Option>, +} diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/router/src/analytics/refunds/metrics.rs new file mode 100644 index 000000000000..d4f509b4a1e3 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + refunds::{ + RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier, RefundType, + }, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use time::PrimitiveDateTime; +mod refund_count; +mod refund_processed_amount; +mod refund_success_count; +mod refund_success_rate; +use refund_count::RefundCount; +use refund_processed_amount::RefundProcessedAmount; +use refund_success_count::RefundSuccessCount; +use refund_success_rate::RefundSuccessRate; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +#[derive(Debug, Eq, PartialEq)] +pub struct RefundMetricRow { + pub currency: Option>, + pub refund_status: Option>, + pub connector: Option, + pub refund_type: Option>, + pub total: Option, + pub count: Option, + pub start_bucket: Option, + pub end_bucket: Option, +} + +pub trait RefundMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait RefundMetric +where + T: AnalyticsDataSource + RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl RefundMetric for RefundMetrics +where + T: AnalyticsDataSource + RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::RefundSuccessRate => { + RefundSuccessRate::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundCount => { + RefundCount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundSuccessCount => { + RefundSuccessCount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundProcessedAmount => { + RefundProcessedAmount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/router/src/analytics/refunds/metrics/refund_count.rs new file mode 100644 index 000000000000..471327235073 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_count.rs @@ -0,0 +1,116 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RefundCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0), + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs new file mode 100644 index 000000000000..c5f3a706aaef --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct RefundProcessedAmount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundProcessedAmount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "refund_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/router/src/analytics/refunds/metrics/refund_success_count.rs new file mode 100644 index 000000000000..0c8032908fd7 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_success_count.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RefundSuccessCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs new file mode 100644 index 000000000000..42f9ccf8d3c0 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct RefundSuccessRate {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessRate +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(RefundDimensions::RefundStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/router/src/analytics/refunds/types.rs new file mode 100644 index 000000000000..fbfd69972671 --- /dev/null +++ b/crates/router/src/analytics/refunds/types.rs @@ -0,0 +1,41 @@ +use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; +use error_stack::ResultExt; + +use crate::analytics::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for RefundFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + + if !self.refund_status.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::RefundStatus, &self.refund_status) + .attach_printable("Error adding refund status filter")?; + } + + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + + if !self.refund_type.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::RefundType, &self.refund_type) + .attach_printable("Error adding auth type filter")?; + } + + Ok(()) + } +} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs new file mode 100644 index 000000000000..298ec61ec903 --- /dev/null +++ b/crates/router/src/analytics/routes.rs @@ -0,0 +1,145 @@ +use actix_web::{web, Responder, Scope}; +use api_models::analytics::{ + GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, + GetRefundMetricRequest, +}; +use router_env::AnalyticsFlow; + +use super::{core::*, payments, refunds, types::AnalyticsDomain}; +use crate::{ + core::api_locking, + services::{api, authentication as auth, authentication::AuthenticationData}, + AppState, +}; + +pub struct Analytics; + +impl Analytics { + pub fn server(state: AppState) -> Scope { + let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); + route + .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) + .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) + .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) + .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + } +} + +pub async fn get_info( + state: web::Data, + req: actix_web::HttpRequest, + domain: actix_web::web::Path, +) -> impl Responder { + let flow = AnalyticsFlow::GetInfo; + api::server_wrap( + flow, + state, + &req, + domain.into_inner(), + |_, _, domain| get_domain_info(domain), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +/// # Panics +/// +/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. +pub async fn get_payment_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentMetricRequest; 1]>, +) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentMetrics; + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| { + payments::get_metrics(state.pool.clone(), auth.merchant_account, req) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +/// # Panics +/// +/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. +pub async fn get_refunds_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetRefundMetricRequest; 1]>, +) -> impl Responder { + #[allow(clippy::expect_used)] + // safety: This shouldn't panic owing to the data type + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetRefundMetricRequest"); + let flow = AnalyticsFlow::GetRefundsMetrics; + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| { + refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +pub async fn get_payment_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentFilters; + api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| { + payment_filters_core(state.pool.clone(), req, auth.merchant_account) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +pub async fn get_refund_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = AnalyticsFlow::GetRefundFilters; + api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req: GetRefundFilterRequest| { + refund_filter_core(state.pool.clone(), req, auth.merchant_account) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/router/src/analytics/sqlx.rs new file mode 100644 index 000000000000..5c3060c35a92 --- /dev/null +++ b/crates/router/src/analytics/sqlx.rs @@ -0,0 +1,386 @@ +use std::{fmt::Display, str::FromStr}; + +use api_models::analytics::refunds::RefundType; +use common_enums::enums::{ + AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, +}; +use common_utils::errors::{CustomResult, ParsingError}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::{kms, kms::decrypt::KmsDecrypt}; +#[cfg(not(feature = "kms"))] +use masking::PeekInterface; +use sqlx::{ + postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, + Decode, Encode, + Error::ColumnNotFound, + FromRow, Pool, Postgres, Row, +}; +use time::PrimitiveDateTime; + +use super::{ + query::{Aggregate, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + }, +}; +use crate::configs::settings::Database; + +#[derive(Debug, Clone)] +pub struct SqlxClient { + pool: Pool, +} + +impl SqlxClient { + pub async fn from_conf( + conf: &Database, + #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + ) -> Self { + #[cfg(feature = "kms")] + #[allow(clippy::expect_used)] + let password = conf + .password + .decrypt_inner(kms_client) + .await + .expect("Failed to KMS decrypt database password"); + + #[cfg(not(feature = "kms"))] + let password = &conf.password.peek(); + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + conf.username, password, conf.host, conf.port, conf.dbname + ); + #[allow(clippy::expect_used)] + let pool = PgPoolOptions::new() + .max_connections(conf.pool_size) + .acquire_timeout(std::time::Duration::from_secs(conf.connection_timeout)) + .connect_lazy(&database_url) + .expect("SQLX Pool Creation failed"); + Self { pool } + } +} + +pub trait DbType { + fn name() -> &'static str; +} + +macro_rules! db_type { + ($a: ident, $str: tt) => { + impl DbType for $a { + fn name() -> &'static str { + stringify!($str) + } + } + }; + ($a:ident) => { + impl DbType for $a { + fn name() -> &'static str { + stringify!($a) + } + } + }; +} + +db_type!(Currency); +db_type!(AuthenticationType); +db_type!(AttemptStatus); +db_type!(PaymentMethod, TEXT); +db_type!(RefundStatus); +db_type!(RefundType); + +impl<'q, Type> Encode<'q, Postgres> for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> sqlx::encode::IsNull { + self.0.to_string().encode(buf) + } + fn size_hint(&self) -> usize { + self.0.to_string().size_hint() + } +} + +impl<'r, Type> Decode<'r, Postgres> for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn decode( + value: PgValueRef<'r>, + ) -> Result> { + let str_value = <&'r str as Decode<'r, Postgres>>::decode(value)?; + Type::from_str(str_value).map(DBEnumWrapper).or(Err(format!( + "invalid value {:?} for enum {}", + str_value, + Type::name() + ) + .into())) + } +} + +impl sqlx::Type for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name(Type::name()) + } +} + +impl LoadRow for SqlxClient +where + for<'a> T: FromRow<'a, PgRow>, +{ + fn load_row(row: PgRow) -> CustomResult { + T::from_row(&row) + .into_report() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} +impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} +impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} +impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} + +#[async_trait::async_trait] +impl AnalyticsDataSource for SqlxClient { + type Row = PgRow; + + async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> + where + Self: LoadRow, + { + sqlx::query(&format!("{query};")) + .fetch_all(&self.pool) + .await + .into_report() + .change_context(QueryExecutionError::DatabaseError) + .attach_printable_lazy(|| format!("Failed to run query {query}"))? + .into_iter() + .map(Self::load_row) + .collect::, _>>() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_status: Option> = + row.try_get("refund_status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_type: Option> = + row.try_get("refund_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + refund_status, + connector, + refund_type, + total, + count, + start_bucket, + end_bucket, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + total, + count, + start_bucket, + end_bucket, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_status: Option> = + row.try_get("refund_status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_type: Option> = + row.try_get("refund_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { + currency, + refund_status, + connector, + refund_type, + }) + } +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self) -> error_stack::Result { + Ok(self.to_string()) + } +} + +impl ToSql for AnalyticsCollection { + fn to_sql(&self) -> error_stack::Result { + match self { + Self::Payment => Ok("payment_attempt".to_string()), + Self::Refund => Ok("refund".to_string()), + } + } +} + +impl ToSql for Aggregate +where + T: ToSql, +{ + fn to_sql(&self) -> error_stack::Result { + Ok(match self { + Self::Count { field: _, alias } => { + format!( + "count(*){}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Sum { field, alias } => { + format!( + "sum({}){}", + field.to_sql().attach_printable("Failed to sum aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Min { field, alias } => { + format!( + "min({}){}", + field.to_sql().attach_printable("Failed to min aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Max { field, alias } => { + format!( + "max({}){}", + field.to_sql().attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} diff --git a/crates/router/src/analytics/types.rs b/crates/router/src/analytics/types.rs new file mode 100644 index 000000000000..5173e6214de4 --- /dev/null +++ b/crates/router/src/analytics/types.rs @@ -0,0 +1,114 @@ +use std::{fmt::Display, str::FromStr}; + +use common_utils::errors::{CustomResult, ErrorSwitch, ParsingError}; +use error_stack::{report, Report, ResultExt}; + +use super::query::QueryBuildingError; + +#[derive(serde::Deserialize, Debug, masking::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalyticsDomain { + Payments, + Refunds, +} + +#[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] +pub enum AnalyticsCollection { + Payment, + Refund, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +#[serde(transparent)] +pub struct DBEnumWrapper(pub T); + +impl AsRef for DBEnumWrapper { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl FromStr for DBEnumWrapper +where + T: FromStr + Display, +{ + type Err = Report; + + fn from_str(s: &str) -> Result { + T::from_str(s) + .map_err(|_er| report!(ParsingError::EnumParseFailure(std::any::type_name::()))) + .map(DBEnumWrapper) + .attach_printable_lazy(|| format!("raw_value: {s}")) + } +} + +// Analytics Framework + +pub trait RefundAnalytics {} + +#[async_trait::async_trait] +pub trait AnalyticsDataSource +where + Self: Sized + Sync + Send, +{ + type Row; + async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> + where + Self: LoadRow; +} + +pub trait LoadRow +where + Self: AnalyticsDataSource, + T: Sized, +{ + fn load_row(row: Self::Row) -> CustomResult; +} + +#[derive(thiserror::Error, Debug)] +pub enum MetricsError { + #[error("Error building query")] + QueryBuildingError, + #[error("Error running Query")] + QueryExecutionFailure, + #[error("Error processing query results")] + PostProcessingFailure, + #[allow(dead_code)] + #[error("Not Implemented")] + NotImplemented, +} + +#[derive(Debug, thiserror::Error)] +pub enum QueryExecutionError { + #[error("Failed to extract domain rows")] + RowExtractionFailure, + #[error("Database error")] + DatabaseError, +} + +pub type MetricsResult = CustomResult; + +impl ErrorSwitch for QueryBuildingError { + fn switch(&self) -> MetricsError { + MetricsError::QueryBuildingError + } +} + +pub type FiltersResult = CustomResult; + +#[derive(thiserror::Error, Debug)] +pub enum FiltersError { + #[error("Error building query")] + QueryBuildingError, + #[error("Error running Query")] + QueryExecutionFailure, + #[allow(dead_code)] + #[error("Not Implemented")] + NotImplemented, +} + +impl ErrorSwitch for QueryBuildingError { + fn switch(&self) -> FiltersError { + FiltersError::QueryBuildingError + } +} diff --git a/crates/router/src/analytics/utils.rs b/crates/router/src/analytics/utils.rs new file mode 100644 index 000000000000..f7e6ea69dc37 --- /dev/null +++ b/crates/router/src/analytics/utils.rs @@ -0,0 +1,22 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentMetrics}, + refunds::{RefundDimensions, RefundMetrics}, + NameDescription, +}; +use strum::IntoEnumIterator; + +pub fn get_payment_dimensions() -> Vec { + PaymentDimensions::iter().map(Into::into).collect() +} + +pub fn get_refund_dimensions() -> Vec { + RefundDimensions::iter().map(Into::into).collect() +} + +pub fn get_payment_metrics_info() -> Vec { + PaymentMetrics::iter().map(Into::into).collect() +} + +pub fn get_refund_metrics_info() -> Vec { + RefundMetrics::iter().map(Into::into).collect() +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 204060b37aa0..b48a52e53b1d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -16,6 +16,8 @@ pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; +#[cfg(feature = "olap")] +use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, @@ -101,6 +103,8 @@ pub struct Settings { pub lock_settings: LockSettings, pub temp_locker_enable_config: TempLockerEnableConfig, pub payment_link: PaymentLink, + #[cfg(feature = "olap")] + pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 11efec64055b..b91a79f072b0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] +#[cfg(feature = "olap")] +mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -141,6 +143,7 @@ pub fn mk_app( .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) + .service(routes::Analytics::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 307797e8ac9d..7bd0541f51f5 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -37,3 +37,5 @@ pub use self::app::{ }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; +#[cfg(feature = "olap")] +pub use crate::analytics::routes::{self as analytics, Analytics}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5b16e93404ae..5033a91717f4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -42,6 +42,8 @@ pub struct AppState { #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, + #[cfg(feature = "olap")] + pub pool: crate::analytics::AnalyticsProvider, } impl scheduler::SchedulerAppState for AppState { @@ -124,6 +126,14 @@ impl AppState { ), }; + #[cfg(feature = "olap")] + let pool = crate::analytics::AnalyticsProvider::from_conf( + &conf.analytics, + #[cfg(feature = "kms")] + kms_client, + ) + .await; + #[cfg(feature = "kms")] #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { @@ -145,6 +155,8 @@ impl AppState { kms_secrets: Arc::new(kms_secrets), api_client, event_handler: Box::::default(), + #[cfg(feature = "olap")] + pool, } } diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index d3612767ff9d..e75606aa1531 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -1,5 +1,5 @@ #![forbid(unsafe_code)] -#![warn(missing_docs, missing_debug_implementations)] +#![warn(missing_debug_implementations)] //! //! Environment of payment router: logger, basic config, its environment awareness. @@ -22,6 +22,7 @@ pub mod vergen; pub use logger::*; pub use once_cell; pub use opentelemetry; +use strum::Display; pub use tracing; #[cfg(feature = "actix_web")] pub use tracing_actix_web; @@ -29,3 +30,19 @@ pub use tracing_appender; #[doc(inline)] pub use self::env::*; +use crate::types::FlowMetric; + +/// Analytics Flow routes Enums +/// Info - Dimensions and filters available for the domain +/// Filters - Set of values present for the dimension +/// Metrics - Analytical data on dimensions and metrics +#[derive(Debug, Display, Clone, PartialEq, Eq)] +pub enum AnalyticsFlow { + GetInfo, + GetPaymentFilters, + GetRefundFilters, + GetRefundsMetrics, + GetPaymentMetrics, +} + +impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/metrics.rs b/crates/router_env/src/metrics.rs index e4943699ee5b..14402a7a6e91 100644 --- a/crates/router_env/src/metrics.rs +++ b/crates/router_env/src/metrics.rs @@ -63,3 +63,22 @@ macro_rules! histogram_metric { > = once_cell::sync::Lazy::new(|| $meter.f64_histogram($description).init()); }; } + +/// Create a [`Histogram`][Histogram] u64 metric with the specified name and an optional description, +/// associated with the specified meter. Note that the meter must be to a valid [`Meter`][Meter]. +/// +/// [Histogram]: opentelemetry::metrics::Histogram +/// [Meter]: opentelemetry::metrics::Meter +#[macro_export] +macro_rules! histogram_metric_u64 { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); + }; + ($name:ident, $meter:ident, $description:literal) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); + }; +} diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 7cdbc8dd6fdd..96f215ab08f4 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -235,5 +235,17 @@ bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} +[analytics] +source = "sqlx" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "localhost" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 +connection_timeout = 10 + [kv_config] ttl = 300 # 5 * 60 seconds From e40a29351c7aa7b86a5684959a84f0236104cafd Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:31:06 +0530 Subject: [PATCH 03/57] fix(router): make customer_id optional when billing and shipping address is passed in payments create, update (#2762) --- crates/diesel_models/src/address.rs | 4 +-- crates/diesel_models/src/schema.rs | 2 +- crates/router/src/core/payments/helpers.rs | 28 +++++-------------- .../payments/operations/payment_approve.rs | 2 -- .../operations/payment_complete_authorize.rs | 2 -- .../payments/operations/payment_confirm.rs | 2 -- .../payments/operations/payment_create.rs | 2 -- .../payments/operations/payment_update.rs | 2 -- crates/router/src/db/address.rs | 3 +- crates/router/src/types/domain/address.rs | 2 +- crates/router/src/utils.rs | 2 +- .../down.sql | 2 ++ .../up.sql | 2 ++ 13 files changed, 18 insertions(+), 37 deletions(-) create mode 100644 migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql create mode 100644 migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql diff --git a/crates/diesel_models/src/address.rs b/crates/diesel_models/src/address.rs index e67f37c90465..03dedfd60d8f 100644 --- a/crates/diesel_models/src/address.rs +++ b/crates/diesel_models/src/address.rs @@ -19,7 +19,7 @@ pub struct AddressNew { pub last_name: Option, pub phone_number: Option, pub country_code: Option, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub created_at: PrimitiveDateTime, @@ -45,7 +45,7 @@ pub struct Address { pub country_code: Option, pub created_at: PrimitiveDateTime, pub modified_at: PrimitiveDateTime, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub updated_by: String, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 02abfb842b8d..e214fa364ddd 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -24,7 +24,7 @@ diesel::table! { created_at -> Timestamp, modified_at -> Timestamp, #[max_length = 64] - customer_id -> Varchar, + customer_id -> Nullable, #[max_length = 64] merchant_id -> Varchar, #[max_length = 64] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index af67d30ec6c3..f42e4985380c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -221,8 +221,6 @@ pub async fn create_or_update_address_for_payment_by_request( None => match req_address { Some(address) => { // generate a new address here - let customer_id = customer_id.get_required_value("customer_id")?; - let address_details = address.address.clone().unwrap_or_default(); Some( db.insert_address_for_payments( @@ -282,7 +280,6 @@ pub async fn create_or_find_address_for_payment_by_request( None => match req_address { Some(address) => { // generate a new address here - let customer_id = customer_id.get_required_value("customer_id")?; let address_details = address.address.clone().unwrap_or_default(); Some( @@ -317,7 +314,7 @@ pub async fn get_domain_address_for_payments( address_details: api_models::payments::AddressDetails, address: &api_models::payments::Address, merchant_id: &str, - customer_id: &str, + customer_id: Option<&String>, payment_id: &str, key: &[u8], storage_scheme: enums::MerchantStorageScheme, @@ -332,7 +329,7 @@ pub async fn get_domain_address_for_payments( .async_lift(|inner| types::encrypt_optional(inner, key)) .await?, country_code: address.phone.as_ref().and_then(|a| a.country_code.clone()), - customer_id: customer_id.to_string(), + customer_id: customer_id.cloned(), merchant_id: merchant_id.to_string(), address_id: generate_id(consts::ID_LENGTH, "add"), city: address_details.city, @@ -763,25 +760,14 @@ fn validate_new_mandate_request( } pub fn validate_customer_id_mandatory_cases( - has_shipping: bool, - has_billing: bool, has_setup_future_usage: bool, customer_id: &Option, ) -> RouterResult<()> { - match ( - has_shipping, - has_billing, - has_setup_future_usage, - customer_id, - ) { - (true, _, _, None) | (_, true, _, None) | (_, _, true, None) => { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: "customer_id is mandatory when shipping or billing \ - address is given or when setup_future_usage is given" - .to_string(), - }) - .into_report() - } + match (has_setup_future_usage, customer_id) { + (true, None) => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "customer_id is mandatory when setup_future_usage is given".to_string(), + }) + .into_report(), _ => Ok(()), } } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index a1d50a9049aa..16bb84f69ddb 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -130,8 +130,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 0e357f08734e..a2f5292a37f7 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -139,8 +139,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 9e1f12b6bca2..8842963990b6 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -284,8 +284,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2d31f82aeb00..f3b777534bf7 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -538,8 +538,6 @@ impl ValidateRequest if request.confirm.unwrap_or(false) { helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 20f7bdb9120f..9244fc022d9e 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -763,7 +763,8 @@ impl AddressInterface for MockDb { .await .iter_mut() .find(|address| { - address.customer_id == customer_id && address.merchant_id == merchant_id + address.customer_id == Some(customer_id.to_string()) + && address.merchant_id == merchant_id }) .map(|a| { let address_updated = diff --git a/crates/router/src/types/domain/address.rs b/crates/router/src/types/domain/address.rs index 008cead1ebeb..ddf9c2152e94 100644 --- a/crates/router/src/types/domain/address.rs +++ b/crates/router/src/types/domain/address.rs @@ -35,7 +35,7 @@ pub struct Address { #[serde(skip_serializing)] #[serde(with = "custom_serde::iso8601")] pub modified_at: PrimitiveDateTime, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub updated_by: String, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 84a75d397e31..386bd02ae94b 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -566,7 +566,7 @@ impl CustomerAddress for api_models::customers::CustomerRequest { .async_lift(|inner| encrypt_optional(inner, key)) .await?, country_code: self.phone_country_code.clone(), - customer_id: customer_id.to_string(), + customer_id: Some(customer_id.to_string()), merchant_id: merchant_id.to_string(), address_id: generate_id(consts::ID_LENGTH, "add"), payment_id: None, diff --git a/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql new file mode 100644 index 000000000000..b148e66d8750 --- /dev/null +++ b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE address ALTER COLUMN customer_id SET NOT NULL; \ No newline at end of file diff --git a/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql new file mode 100644 index 000000000000..98826c41e79c --- /dev/null +++ b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE address ALTER COLUMN customer_id DROP NOT NULL; \ No newline at end of file From 772f03ee3836ce86de3874f6a5e7f636718e6034 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:19:56 +0000 Subject: [PATCH 04/57] test(postman): update postman collection files --- .../multisafepay.postman_collection.json | 229 +++++++++++++++++- .../stripe.postman_collection.json | 192 +++++++-------- 2 files changed, 324 insertions(+), 97 deletions(-) diff --git a/postman/collection-json/multisafepay.postman_collection.json b/postman/collection-json/multisafepay.postman_collection.json index 8ba70b7c654f..50a2655c5b05 100644 --- a/postman/collection-json/multisafepay.postman_collection.json +++ b/postman/collection-json/multisafepay.postman_collection.json @@ -532,7 +532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"routing\":{\"type\":\"single\",\"data\":\"multisafepay\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1875,6 +1875,233 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario6- Create payment with Invalid Merchant ID", + "item": [ + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"wrongAPIKey\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate error message in the JSON Body", + "pm.test(\"[POST]::/payments - Validate error message\", function () {", + " pm.expect(jsonData.error_message).to.not.be.null", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"routing\":{\"type\":\"single\",\"data\":\"multisafepay\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 393b2979289b..b10bcd2a3b83 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -81,19 +81,19 @@ "name": "MerchantAccounts", "item": [ { - "name": "Merchant Account - List", + "name": "Merchant Account - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -105,6 +105,19 @@ " jsonData = pm.response.json();", "} catch (e) { }", "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", @@ -130,6 +143,36 @@ " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -157,55 +200,53 @@ } ] }, - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, "url": { - "raw": "{{baseUrl}}/accounts/list", + "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ - "accounts", - "list" - ], - "query": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false - } - ], - "variable": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "description": "(Required) - Organization id" - } + "accounts" ] }, - "description": "List merchant accounts for an organization" + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." }, "response": [] }, { - "name": "Merchant Account - Create", + "name": "Merchant Account - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -215,20 +256,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", + "} catch (e) {}", "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", @@ -255,36 +283,6 @@ " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", " );", "}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", - " console.log(", - " \"- use {{organization_id}} as collection variable for value\",", - " jsonData.organization_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/accounts - Organization id is generated\",", - " function () {", - " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ "" ], "type": "text/javascript" @@ -312,53 +310,48 @@ } ] }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" - }, "url": { - "raw": "{{baseUrl}}/accounts", + "raw": "{{baseUrl}}/accounts/:id", "host": [ "{{baseUrl}}" ], "path": [ - "accounts" + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } ] }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + "description": "Retrieve a merchant account details." }, "response": [] }, { - "name": "Merchant Account - Retrieve", + "name": "Merchant Account - List", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -368,7 +361,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", @@ -430,23 +423,30 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/:id", + "raw": "{{baseUrl}}/accounts/list", "host": [ "{{baseUrl}}" ], "path": [ "accounts", - ":id" + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "disabled": false + } ], "variable": [ { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" } ] }, - "description": "Retrieve a merchant account details." + "description": "List merchant accounts for an organization" }, "response": [] }, From 15026977461c46249c7ff1e3f6373f3f074949cf Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:19:57 +0000 Subject: [PATCH 05/57] chore(version): v1.70.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf1cc629d8e..a6f88af7c8b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.70.0 (2023-11-03) + +### Features + +- **analytics:** Analytics APIs ([#2676](https://github.com/juspay/hyperswitch/pull/2676)) ([`c0a5e7b`](https://github.com/juspay/hyperswitch/commit/c0a5e7b7d945095053606e35c9bb23a06090c4e3)) +- **connector:** [Multisafepay] add error handling ([#2595](https://github.com/juspay/hyperswitch/pull/2595)) ([`b3c846d`](https://github.com/juspay/hyperswitch/commit/b3c846d637dd32a2d6d7044c118abbb2616642f0)) +- **events:** Add api auth type details to events ([#2760](https://github.com/juspay/hyperswitch/pull/2760)) ([`1094493`](https://github.com/juspay/hyperswitch/commit/10944937a02502e0727f16368d8d055e575dd518)) + +### Bug Fixes + +- **router:** Make customer_id optional when billing and shipping address is passed in payments create, update ([#2762](https://github.com/juspay/hyperswitch/pull/2762)) ([`e40a293`](https://github.com/juspay/hyperswitch/commit/e40a29351c7aa7b86a5684959a84f0236104cafd)) +- Null fields in payments respose ([#2745](https://github.com/juspay/hyperswitch/pull/2745)) ([`42261a5`](https://github.com/juspay/hyperswitch/commit/42261a5306bb99d3e20eb3aa734a895e589b1d94)) + +### Testing + +- **postman:** Update postman collection files ([`772f03e`](https://github.com/juspay/hyperswitch/commit/772f03ee3836ce86de3874f6a5e7f636718e6034)) + +**Full Changelog:** [`v1.69.0...v1.70.0`](https://github.com/juspay/hyperswitch/compare/v1.69.0...v1.70.0) + +- - - + + ## 1.69.0 (2023-10-31) ### Features From 169d33bf8157b1a9910c841c8c55eddc4d2ad168 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Fri, 3 Nov 2023 17:29:32 +0530 Subject: [PATCH 06/57] revert: fix(analytics): feat(analytics): analytics APIs (#2777) --- Cargo.lock | 281 +-------- config/config.example.toml | 14 +- config/docker_compose.toml | 11 - crates/api_models/src/analytics.rs | 137 ----- crates/api_models/src/analytics/payments.rs | 176 ------ crates/api_models/src/analytics/refunds.rs | 177 ------ crates/api_models/src/lib.rs | 1 - crates/common_utils/src/custom_serde.rs | 48 -- crates/router/Cargo.toml | 3 - crates/router/src/analytics.rs | 123 ---- crates/router/src/analytics/core.rs | 96 ---- crates/router/src/analytics/errors.rs | 32 -- crates/router/src/analytics/metrics.rs | 9 - .../router/src/analytics/metrics/request.rs | 60 -- crates/router/src/analytics/payments.rs | 13 - .../src/analytics/payments/accumulator.rs | 150 ----- crates/router/src/analytics/payments/core.rs | 129 ----- .../router/src/analytics/payments/filters.rs | 58 -- .../router/src/analytics/payments/metrics.rs | 137 ----- .../payments/metrics/avg_ticket_size.rs | 126 ----- .../payments/metrics/payment_count.rs | 117 ---- .../metrics/payment_processed_amount.rs | 128 ----- .../payments/metrics/payment_success_count.rs | 127 ----- .../payments/metrics/success_rate.rs | 123 ---- crates/router/src/analytics/payments/types.rs | 46 -- crates/router/src/analytics/query.rs | 533 ------------------ crates/router/src/analytics/refunds.rs | 10 - .../src/analytics/refunds/accumulator.rs | 110 ---- crates/router/src/analytics/refunds/core.rs | 104 ---- .../router/src/analytics/refunds/filters.rs | 59 -- .../router/src/analytics/refunds/metrics.rs | 126 ----- .../analytics/refunds/metrics/refund_count.rs | 116 ---- .../metrics/refund_processed_amount.rs | 122 ---- .../refunds/metrics/refund_success_count.rs | 122 ---- .../refunds/metrics/refund_success_rate.rs | 117 ---- crates/router/src/analytics/refunds/types.rs | 41 -- crates/router/src/analytics/routes.rs | 145 ----- crates/router/src/analytics/sqlx.rs | 386 ------------- crates/router/src/analytics/types.rs | 114 ---- crates/router/src/analytics/utils.rs | 22 - crates/router/src/configs/settings.rs | 4 - crates/router/src/lib.rs | 3 - crates/router/src/routes.rs | 2 - crates/router/src/routes/app.rs | 12 - crates/router_env/src/lib.rs | 19 +- crates/router_env/src/metrics.rs | 19 - loadtest/config/development.toml | 12 - 47 files changed, 13 insertions(+), 4507 deletions(-) delete mode 100644 crates/api_models/src/analytics.rs delete mode 100644 crates/api_models/src/analytics/payments.rs delete mode 100644 crates/api_models/src/analytics/refunds.rs delete mode 100644 crates/router/src/analytics.rs delete mode 100644 crates/router/src/analytics/core.rs delete mode 100644 crates/router/src/analytics/errors.rs delete mode 100644 crates/router/src/analytics/metrics.rs delete mode 100644 crates/router/src/analytics/metrics/request.rs delete mode 100644 crates/router/src/analytics/payments.rs delete mode 100644 crates/router/src/analytics/payments/accumulator.rs delete mode 100644 crates/router/src/analytics/payments/core.rs delete mode 100644 crates/router/src/analytics/payments/filters.rs delete mode 100644 crates/router/src/analytics/payments/metrics.rs delete mode 100644 crates/router/src/analytics/payments/metrics/avg_ticket_size.rs delete mode 100644 crates/router/src/analytics/payments/metrics/payment_count.rs delete mode 100644 crates/router/src/analytics/payments/metrics/payment_processed_amount.rs delete mode 100644 crates/router/src/analytics/payments/metrics/payment_success_count.rs delete mode 100644 crates/router/src/analytics/payments/metrics/success_rate.rs delete mode 100644 crates/router/src/analytics/payments/types.rs delete mode 100644 crates/router/src/analytics/query.rs delete mode 100644 crates/router/src/analytics/refunds.rs delete mode 100644 crates/router/src/analytics/refunds/accumulator.rs delete mode 100644 crates/router/src/analytics/refunds/core.rs delete mode 100644 crates/router/src/analytics/refunds/filters.rs delete mode 100644 crates/router/src/analytics/refunds/metrics.rs delete mode 100644 crates/router/src/analytics/refunds/metrics/refund_count.rs delete mode 100644 crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs delete mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_count.rs delete mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_rate.rs delete mode 100644 crates/router/src/analytics/refunds/types.rs delete mode 100644 crates/router/src/analytics/routes.rs delete mode 100644 crates/router/src/analytics/sqlx.rs delete mode 100644 crates/router/src/analytics/types.rs delete mode 100644 crates/router/src/analytics/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 3ad36999acdc..665703f3d505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "smallvec", "tokio", @@ -361,12 +361,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -589,15 +583,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "atoi" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic" version = "0.5.3" @@ -1147,21 +1132,10 @@ dependencies = [ "async-trait", "futures-channel", "futures-util", - "parking_lot 0.12.1", + "parking_lot", "tokio", ] -[[package]] -name = "bigdecimal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "bincode" version = "1.3.3" @@ -1617,21 +1591,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" - [[package]] name = "crc16" version = "0.4.0" @@ -1690,16 +1649,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1799,7 +1748,7 @@ dependencies = [ "hashbrown 0.14.1", "lock_api", "once_cell", - "parking_lot_core 0.9.8", + "parking_lot_core", ] [[package]] @@ -2022,12 +1971,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "drainer" version = "0.1.0" @@ -2194,12 +2137,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flate2" version = "1.0.27" @@ -2265,7 +2202,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot 0.12.1", + "parking_lot", "rand 0.8.5", "redis-protocol", "semver", @@ -2372,17 +2309,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot 0.11.2", -] - [[package]] name = "futures-io" version = "0.3.28" @@ -2585,28 +2511,12 @@ name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" -dependencies = [ - "ahash 0.8.3", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.1", -] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "hermit-abi" @@ -2620,15 +2530,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -3312,7 +3213,7 @@ dependencies = [ "crossbeam-utils", "futures-util", "once_cell", - "parking_lot 0.12.1", + "parking_lot", "quanta", "rustc_version", "scheduled-thread-pool", @@ -3621,17 +3522,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -3639,21 +3529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.8", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -4041,7 +3917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot 0.12.1", + "parking_lot", "scheduled-thread-pool", ] @@ -4358,12 +4234,10 @@ dependencies = [ "aws-sdk-s3", "base64 0.21.4", "bb8", - "bigdecimal", "blake3", "bytes", "cards", "clap", - "common_enums", "common_utils", "config", "data_models", @@ -4412,7 +4286,6 @@ dependencies = [ "sha-1 0.9.8", "signal-hook", "signal-hook-tokio", - "sqlx", "storage_impl", "strum 0.24.1", "tera", @@ -4686,7 +4559,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot 0.12.1", + "parking_lot", ] [[package]] @@ -4913,7 +4786,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot 0.12.1", + "parking_lot", "serial_test_derive", ] @@ -5106,111 +4979,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "sqlformat" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" -dependencies = [ - "itertools 0.11.0", - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" -dependencies = [ - "sqlx-core", - "sqlx-macros", -] - -[[package]] -name = "sqlx-core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" -dependencies = [ - "ahash 0.7.6", - "atoi", - "base64 0.13.1", - "bigdecimal", - "bitflags 1.3.2", - "byteorder", - "bytes", - "crc", - "crossbeam-queue", - "dirs", - "dotenvy", - "either", - "event-listener", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-util", - "hashlink", - "hex", - "hkdf", - "hmac", - "indexmap 1.9.3", - "itoa", - "libc", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "paste", - "percent-encoding", - "rand 0.8.5", - "serde", - "serde_json", - "sha1", - "sha2", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", - "time", - "tokio-stream", - "url", - "whoami", -] - -[[package]] -name = "sqlx-macros" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" -dependencies = [ - "dotenvy", - "either", - "heck", - "once_cell", - "proc-macro2", - "quote", - "sha2", - "sqlx-core", - "sqlx-rt", - "syn 1.0.109", - "url", -] - -[[package]] -name = "sqlx-rt" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" -dependencies = [ - "native-tls", - "once_cell", - "tokio", - "tokio-native-tls", -] - [[package]] name = "storage_impl" version = "0.1.0" @@ -5255,17 +5023,6 @@ dependencies = [ "regex", ] -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "strsim" version = "0.10.0" @@ -5500,7 +5257,7 @@ dependencies = [ "futures", "http", "log", - "parking_lot 0.12.1", + "parking_lot", "serde", "serde_json", "serde_repr", @@ -5618,7 +5375,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.4", @@ -6047,12 +5804,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "unidecode" version = "0.3.0" @@ -6343,16 +6094,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/config/config.example.toml b/config/config.example.toml index 5943c05e6106..59083d6c71d3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -433,22 +433,10 @@ apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm + [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" -# Analytics configuration. -[analytics] -source = "sqlx" # The Analytics source/strategy to be used - -[analytics.sqlx] -username = "db_user" # Analytics DB Username -password = "db_pass" # Analytics DB Password -host = "localhost" # Analytics DB Host -port = 5432 # Analytics DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds - # Config for KV setup [kv_config] # TTL for KV in seconds diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 4e630cd46f89..20ca175ceb84 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -317,16 +317,5 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 -[analytics] -source = "sqlx" - -[analytics.sqlx] -username = "db_user" -password = "db_pass" -host = "pg" -port = 5432 -dbname = "hyperswitch_db" -pool_size = 5 - [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs deleted file mode 100644 index 3d94cf21fd26..000000000000 --- a/crates/api_models/src/analytics.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::collections::HashSet; - -use time::PrimitiveDateTime; - -use self::{ - payments::{PaymentDimensions, PaymentMetrics}, - refunds::{RefundDimensions, RefundMetrics}, -}; - -pub mod payments; -pub mod refunds; - -#[derive(Debug, serde::Serialize)] -pub struct NameDescription { - pub name: String, - pub desc: String, -} - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInfoResponse { - pub metrics: Vec, - pub download_dimensions: Option>, - pub dimensions: Vec, -} - -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub struct TimeRange { - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] -pub struct TimeSeries { - pub granularity: Granularity, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] -pub enum Granularity { - #[serde(rename = "G_ONEMIN")] - OneMin, - #[serde(rename = "G_FIVEMIN")] - FiveMin, - #[serde(rename = "G_FIFTEENMIN")] - FifteenMin, - #[serde(rename = "G_THIRTYMIN")] - ThirtyMin, - #[serde(rename = "G_ONEHOUR")] - OneHour, - #[serde(rename = "G_ONEDAY")] - OneDay, -} - -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetPaymentMetricRequest { - pub time_series: Option, - pub time_range: TimeRange, - #[serde(default)] - pub group_by_names: Vec, - #[serde(default)] - pub filters: payments::PaymentFilters, - pub metrics: HashSet, - #[serde(default)] - pub delta: bool, -} - -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRefundMetricRequest { - pub time_series: Option, - pub time_range: TimeRange, - #[serde(default)] - pub group_by_names: Vec, - #[serde(default)] - pub filters: refunds::RefundFilters, - pub metrics: HashSet, - #[serde(default)] - pub delta: bool, -} - -#[derive(Debug, serde::Serialize)] -pub struct AnalyticsMetadata { - pub current_time_range: TimeRange, -} - -#[derive(Debug, serde::Deserialize, masking::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetPaymentFiltersRequest { - pub time_range: TimeRange, - #[serde(default)] - pub group_by_names: Vec, -} - -#[derive(Debug, Default, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PaymentFiltersResponse { - pub query_data: Vec, -} - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FilterValue { - pub dimension: PaymentDimensions, - pub values: Vec, -} - -#[derive(Debug, serde::Deserialize, masking::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRefundFilterRequest { - pub time_range: TimeRange, - #[serde(default)] - pub group_by_names: Vec, -} - -#[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RefundFiltersResponse { - pub query_data: Vec, -} - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RefundFilterValue { - pub dimension: RefundDimensions, - pub values: Vec, -} - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MetricsResponse { - pub query_data: Vec, - pub meta_data: [AnalyticsMetadata; 1], -} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs deleted file mode 100644 index 5e1d673736e5..000000000000 --- a/crates/api_models/src/analytics/payments.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; - -use super::{NameDescription, TimeRange}; -use crate::enums::Connector; - -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] -pub struct PaymentFilters { - #[serde(default)] - pub currency: Vec, - #[serde(default)] - pub status: Vec, - #[serde(default)] - pub connector: Vec, - #[serde(default)] - pub auth_type: Vec, - #[serde(default)] - pub payment_method: Vec, -} - -#[derive( - Debug, - serde::Serialize, - serde::Deserialize, - strum::AsRefStr, - PartialEq, - PartialOrd, - Eq, - Ord, - strum::Display, - strum::EnumIter, - Clone, - Copy, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum PaymentDimensions { - // Do not change the order of these enums - // Consult the Dashboard FE folks since these also affects the order of metrics on FE - Connector, - PaymentMethod, - Currency, - #[strum(serialize = "authentication_type")] - #[serde(rename = "authentication_type")] - AuthType, - #[strum(serialize = "status")] - #[serde(rename = "status")] - PaymentStatus, -} - -#[derive( - Clone, - Debug, - Hash, - PartialEq, - Eq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumIter, - strum::AsRefStr, -)] -#[strum(serialize_all = "snake_case")] -#[serde(rename_all = "snake_case")] -pub enum PaymentMetrics { - PaymentSuccessRate, - PaymentCount, - PaymentSuccessCount, - PaymentProcessedAmount, - AvgTicketSize, -} - -pub mod metric_behaviour { - pub struct PaymentSuccessRate; - pub struct PaymentCount; - pub struct PaymentSuccessCount; - pub struct PaymentProcessedAmount; - pub struct AvgTicketSize; -} - -impl From for NameDescription { - fn from(value: PaymentMetrics) -> Self { - Self { - name: value.to_string(), - desc: String::new(), - } - } -} - -impl From for NameDescription { - fn from(value: PaymentDimensions) -> Self { - Self { - name: value.to_string(), - desc: String::new(), - } - } -} - -#[derive(Debug, serde::Serialize, Eq)] -pub struct PaymentMetricsBucketIdentifier { - pub currency: Option, - pub status: Option, - pub connector: Option, - #[serde(rename = "authentication_type")] - pub auth_type: Option, - pub payment_method: Option, - #[serde(rename = "time_range")] - pub time_bucket: TimeRange, - // Coz FE sucks - #[serde(rename = "time_bucket")] - #[serde(with = "common_utils::custom_serde::iso8601custom")] - pub start_time: time::PrimitiveDateTime, -} - -impl PaymentMetricsBucketIdentifier { - pub fn new( - currency: Option, - status: Option, - connector: Option, - auth_type: Option, - payment_method: Option, - normalized_time_range: TimeRange, - ) -> Self { - Self { - currency, - status, - connector, - auth_type, - payment_method, - time_bucket: normalized_time_range, - start_time: normalized_time_range.start_time, - } - } -} - -impl Hash for PaymentMetricsBucketIdentifier { - fn hash(&self, state: &mut H) { - self.currency.hash(state); - self.status.map(|i| i.to_string()).hash(state); - self.connector.hash(state); - self.auth_type.map(|i| i.to_string()).hash(state); - self.payment_method.hash(state); - self.time_bucket.hash(state); - } -} - -impl PartialEq for PaymentMetricsBucketIdentifier { - fn eq(&self, other: &Self) -> bool { - let mut left = DefaultHasher::new(); - self.hash(&mut left); - let mut right = DefaultHasher::new(); - other.hash(&mut right); - left.finish() == right.finish() - } -} - -#[derive(Debug, serde::Serialize)] -pub struct PaymentMetricsBucketValue { - pub payment_success_rate: Option, - pub payment_count: Option, - pub payment_success_count: Option, - pub payment_processed_amount: Option, - pub avg_ticket_size: Option, -} - -#[derive(Debug, serde::Serialize)] -pub struct MetricsBucketResponse { - #[serde(flatten)] - pub values: PaymentMetricsBucketValue, - #[serde(flatten)] - pub dimensions: PaymentMetricsBucketIdentifier, -} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs deleted file mode 100644 index 1ee05db41f20..000000000000 --- a/crates/api_models/src/analytics/refunds.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - -use common_enums::enums::{Currency, RefundStatus}; - -#[derive( - Clone, - Copy, - Debug, - Default, - Eq, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, -)] -// TODO RefundType common_enums need to mapped to storage_model -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum RefundType { - InstantRefund, - #[default] - RegularRefund, - RetryRefund, -} - -use super::{NameDescription, TimeRange}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] -pub struct RefundFilters { - #[serde(default)] - pub currency: Vec, - #[serde(default)] - pub refund_status: Vec, - #[serde(default)] - pub connector: Vec, - #[serde(default)] - pub refund_type: Vec, -} - -#[derive( - Debug, - serde::Serialize, - serde::Deserialize, - strum::AsRefStr, - PartialEq, - PartialOrd, - Eq, - Ord, - strum::Display, - strum::EnumIter, - Clone, - Copy, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum RefundDimensions { - Currency, - RefundStatus, - Connector, - RefundType, -} - -#[derive( - Clone, - Debug, - Hash, - PartialEq, - Eq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumIter, - strum::AsRefStr, -)] -#[strum(serialize_all = "snake_case")] -#[serde(rename_all = "snake_case")] -pub enum RefundMetrics { - RefundSuccessRate, - RefundCount, - RefundSuccessCount, - RefundProcessedAmount, -} - -pub mod metric_behaviour { - pub struct RefundSuccessRate; - pub struct RefundCount; - pub struct RefundSuccessCount; - pub struct RefundProcessedAmount; -} - -impl From for NameDescription { - fn from(value: RefundMetrics) -> Self { - Self { - name: value.to_string(), - desc: String::new(), - } - } -} - -impl From for NameDescription { - fn from(value: RefundDimensions) -> Self { - Self { - name: value.to_string(), - desc: String::new(), - } - } -} - -#[derive(Debug, serde::Serialize, Eq)] -pub struct RefundMetricsBucketIdentifier { - pub currency: Option, - pub refund_status: Option, - pub connector: Option, - pub refund_type: Option, - #[serde(rename = "time_range")] - pub time_bucket: TimeRange, - #[serde(rename = "time_bucket")] - #[serde(with = "common_utils::custom_serde::iso8601custom")] - pub start_time: time::PrimitiveDateTime, -} - -impl Hash for RefundMetricsBucketIdentifier { - fn hash(&self, state: &mut H) { - self.currency.hash(state); - self.refund_status.map(|i| i.to_string()).hash(state); - self.connector.hash(state); - self.refund_type.hash(state); - self.time_bucket.hash(state); - } -} -impl PartialEq for RefundMetricsBucketIdentifier { - fn eq(&self, other: &Self) -> bool { - let mut left = DefaultHasher::new(); - self.hash(&mut left); - let mut right = DefaultHasher::new(); - other.hash(&mut right); - left.finish() == right.finish() - } -} - -impl RefundMetricsBucketIdentifier { - pub fn new( - currency: Option, - refund_status: Option, - connector: Option, - refund_type: Option, - normalized_time_range: TimeRange, - ) -> Self { - Self { - currency, - refund_status, - connector, - refund_type, - time_bucket: normalized_time_range, - start_time: normalized_time_range.start_time, - } - } -} - -#[derive(Debug, serde::Serialize)] -pub struct RefundMetricsBucketValue { - pub refund_success_rate: Option, - pub refund_count: Option, - pub refund_success_count: Option, - pub refund_processed_amount: Option, -} - -#[derive(Debug, serde::Serialize)] -pub struct RefundMetricsBucketResponse { - #[serde(flatten)] - pub values: RefundMetricsBucketValue, - #[serde(flatten)] - pub dimensions: RefundMetricsBucketIdentifier, -} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index b71645e2d163..dab1b46adbad 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -1,6 +1,5 @@ #![forbid(unsafe_code)] pub mod admin; -pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index edbfa143a667..d64abe38e5b0 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -170,51 +170,3 @@ pub mod json_string { serde_json::from_str(&j).map_err(de::Error::custom) } } - -/// Use a custom ISO 8601 format when serializing and deserializing -/// [`PrimitiveDateTime`][PrimitiveDateTime]. -/// -/// [PrimitiveDateTime]: ::time::PrimitiveDateTime -pub mod iso8601custom { - - use serde::{ser::Error as _, Deserializer, Serialize, Serializer}; - use time::{ - format_description::well_known::{ - iso8601::{Config, EncodedConfig, TimePrecision}, - Iso8601, - }, - serde::iso8601, - PrimitiveDateTime, UtcOffset, - }; - - const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT - .set_time_precision(TimePrecision::Second { - decimal_digits: None, - }) - .encode(); - - /// Serialize a [`PrimitiveDateTime`] using the well-known ISO 8601 format. - pub fn serialize(date_time: &PrimitiveDateTime, serializer: S) -> Result - where - S: Serializer, - { - date_time - .assume_utc() - .format(&Iso8601::) - .map_err(S::Error::custom)? - .replace('T', " ") - .replace('Z', "") - .serialize(serializer) - } - - /// Deserialize an [`PrimitiveDateTime`] from its ISO 8601 representation. - pub fn deserialize<'a, D>(deserializer: D) -> Result - where - D: Deserializer<'a>, - { - iso8601::deserialize(deserializer).map(|offset_date_time| { - let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC); - PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time()) - }) - } -} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 0349a6b9d4c1..81b23314ffb8 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -41,7 +41,6 @@ aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } base64 = "0.21.2" bb8 = "0.8" -bigdecimal = "0.3.1" blake3 = "1.3.3" bytes = "1.4.0" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } @@ -79,7 +78,6 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" signal-hook = "0.3.15" strum = { version = "0.24.1", features = ["derive"] } -sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } @@ -97,7 +95,6 @@ digest = "0.9" api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } cards = { version = "0.1.0", path = "../cards" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } -common_enums = { version = "0.1.0", path = "../common_enums"} external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs deleted file mode 100644 index fbb848ea963d..000000000000 --- a/crates/router/src/analytics.rs +++ /dev/null @@ -1,123 +0,0 @@ -mod core; -mod errors; -pub mod metrics; -mod payments; -mod query; -mod refunds; -pub mod routes; - -mod sqlx; -mod types; -mod utils; - -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use router_env::{instrument, tracing}; - -use self::{ - payments::metrics::{PaymentMetric, PaymentMetricRow}, - refunds::metrics::{RefundMetric, RefundMetricRow}, - sqlx::SqlxClient, -}; -use crate::configs::settings::Database; - -#[derive(Clone, Debug)] -pub enum AnalyticsProvider { - Sqlx(SqlxClient), -} - -impl AnalyticsProvider { - #[instrument(skip_all)] - pub async fn get_payment_metrics( - &self, - metric: &PaymentMetrics, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - // Metrics to get the fetch time for each payment metric - metrics::request::record_operation_time( - async { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } - }, - &metrics::METRIC_FETCH_TIME, - metric, - self, - ) - .await - } - - pub async fn get_refund_metrics( - &self, - metric: &RefundMetrics, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } - } - - pub async fn from_conf( - config: &AnalyticsConfig, - #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, - ) -> Self { - match config { - AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( - SqlxClient::from_conf( - sqlx, - #[cfg(feature = "kms")] - kms_client, - ) - .await, - ), - } - } -} - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "source")] -#[serde(rename_all = "lowercase")] -pub enum AnalyticsConfig { - Sqlx { sqlx: Database }, -} - -impl Default for AnalyticsConfig { - fn default() -> Self { - Self::Sqlx { - sqlx: Database::default(), - } - } -} diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs deleted file mode 100644 index bf124a6c0e85..000000000000 --- a/crates/router/src/analytics/core.rs +++ /dev/null @@ -1,96 +0,0 @@ -use api_models::analytics::{ - payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, - GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, - RefundFiltersResponse, -}; -use error_stack::ResultExt; - -use super::{ - errors::{self, AnalyticsError}, - payments::filters::{get_payment_filter_for_dimension, FilterRow}, - refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, - types::AnalyticsDomain, - utils, AnalyticsProvider, -}; -use crate::{services::ApplicationResponse, types::domain}; - -pub type AnalyticsApiResponse = errors::AnalyticsResult>; - -pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { - let info = match domain { - AnalyticsDomain::Payments => GetInfoResponse { - metrics: utils::get_payment_metrics_info(), - download_dimensions: None, - dimensions: utils::get_payment_dimensions(), - }, - AnalyticsDomain::Refunds => GetInfoResponse { - metrics: utils::get_refund_metrics_info(), - download_dimensions: None, - dimensions: utils::get_refund_dimensions(), - }, - }; - Ok(ApplicationResponse::Json(info)) -} - -pub async fn payment_filters_core( - pool: AnalyticsProvider, - req: GetPaymentFiltersRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = PaymentFiltersResponse::default(); - - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: FilterRow| match dim { - PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), - PaymentDimensions::Connector => fil.connector, - PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentMethod => fil.payment_method, - }) - .collect::>(); - res.query_data.push(FilterValue { - dimension: dim, - values, - }) - } - - Ok(ApplicationResponse::Json(res)) -} - -pub async fn refund_filter_core( - pool: AnalyticsProvider, - req: GetRefundFilterRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = RefundFiltersResponse::default(); - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: RefundFilterRow| match dim { - RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), - RefundDimensions::Connector => fil.connector, - RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), - }) - .collect::>(); - res.query_data.push(RefundFilterValue { - dimension: dim, - values, - }) - } - Ok(ApplicationResponse::Json(res)) -} diff --git a/crates/router/src/analytics/errors.rs b/crates/router/src/analytics/errors.rs deleted file mode 100644 index da0b2f239cd7..000000000000 --- a/crates/router/src/analytics/errors.rs +++ /dev/null @@ -1,32 +0,0 @@ -use api_models::errors::types::{ApiError, ApiErrorResponse}; -use common_utils::errors::{CustomResult, ErrorSwitch}; - -pub type AnalyticsResult = CustomResult; - -#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] -pub enum AnalyticsError { - #[allow(dead_code)] - #[error("Not implemented: {0}")] - NotImplemented(&'static str), - #[error("Unknown Analytics Error")] - UnknownError, -} - -impl ErrorSwitch for AnalyticsError { - fn switch(&self) -> ApiErrorResponse { - match self { - Self::NotImplemented(feature) => ApiErrorResponse::NotImplemented(ApiError::new( - "IR", - 0, - format!("{feature} is not implemented."), - None, - )), - Self::UnknownError => ApiErrorResponse::InternalServerError(ApiError::new( - "HE", - 0, - "Something went wrong", - None, - )), - } - } -} diff --git a/crates/router/src/analytics/metrics.rs b/crates/router/src/analytics/metrics.rs deleted file mode 100644 index 6222315a8c06..000000000000 --- a/crates/router/src/analytics/metrics.rs +++ /dev/null @@ -1,9 +0,0 @@ -use router_env::{global_meter, histogram_metric, histogram_metric_u64, metrics_context}; - -metrics_context!(CONTEXT); -global_meter!(GLOBAL_METER, "ROUTER_API"); - -histogram_metric!(METRIC_FETCH_TIME, GLOBAL_METER); -histogram_metric_u64!(BUCKETS_FETCHED, GLOBAL_METER); - -pub mod request; diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/router/src/analytics/metrics/request.rs deleted file mode 100644 index b7c202f2db25..000000000000 --- a/crates/router/src/analytics/metrics/request.rs +++ /dev/null @@ -1,60 +0,0 @@ -pub fn add_attributes>( - key: &'static str, - value: T, -) -> router_env::opentelemetry::KeyValue { - router_env::opentelemetry::KeyValue::new(key, value) -} - -#[inline] -pub async fn record_operation_time( - future: F, - metric: &once_cell::sync::Lazy>, - metric_name: &api_models::analytics::payments::PaymentMetrics, - source: &crate::analytics::AnalyticsProvider, -) -> R -where - F: futures::Future, -{ - let (result, time) = time_future(future).await; - let attributes = &[ - add_attributes("metric_name", metric_name.to_string()), - add_attributes( - "source", - match source { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), - ]; - let value = time.as_secs_f64(); - metric.record(&super::CONTEXT, value, attributes); - - router_env::logger::debug!("Attributes: {:?}, Time: {}", attributes, value); - result -} - -use std::time; - -#[inline] -pub async fn time_future(future: F) -> (R, time::Duration) -where - F: futures::Future, -{ - let start = time::Instant::now(); - let result = future.await; - let time_spent = start.elapsed(); - (result, time_spent) -} - -#[macro_export] -macro_rules! histogram_metric { - ($name:ident, $meter:ident) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); - }; - ($name:ident, $meter:ident, $description:literal) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); - }; -} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs deleted file mode 100644 index 527bf75a3c72..000000000000 --- a/crates/router/src/analytics/payments.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod accumulator; -mod core; -pub mod filters; -pub mod metrics; -pub mod types; -pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; - -pub trait PaymentAnalytics: - metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics -{ -} - -pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/router/src/analytics/payments/accumulator.rs deleted file mode 100644 index 5eebd0974693..000000000000 --- a/crates/router/src/analytics/payments/accumulator.rs +++ /dev/null @@ -1,150 +0,0 @@ -use api_models::analytics::payments::PaymentMetricsBucketValue; -use common_enums::enums as storage_enums; -use router_env::logger; - -use super::metrics::PaymentMetricRow; - -#[derive(Debug, Default)] -pub struct PaymentMetricsAccumulator { - pub payment_success_rate: SuccessRateAccumulator, - pub payment_count: CountAccumulator, - pub payment_success: CountAccumulator, - pub processed_amount: SumAccumulator, - pub avg_ticket_size: AverageAccumulator, -} - -#[derive(Debug, Default)] -pub struct SuccessRateAccumulator { - pub success: i64, - pub total: i64, -} - -#[derive(Debug, Default)] -#[repr(transparent)] -pub struct CountAccumulator { - pub count: Option, -} - -#[derive(Debug, Default)] -#[repr(transparent)] -pub struct SumAccumulator { - pub total: Option, -} - -#[derive(Debug, Default)] -pub struct AverageAccumulator { - pub total: u32, - pub count: u32, -} - -pub trait PaymentMetricAccumulator { - type MetricOutput; - - fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow); - - fn collect(self) -> Self::MetricOutput; -} - -impl PaymentMetricAccumulator for SuccessRateAccumulator { - type MetricOutput = Option; - - fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { - if let Some(ref status) = metrics.status { - if status.as_ref() == &storage_enums::AttemptStatus::Charged { - self.success += metrics.count.unwrap_or_default(); - } - }; - self.total += metrics.count.unwrap_or_default(); - } - - fn collect(self) -> Self::MetricOutput { - if self.total <= 0 { - None - } else { - Some( - f64::from(u32::try_from(self.success).ok()?) * 100.0 - / f64::from(u32::try_from(self.total).ok()?), - ) - } - } -} - -impl PaymentMetricAccumulator for CountAccumulator { - type MetricOutput = Option; - #[inline] - fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { - self.count = match (self.count, metrics.count) { - (None, None) => None, - (None, i @ Some(_)) | (i @ Some(_), None) => i, - (Some(a), Some(b)) => Some(a + b), - } - } - #[inline] - fn collect(self) -> Self::MetricOutput { - self.count.and_then(|i| u64::try_from(i).ok()) - } -} - -impl PaymentMetricAccumulator for SumAccumulator { - type MetricOutput = Option; - #[inline] - fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { - self.total = match ( - self.total, - metrics - .total - .as_ref() - .and_then(bigdecimal::ToPrimitive::to_i64), - ) { - (None, None) => None, - (None, i @ Some(_)) | (i @ Some(_), None) => i, - (Some(a), Some(b)) => Some(a + b), - } - } - #[inline] - fn collect(self) -> Self::MetricOutput { - u64::try_from(self.total.unwrap_or(0)).ok() - } -} - -impl PaymentMetricAccumulator for AverageAccumulator { - type MetricOutput = Option; - - fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { - let total = metrics - .total - .as_ref() - .and_then(bigdecimal::ToPrimitive::to_u32); - let count = metrics.count.and_then(|total| u32::try_from(total).ok()); - - match (total, count) { - (Some(total), Some(count)) => { - self.total += total; - self.count += count; - } - _ => { - logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); - } - } - } - - fn collect(self) -> Self::MetricOutput { - if self.count == 0 { - None - } else { - Some(f64::from(self.total) / f64::from(self.count)) - } - } -} - -impl PaymentMetricsAccumulator { - pub fn collect(self) -> PaymentMetricsBucketValue { - PaymentMetricsBucketValue { - payment_success_rate: self.payment_success_rate.collect(), - payment_count: self.payment_count.collect(), - payment_success_count: self.payment_success.collect(), - payment_processed_amount: self.processed_amount.collect(), - avg_ticket_size: self.avg_ticket_size.collect(), - } - } -} diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs deleted file mode 100644 index 23eca8879a70..000000000000 --- a/crates/router/src/analytics/payments/core.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, - AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - instrument, logger, - tracing::{self, Instrument}, -}; - -use super::PaymentMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, metrics, - payments::PaymentMetricAccumulator, AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -#[instrument(skip_all)] -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetPaymentMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap< - PaymentMetricsBucketIdentifier, - PaymentMetricsAccumulator, - > = HashMap::new(); - - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_payments_query", - payment_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_payment_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - let data = data?; - let attributes = &[ - metrics::request::add_attributes("metric_type", metric.to_string()), - metrics::request::add_attributes( - "source", - match pool { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), - ]; - - let value = u64::try_from(data.len()); - if let Ok(val) = value { - metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); - logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); - } - - for (id, value) in data { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - PaymentMetrics::PaymentSuccessRate => metrics_builder - .payment_success_rate - .add_metrics_bucket(&value), - PaymentMetrics::PaymentCount => { - metrics_builder.payment_count.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentSuccessCount => { - metrics_builder.payment_success.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - PaymentMetrics::AvgTicketSize => { - metrics_builder.avg_ticket_size.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/router/src/analytics/payments/filters.rs deleted file mode 100644 index f009aaa76329..000000000000 --- a/crates/router/src/analytics/payments/filters.rs +++ /dev/null @@ -1,58 +0,0 @@ -use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, - types::{ - AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, - LoadRow, - }, -}; - -pub trait PaymentFilterAnalytics: LoadRow {} - -pub async fn get_payment_filter_for_dimension( - dimension: PaymentDimensions, - merchant: &String, - time_range: &TimeRange, - pool: &T, -) -> FiltersResult> -where - T: AnalyticsDataSource + PaymentFilterAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - - query_builder.add_select_column(dimension).switch()?; - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant) - .switch()?; - - query_builder.set_distinct(); - - query_builder - .execute_query::(pool) - .await - .change_context(FiltersError::QueryBuildingError)? - .change_context(FiltersError::QueryExecutionFailure) -} - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] -pub struct FilterRow { - pub currency: Option>, - pub status: Option>, - pub connector: Option, - pub authentication_type: Option>, - pub payment_method: Option, -} diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/router/src/analytics/payments/metrics.rs deleted file mode 100644 index f492e5bd4df9..000000000000 --- a/crates/router/src/analytics/payments/metrics.rs +++ /dev/null @@ -1,137 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use time::PrimitiveDateTime; - -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, -}; - -mod avg_ticket_size; -mod payment_count; -mod payment_processed_amount; -mod payment_success_count; -mod success_rate; - -use avg_ticket_size::AvgTicketSize; -use payment_count::PaymentCount; -use payment_processed_amount::PaymentProcessedAmount; -use payment_success_count::PaymentSuccessCount; -use success_rate::PaymentSuccessRate; - -#[derive(Debug, PartialEq, Eq)] -pub struct PaymentMetricRow { - pub currency: Option>, - pub status: Option>, - pub connector: Option, - pub authentication_type: Option>, - pub payment_method: Option, - pub total: Option, - pub count: Option, - pub start_bucket: Option, - pub end_bucket: Option, -} - -pub trait PaymentMetricAnalytics: LoadRow {} - -#[async_trait::async_trait] -pub trait PaymentMetric -where - T: AnalyticsDataSource + PaymentMetricAnalytics, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult>; -} - -#[async_trait::async_trait] -impl PaymentMetric for PaymentMetrics -where - T: AnalyticsDataSource + PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - match self { - Self::PaymentSuccessRate => { - PaymentSuccessRate - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::PaymentCount => { - PaymentCount - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::PaymentSuccessCount => { - PaymentSuccessCount - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::PaymentProcessedAmount => { - PaymentProcessedAmount - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::AvgTicketSize => { - AvgTicketSize - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } - } -} diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs deleted file mode 100644 index 2230d870e68a..000000000000 --- a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs +++ /dev/null @@ -1,126 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::{PaymentMetric, PaymentMetricRow}; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct AvgTicketSize; - -#[async_trait::async_trait] -impl PaymentMetric for AvgTicketSize -where - T: AnalyticsDataSource + super::PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Sum { - field: "amount", - alias: Some("total"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - PaymentMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - i.status.as_ref().map(|i| i.0), - i.connector.clone(), - i.authentication_type.as_ref().map(|i| i.0), - i.payment_method.clone(), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/router/src/analytics/payments/metrics/payment_count.rs deleted file mode 100644 index 661cec3dac36..000000000000 --- a/crates/router/src/analytics/payments/metrics/payment_count.rs +++ /dev/null @@ -1,117 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct PaymentCount; - -#[async_trait::async_trait] -impl super::PaymentMetric for PaymentCount -where - T: AnalyticsDataSource + super::PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - PaymentMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - i.status.as_ref().map(|i| i.0), - i.connector.clone(), - i.authentication_type.as_ref().map(|i| i.0), - i.payment_method.clone(), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, crate::analytics::query::PostProcessingError>>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs deleted file mode 100644 index 2ec0c6f18f9c..000000000000 --- a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs +++ /dev/null @@ -1,128 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct PaymentProcessedAmount; - -#[async_trait::async_trait] -impl super::PaymentMetric for PaymentProcessedAmount -where - T: AnalyticsDataSource + super::PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Sum { - field: "amount", - alias: Some("total"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .add_filter_clause( - PaymentDimensions::PaymentStatus, - storage_enums::AttemptStatus::Charged, - ) - .switch()?; - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - PaymentMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.authentication_type.as_ref().map(|i| i.0), - i.payment_method.clone(), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/router/src/analytics/payments/metrics/payment_success_count.rs deleted file mode 100644 index 8245fe7aeb88..000000000000 --- a/crates/router/src/analytics/payments/metrics/payment_success_count.rs +++ /dev/null @@ -1,127 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct PaymentSuccessCount; - -#[async_trait::async_trait] -impl super::PaymentMetric for PaymentSuccessCount -where - T: AnalyticsDataSource + super::PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .add_filter_clause( - PaymentDimensions::PaymentStatus, - storage_enums::AttemptStatus::Charged, - ) - .switch()?; - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - PaymentMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.authentication_type.as_ref().map(|i| i.0), - i.payment_method.clone(), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/router/src/analytics/payments/metrics/success_rate.rs deleted file mode 100644 index c63956d4b157..000000000000 --- a/crates/router/src/analytics/payments/metrics/success_rate.rs +++ /dev/null @@ -1,123 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct PaymentSuccessRate; - -#[async_trait::async_trait] -impl super::PaymentMetric for PaymentSuccessRate -where - T: AnalyticsDataSource + super::PaymentMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); - let mut dimensions = dimensions.to_vec(); - - dimensions.push(PaymentDimensions::PaymentStatus); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - PaymentMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.authentication_type.as_ref().map(|i| i.0), - i.payment_method.clone(), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/payments/types.rs b/crates/router/src/analytics/payments/types.rs deleted file mode 100644 index fdfbedef383d..000000000000 --- a/crates/router/src/analytics/payments/types.rs +++ /dev/null @@ -1,46 +0,0 @@ -use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; -use error_stack::ResultExt; - -use crate::analytics::{ - query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource}, -}; - -impl QueryFilter for PaymentFilters -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { - if !self.currency.is_empty() { - builder - .add_filter_in_range_clause(PaymentDimensions::Currency, &self.currency) - .attach_printable("Error adding currency filter")?; - } - - if !self.status.is_empty() { - builder - .add_filter_in_range_clause(PaymentDimensions::PaymentStatus, &self.status) - .attach_printable("Error adding payment status filter")?; - } - - if !self.connector.is_empty() { - builder - .add_filter_in_range_clause(PaymentDimensions::Connector, &self.connector) - .attach_printable("Error adding connector filter")?; - } - - if !self.auth_type.is_empty() { - builder - .add_filter_in_range_clause(PaymentDimensions::AuthType, &self.auth_type) - .attach_printable("Error adding auth type filter")?; - } - - if !self.payment_method.is_empty() { - builder - .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) - .attach_printable("Error adding payment method filter")?; - } - Ok(()) - } -} diff --git a/crates/router/src/analytics/query.rs b/crates/router/src/analytics/query.rs deleted file mode 100644 index b1f621d8153d..000000000000 --- a/crates/router/src/analytics/query.rs +++ /dev/null @@ -1,533 +0,0 @@ -#![allow(dead_code)] -use std::marker::PhantomData; - -use api_models::{ - analytics::{ - self as analytics_api, - payments::PaymentDimensions, - refunds::{RefundDimensions, RefundType}, - Granularity, - }, - enums::Connector, - refunds::RefundStatus, -}; -use common_enums::{ - enums as storage_enums, - enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, -}; -use common_utils::errors::{CustomResult, ParsingError}; -use error_stack::{IntoReport, ResultExt}; -use router_env::logger; - -use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; -use crate::analytics::types::QueryExecutionError; -pub type QueryResult = error_stack::Result; -pub trait QueryFilter -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; -} - -pub trait GroupByClause -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - fn set_group_by_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; -} - -pub trait SeriesBucket { - type SeriesType; - type GranularityLevel; - - fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel; - - fn get_bucket_size(&self) -> u8; - - fn clip_to_start( - &self, - value: Self::SeriesType, - ) -> error_stack::Result; - - fn clip_to_end( - &self, - value: Self::SeriesType, - ) -> error_stack::Result; -} - -impl QueryFilter for analytics_api::TimeRange -where - T: AnalyticsDataSource, - time::PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, -{ - fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { - builder.add_custom_filter_clause("created_at", self.start_time, FilterTypes::Gte)?; - if let Some(end) = self.end_time { - builder.add_custom_filter_clause("created_at", end, FilterTypes::Lte)?; - } - Ok(()) - } -} - -impl GroupByClause for Granularity { - fn set_group_by_clause( - &self, - builder: &mut QueryBuilder, - ) -> QueryResult<()> { - let trunc_scale = self.get_lowest_common_granularity_level(); - - let granularity_bucket_scale = match self { - Self::OneMin => None, - Self::FiveMin | Self::FifteenMin | Self::ThirtyMin => Some("minute"), - Self::OneHour | Self::OneDay => None, - }; - - let granularity_divisor = self.get_bucket_size(); - - builder - .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) - .attach_printable("Error adding time prune group by")?; - if let Some(scale) = granularity_bucket_scale { - builder - .add_group_by_clause(format!( - "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" - )) - .attach_printable("Error adding time binning group by")?; - } - Ok(()) - } -} - -#[derive(strum::Display)] -#[strum(serialize_all = "lowercase")] -pub enum TimeGranularityLevel { - Minute, - Hour, - Day, -} - -impl SeriesBucket for Granularity { - type SeriesType = time::PrimitiveDateTime; - - type GranularityLevel = TimeGranularityLevel; - - fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel { - match self { - Self::OneMin => TimeGranularityLevel::Minute, - Self::FiveMin | Self::FifteenMin | Self::ThirtyMin | Self::OneHour => { - TimeGranularityLevel::Hour - } - Self::OneDay => TimeGranularityLevel::Day, - } - } - - fn get_bucket_size(&self) -> u8 { - match self { - Self::OneMin => 60, - Self::FiveMin => 5, - Self::FifteenMin => 15, - Self::ThirtyMin => 30, - Self::OneHour => 60, - Self::OneDay => 24, - } - } - - fn clip_to_start( - &self, - value: Self::SeriesType, - ) -> error_stack::Result { - let clip_start = |value: u8, modulo: u8| -> u8 { value - value % modulo }; - - let clipped_time = match ( - self.get_lowest_common_granularity_level(), - self.get_bucket_size(), - ) { - (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT - .replace_second(clip_start(value.second(), i)) - .and_then(|t| t.replace_minute(value.minute())) - .and_then(|t| t.replace_hour(value.hour())), - (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT - .replace_minute(clip_start(value.minute(), i)) - .and_then(|t| t.replace_hour(value.hour())), - (TimeGranularityLevel::Day, i) => { - time::Time::MIDNIGHT.replace_hour(clip_start(value.hour(), i)) - } - } - .into_report() - .change_context(PostProcessingError::BucketClipping)?; - - Ok(value.replace_time(clipped_time)) - } - - fn clip_to_end( - &self, - value: Self::SeriesType, - ) -> error_stack::Result { - let clip_end = |value: u8, modulo: u8| -> u8 { value + modulo - 1 - value % modulo }; - - let clipped_time = match ( - self.get_lowest_common_granularity_level(), - self.get_bucket_size(), - ) { - (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT - .replace_second(clip_end(value.second(), i)) - .and_then(|t| t.replace_minute(value.minute())) - .and_then(|t| t.replace_hour(value.hour())), - (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT - .replace_minute(clip_end(value.minute(), i)) - .and_then(|t| t.replace_hour(value.hour())), - (TimeGranularityLevel::Day, i) => { - time::Time::MIDNIGHT.replace_hour(clip_end(value.hour(), i)) - } - } - .into_report() - .change_context(PostProcessingError::BucketClipping) - .attach_printable_lazy(|| format!("Bucket Clip Error: {value}"))?; - - Ok(value.replace_time(clipped_time)) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum QueryBuildingError { - #[allow(dead_code)] - #[error("Not Implemented: {0}")] - NotImplemented(String), - #[error("Failed to Serialize to SQL")] - SqlSerializeError, - #[error("Failed to build sql query: {0}")] - InvalidQuery(&'static str), -} - -#[derive(thiserror::Error, Debug)] -pub enum PostProcessingError { - #[error("Error Clipping values to bucket sizes")] - BucketClipping, -} - -#[derive(Debug)] -pub enum Aggregate { - Count { - field: Option, - alias: Option<&'static str>, - }, - Sum { - field: R, - alias: Option<&'static str>, - }, - Min { - field: R, - alias: Option<&'static str>, - }, - Max { - field: R, - alias: Option<&'static str>, - }, -} - -#[derive(Debug)] -pub struct QueryBuilder -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - columns: Vec, - filters: Vec<(String, FilterTypes, String)>, - group_by: Vec, - having: Option>, - table: AnalyticsCollection, - distinct: bool, - db_type: PhantomData, -} - -pub trait ToSql { - fn to_sql(&self) -> error_stack::Result; -} - -/// Implement `ToSql` on arrays of types that impl `ToString`. -macro_rules! impl_to_sql_for_to_string { - ($($type:ty),+) => { - $( - impl ToSql for $type { - fn to_sql(&self) -> error_stack::Result { - Ok(self.to_string()) - } - } - )+ - }; -} - -impl_to_sql_for_to_string!( - String, - &str, - &PaymentDimensions, - &RefundDimensions, - PaymentDimensions, - RefundDimensions, - PaymentMethod, - AuthenticationType, - Connector, - AttemptStatus, - RefundStatus, - storage_enums::RefundStatus, - Currency, - RefundType, - &String, - &bool, - &u64 -); - -#[allow(dead_code)] -#[derive(Debug)] -pub enum FilterTypes { - Equal, - EqualBool, - In, - Gte, - Lte, - Gt, -} - -impl QueryBuilder -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - pub fn new(table: AnalyticsCollection) -> Self { - Self { - columns: Default::default(), - filters: Default::default(), - group_by: Default::default(), - having: Default::default(), - table, - distinct: Default::default(), - db_type: Default::default(), - } - } - - pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { - self.columns.push( - column - .to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing select column")?, - ); - Ok(()) - } - - pub fn set_distinct(&mut self) { - self.distinct = true - } - - pub fn add_filter_clause( - &mut self, - key: impl ToSql, - value: impl ToSql, - ) -> QueryResult<()> { - self.add_custom_filter_clause(key, value, FilterTypes::Equal) - } - - pub fn add_bool_filter_clause( - &mut self, - key: impl ToSql, - value: impl ToSql, - ) -> QueryResult<()> { - self.add_custom_filter_clause(key, value, FilterTypes::EqualBool) - } - - pub fn add_custom_filter_clause( - &mut self, - lhs: impl ToSql, - rhs: impl ToSql, - comparison: FilterTypes, - ) -> QueryResult<()> { - self.filters.push(( - lhs.to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing filter key")?, - comparison, - rhs.to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing filter value")?, - )); - Ok(()) - } - - pub fn add_filter_in_range_clause( - &mut self, - key: impl ToSql, - values: &[impl ToSql], - ) -> QueryResult<()> { - let list = values - .iter() - .map(|i| { - // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection - i.to_sql().map(|s| { - let trimmed_str = s.replace(' ', ""); - format!("'{trimmed_str}'") - }) - }) - .collect::, ParsingError>>() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing range filter value")? - .join(", "); - self.add_custom_filter_clause(key, list, FilterTypes::In) - } - - pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { - self.group_by.push( - column - .to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing group by field")?, - ); - Ok(()) - } - - pub fn add_granularity_in_mins(&mut self, granularity: &Granularity) -> QueryResult<()> { - let interval = match granularity { - Granularity::OneMin => "1", - Granularity::FiveMin => "5", - Granularity::FifteenMin => "15", - Granularity::ThirtyMin => "30", - Granularity::OneHour => "60", - Granularity::OneDay => "1440", - }; - let _ = self.add_select_column(format!( - "toStartOfInterval(created_at, INTERVAL {interval} MINUTE) as time_bucket" - )); - Ok(()) - } - - fn get_filter_clause(&self) -> String { - self.filters - .iter() - .map(|(l, op, r)| match op { - FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::Equal => format!("{l} = '{r}'"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= '{r}'"), - FilterTypes::Gt => format!("{l} > {r}"), - FilterTypes::Lte => format!("{l} <= '{r}'"), - }) - .collect::>() - .join(" AND ") - } - - fn get_select_clause(&self) -> String { - self.columns.join(", ") - } - - fn get_group_by_clause(&self) -> String { - self.group_by.join(", ") - } - - #[allow(dead_code)] - pub fn add_having_clause( - &mut self, - aggregate: Aggregate, - filter_type: FilterTypes, - value: impl ToSql, - ) -> QueryResult<()> - where - Aggregate: ToSql, - { - let aggregate = aggregate - .to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing having aggregate")?; - let value = value - .to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing having value")?; - let entry = (aggregate, filter_type, value); - if let Some(having) = &mut self.having { - having.push(entry); - } else { - self.having = Some(vec![entry]); - } - Ok(()) - } - - pub fn get_filter_type_clause(&self) -> Option { - self.having.as_ref().map(|vec| { - vec.iter() - .map(|(l, op, r)| match op { - FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= {r}"), - FilterTypes::Lte => format!("{l} < {r}"), - FilterTypes::Gt => format!("{l} > {r}"), - }) - .collect::>() - .join(" AND ") - }) - } - - pub fn build_query(&mut self) -> QueryResult - where - Aggregate<&'static str>: ToSql, - { - if self.columns.is_empty() { - Err(QueryBuildingError::InvalidQuery( - "No select fields provided", - )) - .into_report()?; - } - let mut query = String::from("SELECT "); - - if self.distinct { - query.push_str("DISTINCT "); - } - - query.push_str(&self.get_select_clause()); - - query.push_str(" FROM "); - - query.push_str( - &self - .table - .to_sql() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Error serializing table value")?, - ); - - if !self.filters.is_empty() { - query.push_str(" WHERE "); - query.push_str(&self.get_filter_clause()); - } - - if !self.group_by.is_empty() { - query.push_str(" GROUP BY "); - query.push_str(&self.get_group_by_clause()); - } - - if self.having.is_some() { - if let Some(condition) = self.get_filter_type_clause() { - query.push_str(" HAVING "); - query.push_str(condition.as_str()); - } - } - Ok(query) - } - - pub async fn execute_query( - &mut self, - store: &P, - ) -> CustomResult, QueryExecutionError>, QueryBuildingError> - where - P: LoadRow, - Aggregate<&'static str>: ToSql, - { - let query = self - .build_query() - .change_context(QueryBuildingError::SqlSerializeError) - .attach_printable("Failed to execute query")?; - logger::debug!(?query); - Ok(store.load_results(query.as_str()).await) - } -} diff --git a/crates/router/src/analytics/refunds.rs b/crates/router/src/analytics/refunds.rs deleted file mode 100644 index a8b52effe76d..000000000000 --- a/crates/router/src/analytics/refunds.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod accumulator; -mod core; - -pub mod filters; -pub mod metrics; -pub mod types; -pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; - -pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} -pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/router/src/analytics/refunds/accumulator.rs deleted file mode 100644 index 3d0c0e659f6c..000000000000 --- a/crates/router/src/analytics/refunds/accumulator.rs +++ /dev/null @@ -1,110 +0,0 @@ -use api_models::analytics::refunds::RefundMetricsBucketValue; -use common_enums::enums as storage_enums; - -use super::metrics::RefundMetricRow; -#[derive(Debug, Default)] -pub struct RefundMetricsAccumulator { - pub refund_success_rate: SuccessRateAccumulator, - pub refund_count: CountAccumulator, - pub refund_success: CountAccumulator, - pub processed_amount: SumAccumulator, -} - -#[derive(Debug, Default)] -pub struct SuccessRateAccumulator { - pub success: i64, - pub total: i64, -} - -#[derive(Debug, Default)] -#[repr(transparent)] -pub struct CountAccumulator { - pub count: Option, -} - -#[derive(Debug, Default)] -#[repr(transparent)] -pub struct SumAccumulator { - pub total: Option, -} - -pub trait RefundMetricAccumulator { - type MetricOutput; - - fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow); - - fn collect(self) -> Self::MetricOutput; -} - -impl RefundMetricAccumulator for CountAccumulator { - type MetricOutput = Option; - #[inline] - fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { - self.count = match (self.count, metrics.count) { - (None, None) => None, - (None, i @ Some(_)) | (i @ Some(_), None) => i, - (Some(a), Some(b)) => Some(a + b), - } - } - #[inline] - fn collect(self) -> Self::MetricOutput { - self.count.and_then(|i| u64::try_from(i).ok()) - } -} - -impl RefundMetricAccumulator for SumAccumulator { - type MetricOutput = Option; - #[inline] - fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { - self.total = match ( - self.total, - metrics - .total - .as_ref() - .and_then(bigdecimal::ToPrimitive::to_i64), - ) { - (None, None) => None, - (None, i @ Some(_)) | (i @ Some(_), None) => i, - (Some(a), Some(b)) => Some(a + b), - } - } - #[inline] - fn collect(self) -> Self::MetricOutput { - self.total.and_then(|i| u64::try_from(i).ok()) - } -} - -impl RefundMetricAccumulator for SuccessRateAccumulator { - type MetricOutput = Option; - - fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { - if let Some(ref refund_status) = metrics.refund_status { - if refund_status.as_ref() == &storage_enums::RefundStatus::Success { - self.success += metrics.count.unwrap_or_default(); - } - }; - self.total += metrics.count.unwrap_or_default(); - } - - fn collect(self) -> Self::MetricOutput { - if self.total <= 0 { - None - } else { - Some( - f64::from(u32::try_from(self.success).ok()?) * 100.0 - / f64::from(u32::try_from(self.total).ok()?), - ) - } - } -} - -impl RefundMetricsAccumulator { - pub fn collect(self) -> RefundMetricsBucketValue { - RefundMetricsBucketValue { - refund_success_rate: self.refund_success_rate.collect(), - refund_count: self.refund_count.collect(), - refund_success_count: self.refund_success.collect(), - refund_processed_amount: self.processed_amount.collect(), - } - } -} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs deleted file mode 100644 index 4c2d2c394181..000000000000 --- a/crates/router/src/analytics/refunds/core.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, - AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - logger, - tracing::{self, Instrument}, -}; - -use super::RefundMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, - AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetRefundMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap = - HashMap::new(); - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_refund_query", - refund_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_refund_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - for (id, value) in data? { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - RefundMetrics::RefundSuccessRate => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { - metrics_builder.refund_count.add_metrics_bucket(&value) - } - RefundMetrics::RefundSuccessCount => { - metrics_builder.refund_success.add_metrics_bucket(&value) - } - RefundMetrics::RefundProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| RefundMetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/router/src/analytics/refunds/filters.rs deleted file mode 100644 index 6b45e9194fad..000000000000 --- a/crates/router/src/analytics/refunds/filters.rs +++ /dev/null @@ -1,59 +0,0 @@ -use api_models::analytics::{ - refunds::{RefundDimensions, RefundType}, - Granularity, TimeRange, -}; -use common_enums::enums::{Currency, RefundStatus}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, - types::{ - AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, - LoadRow, - }, -}; -pub trait RefundFilterAnalytics: LoadRow {} - -pub async fn get_refund_filter_for_dimension( - dimension: RefundDimensions, - merchant: &String, - time_range: &TimeRange, - pool: &T, -) -> FiltersResult> -where - T: AnalyticsDataSource + RefundFilterAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); - - query_builder.add_select_column(dimension).switch()?; - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant) - .switch()?; - - query_builder.set_distinct(); - - query_builder - .execute_query::(pool) - .await - .change_context(FiltersError::QueryBuildingError)? - .change_context(FiltersError::QueryExecutionFailure) -} - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] -pub struct RefundFilterRow { - pub currency: Option>, - pub refund_status: Option>, - pub connector: Option, - pub refund_type: Option>, -} diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/router/src/analytics/refunds/metrics.rs deleted file mode 100644 index d4f509b4a1e3..000000000000 --- a/crates/router/src/analytics/refunds/metrics.rs +++ /dev/null @@ -1,126 +0,0 @@ -use api_models::analytics::{ - refunds::{ - RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier, RefundType, - }, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use time::PrimitiveDateTime; -mod refund_count; -mod refund_processed_amount; -mod refund_success_count; -mod refund_success_rate; -use refund_count::RefundCount; -use refund_processed_amount::RefundProcessedAmount; -use refund_success_count::RefundSuccessCount; -use refund_success_rate::RefundSuccessRate; - -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, -}; - -#[derive(Debug, Eq, PartialEq)] -pub struct RefundMetricRow { - pub currency: Option>, - pub refund_status: Option>, - pub connector: Option, - pub refund_type: Option>, - pub total: Option, - pub count: Option, - pub start_bucket: Option, - pub end_bucket: Option, -} - -pub trait RefundMetricAnalytics: LoadRow {} - -#[async_trait::async_trait] -pub trait RefundMetric -where - T: AnalyticsDataSource + RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult>; -} - -#[async_trait::async_trait] -impl RefundMetric for RefundMetrics -where - T: AnalyticsDataSource + RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - match self { - Self::RefundSuccessRate => { - RefundSuccessRate::default() - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::RefundCount => { - RefundCount::default() - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::RefundSuccessCount => { - RefundSuccessCount::default() - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::RefundProcessedAmount => { - RefundProcessedAmount::default() - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } - } -} diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/router/src/analytics/refunds/metrics/refund_count.rs deleted file mode 100644 index 471327235073..000000000000 --- a/crates/router/src/analytics/refunds/metrics/refund_count.rs +++ /dev/null @@ -1,116 +0,0 @@ -use api_models::analytics::{ - refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct RefundCount {} - -#[async_trait::async_trait] -impl super::RefundMetric for RefundCount -where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .attach_printable("Error adding granularity") - .switch()?; - } - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - RefundMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - i.refund_status.as_ref().map(|i| i.0), - i.connector.clone(), - i.refund_type.as_ref().map(|i| i.0.to_string()), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, crate::analytics::query::PostProcessingError>>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs deleted file mode 100644 index c5f3a706aaef..000000000000 --- a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs +++ /dev/null @@ -1,122 +0,0 @@ -use api_models::analytics::{ - refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; -#[derive(Default)] -pub(super) struct RefundProcessedAmount {} - -#[async_trait::async_trait] -impl super::RefundMetric for RefundProcessedAmount -where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> - where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Sum { - field: "refund_amount", - alias: Some("total"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range - .set_filter_clause(&mut query_builder) - .attach_printable("Error filtering time range") - .switch()?; - - for dim in dimensions.iter() { - query_builder.add_group_by_clause(dim).switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .switch()?; - } - - query_builder - .add_filter_clause( - RefundDimensions::RefundStatus, - storage_enums::RefundStatus::Success, - ) - .switch()?; - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - RefundMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.refund_type.as_ref().map(|i| i.0.to_string()), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, crate::analytics::query::PostProcessingError>>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/router/src/analytics/refunds/metrics/refund_success_count.rs deleted file mode 100644 index 0c8032908fd7..000000000000 --- a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs +++ /dev/null @@ -1,122 +0,0 @@ -use api_models::analytics::{ - refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_enums::enums as storage_enums; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; - -#[derive(Default)] -pub(super) struct RefundSuccessCount {} - -#[async_trait::async_trait] -impl super::RefundMetric for RefundSuccessCount -where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> - where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - { - let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range.set_filter_clause(&mut query_builder).switch()?; - - for dim in dimensions.iter() { - query_builder.add_group_by_clause(dim).switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .switch()?; - } - - query_builder - .add_filter_clause( - RefundDimensions::RefundStatus, - storage_enums::RefundStatus::Success, - ) - .switch()?; - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - RefundMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.refund_type.as_ref().map(|i| i.0.to_string()), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs deleted file mode 100644 index 42f9ccf8d3c0..000000000000 --- a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs +++ /dev/null @@ -1,117 +0,0 @@ -use api_models::analytics::{ - refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use common_utils::errors::ReportSwitchExt; -use error_stack::ResultExt; -use time::PrimitiveDateTime; - -use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, -}; -#[derive(Default)] -pub(super) struct RefundSuccessRate {} - -#[async_trait::async_trait] -impl super::RefundMetric for RefundSuccessRate -where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - PrimitiveDateTime: ToSql, - AnalyticsCollection: ToSql, - Granularity: GroupByClause, - Aggregate<&'static str>: ToSql, -{ - async fn load_metrics( - &self, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - pool: &T, - ) -> MetricsResult> - where - T: AnalyticsDataSource + super::RefundMetricAnalytics, - { - let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); - let mut dimensions = dimensions.to_vec(); - - dimensions.push(RefundDimensions::RefundStatus); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } - - query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Min { - field: "created_at", - alias: Some("start_bucket"), - }) - .switch()?; - query_builder - .add_select_column(Aggregate::Max { - field: "created_at", - alias: Some("end_bucket"), - }) - .switch()?; - - filters.set_filter_clause(&mut query_builder).switch()?; - - query_builder - .add_filter_clause("merchant_id", merchant_id) - .switch()?; - - time_range.set_filter_clause(&mut query_builder).switch()?; - - for dim in dimensions.iter() { - query_builder.add_group_by_clause(dim).switch()?; - } - - if let Some(granularity) = granularity.as_ref() { - granularity - .set_group_by_clause(&mut query_builder) - .switch()?; - } - - query_builder - .execute_query::(pool) - .await - .change_context(MetricsError::QueryBuildingError)? - .change_context(MetricsError::QueryExecutionFailure)? - .into_iter() - .map(|i| { - Ok(( - RefundMetricsBucketIdentifier::new( - i.currency.as_ref().map(|i| i.0), - None, - i.connector.clone(), - i.refund_type.as_ref().map(|i| i.0.to_string()), - TimeRange { - start_time: match (granularity, i.start_bucket) { - (Some(g), Some(st)) => g.clip_to_start(st)?, - _ => time_range.start_time, - }, - end_time: granularity.as_ref().map_or_else( - || Ok(time_range.end_time), - |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), - )?, - }, - ), - i, - )) - }) - .collect::, - crate::analytics::query::PostProcessingError, - >>() - .change_context(MetricsError::PostProcessingFailure) - } -} diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/router/src/analytics/refunds/types.rs deleted file mode 100644 index fbfd69972671..000000000000 --- a/crates/router/src/analytics/refunds/types.rs +++ /dev/null @@ -1,41 +0,0 @@ -use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; -use error_stack::ResultExt; - -use crate::analytics::{ - query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, - types::{AnalyticsCollection, AnalyticsDataSource}, -}; - -impl QueryFilter for RefundFilters -where - T: AnalyticsDataSource, - AnalyticsCollection: ToSql, -{ - fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { - if !self.currency.is_empty() { - builder - .add_filter_in_range_clause(RefundDimensions::Currency, &self.currency) - .attach_printable("Error adding currency filter")?; - } - - if !self.refund_status.is_empty() { - builder - .add_filter_in_range_clause(RefundDimensions::RefundStatus, &self.refund_status) - .attach_printable("Error adding refund status filter")?; - } - - if !self.connector.is_empty() { - builder - .add_filter_in_range_clause(RefundDimensions::Connector, &self.connector) - .attach_printable("Error adding connector filter")?; - } - - if !self.refund_type.is_empty() { - builder - .add_filter_in_range_clause(RefundDimensions::RefundType, &self.refund_type) - .attach_printable("Error adding auth type filter")?; - } - - Ok(()) - } -} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs deleted file mode 100644 index 298ec61ec903..000000000000 --- a/crates/router/src/analytics/routes.rs +++ /dev/null @@ -1,145 +0,0 @@ -use actix_web::{web, Responder, Scope}; -use api_models::analytics::{ - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, -}; -use router_env::AnalyticsFlow; - -use super::{core::*, payments, refunds, types::AnalyticsDomain}; -use crate::{ - core::api_locking, - services::{api, authentication as auth, authentication::AuthenticationData}, - AppState, -}; - -pub struct Analytics; - -impl Analytics { - pub fn server(state: AppState) -> Scope { - let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); - route - .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) - .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) - .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) - .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) - .service(web::resource("{domain}/info").route(web::get().to(get_info))) - } -} - -pub async fn get_info( - state: web::Data, - req: actix_web::HttpRequest, - domain: actix_web::web::Path, -) -> impl Responder { - let flow = AnalyticsFlow::GetInfo; - api::server_wrap( - flow, - state, - &req, - domain.into_inner(), - |_, _, domain| get_domain_info(domain), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. -pub async fn get_payment_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetPaymentMetricRequest; 1]>, -) -> impl Responder { - // safety: This shouldn't panic owing to the data type - #[allow(clippy::expect_used)] - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetPaymentMetricRequest"); - let flow = AnalyticsFlow::GetPaymentMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - payments::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. -pub async fn get_refunds_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetRefundMetricRequest; 1]>, -) -> impl Responder { - #[allow(clippy::expect_used)] - // safety: This shouldn't panic owing to the data type - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetRefundMetricRequest"); - let flow = AnalyticsFlow::GetRefundsMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_payment_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetPaymentFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req| { - payment_filters_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_refund_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetRefundFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req: GetRefundFilterRequest| { - refund_filter_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), - api_locking::LockAction::NotApplicable, - ) - .await -} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/router/src/analytics/sqlx.rs deleted file mode 100644 index 5c3060c35a92..000000000000 --- a/crates/router/src/analytics/sqlx.rs +++ /dev/null @@ -1,386 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use api_models::analytics::refunds::RefundType; -use common_enums::enums::{ - AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, -}; -use common_utils::errors::{CustomResult, ParsingError}; -use error_stack::{IntoReport, ResultExt}; -#[cfg(feature = "kms")] -use external_services::{kms, kms::decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] -use masking::PeekInterface; -use sqlx::{ - postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, - Decode, Encode, - Error::ColumnNotFound, - FromRow, Pool, Postgres, Row, -}; -use time::PrimitiveDateTime; - -use super::{ - query::{Aggregate, ToSql}, - types::{ - AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, - }, -}; -use crate::configs::settings::Database; - -#[derive(Debug, Clone)] -pub struct SqlxClient { - pool: Pool, -} - -impl SqlxClient { - pub async fn from_conf( - conf: &Database, - #[cfg(feature = "kms")] kms_client: &kms::KmsClient, - ) -> Self { - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let password = conf - .password - .decrypt_inner(kms_client) - .await - .expect("Failed to KMS decrypt database password"); - - #[cfg(not(feature = "kms"))] - let password = &conf.password.peek(); - let database_url = format!( - "postgres://{}:{}@{}:{}/{}", - conf.username, password, conf.host, conf.port, conf.dbname - ); - #[allow(clippy::expect_used)] - let pool = PgPoolOptions::new() - .max_connections(conf.pool_size) - .acquire_timeout(std::time::Duration::from_secs(conf.connection_timeout)) - .connect_lazy(&database_url) - .expect("SQLX Pool Creation failed"); - Self { pool } - } -} - -pub trait DbType { - fn name() -> &'static str; -} - -macro_rules! db_type { - ($a: ident, $str: tt) => { - impl DbType for $a { - fn name() -> &'static str { - stringify!($str) - } - } - }; - ($a:ident) => { - impl DbType for $a { - fn name() -> &'static str { - stringify!($a) - } - } - }; -} - -db_type!(Currency); -db_type!(AuthenticationType); -db_type!(AttemptStatus); -db_type!(PaymentMethod, TEXT); -db_type!(RefundStatus); -db_type!(RefundType); - -impl<'q, Type> Encode<'q, Postgres> for DBEnumWrapper -where - Type: DbType + FromStr + Display, -{ - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> sqlx::encode::IsNull { - self.0.to_string().encode(buf) - } - fn size_hint(&self) -> usize { - self.0.to_string().size_hint() - } -} - -impl<'r, Type> Decode<'r, Postgres> for DBEnumWrapper -where - Type: DbType + FromStr + Display, -{ - fn decode( - value: PgValueRef<'r>, - ) -> Result> { - let str_value = <&'r str as Decode<'r, Postgres>>::decode(value)?; - Type::from_str(str_value).map(DBEnumWrapper).or(Err(format!( - "invalid value {:?} for enum {}", - str_value, - Type::name() - ) - .into())) - } -} - -impl sqlx::Type for DBEnumWrapper -where - Type: DbType + FromStr + Display, -{ - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name(Type::name()) - } -} - -impl LoadRow for SqlxClient -where - for<'a> T: FromRow<'a, PgRow>, -{ - fn load_row(row: PgRow) -> CustomResult { - T::from_row(&row) - .into_report() - .change_context(QueryExecutionError::RowExtractionFailure) - } -} - -impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} -impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} -impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} -impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} - -#[async_trait::async_trait] -impl AnalyticsDataSource for SqlxClient { - type Row = PgRow; - - async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> - where - Self: LoadRow, - { - sqlx::query(&format!("{query};")) - .fetch_all(&self.pool) - .await - .into_report() - .change_context(QueryExecutionError::DatabaseError) - .attach_printable_lazy(|| format!("Failed to run query {query}"))? - .into_iter() - .map(Self::load_row) - .collect::, _>>() - .change_context(QueryExecutionError::RowExtractionFailure) - } -} - -impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { - fn from_row(row: &'a PgRow) -> sqlx::Result { - let currency: Option> = - row.try_get("currency").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let refund_status: Option> = - row.try_get("refund_status").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let connector: Option = row.try_get("connector").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let refund_type: Option> = - row.try_get("refund_type").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let total: Option = row.try_get("total").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let count: Option = row.try_get("count").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - - let start_bucket: Option = row - .try_get::, _>("start_bucket")? - .and_then(|dt| dt.replace_millisecond(0).ok()); - let end_bucket: Option = row - .try_get::, _>("end_bucket")? - .and_then(|dt| dt.replace_millisecond(0).ok()); - Ok(Self { - currency, - refund_status, - connector, - refund_type, - total, - count, - start_bucket, - end_bucket, - }) - } -} - -impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { - fn from_row(row: &'a PgRow) -> sqlx::Result { - let currency: Option> = - row.try_get("currency").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let status: Option> = - row.try_get("status").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let connector: Option = row.try_get("connector").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let authentication_type: Option> = - row.try_get("authentication_type").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let payment_method: Option = - row.try_get("payment_method").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let total: Option = row.try_get("total").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let count: Option = row.try_get("count").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - - let start_bucket: Option = row - .try_get::, _>("start_bucket")? - .and_then(|dt| dt.replace_millisecond(0).ok()); - let end_bucket: Option = row - .try_get::, _>("end_bucket")? - .and_then(|dt| dt.replace_millisecond(0).ok()); - Ok(Self { - currency, - status, - connector, - authentication_type, - payment_method, - total, - count, - start_bucket, - end_bucket, - }) - } -} - -impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { - fn from_row(row: &'a PgRow) -> sqlx::Result { - let currency: Option> = - row.try_get("currency").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let status: Option> = - row.try_get("status").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let connector: Option = row.try_get("connector").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let authentication_type: Option> = - row.try_get("authentication_type").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let payment_method: Option = - row.try_get("payment_method").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - Ok(Self { - currency, - status, - connector, - authentication_type, - payment_method, - }) - } -} - -impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { - fn from_row(row: &'a PgRow) -> sqlx::Result { - let currency: Option> = - row.try_get("currency").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let refund_status: Option> = - row.try_get("refund_status").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let connector: Option = row.try_get("connector").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - let refund_type: Option> = - row.try_get("refund_type").or_else(|e| match e { - ColumnNotFound(_) => Ok(Default::default()), - e => Err(e), - })?; - Ok(Self { - currency, - refund_status, - connector, - refund_type, - }) - } -} - -impl ToSql for PrimitiveDateTime { - fn to_sql(&self) -> error_stack::Result { - Ok(self.to_string()) - } -} - -impl ToSql for AnalyticsCollection { - fn to_sql(&self) -> error_stack::Result { - match self { - Self::Payment => Ok("payment_attempt".to_string()), - Self::Refund => Ok("refund".to_string()), - } - } -} - -impl ToSql for Aggregate -where - T: ToSql, -{ - fn to_sql(&self) -> error_stack::Result { - Ok(match self { - Self::Count { field: _, alias } => { - format!( - "count(*){}", - alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) - ) - } - Self::Sum { field, alias } => { - format!( - "sum({}){}", - field.to_sql().attach_printable("Failed to sum aggregate")?, - alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) - ) - } - Self::Min { field, alias } => { - format!( - "min({}){}", - field.to_sql().attach_printable("Failed to min aggregate")?, - alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) - ) - } - Self::Max { field, alias } => { - format!( - "max({}){}", - field.to_sql().attach_printable("Failed to max aggregate")?, - alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) - ) - } - }) - } -} diff --git a/crates/router/src/analytics/types.rs b/crates/router/src/analytics/types.rs deleted file mode 100644 index 5173e6214de4..000000000000 --- a/crates/router/src/analytics/types.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use common_utils::errors::{CustomResult, ErrorSwitch, ParsingError}; -use error_stack::{report, Report, ResultExt}; - -use super::query::QueryBuildingError; - -#[derive(serde::Deserialize, Debug, masking::Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AnalyticsDomain { - Payments, - Refunds, -} - -#[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] -pub enum AnalyticsCollection { - Payment, - Refund, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] -#[serde(transparent)] -pub struct DBEnumWrapper(pub T); - -impl AsRef for DBEnumWrapper { - fn as_ref(&self) -> &T { - &self.0 - } -} - -impl FromStr for DBEnumWrapper -where - T: FromStr + Display, -{ - type Err = Report; - - fn from_str(s: &str) -> Result { - T::from_str(s) - .map_err(|_er| report!(ParsingError::EnumParseFailure(std::any::type_name::()))) - .map(DBEnumWrapper) - .attach_printable_lazy(|| format!("raw_value: {s}")) - } -} - -// Analytics Framework - -pub trait RefundAnalytics {} - -#[async_trait::async_trait] -pub trait AnalyticsDataSource -where - Self: Sized + Sync + Send, -{ - type Row; - async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> - where - Self: LoadRow; -} - -pub trait LoadRow -where - Self: AnalyticsDataSource, - T: Sized, -{ - fn load_row(row: Self::Row) -> CustomResult; -} - -#[derive(thiserror::Error, Debug)] -pub enum MetricsError { - #[error("Error building query")] - QueryBuildingError, - #[error("Error running Query")] - QueryExecutionFailure, - #[error("Error processing query results")] - PostProcessingFailure, - #[allow(dead_code)] - #[error("Not Implemented")] - NotImplemented, -} - -#[derive(Debug, thiserror::Error)] -pub enum QueryExecutionError { - #[error("Failed to extract domain rows")] - RowExtractionFailure, - #[error("Database error")] - DatabaseError, -} - -pub type MetricsResult = CustomResult; - -impl ErrorSwitch for QueryBuildingError { - fn switch(&self) -> MetricsError { - MetricsError::QueryBuildingError - } -} - -pub type FiltersResult = CustomResult; - -#[derive(thiserror::Error, Debug)] -pub enum FiltersError { - #[error("Error building query")] - QueryBuildingError, - #[error("Error running Query")] - QueryExecutionFailure, - #[allow(dead_code)] - #[error("Not Implemented")] - NotImplemented, -} - -impl ErrorSwitch for QueryBuildingError { - fn switch(&self) -> FiltersError { - FiltersError::QueryBuildingError - } -} diff --git a/crates/router/src/analytics/utils.rs b/crates/router/src/analytics/utils.rs deleted file mode 100644 index f7e6ea69dc37..000000000000 --- a/crates/router/src/analytics/utils.rs +++ /dev/null @@ -1,22 +0,0 @@ -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentMetrics}, - refunds::{RefundDimensions, RefundMetrics}, - NameDescription, -}; -use strum::IntoEnumIterator; - -pub fn get_payment_dimensions() -> Vec { - PaymentDimensions::iter().map(Into::into).collect() -} - -pub fn get_refund_dimensions() -> Vec { - RefundDimensions::iter().map(Into::into).collect() -} - -pub fn get_payment_metrics_info() -> Vec { - PaymentMetrics::iter().map(Into::into).collect() -} - -pub fn get_refund_metrics_info() -> Vec { - RefundMetrics::iter().map(Into::into).collect() -} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index b48a52e53b1d..204060b37aa0 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -16,8 +16,6 @@ pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; -#[cfg(feature = "olap")] -use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, @@ -103,8 +101,6 @@ pub struct Settings { pub lock_settings: LockSettings, pub temp_locker_enable_config: TempLockerEnableConfig, pub payment_link: PaymentLink, - #[cfg(feature = "olap")] - pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index b91a79f072b0..11efec64055b 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,8 +1,6 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] -#[cfg(feature = "olap")] -mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -143,7 +141,6 @@ pub fn mk_app( .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) - .service(routes::Analytics::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 7bd0541f51f5..307797e8ac9d 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -37,5 +37,3 @@ pub use self::app::{ }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; -#[cfg(feature = "olap")] -pub use crate::analytics::routes::{self as analytics, Analytics}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5033a91717f4..5b16e93404ae 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -42,8 +42,6 @@ pub struct AppState { #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, - #[cfg(feature = "olap")] - pub pool: crate::analytics::AnalyticsProvider, } impl scheduler::SchedulerAppState for AppState { @@ -126,14 +124,6 @@ impl AppState { ), }; - #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, - #[cfg(feature = "kms")] - kms_client, - ) - .await; - #[cfg(feature = "kms")] #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { @@ -155,8 +145,6 @@ impl AppState { kms_secrets: Arc::new(kms_secrets), api_client, event_handler: Box::::default(), - #[cfg(feature = "olap")] - pool, } } diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index e75606aa1531..d3612767ff9d 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -1,5 +1,5 @@ #![forbid(unsafe_code)] -#![warn(missing_debug_implementations)] +#![warn(missing_docs, missing_debug_implementations)] //! //! Environment of payment router: logger, basic config, its environment awareness. @@ -22,7 +22,6 @@ pub mod vergen; pub use logger::*; pub use once_cell; pub use opentelemetry; -use strum::Display; pub use tracing; #[cfg(feature = "actix_web")] pub use tracing_actix_web; @@ -30,19 +29,3 @@ pub use tracing_appender; #[doc(inline)] pub use self::env::*; -use crate::types::FlowMetric; - -/// Analytics Flow routes Enums -/// Info - Dimensions and filters available for the domain -/// Filters - Set of values present for the dimension -/// Metrics - Analytical data on dimensions and metrics -#[derive(Debug, Display, Clone, PartialEq, Eq)] -pub enum AnalyticsFlow { - GetInfo, - GetPaymentFilters, - GetRefundFilters, - GetRefundsMetrics, - GetPaymentMetrics, -} - -impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/metrics.rs b/crates/router_env/src/metrics.rs index 14402a7a6e91..e4943699ee5b 100644 --- a/crates/router_env/src/metrics.rs +++ b/crates/router_env/src/metrics.rs @@ -63,22 +63,3 @@ macro_rules! histogram_metric { > = once_cell::sync::Lazy::new(|| $meter.f64_histogram($description).init()); }; } - -/// Create a [`Histogram`][Histogram] u64 metric with the specified name and an optional description, -/// associated with the specified meter. Note that the meter must be to a valid [`Meter`][Meter]. -/// -/// [Histogram]: opentelemetry::metrics::Histogram -/// [Meter]: opentelemetry::metrics::Meter -#[macro_export] -macro_rules! histogram_metric_u64 { - ($name:ident, $meter:ident) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); - }; - ($name:ident, $meter:ident, $description:literal) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); - }; -} diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 96f215ab08f4..7cdbc8dd6fdd 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -235,17 +235,5 @@ bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} -[analytics] -source = "sqlx" - -[analytics.sqlx] -username = "db_user" -password = "db_pass" -host = "localhost" -port = 5432 -dbname = "hyperswitch_db" -pool_size = 5 -connection_timeout = 10 - [kv_config] ttl = 300 # 5 * 60 seconds From 603215db05b69ff6f525733afbf61b26dedd00ce Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:06:13 +0000 Subject: [PATCH 07/57] chore(version): v1.70.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f88af7c8b2..a5cf72d19085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.70.1 (2023-11-03) + +### Revert + +- Fix(analytics): feat(analytics): analytics APIs ([#2777](https://github.com/juspay/hyperswitch/pull/2777)) ([`169d33b`](https://github.com/juspay/hyperswitch/commit/169d33bf8157b1a9910c841c8c55eddc4d2ad168)) + +**Full Changelog:** [`v1.70.0...v1.70.1`](https://github.com/juspay/hyperswitch/compare/v1.70.0...v1.70.1) + +- - - + + ## 1.70.0 (2023-11-03) ### Features From 255a4f89a8e0124310d42bb63ad459bd8cde2cba Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:14:58 +0530 Subject: [PATCH 08/57] fix(connector): [Cryptopay]Remove default case handling for Cryptopay (#2699) Co-authored-by: Arjun Karthik --- .../router/src/connector/cryptopay/transformers.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 8d9f277dd0f8..0bc4ff3b3ae6 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -111,10 +111,9 @@ impl TryFrom<&types::ConnectorAuthType> for CryptopayAuthType { } } // PaymentsResponse -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CryptopayPaymentStatus { - #[default] New, Completed, Unresolved, @@ -128,13 +127,14 @@ impl From for enums::AttemptStatus { CryptopayPaymentStatus::New => Self::AuthenticationPending, CryptopayPaymentStatus::Completed => Self::Charged, CryptopayPaymentStatus::Cancelled => Self::Failure, - CryptopayPaymentStatus::Unresolved => Self::Unresolved, - _ => Self::Voided, + CryptopayPaymentStatus::Unresolved | CryptopayPaymentStatus::Refunded => { + Self::Unresolved + } //mapped refunded to Unresolved because refund api is not available, also merchant has done the action on the connector dashboard. } } } -#[derive(Default, Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct CryptopayPaymentsResponse { data: CryptopayPaymentResponseData, } @@ -190,7 +190,7 @@ pub struct CryptopayErrorResponse { pub error: CryptopayErrorData, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct CryptopayPaymentResponseData { pub id: String, pub custom_id: Option, From 1ba6282699b7dff5e6e95c9a14e51c0f8bf749cd Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:21:36 +0530 Subject: [PATCH 09/57] feat(merchant_connector_account): add cache for querying by `merchant_connector_id` (#2738) --- crates/router/src/core/admin.rs | 4 +- crates/router/src/core/cache.rs | 2 +- crates/router/src/core/payments.rs | 5 ++ crates/router/src/db/cache.rs | 37 +++++++++--- crates/router/src/db/merchant_account.rs | 22 ++++--- .../src/db/merchant_connector_account.rs | 60 ++++++++++++++----- 6 files changed, 93 insertions(+), 37 deletions(-) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index cd1243dd3688..5c9f44ffe575 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1020,8 +1020,8 @@ pub async fn update_payment_connector( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, - test_mode: mca.test_mode, - disabled: mca.disabled, + test_mode: req.test_mode, + disabled: req.disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, diff --git a/crates/router/src/core/cache.rs b/crates/router/src/core/cache.rs index cba9a5ec303f..a8ca8395a670 100644 --- a/crates/router/src/core/cache.rs +++ b/crates/router/src/core/cache.rs @@ -10,7 +10,7 @@ pub async fn invalidate( key: &str, ) -> CustomResult, errors::ApiErrorResponse> { let store = state.store.as_ref(); - let result = publish_into_redact_channel(store, CacheKind::All(key.into())) + let result = publish_into_redact_channel(store, [CacheKind::All(key.into())]) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f26b91479ece..9aa0e3c70d25 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -632,6 +632,11 @@ where ) .await?; + if payment_data.payment_attempt.merchant_connector_id.is_none() { + payment_data.payment_attempt.merchant_connector_id = + merchant_connector_account.get_mca_id(); + } + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, diff --git a/crates/router/src/db/cache.rs b/crates/router/src/db/cache.rs index 06ab85591a93..0688665f0c4c 100644 --- a/crates/router/src/db/cache.rs +++ b/crates/router/src/db/cache.rs @@ -100,9 +100,9 @@ where Ok(data) } -pub async fn publish_into_redact_channel<'a>( +pub async fn publish_into_redact_channel<'a, K: IntoIterator> + Send>( store: &dyn StorageInterface, - key: CacheKind<'a>, + keys: K, ) -> CustomResult { let redis_conn = store .get_redis_conn() @@ -111,10 +111,18 @@ pub async fn publish_into_redact_channel<'a>( )) .attach_printable("Failed to get redis connection")?; - redis_conn - .publish(consts::PUB_SUB_CHANNEL, key) - .await - .change_context(errors::StorageError::KVError) + let futures = keys.into_iter().map(|key| async { + redis_conn + .clone() + .publish(consts::PUB_SUB_CHANNEL, key) + .await + .change_context(errors::StorageError::KVError) + }); + + Ok(futures::future::try_join_all(futures) + .await? + .iter() + .sum::()) } pub async fn publish_and_redact<'a, T, F, Fut>( @@ -127,6 +135,21 @@ where Fut: futures::Future> + Send, { let data = fun().await?; - publish_into_redact_channel(store, key).await?; + publish_into_redact_channel(store, [key]).await?; + Ok(data) +} + +pub async fn publish_and_redact_multiple<'a, T, F, Fut, K>( + store: &dyn StorageInterface, + keys: K, + fun: F, +) -> CustomResult +where + F: FnOnce() -> Fut + Send, + Fut: futures::Future> + Send, + K: IntoIterator> + Send, +{ + let data = fun().await?; + publish_into_redact_channel(store, keys).await?; Ok(data) } diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index e0bff7d9069c..0d3ce99b948d 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -399,19 +399,17 @@ async fn publish_and_redact_merchant_account_cache( store: &dyn super::StorageInterface, merchant_account: &storage::MerchantAccount, ) -> CustomResult<(), errors::StorageError> { - super::cache::publish_into_redact_channel( - store, - CacheKind::Accounts(merchant_account.merchant_id.as_str().into()), - ) - .await?; - merchant_account + let publishable_key = merchant_account .publishable_key .as_ref() - .async_map(|pub_key| async { - super::cache::publish_into_redact_channel(store, CacheKind::Accounts(pub_key.into())) - .await - }) - .await - .transpose()?; + .map(|publishable_key| CacheKind::Accounts(publishable_key.into())); + + let mut cache_keys = vec![CacheKind::Accounts( + merchant_account.merchant_id.as_str().into(), + )]; + + cache_keys.extend(publishable_key.into_iter()); + + super::cache::publish_into_redact_channel(store, cache_keys).await?; Ok(()) } diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 9ff3f5121082..ecf52531f28a 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -290,21 +290,40 @@ impl MerchantConnectorAccountInterface for Store { merchant_connector_id: &str, key_store: &domain::MerchantKeyStore, ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( - &conn, - merchant_id, - merchant_connector_id, - ) - .await - .map_err(Into::into) - .into_report() - .async_and_then(|item| async { - item.convert(key_store.key.get_inner()) + let find_call = || async { + let conn = connection::pg_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( + &conn, + merchant_id, + merchant_connector_id, + ) + .await + .map_err(Into::into) + .into_report() + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert(key_store.key.get_inner()) .await .change_context(errors::StorageError::DecryptionError) - }) - .await + } + + #[cfg(feature = "accounts_cache")] + { + super::cache::get_or_populate_in_memory( + self, + &format!("{}_{}", merchant_id, merchant_connector_id), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await? + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + } } async fn insert_merchant_connector_account( @@ -367,6 +386,9 @@ impl MerchantConnectorAccountInterface for Store { "profile_id".to_string(), ))?; + let _merchant_id = this.merchant_id.clone(); + let _merchant_connector_id = this.merchant_connector_id.clone(); + let update_call = || async { let conn = connection::pg_connection_write(self).await?; Conversion::convert(this) @@ -386,9 +408,17 @@ impl MerchantConnectorAccountInterface for Store { #[cfg(feature = "accounts_cache")] { - super::cache::publish_and_redact( + // Redact both the caches as any one or both might be used because of backwards compatibility + super::cache::publish_and_redact_multiple( self, - cache::CacheKind::Accounts(format!("{}_{}", _profile_id, _connector_name).into()), + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id, _connector_name).into(), + ), + cache::CacheKind::Accounts( + format!("{}_{}", _merchant_id, _merchant_connector_id).into(), + ), + ], update_call, ) .await From 6c5de9cee48b71a5cdd342f97221eb3e8e38c2e8 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:32:07 +0530 Subject: [PATCH 10/57] ci(postman): Add postman testcase for customer id being optional when address is passed in Payment Create, Update (#2775) Co-authored-by: swangi-kumari --- .../.meta.json | 6 ++ .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 80 +++++++++++++++ .../Payments - Create/request.json | 98 +++++++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 80 +++++++++++++++ .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve/response.json | 1 + 9 files changed, 309 insertions(+) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js new file mode 100644 index 000000000000..ffcdd527d07c --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json new file mode 100644 index 000000000000..21f054843897 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..20626ecd2a9c --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] From 9b618d24476967d364835d04010d9076a80aeb9c Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Fri, 3 Nov 2023 18:37:31 +0530 Subject: [PATCH 11/57] feat(router): Add Smart Routing to route payments efficiently (#2665) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: shashank_attarde Co-authored-by: Aprabhat19 Co-authored-by: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> --- Cargo.lock | 236 +++ crates/api_models/Cargo.toml | 7 +- crates/api_models/src/admin.rs | 66 - crates/api_models/src/lib.rs | 1 + crates/api_models/src/routing.rs | 594 +++++++ crates/common_utils/Cargo.toml | 1 + crates/common_utils/src/lib.rs | 2 + crates/common_utils/src/static_cache.rs | 91 + crates/diesel_models/src/enums.rs | 22 + crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/query.rs | 1 + .../src/query/routing_algorithm.rs | 200 +++ crates/diesel_models/src/routing_algorithm.rs | 37 + crates/diesel_models/src/schema.rs | 23 + crates/euclid/Cargo.toml | 38 + crates/euclid/benches/backends.rs | 93 ++ crates/euclid/src/backend.rs | 25 + crates/euclid/src/backend/inputs.rs | 39 + crates/euclid/src/backend/interpreter.rs | 180 ++ .../euclid/src/backend/interpreter/types.rs | 81 + crates/euclid/src/backend/vir_interpreter.rs | 583 +++++++ .../src/backend/vir_interpreter/types.rs | 126 ++ crates/euclid/src/dssa.rs | 7 + crates/euclid/src/dssa/analyzer.rs | 447 +++++ crates/euclid/src/dssa/graph.rs | 1478 +++++++++++++++++ crates/euclid/src/dssa/state_machine.rs | 714 ++++++++ crates/euclid/src/dssa/truth.rs | 29 + crates/euclid/src/dssa/types.rs | 158 ++ crates/euclid/src/dssa/utils.rs | 1 + crates/euclid/src/enums.rs | 191 +++ crates/euclid/src/frontend.rs | 3 + crates/euclid/src/frontend/ast.rs | 156 ++ crates/euclid/src/frontend/ast/lowering.rs | 377 +++++ crates/euclid/src/frontend/ast/parser.rs | 441 +++++ crates/euclid/src/frontend/dir.rs | 803 +++++++++ crates/euclid/src/frontend/dir/enums.rs | 321 ++++ crates/euclid/src/frontend/dir/lowering.rs | 295 ++++ .../euclid/src/frontend/dir/transformers.rs | 166 ++ crates/euclid/src/frontend/vir.rs | 37 + crates/euclid/src/lib.rs | 7 + crates/euclid/src/types.rs | 318 ++++ crates/euclid/src/types/transformers.rs | 1 + crates/euclid/src/utils.rs | 3 + crates/euclid/src/utils/dense_map.rs | 224 +++ crates/euclid_macros/Cargo.toml | 16 + crates/euclid_macros/src/inner.rs | 5 + crates/euclid_macros/src/inner/enum_nums.rs | 47 + crates/euclid_macros/src/inner/knowledge.rs | 680 ++++++++ crates/euclid_macros/src/lib.rs | 16 + crates/euclid_wasm/Cargo.toml | 37 + crates/euclid_wasm/src/lib.rs | 227 +++ crates/euclid_wasm/src/types.rs | 7 + crates/euclid_wasm/src/utils.rs | 17 + crates/kgraph_utils/Cargo.toml | 27 + crates/kgraph_utils/benches/evaluation.rs | 113 ++ crates/kgraph_utils/src/error.rs | 14 + crates/kgraph_utils/src/lib.rs | 3 + crates/kgraph_utils/src/mca.rs | 739 +++++++++ crates/kgraph_utils/src/transformers.rs | 724 ++++++++ crates/router/Cargo.toml | 13 +- .../compatibility/stripe/payment_intents.rs | 12 +- .../stripe/payment_intents/types.rs | 14 +- .../src/compatibility/stripe/setup_intents.rs | 4 + .../stripe/setup_intents/types.rs | 14 +- crates/router/src/consts.rs | 2 + crates/router/src/core.rs | 1 + crates/router/src/core/admin.rs | 79 +- crates/router/src/core/errors.rs | 46 + .../router/src/core/payment_methods/cards.rs | 136 +- crates/router/src/core/payments.rs | 717 ++++++-- crates/router/src/core/payments/routing.rs | 950 +++++++++++ .../src/core/payments/routing/transformers.rs | 121 ++ crates/router/src/core/routing.rs | 713 ++++++++ crates/router/src/core/routing/helpers.rs | 479 ++++++ .../router/src/core/routing/transformers.rs | 86 + crates/router/src/core/webhooks.rs | 2 + crates/router/src/db.rs | 2 + crates/router/src/db/routing_algorithm.rs | 199 +++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 4 + crates/router/src/routes/app.rs | 39 + crates/router/src/routes/lock_utils.rs | 12 + crates/router/src/routes/payments.rs | 9 + crates/router/src/routes/routing.rs | 298 ++++ crates/router/src/types/api.rs | 20 +- crates/router/src/types/api/admin.rs | 4 +- crates/router/src/types/api/routing.rs | 41 + crates/router/src/types/storage.rs | 59 +- .../src/types/storage/routing_algorithm.rs | 3 + crates/router/src/types/transformers.rs | 165 +- crates/router/src/workflows/payment_sync.rs | 1 + crates/router/tests/payments.rs | 2 + crates/router/tests/payments2.rs | 2 + crates/router_env/src/logger/types.rs | 20 + .../down.sql | 4 + .../up.sql | 19 + 96 files changed, 15366 insertions(+), 223 deletions(-) create mode 100644 crates/api_models/src/routing.rs create mode 100644 crates/common_utils/src/static_cache.rs create mode 100644 crates/diesel_models/src/query/routing_algorithm.rs create mode 100644 crates/diesel_models/src/routing_algorithm.rs create mode 100644 crates/euclid/Cargo.toml create mode 100644 crates/euclid/benches/backends.rs create mode 100644 crates/euclid/src/backend.rs create mode 100644 crates/euclid/src/backend/inputs.rs create mode 100644 crates/euclid/src/backend/interpreter.rs create mode 100644 crates/euclid/src/backend/interpreter/types.rs create mode 100644 crates/euclid/src/backend/vir_interpreter.rs create mode 100644 crates/euclid/src/backend/vir_interpreter/types.rs create mode 100644 crates/euclid/src/dssa.rs create mode 100644 crates/euclid/src/dssa/analyzer.rs create mode 100644 crates/euclid/src/dssa/graph.rs create mode 100644 crates/euclid/src/dssa/state_machine.rs create mode 100644 crates/euclid/src/dssa/truth.rs create mode 100644 crates/euclid/src/dssa/types.rs create mode 100644 crates/euclid/src/dssa/utils.rs create mode 100644 crates/euclid/src/enums.rs create mode 100644 crates/euclid/src/frontend.rs create mode 100644 crates/euclid/src/frontend/ast.rs create mode 100644 crates/euclid/src/frontend/ast/lowering.rs create mode 100644 crates/euclid/src/frontend/ast/parser.rs create mode 100644 crates/euclid/src/frontend/dir.rs create mode 100644 crates/euclid/src/frontend/dir/enums.rs create mode 100644 crates/euclid/src/frontend/dir/lowering.rs create mode 100644 crates/euclid/src/frontend/dir/transformers.rs create mode 100644 crates/euclid/src/frontend/vir.rs create mode 100644 crates/euclid/src/lib.rs create mode 100644 crates/euclid/src/types.rs create mode 100644 crates/euclid/src/types/transformers.rs create mode 100644 crates/euclid/src/utils.rs create mode 100644 crates/euclid/src/utils/dense_map.rs create mode 100644 crates/euclid_macros/Cargo.toml create mode 100644 crates/euclid_macros/src/inner.rs create mode 100644 crates/euclid_macros/src/inner/enum_nums.rs create mode 100644 crates/euclid_macros/src/inner/knowledge.rs create mode 100644 crates/euclid_macros/src/lib.rs create mode 100644 crates/euclid_wasm/Cargo.toml create mode 100644 crates/euclid_wasm/src/lib.rs create mode 100644 crates/euclid_wasm/src/types.rs create mode 100644 crates/euclid_wasm/src/utils.rs create mode 100644 crates/kgraph_utils/Cargo.toml create mode 100644 crates/kgraph_utils/benches/evaluation.rs create mode 100644 crates/kgraph_utils/src/error.rs create mode 100644 crates/kgraph_utils/src/lib.rs create mode 100644 crates/kgraph_utils/src/mca.rs create mode 100644 crates/kgraph_utils/src/transformers.rs create mode 100644 crates/router/src/core/payments/routing.rs create mode 100644 crates/router/src/core/payments/routing/transformers.rs create mode 100644 crates/router/src/core/routing.rs create mode 100644 crates/router/src/core/routing/helpers.rs create mode 100644 crates/router/src/core/routing/transformers.rs create mode 100644 crates/router/src/db/routing_algorithm.rs create mode 100644 crates/router/src/routes/routing.rs create mode 100644 crates/router/src/types/api/routing.rs create mode 100644 crates/router/src/types/storage/routing_algorithm.rs create mode 100644 migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql create mode 100644 migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql diff --git a/Cargo.lock b/Cargo.lock index 665703f3d505..886a8b50acc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstyle" version = "1.0.0" @@ -397,6 +403,7 @@ dependencies = [ "common_enums", "common_utils", "error-stack", + "euclid", "masking", "mime", "reqwest", @@ -1343,6 +1350,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.83" @@ -1413,6 +1426,33 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.3.4" @@ -1497,6 +1537,7 @@ dependencies = [ "reqwest", "ring", "router_env", + "rustc-hash", "serde", "serde_json", "serde_urlencoded", @@ -1615,6 +1656,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -2022,6 +2099,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.4" @@ -2063,6 +2149,52 @@ dependencies = [ "serde", ] +[[package]] +name = "euclid" +version = "0.1.0" +dependencies = [ + "common_enums", + "criterion", + "erased-serde", + "euclid_macros", + "frunk", + "frunk_core", + "nom", + "once_cell", + "rustc-hash", + "serde", + "serde_json", + "strum 0.25.0", + "thiserror", +] + +[[package]] +name = "euclid_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rustc-hash", + "strum 0.24.1", + "syn 1.0.109", +] + +[[package]] +name = "euclid_wasm" +version = "0.1.0" +dependencies = [ + "api_models", + "euclid", + "getrandom 0.2.10", + "kgraph_utils", + "once_cell", + "ron-parser", + "serde", + "serde-wasm-bindgen", + "strum 0.25.0", + "wasm-bindgen", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2415,8 +2547,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2497,6 +2631,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -2811,6 +2951,17 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.17", + "windows-sys", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2905,6 +3056,19 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kgraph_utils" +version = "0.1.0" +dependencies = [ + "api_models", + "criterion", + "euclid", + "masking", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3365,6 +3529,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3729,6 +3899,34 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.16.8" @@ -4216,6 +4414,19 @@ dependencies = [ "serde", ] +[[package]] +name = "ron-parser" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7280c46017fafbe4275179689e446a9b0db3bd91ea61aaee22841ef618405a" +dependencies = [ + "nom", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "router" version = "0.2.0" @@ -4248,6 +4459,7 @@ dependencies = [ "dyn-clone", "encoding_rs", "error-stack", + "euclid", "external_services", "futures", "hex", @@ -4257,6 +4469,7 @@ dependencies = [ "infer 0.13.0", "josekit", "jsonwebtoken", + "kgraph_utils", "literally", "masking", "maud", @@ -4268,6 +4481,7 @@ dependencies = [ "openssl", "qrcode", "rand 0.8.5", + "rand_chacha 0.3.1", "redis_interface", "regex", "reqwest", @@ -4275,6 +4489,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rustc-hash", "scheduler", "serde", "serde_json", @@ -4651,6 +4866,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -5349,6 +5575,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce61d30d36f5..d15fdeabf387 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -9,8 +9,12 @@ license.workspace = true [features] default = ["payouts"] +business_profile_routing = [] +connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] -dummy_connector = ["common_enums/dummy_connector"] +backwards_compatibility = ["connector_choice_bcompat"] +connector_choice_mca_id = ["euclid/connector_choice_mca_id"] +dummy_connector = ["common_enums/dummy_connector", "euclid/dummy_connector"] detailed_errors = [] payouts = [] @@ -32,5 +36,6 @@ thiserror = "1.0.40" cards = { version = "0.1.0", path = "../cards" } common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } +euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking" } router_derive = { version = "0.1.0", path = "../router_derive" } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index b1a258e6b26c..037d223754a0 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -443,72 +443,6 @@ pub mod payout_routing_algorithm { } } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum RoutingAlgorithm { - Single(RoutableConnectorChoice), -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum RoutableConnectorChoice { - ConnectorName(api_enums::RoutableConnectors), - ConnectorId { - merchant_connector_id: String, - connector: api_enums::RoutableConnectors, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde( - tag = "type", - content = "data", - rename_all = "snake_case", - from = "StraightThroughAlgorithmSerde", - into = "StraightThroughAlgorithmSerde" -)] -pub enum StraightThroughAlgorithm { - Single(RoutableConnectorChoice), -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum StraightThroughAlgorithmInner { - Single(RoutableConnectorChoice), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum StraightThroughAlgorithmSerde { - Direct(StraightThroughAlgorithmInner), - Nested { - algorithm: StraightThroughAlgorithmInner, - }, -} - -impl From for StraightThroughAlgorithm { - fn from(value: StraightThroughAlgorithmSerde) -> Self { - let inner = match value { - StraightThroughAlgorithmSerde::Direct(algorithm) => algorithm, - StraightThroughAlgorithmSerde::Nested { algorithm } => algorithm, - }; - - match inner { - StraightThroughAlgorithmInner::Single(conn) => Self::Single(conn), - } - } -} - -impl From for StraightThroughAlgorithmSerde { - fn from(value: StraightThroughAlgorithm) -> Self { - let inner = match value { - StraightThroughAlgorithm::Single(conn) => StraightThroughAlgorithmInner::Single(conn), - }; - - Self::Nested { algorithm: inner } - } -} - #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PrimaryBusinessDetails { diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index dab1b46adbad..ec272514e38a 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -17,5 +17,6 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod routing; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs new file mode 100644 index 000000000000..95d4c5e10ece --- /dev/null +++ b/crates/api_models/src/routing.rs @@ -0,0 +1,594 @@ +use std::fmt::Debug; + +use common_utils::errors::ParsingError; +use error_stack::IntoReport; +use euclid::{ + dssa::types::EuclidAnalysable, + enums as euclid_enums, + frontend::{ + ast, + dir::{DirKeyKind, EuclidDirFilter}, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::enums::{self, RoutableConnectors}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum ConnectorSelection { + Priority(Vec), + VolumeSplit(Vec), +} + +impl ConnectorSelection { + pub fn get_connector_list(&self) -> Vec { + match self { + Self::Priority(list) => list.clone(), + Self::VolumeSplit(splits) => { + splits.iter().map(|split| split.connector.clone()).collect() + } + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingConfigRequest { + pub name: Option, + pub description: Option, + pub algorithm: Option, + pub profile_id: Option, +} + +#[cfg(feature = "business_profile_routing")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct RoutingRetrieveQuery { + pub limit: Option, + pub offset: Option, + + pub profile_id: Option, +} + +#[cfg(feature = "business_profile_routing")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct RoutingRetrieveLinkQuery { + pub profile_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingRetrieveResponse { + pub algorithm: Option, +} + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum LinkedRoutingConfigRetrieveResponse { + MerchantAccountBased(RoutingRetrieveResponse), + ProfileBased(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MerchantRoutingAlgorithm { + pub id: String, + #[cfg(feature = "business_profile_routing")] + pub profile_id: String, + pub name: String, + pub description: String, + pub algorithm: RoutingAlgorithm, + pub created_at: i64, + pub modified_at: i64, +} + +impl EuclidDirFilter for ConnectorSelection { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardBin, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::PayLaterType, + DirKeyKind::WalletType, + DirKeyKind::UpiType, + DirKeyKind::BankRedirectType, + DirKeyKind::BankDebitType, + DirKeyKind::CryptoType, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::AuthenticationType, + DirKeyKind::MandateAcceptanceType, + DirKeyKind::MandateType, + DirKeyKind::PaymentType, + DirKeyKind::SetupFutureUsage, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + DirKeyKind::BusinessLabel, + DirKeyKind::MetaData, + DirKeyKind::RewardType, + DirKeyKind::VoucherType, + DirKeyKind::CardRedirectType, + DirKeyKind::BankTransferType, + ]; +} + +impl EuclidAnalysable for ConnectorSelection { + fn get_dir_value_for_analysis( + &self, + rule_name: String, + ) -> Vec<(euclid::frontend::dir::DirValue, euclid::types::Metadata)> { + self.get_connector_list() + .into_iter() + .map(|connector_choice| { + let connector_name = connector_choice.connector.to_string(); + #[cfg(not(feature = "connector_choice_mca_id"))] + let sub_label = connector_choice.sub_label.clone(); + #[cfg(feature = "connector_choice_mca_id")] + let mca_id = connector_choice.merchant_connector_id.clone(); + + ( + euclid::frontend::dir::DirValue::Connector(Box::new(connector_choice.into())), + std::collections::HashMap::from_iter([( + "CONNECTOR_SELECTION".to_string(), + #[cfg(feature = "connector_choice_mca_id")] + serde_json::json!({ + "rule_name": rule_name, + "connector_name": connector_name, + "mca_id": mca_id, + }), + #[cfg(not(feature = "connector_choice_mca_id"))] + serde_json ::json!({ + "rule_name": rule_name, + "connector_name": connector_name, + "sub_label": sub_label, + }), + )]), + ) + }) + .collect() + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConnectorVolumeSplit { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[cfg(feature = "connector_choice_bcompat")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum RoutableChoiceKind { + OnlyConnector, + FullStruct, +} + +#[cfg(feature = "connector_choice_bcompat")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum RoutableChoiceSerde { + OnlyConnector(Box), + FullStruct { + connector: RoutableConnectors, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: Option, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr( + feature = "connector_choice_bcompat", + serde(from = "RoutableChoiceSerde"), + serde(into = "RoutableChoiceSerde") +)] +#[cfg_attr(not(feature = "connector_choice_bcompat"), derive(PartialEq, Eq))] +pub struct RoutableConnectorChoice { + #[cfg(feature = "connector_choice_bcompat")] + pub choice_kind: RoutableChoiceKind, + pub connector: RoutableConnectors, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +impl ToString for RoutableConnectorChoice { + fn to_string(&self) -> String { + #[cfg(feature = "connector_choice_mca_id")] + let base = self.connector.to_string(); + + #[cfg(not(feature = "connector_choice_mca_id"))] + let base = { + let mut sub_base = self.connector.to_string(); + if let Some(ref label) = self.sub_label { + sub_base.push('_'); + sub_base.push_str(label); + } + + sub_base + }; + + base + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl PartialEq for RoutableConnectorChoice { + fn eq(&self, other: &Self) -> bool { + #[cfg(not(feature = "connector_choice_mca_id"))] + { + self.connector.eq(&other.connector) && self.sub_label.eq(&other.sub_label) + } + + #[cfg(feature = "connector_choice_mca_id")] + { + self.connector.eq(&other.connector) + && self.merchant_connector_id.eq(&other.merchant_connector_id) + } + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl Eq for RoutableConnectorChoice {} + +#[cfg(feature = "connector_choice_bcompat")] +impl From for RoutableConnectorChoice { + fn from(value: RoutableChoiceSerde) -> Self { + match value { + RoutableChoiceSerde::OnlyConnector(connector) => Self { + choice_kind: RoutableChoiceKind::OnlyConnector, + connector: *connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + + RoutableChoiceSerde::FullStruct { + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label, + } => Self { + choice_kind: RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label, + }, + } + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl From for RoutableChoiceSerde { + fn from(value: RoutableConnectorChoice) -> Self { + match value.choice_kind { + RoutableChoiceKind::OnlyConnector => Self::OnlyConnector(Box::new(value.connector)), + RoutableChoiceKind::FullStruct => Self::FullStruct { + connector: value.connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: value.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: value.sub_label, + }, + } + } +} + +impl From for ast::ConnectorChoice { + fn from(value: RoutableConnectorChoice) -> Self { + Self { + connector: match value.connector { + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector1 => euclid_enums::Connector::DummyConnector1, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector2 => euclid_enums::Connector::DummyConnector2, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector3 => euclid_enums::Connector::DummyConnector3, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector4 => euclid_enums::Connector::DummyConnector4, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector5 => euclid_enums::Connector::DummyConnector5, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector6 => euclid_enums::Connector::DummyConnector6, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector7 => euclid_enums::Connector::DummyConnector7, + RoutableConnectors::Aci => euclid_enums::Connector::Aci, + RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, + RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, + RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, + RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, + RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, + RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, + RoutableConnectors::Boku => euclid_enums::Connector::Boku, + RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, + RoutableConnectors::Cashtocode => euclid_enums::Connector::Cashtocode, + RoutableConnectors::Checkout => euclid_enums::Connector::Checkout, + RoutableConnectors::Coinbase => euclid_enums::Connector::Coinbase, + RoutableConnectors::Cryptopay => euclid_enums::Connector::Cryptopay, + RoutableConnectors::Cybersource => euclid_enums::Connector::Cybersource, + RoutableConnectors::Dlocal => euclid_enums::Connector::Dlocal, + RoutableConnectors::Fiserv => euclid_enums::Connector::Fiserv, + RoutableConnectors::Forte => euclid_enums::Connector::Forte, + RoutableConnectors::Globalpay => euclid_enums::Connector::Globalpay, + RoutableConnectors::Globepay => euclid_enums::Connector::Globepay, + RoutableConnectors::Gocardless => euclid_enums::Connector::Gocardless, + RoutableConnectors::Helcim => euclid_enums::Connector::Helcim, + RoutableConnectors::Iatapay => euclid_enums::Connector::Iatapay, + RoutableConnectors::Klarna => euclid_enums::Connector::Klarna, + RoutableConnectors::Mollie => euclid_enums::Connector::Mollie, + RoutableConnectors::Multisafepay => euclid_enums::Connector::Multisafepay, + RoutableConnectors::Nexinets => euclid_enums::Connector::Nexinets, + RoutableConnectors::Nmi => euclid_enums::Connector::Nmi, + RoutableConnectors::Noon => euclid_enums::Connector::Noon, + RoutableConnectors::Nuvei => euclid_enums::Connector::Nuvei, + RoutableConnectors::Opennode => euclid_enums::Connector::Opennode, + RoutableConnectors::Payme => euclid_enums::Connector::Payme, + RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, + RoutableConnectors::Payu => euclid_enums::Connector::Payu, + RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, + RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, + RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, + RoutableConnectors::Square => euclid_enums::Connector::Square, + RoutableConnectors::Stax => euclid_enums::Connector::Stax, + RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, + RoutableConnectors::Trustpay => euclid_enums::Connector::Trustpay, + RoutableConnectors::Tsys => euclid_enums::Connector::Tsys, + RoutableConnectors::Volt => euclid_enums::Connector::Volt, + RoutableConnectors::Wise => euclid_enums::Connector::Wise, + RoutableConnectors::Worldline => euclid_enums::Connector::Worldline, + RoutableConnectors::Worldpay => euclid_enums::Connector::Worldpay, + RoutableConnectors::Zen => euclid_enums::Connector::Zen, + }, + + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: value.sub_label, + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DetailedConnectorChoice { + pub connector: RoutableConnectors, + pub business_label: Option, + pub business_country: Option, + pub business_sub_label: Option, +} + +impl DetailedConnectorChoice { + pub fn get_connector_label(&self) -> Option { + self.business_country + .as_ref() + .zip(self.business_label.as_ref()) + .map(|(business_country, business_label)| { + let mut base_label = format!( + "{}_{:?}_{}", + self.connector, business_country, business_label + ); + + if let Some(ref sub_label) = self.business_sub_label { + base_label.push('_'); + base_label.push_str(sub_label); + } + + base_label + }) + } +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutingAlgorithmKind { + Single, + Priority, + VolumeSplit, + Advanced, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde( + tag = "type", + content = "data", + rename_all = "snake_case", + try_from = "RoutingAlgorithmSerde" +)] +pub enum RoutingAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(euclid::frontend::ast::Program), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum RoutingAlgorithmSerde { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(euclid::frontend::ast::Program), +} + +impl TryFrom for RoutingAlgorithm { + type Error = error_stack::Report; + + fn try_from(value: RoutingAlgorithmSerde) -> Result { + match &value { + RoutingAlgorithmSerde::Priority(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Priority Algorithm", + )) + .into_report()? + } + RoutingAlgorithmSerde::VolumeSplit(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Volume split Algorithm", + )) + .into_report()? + } + _ => {} + }; + Ok(match value { + RoutingAlgorithmSerde::Single(i) => Self::Single(i), + RoutingAlgorithmSerde::Priority(i) => Self::Priority(i), + RoutingAlgorithmSerde::VolumeSplit(i) => Self::VolumeSplit(i), + RoutingAlgorithmSerde::Advanced(i) => Self::Advanced(i), + }) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde( + tag = "type", + content = "data", + rename_all = "snake_case", + try_from = "StraightThroughAlgorithmSerde", + into = "StraightThroughAlgorithmSerde" +)] +pub enum StraightThroughAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum StraightThroughAlgorithmInner { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum StraightThroughAlgorithmSerde { + Direct(StraightThroughAlgorithmInner), + Nested { + algorithm: StraightThroughAlgorithmInner, + }, +} + +impl TryFrom for StraightThroughAlgorithm { + type Error = error_stack::Report; + + fn try_from(value: StraightThroughAlgorithmSerde) -> Result { + let inner = match value { + StraightThroughAlgorithmSerde::Direct(algorithm) => algorithm, + StraightThroughAlgorithmSerde::Nested { algorithm } => algorithm, + }; + + match &inner { + StraightThroughAlgorithmInner::Priority(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Priority Algorithm", + )) + .into_report()? + } + StraightThroughAlgorithmInner::VolumeSplit(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Volume split Algorithm", + )) + .into_report()? + } + _ => {} + }; + + Ok(match inner { + StraightThroughAlgorithmInner::Single(single) => Self::Single(single), + StraightThroughAlgorithmInner::Priority(plist) => Self::Priority(plist), + StraightThroughAlgorithmInner::VolumeSplit(vsplit) => Self::VolumeSplit(vsplit), + }) + } +} + +impl From for StraightThroughAlgorithmSerde { + fn from(value: StraightThroughAlgorithm) -> Self { + let inner = match value { + StraightThroughAlgorithm::Single(conn) => StraightThroughAlgorithmInner::Single(conn), + StraightThroughAlgorithm::Priority(plist) => { + StraightThroughAlgorithmInner::Priority(plist) + } + StraightThroughAlgorithm::VolumeSplit(vsplit) => { + StraightThroughAlgorithmInner::VolumeSplit(vsplit) + } + }; + + Self::Nested { algorithm: inner } + } +} + +impl From for RoutingAlgorithm { + fn from(value: StraightThroughAlgorithm) -> Self { + match value { + StraightThroughAlgorithm::Single(conn) => Self::Single(conn), + StraightThroughAlgorithm::Priority(conns) => Self::Priority(conns), + StraightThroughAlgorithm::VolumeSplit(splits) => Self::VolumeSplit(splits), + } + } +} + +impl RoutingAlgorithm { + pub fn get_kind(&self) -> RoutingAlgorithmKind { + match self { + Self::Single(_) => RoutingAlgorithmKind::Single, + Self::Priority(_) => RoutingAlgorithmKind::Priority, + Self::VolumeSplit(_) => RoutingAlgorithmKind::VolumeSplit, + Self::Advanced(_) => RoutingAlgorithmKind::Advanced, + } + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingAlgorithmRef { + pub algorithm_id: Option, + pub timestamp: i64, + pub config_algo_id: Option, + pub surcharge_config_algo_id: Option, +} + +impl RoutingAlgorithmRef { + pub fn update_algorithm_id(&mut self, new_id: String) { + self.algorithm_id = Some(new_id); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } + + pub fn update_conditional_config_id(&mut self, ids: String) { + self.config_algo_id = Some(ids); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } + + pub fn update_surcharge_config_id(&mut self, ids: String) { + self.surcharge_config_algo_id = Some(ids); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct RoutingDictionaryRecord { + pub id: String, + #[cfg(feature = "business_profile_routing")] + pub profile_id: String, + pub name: String, + pub kind: RoutingAlgorithmKind, + pub description: String, + pub created_at: i64, + pub modified_at: i64, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingDictionary { + pub merchant_id: String, + pub active_id: Option, + pub records: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(untagged)] +pub enum RoutingKind { + Config(RoutingDictionary), + RoutingAlgorithm(Vec), +} diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index e319cf86ccd0..c1fd91a351c7 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -28,6 +28,7 @@ rand = "0.8.5" regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = { version = "0.16.20", features = ["std"] } +rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" serde_urlencoded = "0.7.1" diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index ca6bba480063..724c3bca0a27 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -13,6 +13,8 @@ pub mod pii; pub mod request; #[cfg(feature = "signals")] pub mod signals; +#[allow(missing_docs)] // Todo: add docs +pub mod static_cache; pub mod types; pub mod validation; diff --git a/crates/common_utils/src/static_cache.rs b/crates/common_utils/src/static_cache.rs new file mode 100644 index 000000000000..ca608fa9a3b5 --- /dev/null +++ b/crates/common_utils/src/static_cache.rs @@ -0,0 +1,91 @@ +use std::sync::{Arc, RwLock}; + +use once_cell::sync::Lazy; +use rustc_hash::FxHashMap; + +#[derive(Debug)] +pub struct CacheEntry { + data: Arc, + timestamp: i64, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum CacheError { + #[error("Could not acquire the lock for cache entry")] + CouldNotAcquireLock, + #[error("Entry not found in cache")] + EntryNotFound, +} + +#[derive(Debug)] +pub struct StaticCache { + data: Lazy>>>, +} + +impl StaticCache +where + T: Send, +{ + pub const fn new() -> Self { + Self { + data: Lazy::new(|| RwLock::new(FxHashMap::default())), + } + } + + pub fn present(&self, key: &String) -> Result { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + Ok(the_map.get(key).is_some()) + } + + pub fn expired(&self, key: &String, timestamp: i64) -> Result { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + Ok(match the_map.get(key) { + None => false, + Some(entry) => timestamp > entry.timestamp, + }) + } + + pub fn retrieve(&self, key: &String) -> Result, CacheError> { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + let cache_entry = the_map.get(key).ok_or(CacheError::EntryNotFound)?; + + Ok(Arc::clone(&cache_entry.data)) + } + + pub fn save(&self, key: String, data: T, timestamp: i64) -> Result<(), CacheError> { + let mut the_map = self + .data + .write() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + let entry = CacheEntry { + data: Arc::new(data), + timestamp, + }; + + the_map.insert(key, entry); + Ok(()) + } + + pub fn clear(&self) -> Result<(), CacheError> { + let mut the_map = self + .data + .write() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + the_map.clear(); + Ok(()) + } +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index b73eeefbb10b..0e06a324f038 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -14,6 +14,7 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRoutingAlgorithmKind as RoutingAlgorithmKind, }; } pub use common_enums::*; @@ -21,6 +22,27 @@ use common_utils::pii; use diesel::serialize::{Output, ToSql}; use time::PrimitiveDateTime; +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutingAlgorithmKind { + Single, + Priority, + VolumeSplit, + Advanced, +} + #[derive( Clone, Copy, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 528446678015..2d459499a1bd 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -34,6 +34,7 @@ pub mod process_tracker; pub mod query; pub mod refund; pub mod reverse_lookup; +pub mod routing_algorithm; #[allow(unused_qualifications)] pub mod schema; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 6b705e29873e..aeb09b969f13 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -26,3 +26,4 @@ pub mod payouts; pub mod process_tracker; pub mod refund; pub mod reverse_lookup; +pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query/routing_algorithm.rs b/crates/diesel_models/src/query/routing_algorithm.rs new file mode 100644 index 000000000000..533ac7194c41 --- /dev/null +++ b/crates/diesel_models/src/query/routing_algorithm.rs @@ -0,0 +1,200 @@ +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, QueryDsl}; +use error_stack::{IntoReport, ResultExt}; +use router_env::tracing::{self, instrument}; +use time::PrimitiveDateTime; + +use crate::{ + enums, + errors::DatabaseError, + query::generics, + routing_algorithm::{RoutingAlgorithm, RoutingAlgorithmMetadata, RoutingProfileMetadata}, + schema::routing_algorithm::dsl, + PgPooledConn, StorageResult, +}; + +impl RoutingAlgorithm { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } + + #[instrument(skip(conn))] + pub async fn find_by_algorithm_id_merchant_id( + conn: &PgPooledConn, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn find_by_algorithm_id_profile_id( + conn: &PgPooledConn, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::profile_id.eq(profile_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn find_metadata_by_algorithm_id_profile_id( + conn: &PgPooledConn, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + Self::table() + .select(( + dsl::profile_id, + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter( + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::profile_id.eq(profile_id.to_owned())), + ) + .limit(1) + .load_async::<( + String, + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .next() + .ok_or(DatabaseError::NotFound) + .into_report() + .map( + |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingProfileMetadata { + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + } + + #[instrument(skip(conn))] + pub async fn list_metadata_by_profile_id( + conn: &PgPooledConn, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + Ok(Self::table() + .select(( + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter(dsl::profile_id.eq(profile_id.to_owned())) + .limit(limit) + .offset(offset) + .load_async::<( + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .map( + |(algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingAlgorithmMetadata { + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + .collect()) + } + + #[instrument(skip(conn))] + pub async fn list_metadata_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + Ok(Self::table() + .select(( + dsl::profile_id, + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .limit(limit) + .offset(offset) + .order(dsl::modified_at.desc()) + .load_async::<( + String, + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .map( + |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingProfileMetadata { + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + .collect()) + } +} diff --git a/crates/diesel_models/src/routing_algorithm.rs b/crates/diesel_models/src/routing_algorithm.rs new file mode 100644 index 000000000000..09f9baf7edb9 --- /dev/null +++ b/crates/diesel_models/src/routing_algorithm.rs @@ -0,0 +1,37 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::{enums, schema::routing_algorithm}; + +#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = routing_algorithm, primary_key(algorithm_id))] +pub struct RoutingAlgorithm { + pub algorithm_id: String, + pub profile_id: String, + pub merchant_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub algorithm_data: serde_json::Value, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} + +pub struct RoutingAlgorithmMetadata { + pub algorithm_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} + +pub struct RoutingProfileMetadata { + pub profile_id: String, + pub algorithm_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e214fa364ddd..2923c719c8f7 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -874,6 +874,28 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + routing_algorithm (algorithm_id) { + #[max_length = 64] + algorithm_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + name -> Varchar, + #[max_length = 256] + description -> Nullable, + kind -> RoutingAlgorithmKind, + algorithm_data -> Jsonb, + created_at -> Timestamp, + modified_at -> Timestamp, + } +} + diesel::allow_tables_to_appear_in_same_query!( address, api_keys, @@ -902,4 +924,5 @@ diesel::allow_tables_to_appear_in_same_query!( process_tracker, refund, reverse_lookup, + routing_algorithm, ); diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml new file mode 100644 index 000000000000..f0e24b1ff63c --- /dev/null +++ b/crates/euclid/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "euclid" +description = "DSL for static routing" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +frunk = "0.4.1" +frunk_core = "0.4.1" +nom = { version = "7.1.3", features = ["alloc"], optional = true } +once_cell = "1.18.0" +rustc-hash = "1.1.0" +serde = { version = "1.0.163", features = ["derive", "rc"] } +serde_json = "1.0.96" +erased-serde = "0.3.28" +strum = { version = "0.25", features = ["derive"] } +thiserror = "1.0.43" + +# First party dependencies +common_enums = { version = "0.1.0", path = "../common_enums" } +euclid_macros = { version = "0.1.0", path = "../euclid_macros" } + +[features] +ast_parser = ["dep:nom"] +valued_jit = [] +connector_choice_bcompat = [] +connector_choice_mca_id = [] +dummy_connector = [] +backwards_compatibility = ["connector_choice_bcompat"] + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "backends" +harness = false +required-features = ["ast_parser", "valued_jit"] diff --git a/crates/euclid/benches/backends.rs b/crates/euclid/benches/backends.rs new file mode 100644 index 000000000000..9d29c41d34c6 --- /dev/null +++ b/crates/euclid/benches/backends.rs @@ -0,0 +1,93 @@ +#![allow(unused, clippy::expect_used)] + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use euclid::{ + backend::{inputs, EuclidBackend, InterpreterBackend, VirInterpreterBackend}, + enums, + frontend::ast::{self, parser}, + types::DummyOutput, +}; + +fn get_program_data() -> (ast::Program, inputs::BackendInput) { + let code1 = r#" + default: ["stripe", "adyen", "checkout"] + + stripe_first: ["stripe", "aci"] + { + payment_method = card & amount = 40 { + payment_method = (card, bank_redirect) + amount = (40, 50) + } + } + + adyen_first: ["adyen", "checkout"] + { + payment_method = bank_redirect & amount > 60 { + payment_method = (card, bank_redirect) + amount = (40, 50) + } + } + + auth_first: ["authorizedotnet", "adyen"] + { + payment_method = wallet + } + "#; + + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Sofort), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let (_, program) = parser::program(code1).expect("Parser"); + + (program, inp) +} + +fn interpreter_vs_jit_vs_vir_interpreter(c: &mut Criterion) { + let (program, binputs) = get_program_data(); + + let interp_b = InterpreterBackend::with_program(program.clone()).expect("Interpreter backend"); + + let vir_interp_b = + VirInterpreterBackend::with_program(program).expect("Vir Interpreter Backend"); + + c.bench_function("Raw Interpreter Backend", |b| { + b.iter(|| { + interp_b + .execute(binputs.clone()) + .expect("Interpreter EXECUTION"); + }); + }); + + c.bench_function("Valued Interpreter Backend", |b| { + b.iter(|| { + vir_interp_b + .execute(binputs.clone()) + .expect("Vir Interpreter execution"); + }) + }); +} + +criterion_group!(benches, interpreter_vs_jit_vs_vir_interpreter); +criterion_main!(benches); diff --git a/crates/euclid/src/backend.rs b/crates/euclid/src/backend.rs new file mode 100644 index 000000000000..caf0a87b69cb --- /dev/null +++ b/crates/euclid/src/backend.rs @@ -0,0 +1,25 @@ +pub mod inputs; +pub mod interpreter; +#[cfg(feature = "valued_jit")] +pub mod vir_interpreter; + +pub use inputs::BackendInput; +pub use interpreter::InterpreterBackend; +#[cfg(feature = "valued_jit")] +pub use vir_interpreter::VirInterpreterBackend; + +use crate::frontend::ast; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct BackendOutput { + pub rule_name: Option, + pub connector_selection: O, +} + +pub trait EuclidBackend: Sized { + type Error: serde::Serialize; + + fn with_program(program: ast::Program) -> Result; + + fn execute(&self, input: BackendInput) -> Result, Self::Error>; +} diff --git a/crates/euclid/src/backend/inputs.rs b/crates/euclid/src/backend/inputs.rs new file mode 100644 index 000000000000..18298d4c358d --- /dev/null +++ b/crates/euclid/src/backend/inputs.rs @@ -0,0 +1,39 @@ +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; + +use crate::enums; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MandateData { + pub mandate_acceptance_type: Option, + pub mandate_type: Option, + pub payment_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentMethodInput { + pub payment_method: Option, + pub payment_method_type: Option, + pub card_network: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentInput { + pub amount: i64, + pub currency: enums::Currency, + pub authentication_type: Option, + pub card_bin: Option, + pub capture_method: Option, + pub business_country: Option, + pub billing_country: Option, + pub business_label: Option, + pub setup_future_usage: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackendInput { + pub metadata: Option>, + pub payment: PaymentInput, + pub payment_method: PaymentMethodInput, + pub mandate: MandateData, +} diff --git a/crates/euclid/src/backend/interpreter.rs b/crates/euclid/src/backend/interpreter.rs new file mode 100644 index 000000000000..bf0a561bf3f3 --- /dev/null +++ b/crates/euclid/src/backend/interpreter.rs @@ -0,0 +1,180 @@ +pub mod types; + +use crate::{ + backend::{self, inputs, EuclidBackend}, + frontend::ast, +}; + +pub struct InterpreterBackend { + program: ast::Program, +} + +impl InterpreterBackend +where + O: Clone, +{ + fn eval_number_comparison_array( + num: i64, + array: &[ast::NumberComparison], + ) -> Result { + for comparison in array { + let other = comparison.number; + let res = match comparison.comparison_type { + ast::ComparisonType::GreaterThan => num > other, + ast::ComparisonType::LessThan => num < other, + ast::ComparisonType::LessThanEqual => num <= other, + ast::ComparisonType::GreaterThanEqual => num >= other, + ast::ComparisonType::Equal => num == other, + ast::ComparisonType::NotEqual => num != other, + }; + + if res { + return Ok(true); + } + } + + Ok(false) + } + + fn eval_comparison( + comparison: &ast::Comparison, + ctx: &types::Context, + ) -> Result { + use ast::{ComparisonType::*, ValueType::*}; + + let value = ctx + .get(&comparison.lhs) + .ok_or_else(|| types::InterpreterError { + error_type: types::InterpreterErrorType::InvalidKey(comparison.lhs.clone()), + metadata: comparison.metadata.clone(), + })?; + + if let Some(val) = value { + match (val, &comparison.comparison, &comparison.value) { + (EnumVariant(e1), Equal, EnumVariant(e2)) => Ok(e1 == e2), + (EnumVariant(e1), NotEqual, EnumVariant(e2)) => Ok(e1 != e2), + (EnumVariant(e), Equal, EnumVariantArray(evec)) => Ok(evec.iter().any(|v| e == v)), + (EnumVariant(e), NotEqual, EnumVariantArray(evec)) => { + Ok(evec.iter().all(|v| e != v)) + } + (Number(n1), Equal, Number(n2)) => Ok(n1 == n2), + (Number(n1), NotEqual, Number(n2)) => Ok(n1 != n2), + (Number(n1), LessThanEqual, Number(n2)) => Ok(n1 <= n2), + (Number(n1), GreaterThanEqual, Number(n2)) => Ok(n1 >= n2), + (Number(n1), LessThan, Number(n2)) => Ok(n1 < n2), + (Number(n1), GreaterThan, Number(n2)) => Ok(n1 > n2), + (Number(n), Equal, NumberArray(nvec)) => Ok(nvec.iter().any(|v| v == n)), + (Number(n), NotEqual, NumberArray(nvec)) => Ok(nvec.iter().all(|v| v != n)), + (Number(n), Equal, NumberComparisonArray(ncvec)) => { + Self::eval_number_comparison_array(*n, ncvec) + } + _ => Err(types::InterpreterError { + error_type: types::InterpreterErrorType::InvalidComparison, + metadata: comparison.metadata.clone(), + }), + } + } else { + Ok(false) + } + } + + fn eval_if_condition( + condition: &ast::IfCondition, + ctx: &types::Context, + ) -> Result { + for comparison in condition { + let res = Self::eval_comparison(comparison, ctx)?; + + if !res { + return Ok(false); + } + } + + Ok(true) + } + + fn eval_if_statement( + stmt: &ast::IfStatement, + ctx: &types::Context, + ) -> Result { + let cond_res = Self::eval_if_condition(&stmt.condition, ctx)?; + + if !cond_res { + return Ok(false); + } + + if let Some(ref nested) = stmt.nested { + for nested_if in nested { + let res = Self::eval_if_statement(nested_if, ctx)?; + + if res { + return Ok(true); + } + } + + return Ok(false); + } + + Ok(true) + } + + fn eval_rule_statements( + statements: &[ast::IfStatement], + ctx: &types::Context, + ) -> Result { + for stmt in statements { + let res = Self::eval_if_statement(stmt, ctx)?; + + if res { + return Ok(true); + } + } + + Ok(false) + } + + #[inline] + fn eval_rule( + rule: &ast::Rule, + ctx: &types::Context, + ) -> Result { + Self::eval_rule_statements(&rule.statements, ctx) + } + + fn eval_program( + program: &ast::Program, + ctx: &types::Context, + ) -> Result, types::InterpreterError> { + for rule in &program.rules { + let res = Self::eval_rule(rule, ctx)?; + + if res { + return Ok(backend::BackendOutput { + connector_selection: rule.connector_selection.clone(), + rule_name: Some(rule.name.clone()), + }); + } + } + + Ok(backend::BackendOutput { + connector_selection: program.default_selection.clone(), + rule_name: None, + }) + } +} + +impl EuclidBackend for InterpreterBackend +where + O: Clone, +{ + type Error = types::InterpreterError; + + fn with_program(program: ast::Program) -> Result { + Ok(Self { program }) + } + + fn execute(&self, input: inputs::BackendInput) -> Result, Self::Error> { + let ctx: types::Context = input.into(); + Self::eval_program(&self.program, &ctx) + } +} diff --git a/crates/euclid/src/backend/interpreter/types.rs b/crates/euclid/src/backend/interpreter/types.rs new file mode 100644 index 000000000000..a6384dbdf3ce --- /dev/null +++ b/crates/euclid/src/backend/interpreter/types.rs @@ -0,0 +1,81 @@ +use std::{collections::HashMap, fmt, ops::Deref, string::ToString}; + +use serde::Serialize; + +use crate::{backend::inputs, frontend::ast::ValueType, types::EuclidKey}; + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum InterpreterErrorType { + #[error("Invalid key received '{0}'")] + InvalidKey(String), + #[error("Invalid Comparison")] + InvalidComparison, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +pub struct InterpreterError { + pub error_type: InterpreterErrorType, + pub metadata: HashMap, +} + +impl fmt::Display for InterpreterError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + InterpreterErrorType::fmt(&self.error_type, f) + } +} + +pub struct Context(HashMap>); + +impl Deref for Context { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Context { + fn from(input: inputs::BackendInput) -> Self { + let ctx = HashMap::>::from_iter([ + ( + EuclidKey::PaymentMethod.to_string(), + input + .payment_method + .payment_method + .map(|pm| ValueType::EnumVariant(pm.to_string())), + ), + ( + EuclidKey::PaymentMethodType.to_string(), + input + .payment_method + .payment_method_type + .map(|pt| ValueType::EnumVariant(pt.to_string())), + ), + ( + EuclidKey::AuthenticationType.to_string(), + input + .payment + .authentication_type + .map(|at| ValueType::EnumVariant(at.to_string())), + ), + ( + EuclidKey::CaptureMethod.to_string(), + input + .payment + .capture_method + .map(|cm| ValueType::EnumVariant(cm.to_string())), + ), + ( + EuclidKey::PaymentAmount.to_string(), + Some(ValueType::Number(input.payment.amount)), + ), + ( + EuclidKey::PaymentCurrency.to_string(), + Some(ValueType::EnumVariant(input.payment.currency.to_string())), + ), + ]); + + Self(ctx) + } +} diff --git a/crates/euclid/src/backend/vir_interpreter.rs b/crates/euclid/src/backend/vir_interpreter.rs new file mode 100644 index 000000000000..b7be62cf6740 --- /dev/null +++ b/crates/euclid/src/backend/vir_interpreter.rs @@ -0,0 +1,583 @@ +pub mod types; + +use crate::{ + backend::{self, inputs, EuclidBackend}, + frontend::{ + ast, + dir::{self, EuclidDirFilter}, + vir, + }, +}; + +pub struct VirInterpreterBackend { + program: vir::ValuedProgram, +} + +impl VirInterpreterBackend +where + O: Clone, +{ + #[inline] + fn eval_comparison(comp: &vir::ValuedComparison, ctx: &types::Context) -> bool { + match &comp.logic { + vir::ValuedComparisonLogic::PositiveDisjunction => { + comp.values.iter().any(|v| ctx.check_presence(v)) + } + vir::ValuedComparisonLogic::NegativeConjunction => { + comp.values.iter().all(|v| !ctx.check_presence(v)) + } + } + } + + #[inline] + fn eval_condition(cond: &vir::ValuedIfCondition, ctx: &types::Context) -> bool { + cond.iter().all(|comp| Self::eval_comparison(comp, ctx)) + } + + fn eval_statement(stmt: &vir::ValuedIfStatement, ctx: &types::Context) -> bool { + Self::eval_condition(&stmt.condition, ctx) + .then(|| { + stmt.nested.as_ref().map_or(true, |nested_stmts| { + nested_stmts.iter().any(|s| Self::eval_statement(s, ctx)) + }) + }) + .unwrap_or(false) + } + + fn eval_rule(rule: &vir::ValuedRule, ctx: &types::Context) -> bool { + rule.statements + .iter() + .any(|stmt| Self::eval_statement(stmt, ctx)) + } + + fn eval_program( + program: &vir::ValuedProgram, + ctx: &types::Context, + ) -> backend::BackendOutput { + program + .rules + .iter() + .find(|rule| Self::eval_rule(rule, ctx)) + .map_or_else( + || backend::BackendOutput { + connector_selection: program.default_selection.clone(), + rule_name: None, + }, + |rule| backend::BackendOutput { + connector_selection: rule.connector_selection.clone(), + rule_name: Some(rule.name.clone()), + }, + ) + } +} + +impl EuclidBackend for VirInterpreterBackend +where + O: Clone + EuclidDirFilter, +{ + type Error = types::VirInterpreterError; + + fn with_program(program: ast::Program) -> Result { + let dir_program = ast::lowering::lower_program(program) + .map_err(types::VirInterpreterError::LoweringError)?; + + let vir_program = dir::lowering::lower_program(dir_program) + .map_err(types::VirInterpreterError::LoweringError)?; + + Ok(Self { + program: vir_program, + }) + } + + fn execute( + &self, + input: inputs::BackendInput, + ) -> Result, Self::Error> { + let ctx = types::Context::from_input(input); + Ok(Self::eval_program(&self.program, &ctx)) + } +} +#[cfg(all(test, feature = "ast_parser"))] +mod test { + #![allow(clippy::expect_used)] + use rustc_hash::FxHashMap; + + use super::*; + use crate::{enums, types::DummyOutput}; + + #[test] + fn test_execution() { + let program_str = r#" + default: [ "stripe", "adyen"] + + rule_1: ["stripe"] + { + pay_later = klarna + } + + rule_2: ["adyen"] + { + pay_later = affirm + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_2"); + } + #[test] + fn test_payment_type() { + let program_str = r#" + default: ["stripe", "adyen"] + rule_1: ["stripe"] + { + payment_type = setup_mandate + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: Some(enums::PaymentType::SetupMandate), + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_mandate_type() { + let program_str = r#" + default: ["stripe", "adyen"] + rule_1: ["stripe"] + { + mandate_type = single_use + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: Some(enums::MandateType::SingleUse), + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_mandate_acceptance_type() { + let program_str = r#" + default: ["stripe","adyen"] + rule_1: ["stripe"] + { + mandate_acceptance_type = online + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: Some(enums::MandateAcceptanceType::Online), + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_card_bin() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + card_bin="123456" + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_payment_amount() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount = 32 + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_payment_method() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + payment_method = pay_later + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_future_usage() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + setup_future_usage = off_session + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: Some(enums::SetupFutureUsage::OffSession), + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_metadata_execution() { + let program_str = r#" + default: ["stripe"," adyen"] + + rule_1: ["stripe"] + { + "metadata_key" = "arbitrary meta" + } + "#; + let mut meta_map = FxHashMap::default(); + meta_map.insert("metadata_key".to_string(), "arbitrary meta".to_string()); + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: Some(meta_map), + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_less_than_operator() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount>=123 + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp_greater = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 150, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + let mut inp_equal = inp_greater.clone(); + inp_equal.payment.amount = 123; + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result_greater = backend.execute(inp_greater).expect("Execution"); + let result_equal = backend.execute(inp_equal).expect("Execution"); + assert_eq!( + result_equal.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + assert_eq!( + result_greater.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + } + + #[test] + fn test_greater_than_operator() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount<=123 + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp_lower = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 120, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + let mut inp_equal = inp_lower.clone(); + inp_equal.payment.amount = 123; + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result_equal = backend.execute(inp_equal).expect("Execution"); + let result_lower = backend.execute(inp_lower).expect("Execution"); + assert_eq!( + result_equal.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + assert_eq!( + result_lower.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + } +} diff --git a/crates/euclid/src/backend/vir_interpreter/types.rs b/crates/euclid/src/backend/vir_interpreter/types.rs new file mode 100644 index 000000000000..a144cdaafd08 --- /dev/null +++ b/crates/euclid/src/backend/vir_interpreter/types.rs @@ -0,0 +1,126 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + backend::inputs::BackendInput, + dssa, + types::{self, EuclidKey, EuclidValue, MetadataValue, NumValueRefinement, StrValue}, +}; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum VirInterpreterError { + #[error("Error when lowering the program: {0:?}")] + LoweringError(dssa::types::AnalysisError), +} + +pub struct Context { + atomic_values: FxHashSet, + numeric_values: FxHashMap, +} + +impl Context { + pub fn check_presence(&self, value: &EuclidValue) -> bool { + let key = value.get_key(); + + match key.key_type() { + types::DataType::MetadataValue => self.atomic_values.contains(value), + types::DataType::StrValue => self.atomic_values.contains(value), + types::DataType::EnumVariant => self.atomic_values.contains(value), + types::DataType::Number => { + let ctx_num_value = self + .numeric_values + .get(&key) + .and_then(|value| value.get_num_value()); + + value.get_num_value().zip(ctx_num_value).map_or( + false, + |(program_value, ctx_value)| { + let program_num = program_value.number; + let ctx_num = ctx_value.number; + + match &program_value.refinement { + None => program_num == ctx_num, + Some(NumValueRefinement::NotEqual) => ctx_num != program_num, + Some(NumValueRefinement::GreaterThan) => ctx_num > program_num, + Some(NumValueRefinement::GreaterThanEqual) => ctx_num >= program_num, + Some(NumValueRefinement::LessThanEqual) => ctx_num <= program_num, + Some(NumValueRefinement::LessThan) => ctx_num < program_num, + } + }, + ) + } + } + } + + pub fn from_input(input: BackendInput) -> Self { + let payment = input.payment; + let payment_method = input.payment_method; + let meta_data = input.metadata; + let payment_mandate = input.mandate; + + let mut enum_values: FxHashSet = + FxHashSet::from_iter([EuclidValue::PaymentCurrency(payment.currency)]); + + if let Some(pm) = payment_method.payment_method { + enum_values.insert(EuclidValue::PaymentMethod(pm)); + } + + if let Some(pmt) = payment_method.payment_method_type { + enum_values.insert(EuclidValue::PaymentMethodType(pmt)); + } + + if let Some(met) = meta_data { + for (key, value) in met.into_iter() { + enum_values.insert(EuclidValue::Metadata(MetadataValue { key, value })); + } + } + + if let Some(at) = payment.authentication_type { + enum_values.insert(EuclidValue::AuthenticationType(at)); + } + + if let Some(capture_method) = payment.capture_method { + enum_values.insert(EuclidValue::CaptureMethod(capture_method)); + } + + if let Some(country) = payment.business_country { + enum_values.insert(EuclidValue::BusinessCountry(country)); + } + + if let Some(country) = payment.billing_country { + enum_values.insert(EuclidValue::BillingCountry(country)); + } + if let Some(card_bin) = payment.card_bin { + enum_values.insert(EuclidValue::CardBin(StrValue { value: card_bin })); + } + if let Some(business_label) = payment.business_label { + enum_values.insert(EuclidValue::BusinessLabel(StrValue { + value: business_label, + })); + } + if let Some(setup_future_usage) = payment.setup_future_usage { + enum_values.insert(EuclidValue::SetupFutureUsage(setup_future_usage)); + } + if let Some(payment_type) = payment_mandate.payment_type { + enum_values.insert(EuclidValue::PaymentType(payment_type)); + } + if let Some(mandate_type) = payment_mandate.mandate_type { + enum_values.insert(EuclidValue::MandateType(mandate_type)); + } + if let Some(mandate_acceptance_type) = payment_mandate.mandate_acceptance_type { + enum_values.insert(EuclidValue::MandateAcceptanceType(mandate_acceptance_type)); + } + + let numeric_values: FxHashMap = FxHashMap::from_iter([( + EuclidKey::PaymentAmount, + EuclidValue::PaymentAmount(types::NumValue { + number: payment.amount, + refinement: None, + }), + )]); + + Self { + atomic_values: enum_values, + numeric_values, + } + } +} diff --git a/crates/euclid/src/dssa.rs b/crates/euclid/src/dssa.rs new file mode 100644 index 000000000000..2f6f35dfb27c --- /dev/null +++ b/crates/euclid/src/dssa.rs @@ -0,0 +1,7 @@ +//! Domain Specific Static Analyzer +pub mod analyzer; +pub mod graph; +pub mod state_machine; +pub mod truth; +pub mod types; +pub mod utils; diff --git a/crates/euclid/src/dssa/analyzer.rs b/crates/euclid/src/dssa/analyzer.rs new file mode 100644 index 000000000000..149ed1fd79cd --- /dev/null +++ b/crates/euclid/src/dssa/analyzer.rs @@ -0,0 +1,447 @@ +//! Static Analysis for the Euclid Rule DSL +//! +//! Exposes certain functions that can be used to perform static analysis over programs +//! in the Euclid Rule DSL. These include standard control flow analyses like testing +//! conflicting assertions, to Domain Specific Analyses making use of the +//! [`Knowledge Graph Framework`](crate::dssa::graph). +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{graph::Memoization, types::EuclidAnalysable}; +use crate::{ + dssa::{graph, state_machine, truth, types}, + frontend::{ + ast, + dir::{self, EuclidDirFilter}, + vir, + }, + types::{DataType, Metadata}, +}; + +/// Analyses conflicting assertions on the same key in a conjunctive context. +/// +/// For example, +/// ```notrust +/// payment_method = card && ... && payment_method = bank_debit +/// ```notrust +/// This is a condition that will never evaluate to `true` given a single +/// payment method and needs to be caught in analysis. +pub fn analyze_conflicting_assertions( + keywise_assertions: &FxHashMap>, + assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>, +) -> Result<(), types::AnalysisError> { + for (key, value_set) in keywise_assertions { + if value_set.len() > 1 { + let err_type = types::AnalysisErrorType::ConflictingAssertions { + key: key.clone(), + values: value_set + .iter() + .map(|val| types::ValueData { + value: (*val).clone(), + metadata: assertion_metadata + .get(val) + .map(|meta| (*meta).clone()) + .unwrap_or_default(), + }) + .collect(), + }; + + Err(types::AnalysisError { + error_type: err_type, + metadata: Default::default(), + })?; + } + } + Ok(()) +} + +/// Analyses exhaustive negations on the same key in a conjunctive context. +/// +/// For example, +/// ```notrust +/// authentication_type /= three_ds && ... && authentication_type /= no_three_ds +/// ```notrust +/// This is a condition that will never evaluate to `true` given any authentication_type +/// since all the possible values authentication_type can take have been negated. +pub fn analyze_exhaustive_negations( + keywise_negations: &FxHashMap>, + keywise_negation_metadata: &FxHashMap>, +) -> Result<(), types::AnalysisError> { + for (key, negation_set) in keywise_negations { + let mut value_set = if let Some(set) = key.kind.get_value_set() { + set + } else { + continue; + }; + + value_set.retain(|val| !negation_set.contains(val)); + + if value_set.is_empty() { + let error_type = types::AnalysisErrorType::ExhaustiveNegation { + key: key.clone(), + metadata: keywise_negation_metadata + .get(key) + .cloned() + .unwrap_or_default() + .iter() + .cloned() + .cloned() + .collect(), + }; + + Err(types::AnalysisError { + error_type, + metadata: Default::default(), + })?; + } + } + Ok(()) +} + +fn analyze_negated_assertions( + keywise_assertions: &FxHashMap>, + assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>, + keywise_negations: &FxHashMap>, + negation_metadata: &FxHashMap<&dir::DirValue, &Metadata>, +) -> Result<(), types::AnalysisError> { + for (key, negation_set) in keywise_negations { + let assertion_set = if let Some(set) = keywise_assertions.get(key) { + set + } else { + continue; + }; + + let intersection = negation_set & assertion_set; + + intersection.iter().next().map_or(Ok(()), |val| { + let error_type = types::AnalysisErrorType::NegatedAssertion { + value: (*val).clone(), + assertion_metadata: assertion_metadata + .get(*val) + .cloned() + .cloned() + .unwrap_or_default(), + negation_metadata: negation_metadata + .get(*val) + .cloned() + .cloned() + .unwrap_or_default(), + }; + + Err(types::AnalysisError { + error_type, + metadata: Default::default(), + }) + })?; + } + Ok(()) +} + +fn perform_condition_analyses( + context: &types::ConjunctiveContext<'_>, +) -> Result<(), types::AnalysisError> { + let mut assertion_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default(); + let mut keywise_assertions: FxHashMap> = + FxHashMap::default(); + let mut negation_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default(); + let mut keywise_negation_metadata: FxHashMap> = + FxHashMap::default(); + let mut keywise_negations: FxHashMap> = + FxHashMap::default(); + + for ctx_val in context { + let key = if let Some(k) = ctx_val.value.get_key() { + k + } else { + continue; + }; + + if let dir::DirKeyKind::Connector = key.kind { + continue; + } + + if !matches!(key.kind.get_type(), DataType::EnumVariant) { + continue; + } + + match ctx_val.value { + types::CtxValueKind::Assertion(val) => { + keywise_assertions + .entry(key.clone()) + .or_default() + .insert(val); + + assertion_metadata.insert(val, ctx_val.metadata); + } + + types::CtxValueKind::Negation(vals) => { + let negation_set = keywise_negations.entry(key.clone()).or_default(); + + for val in vals { + negation_set.insert(val); + negation_metadata.insert(val, ctx_val.metadata); + } + + keywise_negation_metadata + .entry(key.clone()) + .or_default() + .push(ctx_val.metadata); + } + } + } + + analyze_conflicting_assertions(&keywise_assertions, &assertion_metadata)?; + analyze_exhaustive_negations(&keywise_negations, &keywise_negation_metadata)?; + analyze_negated_assertions( + &keywise_assertions, + &assertion_metadata, + &keywise_negations, + &negation_metadata, + )?; + + Ok(()) +} + +fn perform_context_analyses( + context: &types::ConjunctiveContext<'_>, + knowledge_graph: &graph::KnowledgeGraph<'_>, +) -> Result<(), types::AnalysisError> { + perform_condition_analyses(context)?; + let mut memo = Memoization::new(); + knowledge_graph + .perform_context_analysis(context, &mut memo) + .map_err(|err| types::AnalysisError { + error_type: types::AnalysisErrorType::GraphAnalysis(err, memo), + metadata: Default::default(), + })?; + Ok(()) +} + +pub fn analyze( + program: ast::Program, + knowledge_graph: Option<&graph::KnowledgeGraph<'_>>, +) -> Result, types::AnalysisError> { + let dir_program = ast::lowering::lower_program(program)?; + + let selection_data = state_machine::make_connector_selection_data(&dir_program); + let mut ctx_manager = state_machine::AnalysisContextManager::new(&dir_program, &selection_data); + while let Some(ctx) = ctx_manager.advance().map_err(|err| types::AnalysisError { + metadata: Default::default(), + error_type: types::AnalysisErrorType::StateMachine(err), + })? { + perform_context_analyses(ctx, knowledge_graph.unwrap_or(&truth::ANALYSIS_GRAPH))?; + } + + dir::lowering::lower_program(dir_program) +} + +#[cfg(all(test, feature = "ast_parser"))] +mod tests { + #![allow(clippy::panic, clippy::expect_used)] + + use std::{ops::Deref, sync::Weak}; + + use euclid_macros::knowledge; + + use super::*; + use crate::{dirval, types::DummyOutput}; + + #[test] + fn test_conflicting_assertion_detection() { + let program_str = r#" + default: ["stripe", "adyen"] + + stripe_first: ["stripe", "adyen"] + { + payment_method = wallet { + amount > 500 & capture_method = automatic + amount < 500 & payment_method = card + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::ConflictingAssertions { key, values }, + .. + }) = analysis_result + { + assert!( + matches!(key.kind, dir::DirKeyKind::PaymentMethod), + "Key should be payment_method" + ); + let values: Vec = values.into_iter().map(|v| v.value).collect(); + assert_eq!(values.len(), 2, "There should be 2 conflicting conditions"); + assert!( + values.contains(&dirval!(PaymentMethod = Wallet)), + "Condition should include payment_method = wallet" + ); + assert!( + values.contains(&dirval!(PaymentMethod = Card)), + "Condition should include payment_method = card" + ); + } else { + panic!("Did not receive conflicting assertions error"); + } + } + + #[test] + fn test_exhaustive_negation_detection() { + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + payment_method /= wallet { + capture_method = manual & payment_method /= card { + authentication_type = three_ds & payment_method /= pay_later { + amount > 1000 & payment_method /= bank_redirect { + payment_method /= crypto + & payment_method /= bank_debit + & payment_method /= bank_transfer + & payment_method /= upi + & payment_method /= reward + & payment_method /= voucher + & payment_method /= gift_card + + } + } + } + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::ExhaustiveNegation { key, .. }, + .. + }) = analysis_result + { + assert!( + matches!(key.kind, dir::DirKeyKind::PaymentMethod), + "Expected key to be payment_method" + ); + } else { + panic!("Expected exhaustive negation error"); + } + } + + #[test] + fn test_negated_assertions_detection() { + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + payment_method = wallet { + amount > 500 { + capture_method = automatic + } + + amount < 501 { + payment_method /= wallet + } + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::NegatedAssertion { value, .. }, + .. + }) = analysis_result + { + assert_eq!( + value, + dirval!(PaymentMethod = Wallet), + "Expected to catch payment_method = wallet as conflict" + ); + } else { + panic!("Expected negated assertion error"); + } + } + + #[test] + fn test_negation_graph_analysis() { + let graph = knowledge! {crate + CaptureMethod(Automatic) ->> PaymentMethod(Card); + }; + + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + amount > 500 { + payment_method = pay_later + } + + amount < 500 { + payment_method /= wallet & payment_method /= pay_later + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Graph"); + let analysis_result = analyze(program, Some(&graph)); + + let error_type = match analysis_result { + Err(types::AnalysisError { error_type, .. }) => error_type, + _ => panic!("Error_type not found"), + }; + + let a_err = match error_type { + types::AnalysisErrorType::GraphAnalysis(trace, memo) => (trace, memo), + _ => panic!("Graph Analysis not found"), + }; + + let (trace, metadata) = match a_err.0 { + graph::AnalysisError::NegationTrace { trace, metadata } => (trace, metadata), + _ => panic!("Negation Trace not found"), + }; + + let predecessor = match Weak::upgrade(&trace) + .expect("Expected Arc not found") + .deref() + .clone() + { + graph::AnalysisTrace::Value { predecessors, .. } => { + let _value = graph::NodeValue::Value(dir::DirValue::PaymentMethod( + dir::enums::PaymentMethod::Card, + )); + let _relation = graph::Relation::Positive; + predecessors + } + _ => panic!("Expected Negation Trace for payment method = card"), + }; + + let pred = match predecessor { + Some(graph::ValueTracePredecessor::Mandatory(predecessor)) => predecessor, + _ => panic!("No predecessor found"), + }; + assert_eq!( + metadata.len(), + 2, + "Expected two metadats for wallet and pay_later" + ); + assert!(matches!( + *Weak::upgrade(&pred) + .expect("Expected Arc not found") + .deref(), + graph::AnalysisTrace::Value { + value: graph::NodeValue::Value(dir::DirValue::CaptureMethod( + dir::enums::CaptureMethod::Automatic + )), + relation: graph::Relation::Positive, + info: None, + metadata: None, + predecessors: None, + } + )); + } +} diff --git a/crates/euclid/src/dssa/graph.rs b/crates/euclid/src/dssa/graph.rs new file mode 100644 index 000000000000..bd23ae385226 --- /dev/null +++ b/crates/euclid/src/dssa/graph.rs @@ -0,0 +1,1478 @@ +use std::{ + fmt::Debug, + hash::Hash, + ops::{Deref, DerefMut}, + sync::{Arc, Weak}, +}; + +use erased_serde::{self, Serialize as ErasedSerialize}; +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::Serialize; + +use crate::{ + dssa::types, + frontend::dir, + types::{DataType, Metadata}, + utils, +}; + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash, strum::Display)] +pub enum Strength { + Weak, + Normal, + Strong, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Relation { + Positive, + Negative, +} + +impl From for bool { + fn from(value: Relation) -> Self { + matches!(value, Relation::Positive) + } +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)] +pub struct NodeId(usize); + +impl utils::EntityId for NodeId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DomainInfo<'a> { + pub domain_identifier: DomainIdentifier<'a>, + pub domain_description: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DomainIdentifier<'a>(&'a str); + +impl<'a> DomainIdentifier<'a> { + pub fn new(domain_identifier: &'a str) -> Self { + Self(domain_identifier) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DomainId(usize); + +impl utils::EntityId for DomainId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EdgeId(usize); + +impl utils::EntityId for EdgeId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Memoization(FxHashMap<(NodeId, Relation, Strength), Result<(), Arc>>); + +impl Memoization { + pub fn new() -> Self { + Self(FxHashMap::default()) + } +} + +impl Default for Memoization { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Deref for Memoization { + type Target = FxHashMap<(NodeId, Relation, Strength), Result<(), Arc>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Memoization { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +#[derive(Debug, Clone)] +pub struct Edge { + pub strength: Strength, + pub relation: Relation, + pub pred: NodeId, + pub succ: NodeId, +} + +#[derive(Debug)] +pub struct Node { + pub node_type: NodeType, + pub preds: Vec, + pub succs: Vec, + pub domain_ids: Vec, +} + +impl Node { + fn new(node_type: NodeType, domain_ids: Vec) -> Self { + Self { + node_type, + preds: Vec::new(), + succs: Vec::new(), + domain_ids, + } + } +} + +pub trait KgraphMetadata: ErasedSerialize + std::any::Any + Sync + Send + Debug {} +erased_serde::serialize_trait_object!(KgraphMetadata); + +impl KgraphMetadata for M where M: ErasedSerialize + std::any::Any + Sync + Send + Debug {} + +#[derive(Debug)] +pub struct KnowledgeGraph<'a> { + domain: utils::DenseMap>, + nodes: utils::DenseMap, + edges: utils::DenseMap, + value_map: FxHashMap, + node_info: utils::DenseMap>, + node_metadata: utils::DenseMap>>, +} + +pub struct KnowledgeGraphBuilder<'a> { + domain: utils::DenseMap>, + nodes: utils::DenseMap, + edges: utils::DenseMap, + domain_identifier_map: FxHashMap, DomainId>, + value_map: FxHashMap, + edges_map: FxHashMap<(NodeId, NodeId), EdgeId>, + node_info: utils::DenseMap>, + node_metadata: utils::DenseMap>>, +} + +impl<'a> Default for KnowledgeGraphBuilder<'a> { + #[inline] + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NodeType { + AllAggregator, + AnyAggregator, + InAggregator(FxHashSet), + Value(NodeValue), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum NodeValue { + Key(dir::DirKey), + Value(dir::DirValue), +} + +impl From for NodeValue { + fn from(value: dir::DirValue) -> Self { + Self::Value(value) + } +} + +impl From for NodeValue { + fn from(key: dir::DirKey) -> Self { + Self::Key(key) + } +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "predecessor", rename_all = "snake_case")] +pub enum ValueTracePredecessor { + Mandatory(Box>), + OneOf(Vec>), +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "trace", rename_all = "snake_case")] +pub enum AnalysisTrace { + Value { + value: NodeValue, + relation: Relation, + predecessors: Option, + info: Option<&'static str>, + metadata: Option>, + }, + + AllAggregation { + unsatisfied: Vec>, + info: Option<&'static str>, + metadata: Option>, + }, + + AnyAggregation { + unsatisfied: Vec>, + info: Option<&'static str>, + metadata: Option>, + }, + + InAggregation { + expected: Vec, + found: Option, + relation: Relation, + info: Option<&'static str>, + metadata: Option>, + }, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "details", rename_all = "snake_case")] +pub enum AnalysisError { + Graph(GraphError), + AssertionTrace { + trace: Weak, + metadata: Metadata, + }, + NegationTrace { + trace: Weak, + metadata: Vec, + }, +} + +impl AnalysisError { + fn assertion_from_graph_error(metadata: &Metadata, graph_error: GraphError) -> Self { + match graph_error { + GraphError::AnalysisError(trace) => Self::AssertionTrace { + trace, + metadata: metadata.clone(), + }, + + other => Self::Graph(other), + } + } + + fn negation_from_graph_error(metadata: Vec<&Metadata>, graph_error: GraphError) -> Self { + match graph_error { + GraphError::AnalysisError(trace) => Self::NegationTrace { + trace, + metadata: metadata.iter().map(|m| (*m).clone()).collect(), + }, + + other => Self::Graph(other), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum GraphError { + #[error("An edge was not found in the graph")] + EdgeNotFound, + #[error("Attempted to create a conflicting edge between two nodes")] + ConflictingEdgeCreated, + #[error("Cycle detected in graph")] + CycleDetected, + #[error("Domain wasn't found in the Graph")] + DomainNotFound, + #[error("Malformed Graph: {reason}")] + MalformedGraph { reason: String }, + #[error("A node was not found in the graph")] + NodeNotFound, + #[error("A value node was not found: {0:#?}")] + ValueNodeNotFound(dir::DirValue), + #[error("No values provided for an 'in' aggregator node")] + NoInAggregatorValues, + #[error("Error during analysis: {0:#?}")] + AnalysisError(Weak), +} + +impl GraphError { + fn get_analysis_trace(self) -> Result, Self> { + match self { + Self::AnalysisError(trace) => Ok(trace), + _ => Err(self), + } + } +} + +impl PartialEq for NodeValue { + fn eq(&self, other: &dir::DirValue) -> bool { + match self { + Self::Key(dir_key) => *dir_key == other.get_key(), + Self::Value(dir_value) if dir_value.get_key() == other.get_key() => { + if let (Some(left), Some(right)) = + (dir_value.get_num_value(), other.get_num_value()) + { + left.fits(&right) + } else { + dir::DirValue::check_equality(dir_value, other) + } + } + Self::Value(_) => false, + } + } +} + +pub struct AnalysisContext { + keywise_values: FxHashMap>, +} + +impl AnalysisContext { + pub fn from_dir_values(vals: impl IntoIterator) -> Self { + let mut keywise_values: FxHashMap> = + FxHashMap::default(); + + for dir_val in vals { + let key = dir_val.get_key(); + let set = keywise_values.entry(key).or_default(); + set.insert(dir_val); + } + + Self { keywise_values } + } + + fn check_presence(&self, value: &NodeValue, weak: bool) -> bool { + match value { + NodeValue::Key(k) => self.keywise_values.contains_key(k) || weak, + NodeValue::Value(val) => { + let key = val.get_key(); + let value_set = if let Some(set) = self.keywise_values.get(&key) { + set + } else { + return weak; + }; + + match key.kind.get_type() { + DataType::EnumVariant | DataType::StrValue | DataType::MetadataValue => { + value_set.contains(val) + } + DataType::Number => val.get_num_value().map_or(false, |num_val| { + value_set.iter().any(|ctx_val| { + ctx_val + .get_num_value() + .map_or(false, |ctx_num_val| num_val.fits(&ctx_num_val)) + }) + }), + } + } + } + } + + pub fn insert(&mut self, value: dir::DirValue) { + self.keywise_values + .entry(value.get_key()) + .or_default() + .insert(value); + } + + pub fn remove(&mut self, value: dir::DirValue) { + let set = self.keywise_values.entry(value.get_key()).or_default(); + + set.remove(&value); + + if set.is_empty() { + self.keywise_values.remove(&value.get_key()); + } + } +} + +impl<'a> KnowledgeGraphBuilder<'a> { + pub fn new() -> Self { + Self { + domain: utils::DenseMap::new(), + nodes: utils::DenseMap::new(), + edges: utils::DenseMap::new(), + domain_identifier_map: FxHashMap::default(), + value_map: FxHashMap::default(), + edges_map: FxHashMap::default(), + node_info: utils::DenseMap::new(), + node_metadata: utils::DenseMap::new(), + } + } + + pub fn build(self) -> KnowledgeGraph<'a> { + KnowledgeGraph { + domain: self.domain, + nodes: self.nodes, + edges: self.edges, + value_map: self.value_map, + node_info: self.node_info, + node_metadata: self.node_metadata, + } + } + + pub fn make_domain( + &mut self, + domain_identifier: DomainIdentifier<'a>, + domain_description: String, + ) -> Result { + Ok(self + .domain_identifier_map + .clone() + .get(&domain_identifier) + .map_or_else( + || { + let domain_id = self.domain.push(DomainInfo { + domain_identifier: domain_identifier.clone(), + domain_description, + }); + self.domain_identifier_map + .insert(domain_identifier.clone(), domain_id); + domain_id + }, + |domain_id| *domain_id, + )) + } + + pub fn make_value_node( + &mut self, + value: NodeValue, + info: Option<&'static str>, + domain_identifiers: Vec>, + metadata: Option, + ) -> Result { + match self.value_map.get(&value).copied() { + Some(node_id) => Ok(node_id), + None => { + let mut domain_ids: Vec = Vec::new(); + domain_identifiers + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let node_id = self + .nodes + .push(Node::new(NodeType::Value(value.clone()), domain_ids)); + let _node_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + self.value_map.insert(value, node_id); + Ok(node_id) + } + } + } + + pub fn make_edge( + &mut self, + pred_id: NodeId, + succ_id: NodeId, + strength: Strength, + relation: Relation, + ) -> Result { + self.ensure_node_exists(pred_id)?; + self.ensure_node_exists(succ_id)?; + self.edges_map + .get(&(pred_id, succ_id)) + .copied() + .and_then(|edge_id| self.edges.get(edge_id).cloned().map(|edge| (edge_id, edge))) + .map_or_else( + || { + let edge_id = self.edges.push(Edge { + strength, + relation, + pred: pred_id, + succ: succ_id, + }); + self.edges_map.insert((pred_id, succ_id), edge_id); + + let pred = self + .nodes + .get_mut(pred_id) + .ok_or(GraphError::NodeNotFound)?; + pred.succs.push(edge_id); + + let succ = self + .nodes + .get_mut(succ_id) + .ok_or(GraphError::NodeNotFound)?; + succ.preds.push(edge_id); + + Ok(edge_id) + }, + |(edge_id, edge)| { + if edge.strength == strength && edge.relation == relation { + Ok(edge_id) + } else { + Err(GraphError::ConflictingEdgeCreated) + } + }, + ) + } + + pub fn make_all_aggregator( + &mut self, + nodes: &[(NodeId, Relation, Strength)], + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + nodes + .iter() + .try_for_each(|(node_id, _, _)| self.ensure_node_exists(*node_id))?; + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let aggregator_id = self + .nodes + .push(Node::new(NodeType::AllAggregator, domain_ids)); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + for (node_id, relation, strength) in nodes { + self.make_edge(*node_id, aggregator_id, *strength, *relation)?; + } + + Ok(aggregator_id) + } + + pub fn make_any_aggregator( + &mut self, + nodes: &[(NodeId, Relation)], + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + nodes + .iter() + .try_for_each(|(node_id, _)| self.ensure_node_exists(*node_id))?; + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let aggregator_id = self + .nodes + .push(Node::new(NodeType::AnyAggregator, domain_ids)); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + for (node_id, relation) in nodes { + self.make_edge(*node_id, aggregator_id, Strength::Strong, *relation)?; + } + + Ok(aggregator_id) + } + + pub fn make_in_aggregator( + &mut self, + values: Vec, + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + let key = values + .first() + .ok_or(GraphError::NoInAggregatorValues)? + .get_key(); + + for val in &values { + if val.get_key() != key { + Err(GraphError::MalformedGraph { + reason: "Values for 'In' aggregator not of same key".to_string(), + })?; + } + } + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let node_id = self.nodes.push(Node::new( + NodeType::InAggregator(FxHashSet::from_iter(values)), + domain_ids, + )); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + Ok(node_id) + } + + fn ensure_node_exists(&self, id: NodeId) -> Result<(), GraphError> { + if self.nodes.contains_key(id) { + Ok(()) + } else { + Err(GraphError::NodeNotFound) + } + } +} + +impl<'a> KnowledgeGraph<'a> { + fn check_node( + &self, + ctx: &AnalysisContext, + node_id: NodeId, + relation: Relation, + strength: Strength, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + let node = self.nodes.get(node_id).ok_or(GraphError::NodeNotFound)?; + if let Some(already_memo) = memo.get(&(node_id, relation, strength)) { + already_memo + .clone() + .map_err(|err| GraphError::AnalysisError(Arc::downgrade(&err))) + } else { + match &node.node_type { + NodeType::AllAggregator => { + let mut unsatisfied = Vec::>::new(); + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + + if let Err(e) = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo) + { + unsatisfied.push(e.get_analysis_trace()?); + } + } + + if !unsatisfied.is_empty() { + let err = Arc::new(AnalysisTrace::AllAggregation { + unsatisfied, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } else { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } + } + + NodeType::AnyAggregator => { + let mut unsatisfied = Vec::>::new(); + let mut matched_one = false; + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + + if let Err(e) = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo) + { + unsatisfied.push(e.get_analysis_trace()?); + } else { + matched_one = true; + } + } + + if matched_one || node.preds.is_empty() { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::AnyAggregation { + unsatisfied: unsatisfied.clone(), + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } + } + + NodeType::InAggregator(expected) => { + let the_key = expected + .iter() + .next() + .ok_or_else(|| GraphError::MalformedGraph { + reason: + "An OnlyIn aggregator node must have at least one expected value" + .to_string(), + })? + .get_key(); + + let ctx_vals = if let Some(vals) = ctx.keywise_values.get(&the_key) { + vals + } else { + return if let Strength::Weak = strength { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::InAggregation { + expected: expected.iter().cloned().collect(), + found: None, + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + }; + }; + + let relation_bool: bool = relation.into(); + for ctx_value in ctx_vals { + if expected.contains(ctx_value) != relation_bool { + let err = Arc::new(AnalysisTrace::InAggregation { + expected: expected.iter().cloned().collect(), + found: Some(ctx_value.clone()), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + } + + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } + + NodeType::Value(val) => { + let in_context = ctx.check_presence(val, matches!(strength, Strength::Weak)); + let relation_bool: bool = relation.into(); + + if in_context != relation_bool { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + predecessors: None, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + + if !relation_bool { + memo.insert((node_id, relation, strength), Ok(())); + return Ok(()); + } + + let mut errors = Vec::>::new(); + let mut matched_one = false; + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + let result = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo); + + match (edge.strength, result) { + (Strength::Strong, Err(trace)) => { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + predecessors: Some(ValueTracePredecessor::Mandatory(Box::new( + trace.get_analysis_trace()?, + ))), + }); + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + + (Strength::Strong, Ok(_)) => { + matched_one = true; + } + + (Strength::Normal | Strength::Weak, Err(trace)) => { + errors.push(trace.get_analysis_trace()?); + } + + (Strength::Normal | Strength::Weak, Ok(_)) => { + matched_one = true; + } + } + } + + if matched_one || node.preds.is_empty() { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + predecessors: Some(ValueTracePredecessor::OneOf(errors.clone())), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } + } + } + } + } + + fn key_analysis( + &self, + key: dir::DirKey, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.value_map + .get(&NodeValue::Key(key)) + .map_or(Ok(()), |node_id| { + self.check_node(ctx, *node_id, Relation::Positive, Strength::Strong, memo) + }) + } + + fn value_analysis( + &self, + val: dir::DirValue, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.value_map + .get(&NodeValue::Value(val)) + .map_or(Ok(()), |node_id| { + self.check_node(ctx, *node_id, Relation::Positive, Strength::Strong, memo) + }) + } + + pub fn check_value_validity( + &self, + val: dir::DirValue, + analysis_ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result { + let maybe_node_id = self.value_map.get(&NodeValue::Value(val)); + + let node_id = if let Some(nid) = maybe_node_id { + nid + } else { + return Ok(false); + }; + + let result = self.check_node( + analysis_ctx, + *node_id, + Relation::Positive, + Strength::Weak, + memo, + ); + + match result { + Ok(_) => Ok(true), + Err(e) => { + e.get_analysis_trace()?; + Ok(false) + } + } + } + + pub fn key_value_analysis( + &self, + val: dir::DirValue, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.key_analysis(val.get_key(), ctx, memo) + .and_then(|_| self.value_analysis(val, ctx, memo)) + } + + fn assertion_analysis( + &self, + positive_ctx: &[(&dir::DirValue, &Metadata)], + analysis_ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + positive_ctx.iter().try_for_each(|(value, metadata)| { + self.key_value_analysis((*value).clone(), analysis_ctx, memo) + .map_err(|e| AnalysisError::assertion_from_graph_error(metadata, e)) + }) + } + + fn negation_analysis( + &self, + negative_ctx: &[(&[dir::DirValue], &Metadata)], + analysis_ctx: &mut AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + let mut keywise_metadata: FxHashMap> = FxHashMap::default(); + let mut keywise_negation: FxHashMap> = + FxHashMap::default(); + + for (values, metadata) in negative_ctx { + let mut metadata_added = false; + + for dir_value in *values { + if !metadata_added { + keywise_metadata + .entry(dir_value.get_key()) + .or_default() + .push(metadata); + + metadata_added = true; + } + + keywise_negation + .entry(dir_value.get_key()) + .or_default() + .insert(dir_value); + } + } + + for (key, negation_set) in keywise_negation { + let all_metadata = keywise_metadata.remove(&key).unwrap_or_default(); + let first_metadata = all_metadata.first().cloned().cloned().unwrap_or_default(); + + self.key_analysis(key.clone(), analysis_ctx, memo) + .map_err(|e| AnalysisError::assertion_from_graph_error(&first_metadata, e))?; + + let mut value_set = if let Some(set) = key.kind.get_value_set() { + set + } else { + continue; + }; + + value_set.retain(|v| !negation_set.contains(v)); + + for value in value_set { + analysis_ctx.insert(value.clone()); + self.value_analysis(value.clone(), analysis_ctx, memo) + .map_err(|e| { + AnalysisError::negation_from_graph_error(all_metadata.clone(), e) + })?; + analysis_ctx.remove(value); + } + } + + Ok(()) + } + + pub fn perform_context_analysis( + &self, + ctx: &types::ConjunctiveContext<'_>, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + let mut analysis_ctx = AnalysisContext::from_dir_values( + ctx.iter() + .filter_map(|ctx_val| ctx_val.value.get_assertion().cloned()), + ); + + let positive_ctx = ctx + .iter() + .filter_map(|ctx_val| { + ctx_val + .value + .get_assertion() + .map(|val| (val, ctx_val.metadata)) + }) + .collect::>(); + self.assertion_analysis(&positive_ctx, &analysis_ctx, memo)?; + + let negative_ctx = ctx + .iter() + .filter_map(|ctx_val| { + ctx_val + .value + .get_negation() + .map(|vals| (vals, ctx_val.metadata)) + }) + .collect::>(); + self.negation_analysis(&negative_ctx, &mut analysis_ctx, memo)?; + + Ok(()) + } + + pub fn combine<'b>(g1: &'b Self, g2: &'b Self) -> Result { + let mut node_builder = KnowledgeGraphBuilder::new(); + let mut g1_old2new_id = utils::DenseMap::::new(); + let mut g2_old2new_id = utils::DenseMap::::new(); + let mut g1_old2new_domain_id = utils::DenseMap::::new(); + let mut g2_old2new_domain_id = utils::DenseMap::::new(); + + let add_domain = |node_builder: &mut KnowledgeGraphBuilder<'a>, + domain: DomainInfo<'a>| + -> Result { + node_builder.make_domain(domain.domain_identifier, domain.domain_description) + }; + + let add_node = |node_builder: &mut KnowledgeGraphBuilder<'a>, + node: &Node, + domains: Vec>| + -> Result { + match &node.node_type { + NodeType::Value(node_value) => { + node_builder.make_value_node(node_value.clone(), None, domains, None::<()>) + } + + NodeType::AllAggregator => { + Ok(node_builder.make_all_aggregator(&[], None, None::<()>, domains)?) + } + + NodeType::AnyAggregator => { + Ok(node_builder.make_any_aggregator(&[], None, None::<()>, Vec::new())?) + } + + NodeType::InAggregator(expected) => Ok(node_builder.make_in_aggregator( + expected.iter().cloned().collect(), + None, + None::<()>, + Vec::new(), + )?), + } + }; + + for (_old_domain_id, domain) in g1.domain.iter() { + let new_domain_id = add_domain(&mut node_builder, domain.clone())?; + g1_old2new_domain_id.push(new_domain_id); + } + + for (_old_domain_id, domain) in g2.domain.iter() { + let new_domain_id = add_domain(&mut node_builder, domain.clone())?; + g2_old2new_domain_id.push(new_domain_id); + } + + for (_old_node_id, node) in g1.nodes.iter() { + let mut domain_identifiers: Vec> = Vec::new(); + for domain_id in &node.domain_ids { + match g1.domain.get(*domain_id) { + Some(domain) => domain_identifiers.push(domain.domain_identifier.clone()), + None => return Err(GraphError::DomainNotFound), + } + } + let new_node_id = add_node(&mut node_builder, node, domain_identifiers.clone())?; + g1_old2new_id.push(new_node_id); + } + + for (_old_node_id, node) in g2.nodes.iter() { + let mut domain_identifiers: Vec> = Vec::new(); + for domain_id in &node.domain_ids { + match g2.domain.get(*domain_id) { + Some(domain) => domain_identifiers.push(domain.domain_identifier.clone()), + None => return Err(GraphError::DomainNotFound), + } + } + let new_node_id = add_node(&mut node_builder, node, domain_identifiers.clone())?; + g2_old2new_id.push(new_node_id); + } + + for edge in g1.edges.values() { + let new_pred_id = g1_old2new_id + .get(edge.pred) + .ok_or(GraphError::NodeNotFound)?; + let new_succ_id = g1_old2new_id + .get(edge.succ) + .ok_or(GraphError::NodeNotFound)?; + + node_builder.make_edge(*new_pred_id, *new_succ_id, edge.strength, edge.relation)?; + } + + for edge in g2.edges.values() { + let new_pred_id = g2_old2new_id + .get(edge.pred) + .ok_or(GraphError::NodeNotFound)?; + let new_succ_id = g2_old2new_id + .get(edge.succ) + .ok_or(GraphError::NodeNotFound)?; + + node_builder.make_edge(*new_pred_id, *new_succ_id, edge.strength, edge.relation)?; + } + + Ok(node_builder.build()) + } +} + +#[cfg(test)] +mod test { + #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + + use euclid_macros::knowledge; + + use super::*; + use crate::{dirval, frontend::dir::enums}; + + #[test] + fn test_strong_positive_relation_success() { + let graph = knowledge! {crate + PaymentMethod(Card) ->> CaptureMethod(Automatic); + PaymentMethod(not Wallet) + & PaymentMethod(not PayLater) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_strong_positive_relation_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) ->> CaptureMethod(Automatic); + PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([dirval!(CaptureMethod = Automatic)]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_strong_negative_relation_success() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_strong_negative_relation_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Wallet), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_normal_one_of_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + assert!(matches!( + *Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected Arc"), + AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::OneOf(_)), + .. + } + )); + } + + #[test] + fn test_all_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_all_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_all_aggregator_mandatory_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let mut memo = Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + &mut memo, + ); + + assert!(matches!( + *Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected Arc"), + AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::Mandatory(_)), + .. + } + )); + } + + #[test] + fn test_in_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_in_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_not_in_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(not in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + dirval!(PaymentMethod = BankRedirect), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_not_in_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(not in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_in_aggregator_failure_trace() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + if let AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::Mandatory(agg_error)), + .. + } = Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected arc") + .deref() + { + assert!(matches!( + *Weak::upgrade(agg_error.deref()).expect("Expected Arc"), + AnalysisTrace::InAggregation { + found: Some(dir::DirValue::PaymentMethod(enums::PaymentMethod::PayLater)), + .. + } + )); + } else { + panic!("Failed unwrapping OnlyInAggregation trace from AnalysisTrace"); + } + } + + #[test] + fn _test_memoization_in_kgraph() { + let mut builder = KnowledgeGraphBuilder::new(); + let _node_1 = builder.make_value_node( + NodeValue::Value(dir::DirValue::PaymentMethod(enums::PaymentMethod::Wallet)), + None, + Vec::new(), + None::<()>, + ); + let _node_2 = builder.make_value_node( + NodeValue::Value(dir::DirValue::BillingCountry(enums::BillingCountry::India)), + None, + Vec::new(), + None::<()>, + ); + let _node_3 = builder.make_value_node( + NodeValue::Value(dir::DirValue::BusinessCountry( + enums::BusinessCountry::UnitedStatesOfAmerica, + )), + None, + Vec::new(), + None::<()>, + ); + let mut memo = Memoization::new(); + let _edge_1 = builder + .make_edge( + _node_1.expect("node1 constructtion failed"), + _node_2.clone().expect("node2 construction failed"), + Strength::Strong, + Relation::Positive, + ) + .expect("Failed to make an edge"); + let _edge_2 = builder + .make_edge( + _node_2.expect("node2 construction failed"), + _node_3.clone().expect("node3 construction failed"), + Strength::Strong, + Relation::Positive, + ) + .expect("Failed to an edge"); + let graph = builder.build(); + let _result = graph.key_value_analysis( + dirval!(BusinessCountry = UnitedStatesOfAmerica), + &AnalysisContext::from_dir_values([ + dirval!(PaymentMethod = Wallet), + dirval!(BillingCountry = India), + dirval!(BusinessCountry = UnitedStatesOfAmerica), + ]), + &mut memo, + ); + let _ans = memo + .0 + .get(&( + _node_3.expect("node3 construction failed"), + Relation::Positive, + Strength::Strong, + )) + .expect("Memoization not workng"); + matches!(_ans, Ok(())); + } +} diff --git a/crates/euclid/src/dssa/state_machine.rs b/crates/euclid/src/dssa/state_machine.rs new file mode 100644 index 000000000000..4cd53911dfe4 --- /dev/null +++ b/crates/euclid/src/dssa/state_machine.rs @@ -0,0 +1,714 @@ +use super::types::EuclidAnalysable; +use crate::{dssa::types, frontend::dir, types::Metadata}; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum StateMachineError { + #[error("Index out of bounds: {0}")] + IndexOutOfBounds(&'static str), +} + +#[derive(Debug)] +struct ComparisonStateMachine<'a> { + values: &'a [dir::DirValue], + logic: &'a dir::DirComparisonLogic, + metadata: &'a Metadata, + count: usize, + ctx_idx: usize, +} + +impl<'a> ComparisonStateMachine<'a> { + #[inline] + fn is_finished(&self) -> bool { + self.count + 1 >= self.values.len() + || matches!(self.logic, dir::DirComparisonLogic::NegativeConjunction) + } + + #[inline] + fn advance(&mut self) { + if let dir::DirComparisonLogic::PositiveDisjunction = self.logic { + self.count = (self.count + 1) % self.values.len(); + } + } + + #[inline] + fn reset(&mut self) { + self.count = 0; + } + + #[inline] + fn put(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + if let dir::DirComparisonLogic::PositiveDisjunction = self.logic { + *context + .get_mut(self.ctx_idx) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while indexing into context", + ))? = types::ContextValue::assertion( + self.values + .get(self.count) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while indexing into values", + ))?, + self.metadata, + ); + } + Ok(()) + } + + #[inline] + fn push(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + match self.logic { + dir::DirComparisonLogic::PositiveDisjunction => { + context.push(types::ContextValue::assertion( + self.values + .get(self.count) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while pushing", + ))?, + self.metadata, + )); + } + + dir::DirComparisonLogic::NegativeConjunction => { + context.push(types::ContextValue::negation(self.values, self.metadata)); + } + } + Ok(()) + } +} + +#[derive(Debug)] +struct ConditionStateMachine<'a> { + state_machines: Vec>, + start_ctx_idx: usize, +} + +impl<'a> ConditionStateMachine<'a> { + fn new(condition: &'a [dir::DirComparison], start_idx: usize) -> Self { + let mut machines = Vec::>::with_capacity(condition.len()); + + let mut machine_idx = start_idx; + for cond in condition { + let machine = ComparisonStateMachine { + values: &cond.values, + logic: &cond.logic, + metadata: &cond.metadata, + count: 0, + ctx_idx: machine_idx, + }; + machines.push(machine); + machine_idx += 1; + } + + Self { + state_machines: machines, + start_ctx_idx: start_idx, + } + } + + fn init(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + for machine in &self.state_machines { + machine.push(context)?; + } + Ok(()) + } + + #[inline] + fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) { + context.truncate(self.start_ctx_idx); + } + + #[inline] + fn is_finished(&self) -> bool { + !self + .state_machines + .iter() + .any(|machine| !machine.is_finished()) + } + + #[inline] + fn get_next_ctx_idx(&self) -> usize { + self.start_ctx_idx + self.state_machines.len() + } + + fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + for machine in self.state_machines.iter_mut().rev() { + if machine.is_finished() { + machine.reset(); + machine.put(context)?; + } else { + machine.advance(); + machine.put(context)?; + break; + } + } + Ok(()) + } +} + +#[derive(Debug)] +struct IfStmtStateMachine<'a> { + condition_machine: ConditionStateMachine<'a>, + nested: Vec<&'a dir::DirIfStatement>, + nested_idx: usize, +} + +impl<'a> IfStmtStateMachine<'a> { + fn new(stmt: &'a dir::DirIfStatement, ctx_start_idx: usize) -> Self { + let condition_machine = ConditionStateMachine::new(&stmt.condition, ctx_start_idx); + let nested: Vec<&'a dir::DirIfStatement> = match &stmt.nested { + None => Vec::new(), + Some(nested_stmts) => nested_stmts.iter().collect(), + }; + + Self { + condition_machine, + nested, + nested_idx: 0, + } + } + + fn init( + &self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result, StateMachineError> { + self.condition_machine.init(context)?; + Ok(self + .nested + .first() + .map(|nested| Self::new(nested, self.condition_machine.get_next_ctx_idx()))) + } + + #[inline] + fn is_finished(&self) -> bool { + self.nested_idx + 1 >= self.nested.len() + } + + #[inline] + fn is_condition_machine_finished(&self) -> bool { + self.condition_machine.is_finished() + } + + #[inline] + fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) { + self.condition_machine.destroy(context); + } + + #[inline] + fn advance_condition_machine( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + self.condition_machine.advance(context)?; + Ok(()) + } + + fn advance(&mut self) -> Result, StateMachineError> { + if self.nested.is_empty() { + Ok(None) + } else { + self.nested_idx = (self.nested_idx + 1) % self.nested.len(); + Ok(Some(Self::new( + self.nested + .get(self.nested_idx) + .ok_or(StateMachineError::IndexOutOfBounds( + "in IfStmtStateMachine while advancing", + ))?, + self.condition_machine.get_next_ctx_idx(), + ))) + } + } +} + +#[derive(Debug)] +struct RuleStateMachine<'a> { + connector_selection_data: &'a [(dir::DirValue, Metadata)], + connectors_added: bool, + if_stmt_machines: Vec>, + running_stack: Vec>, +} + +impl<'a> RuleStateMachine<'a> { + fn new( + rule: &'a dir::DirRule, + connector_selection_data: &'a [(dir::DirValue, Metadata)], + ) -> Self { + let mut if_stmt_machines: Vec> = + Vec::with_capacity(rule.statements.len()); + + for stmt in rule.statements.iter().rev() { + if_stmt_machines.push(IfStmtStateMachine::new( + stmt, + connector_selection_data.len(), + )); + } + + Self { + connector_selection_data, + connectors_added: false, + if_stmt_machines, + running_stack: Vec::new(), + } + } + + fn is_finished(&self) -> bool { + self.if_stmt_machines.is_empty() && self.running_stack.is_empty() + } + + fn init_next( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if self.if_stmt_machines.is_empty() || !self.running_stack.is_empty() { + return Ok(()); + } + + if !self.connectors_added { + for (dir_val, metadata) in self.connector_selection_data { + context.push(types::ContextValue::assertion(dir_val, metadata)); + } + self.connectors_added = true; + } + + context.truncate(self.connector_selection_data.len()); + + if let Some(mut next_running) = self.if_stmt_machines.pop() { + while let Some(nested_running) = next_running.init(context)? { + self.running_stack.push(next_running); + next_running = nested_running; + } + + self.running_stack.push(next_running); + } + + Ok(()) + } + + fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + let mut condition_machines_finished = true; + + for stmt_machine in self.running_stack.iter_mut().rev() { + if !stmt_machine.is_condition_machine_finished() { + condition_machines_finished = false; + stmt_machine.advance_condition_machine(context)?; + break; + } else { + stmt_machine.advance_condition_machine(context)?; + } + } + + if !condition_machines_finished { + return Ok(()); + } + + let mut maybe_next_running: Option> = None; + + while let Some(last) = self.running_stack.last_mut() { + if !last.is_finished() { + maybe_next_running = last.advance()?; + break; + } else { + last.destroy(context); + self.running_stack.pop(); + } + } + + if let Some(mut next_running) = maybe_next_running { + while let Some(nested_running) = next_running.init(context)? { + self.running_stack.push(next_running); + next_running = nested_running; + } + + self.running_stack.push(next_running); + } else { + self.init_next(context)?; + } + + Ok(()) + } +} + +#[derive(Debug)] +pub struct RuleContextManager<'a> { + context: types::ConjunctiveContext<'a>, + machine: RuleStateMachine<'a>, + init: bool, +} + +impl<'a> RuleContextManager<'a> { + pub fn new( + rule: &'a dir::DirRule, + connector_selection_data: &'a [(dir::DirValue, Metadata)], + ) -> Self { + Self { + context: Vec::new(), + machine: RuleStateMachine::new(rule, connector_selection_data), + init: false, + } + } + + pub fn advance(&mut self) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init_next(&mut self.context)?; + Ok(Some(&self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&self.context)) + } + } + } + + pub fn advance_mut( + &mut self, + ) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init_next(&mut self.context)?; + Ok(Some(&mut self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&mut self.context)) + } + } + } +} + +#[derive(Debug)] +pub struct ProgramStateMachine<'a> { + rule_machines: Vec>, + current_rule_machine: Option>, + is_init: bool, +} + +impl<'a> ProgramStateMachine<'a> { + pub fn new( + program: &'a dir::DirProgram, + connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>], + ) -> Self { + let mut rule_machines: Vec> = program + .rules + .iter() + .zip(connector_selection_data.iter()) + .rev() + .map(|(rule, connector_selection_data)| { + RuleStateMachine::new(rule, connector_selection_data) + }) + .collect(); + + Self { + current_rule_machine: rule_machines.pop(), + rule_machines, + is_init: false, + } + } + + pub fn is_finished(&self) -> bool { + self.current_rule_machine + .as_ref() + .map_or(true, |rsm| rsm.is_finished()) + && self.rule_machines.is_empty() + } + + pub fn init( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if !self.is_init { + if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.init_next(context)?; + } + self.is_init = true; + } + + Ok(()) + } + + pub fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if self + .current_rule_machine + .as_ref() + .map_or(true, |rsm| rsm.is_finished()) + { + self.current_rule_machine = self.rule_machines.pop(); + context.clear(); + if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.init_next(context)?; + } + } else if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.advance(context)?; + } + + Ok(()) + } +} + +pub struct AnalysisContextManager<'a> { + context: types::ConjunctiveContext<'a>, + machine: ProgramStateMachine<'a>, + init: bool, +} + +impl<'a> AnalysisContextManager<'a> { + pub fn new( + program: &'a dir::DirProgram, + connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>], + ) -> Self { + let machine = ProgramStateMachine::new(program, connector_selection_data); + let context: types::ConjunctiveContext<'a> = Vec::new(); + + Self { + context, + machine, + init: false, + } + } + + pub fn advance(&mut self) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init(&mut self.context)?; + Ok(Some(&self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&self.context)) + } + } + } +} + +pub fn make_connector_selection_data( + program: &dir::DirProgram, +) -> Vec> { + program + .rules + .iter() + .map(|rule| { + rule.connector_selection + .get_dir_value_for_analysis(rule.name.clone()) + }) + .collect() +} + +#[cfg(all(test, feature = "ast_parser"))] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::{dirval, frontend::ast, types::DummyOutput}; + + #[test] + fn test_correct_contexts() { + let program_str = r#" + default: ["stripe", "adyen"] + + stripe_first: ["stripe", "adyen"] + { + payment_method = wallet { + payment_method = (card, bank_redirect) { + currency = USD + currency = GBP + } + + payment_method = pay_later { + capture_method = automatic + capture_method = manual + } + } + + payment_method = card { + payment_method = (card, bank_redirect) & capture_method = (automatic, manual) { + currency = (USD, GBP) + } + } + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let lowered = ast::lowering::lower_program(program).expect("Lowering"); + + let selection_data = make_connector_selection_data(&lowered); + let mut state_machine = ProgramStateMachine::new(&lowered, &selection_data); + let mut ctx: types::ConjunctiveContext<'_> = Vec::new(); + state_machine.init(&mut ctx).expect("State machine init"); + + let expected_contexts: Vec> = vec![ + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = Card), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = Card), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + dirval!(CaptureMethod = Automatic), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + dirval!(CaptureMethod = Manual), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = GBP), + ], + ]; + + let mut expected_idx = 0usize; + while !state_machine.is_finished() { + let values = ctx + .iter() + .flat_map(|c| match c.value { + types::CtxValueKind::Assertion(val) => vec![val], + types::CtxValueKind::Negation(vals) => vals.iter().collect(), + }) + .collect::>(); + assert_eq!( + values, + expected_contexts[expected_idx] + .iter() + .collect::>() + ); + expected_idx += 1; + state_machine + .advance(&mut ctx) + .expect("State Machine advance"); + } + + assert_eq!(expected_idx, 14); + + let mut ctx_manager = AnalysisContextManager::new(&lowered, &selection_data); + expected_idx = 0; + while let Some(ctx) = ctx_manager.advance().expect("Context Manager Context") { + let values = ctx + .iter() + .flat_map(|c| match c.value { + types::CtxValueKind::Assertion(val) => vec![val], + types::CtxValueKind::Negation(vals) => vals.iter().collect(), + }) + .collect::>(); + assert_eq!( + values, + expected_contexts[expected_idx] + .iter() + .collect::>() + ); + expected_idx += 1; + } + + assert_eq!(expected_idx, 14); + } +} diff --git a/crates/euclid/src/dssa/truth.rs b/crates/euclid/src/dssa/truth.rs new file mode 100644 index 000000000000..17e6e728e68f --- /dev/null +++ b/crates/euclid/src/dssa/truth.rs @@ -0,0 +1,29 @@ +use euclid_macros::knowledge; +use once_cell::sync::Lazy; + +use crate::dssa::graph; + +pub static ANALYSIS_GRAPH: Lazy> = Lazy::new(|| { + knowledge! {crate + // Payment Method should be `Card` for a CardType to be present + PaymentMethod(Card) ->> CardType(any); + + // Payment Method should be `PayLater` for a PayLaterType to be present + PaymentMethod(PayLater) ->> PayLaterType(any); + + // Payment Method should be `Wallet` for a WalletType to be present + PaymentMethod(Wallet) ->> WalletType(any); + + // Payment Method should be `BankRedirect` for a BankRedirectType to + // be present + PaymentMethod(BankRedirect) ->> BankRedirectType(any); + + // Payment Method should be `BankTransfer` for a BankTransferType to + // be present + PaymentMethod(BankTransfer) ->> BankTransferType(any); + + // Payment Method should be `GiftCard` for a GiftCardType to + // be present + PaymentMethod(GiftCard) ->> GiftCardType(any); + } +}); diff --git a/crates/euclid/src/dssa/types.rs b/crates/euclid/src/dssa/types.rs new file mode 100644 index 000000000000..4070e0825ef7 --- /dev/null +++ b/crates/euclid/src/dssa/types.rs @@ -0,0 +1,158 @@ +use std::fmt; + +use serde::Serialize; + +use crate::{ + dssa::{self, graph}, + frontend::{ast, dir}, + types::{DataType, EuclidValue, Metadata}, +}; + +pub trait EuclidAnalysable: Sized { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(dir::DirValue, Metadata)>; +} + +#[derive(Debug, Clone)] +pub enum CtxValueKind<'a> { + Assertion(&'a dir::DirValue), + Negation(&'a [dir::DirValue]), +} + +impl<'a> CtxValueKind<'a> { + pub fn get_assertion(&self) -> Option<&dir::DirValue> { + if let Self::Assertion(val) = self { + Some(val) + } else { + None + } + } + + pub fn get_negation(&self) -> Option<&[dir::DirValue]> { + if let Self::Negation(vals) = self { + Some(vals) + } else { + None + } + } + + pub fn get_key(&self) -> Option { + match self { + Self::Assertion(val) => Some(val.get_key()), + Self::Negation(vals) => vals.first().map(|v| (*v).get_key()), + } + } +} + +#[derive(Debug, Clone)] +pub struct ContextValue<'a> { + pub value: CtxValueKind<'a>, + pub metadata: &'a Metadata, +} + +impl<'a> ContextValue<'a> { + #[inline] + pub fn assertion(value: &'a dir::DirValue, metadata: &'a Metadata) -> Self { + Self { + value: CtxValueKind::Assertion(value), + metadata, + } + } + + #[inline] + pub fn negation(values: &'a [dir::DirValue], metadata: &'a Metadata) -> Self { + Self { + value: CtxValueKind::Negation(values), + metadata, + } + } +} + +pub type ConjunctiveContext<'a> = Vec>; + +#[derive(Clone, Serialize)] +pub enum AnalyzeResult { + AllOk, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +pub struct AnalysisError { + #[serde(flatten)] + pub error_type: AnalysisErrorType, + pub metadata: Metadata, +} +impl fmt::Display for AnalysisError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.error_type.fmt(f) + } +} +#[derive(Debug, Clone, Serialize)] +pub struct ValueData { + pub value: dir::DirValue, + pub metadata: Metadata, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum AnalysisErrorType { + #[error("Invalid program key given: '{0}'")] + InvalidKey(String), + #[error("Invalid variant '{got}' received for key '{key}'")] + InvalidVariant { + key: String, + expected: Vec, + got: String, + }, + #[error( + "Invalid data type for value '{}' (expected {expected}, got {got})", + key + )] + InvalidType { + key: String, + expected: DataType, + got: DataType, + }, + #[error("Invalid comparison '{operator:?}' for value type {value_type}")] + InvalidComparison { + operator: ast::ComparisonType, + value_type: DataType, + }, + #[error("Invalid value received for length as '{value}: {:?}'", message)] + InvalidValue { + key: dir::DirKeyKind, + value: String, + message: Option, + }, + #[error("Conflicting assertions received for key '{}'", .key.kind)] + ConflictingAssertions { + key: dir::DirKey, + values: Vec, + }, + + #[error("Key '{}' exhaustively negated", .key.kind)] + ExhaustiveNegation { + key: dir::DirKey, + metadata: Vec, + }, + #[error("The condition '{value}' was asserted and negated in the same condition")] + NegatedAssertion { + value: dir::DirValue, + assertion_metadata: Metadata, + negation_metadata: Metadata, + }, + #[error("Graph analysis error: {0:#?}")] + GraphAnalysis(graph::AnalysisError, graph::Memoization), + #[error("State machine error")] + StateMachine(dssa::state_machine::StateMachineError), + #[error("Unsupported program key '{0}'")] + UnsupportedProgramKey(dir::DirKeyKind), + #[error("Ran into an unimplemented feature")] + NotImplemented, + #[error("The payment method type is not supported under the payment method")] + NotSupported, +} + +#[derive(Debug, Clone)] +pub enum ValueType { + EnumVariants(Vec), + Number, +} diff --git a/crates/euclid/src/dssa/utils.rs b/crates/euclid/src/dssa/utils.rs new file mode 100644 index 000000000000..df4ff82cbdb7 --- /dev/null +++ b/crates/euclid/src/dssa/utils.rs @@ -0,0 +1 @@ +pub struct Unpacker; diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs new file mode 100644 index 000000000000..4188860ab90f --- /dev/null +++ b/crates/euclid/src/enums.rs @@ -0,0 +1,191 @@ +pub use common_enums::{ + AuthenticationType, CaptureMethod, CardNetwork, Country, Currency, + FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, +}; +use serde::{Deserialize, Serialize}; +use strum::VariantNames; + +pub trait CollectVariants { + fn variants>() -> T; +} +macro_rules! collect_variants { + ($the_enum:ident) => { + impl $crate::enums::CollectVariants for $the_enum { + fn variants() -> T + where + T: FromIterator, + { + Self::VARIANTS.iter().map(|s| String::from(*s)).collect() + } + } + }; +} + +pub(crate) use collect_variants; + +collect_variants!(PaymentMethod); +collect_variants!(PaymentType); +collect_variants!(MandateType); +collect_variants!(MandateAcceptanceType); +collect_variants!(PaymentMethodType); +collect_variants!(CardNetwork); +collect_variants!(AuthenticationType); +collect_variants!(CaptureMethod); +collect_variants!(Currency); +collect_variants!(Country); +collect_variants!(Connector); +collect_variants!(SetupFutureUsage); + +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum Connector { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + DummyConnector1, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + DummyConnector2, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + DummyConnector3, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "stripe_test")] + #[strum(serialize = "stripe_test")] + DummyConnector4, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "adyen_test")] + #[strum(serialize = "adyen_test")] + DummyConnector5, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "checkout_test")] + #[strum(serialize = "checkout_test")] + DummyConnector6, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "paypal_test")] + #[strum(serialize = "paypal_test")] + DummyConnector7, + Aci, + Adyen, + Airwallex, + Authorizedotnet, + Bitpay, + Bambora, + Bluesnap, + Boku, + Braintree, + Cashtocode, + Checkout, + Coinbase, + Cryptopay, + Cybersource, + Dlocal, + Fiserv, + Forte, + Globalpay, + Globepay, + Gocardless, + Helcim, + Iatapay, + Klarna, + Mollie, + Multisafepay, + Nexinets, + Nmi, + Noon, + Nuvei, + Opennode, + Payme, + Paypal, + Payu, + Powertranz, + Rapyd, + Shift4, + Square, + Stax, + Stripe, + Trustpay, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum MandateAcceptanceType { + Online, + Offline, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PaymentType { + SetupMandate, + NonMandate, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum MandateType { + SingleUse, + MultiUse, +} diff --git a/crates/euclid/src/frontend.rs b/crates/euclid/src/frontend.rs new file mode 100644 index 000000000000..17fc8f3502e2 --- /dev/null +++ b/crates/euclid/src/frontend.rs @@ -0,0 +1,3 @@ +pub mod ast; +pub mod dir; +pub mod vir; diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs new file mode 100644 index 000000000000..3adb06ab1873 --- /dev/null +++ b/crates/euclid/src/frontend/ast.rs @@ -0,0 +1,156 @@ +pub mod lowering; +#[cfg(feature = "ast_parser")] +pub mod parser; + +use serde::{Deserialize, Serialize}; + +use crate::{ + enums::Connector, + types::{DataType, Metadata}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct ConnectorChoice { + pub connector: Connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MetadataValue { + pub key: String, + pub value: String, +} + +/// Represents a value in the DSL +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum ValueType { + /// Represents a number literal + Number(i64), + /// Represents an enum variant + EnumVariant(String), + /// Represents a Metadata variant + MetadataVariant(MetadataValue), + /// Represents a arbitrary String value + StrValue(String), + /// Represents an array of numbers. This is basically used for + /// "one of the given numbers" operations + /// eg: payment.method.amount = (1, 2, 3) + NumberArray(Vec), + /// Similar to NumberArray but for enum variants + /// eg: payment.method.cardtype = (debit, credit) + EnumVariantArray(Vec), + /// Like a number array but can include comparisons. Useful for + /// conditions like "500 < amount < 1000" + /// eg: payment.amount = (> 500, < 1000) + NumberComparisonArray(Vec), +} + +impl ValueType { + pub fn get_type(&self) -> DataType { + match self { + Self::Number(_) => DataType::Number, + Self::StrValue(_) => DataType::StrValue, + Self::MetadataVariant(_) => DataType::MetadataValue, + Self::EnumVariant(_) => DataType::EnumVariant, + Self::NumberComparisonArray(_) => DataType::Number, + Self::NumberArray(_) => DataType::Number, + Self::EnumVariantArray(_) => DataType::EnumVariant, + } + } +} + +/// Represents a number comparison for "NumberComparisonArrayValue" +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberComparison { + pub comparison_type: ComparisonType, + pub number: i64, +} + +/// Conditional comparison type +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ComparisonType { + Equal, + NotEqual, + LessThan, + LessThanEqual, + GreaterThan, + GreaterThanEqual, +} + +/// Represents a single comparison condition. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Comparison { + /// The left hand side which will always be a domain input identifier like "payment.method.cardtype" + pub lhs: String, + /// The comparison operator + pub comparison: ComparisonType, + /// The value to compare against + pub value: ValueType, + /// Additional metadata that the Static Analyzer and Backend does not touch. + /// This can be used to store useful information for the frontend and is required for communication + /// between the static analyzer and the frontend. + pub metadata: Metadata, +} + +/// Represents all the conditions of an IF statement +/// eg: +/// +/// ```text +/// payment.method = card & payment.method.cardtype = debit & payment.method.network = diners +/// ``` +pub type IfCondition = Vec; + +/// Represents an IF statement with conditions and optional nested IF statements +/// +/// ```text +/// payment.method = card { +/// payment.method.cardtype = (credit, debit) { +/// payment.method.network = (amex, rupay, diners) +/// } +/// } +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IfStatement { + pub condition: IfCondition, + pub nested: Option>, +} + +/// Represents a rule +/// +/// ```text +/// rule_name: [stripe, adyen, checkout] +/// { +/// payment.method = card { +/// payment.method.cardtype = (credit, debit) { +/// payment.method.network = (amex, rupay, diners) +/// } +/// +/// payment.method.cardtype = credit +/// } +/// } +/// ``` + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rule { + pub name: String, + #[serde(alias = "routingOutput")] + pub connector_selection: O, + pub statements: Vec, +} + +/// The program, having a default connector selection and +/// a bunch of rules. Also can hold arbitrary metadata. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Program { + pub default_selection: O, + pub rules: Vec>, + pub metadata: Metadata, +} diff --git a/crates/euclid/src/frontend/ast/lowering.rs b/crates/euclid/src/frontend/ast/lowering.rs new file mode 100644 index 000000000000..ffce88a35db6 --- /dev/null +++ b/crates/euclid/src/frontend/ast/lowering.rs @@ -0,0 +1,377 @@ +//! Analysis for the Lowering logic in ast +//! +//!Certain functions that can be used to perform the complete lowering of ast to dir. +//!This includes lowering of enums, numbers, strings as well as Comparison logics. + +use std::str::FromStr; + +use crate::{ + dssa::types::{AnalysisError, AnalysisErrorType}, + enums::CollectVariants, + frontend::{ + ast, + dir::{self, enums as dir_enums, EuclidDirFilter}, + }, + types::{self, DataType}, +}; + +/// lowers the provided key (enum variant) & value to the respective DirValue +/// +/// For example +/// ```notrust +/// CardType = Visa +/// ```notrust +/// +/// This serves for the purpose were we have the DirKey as an explicit Enum type and value as one +/// of the member of the same Enum. +/// So particularly it lowers a predefined Enum from DirKey to an Enum of DirValue. + +macro_rules! lower_enum { + ($key:ident, $value:ident) => { + match $value { + ast::ValueType::EnumVariant(ev) => Ok(vec![dir::DirValue::$key( + dir_enums::$key::from_str(&ev).map_err(|_| AnalysisErrorType::InvalidVariant { + key: dir::DirKeyKind::$key.to_string(), + got: ev, + expected: dir_enums::$key::variants(), + })?, + )]), + + ast::ValueType::EnumVariantArray(eva) => eva + .into_iter() + .map(|ev| { + Ok(dir::DirValue::$key( + dir_enums::$key::from_str(&ev).map_err(|_| { + AnalysisErrorType::InvalidVariant { + key: dir::DirKeyKind::$key.to_string(), + got: ev, + expected: dir_enums::$key::variants(), + } + })?, + )) + }) + .collect(), + + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::EnumVariant, + got: $value.get_type(), + }), + } + }; +} + +/// lowers the provided key for a numerical value +/// +/// For example +/// ```notrust +/// payment_amount = 17052001 +/// ```notrust +/// This is for the cases in which there are numerical values involved and they are lowered +/// accordingly on basis of the supplied key, currently payment_amount is the only key having this +/// use case + +macro_rules! lower_number { + ($key:ident, $value:ident, $comp:ident) => { + match $value { + ast::ValueType::Number(num) => Ok(vec![dir::DirValue::$key(types::NumValue { + number: num, + refinement: $comp.into(), + })]), + + ast::ValueType::NumberArray(na) => na + .into_iter() + .map(|num| { + Ok(dir::DirValue::$key(types::NumValue { + number: num, + refinement: $comp.clone().into(), + })) + }) + .collect(), + + ast::ValueType::NumberComparisonArray(nca) => nca + .into_iter() + .map(|nc| { + Ok(dir::DirValue::$key(types::NumValue { + number: nc.number, + refinement: nc.comparison_type.into(), + })) + }) + .collect(), + + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::Number, + got: $value.get_type(), + }), + } + }; +} + +/// lowers the provided key & value to the respective DirValue +/// +/// For example +/// ```notrust +/// card_bin = "123456" +/// ```notrust +/// +/// This serves for the purpose were we have the DirKey as Card_bin and value as an arbitrary string +/// So particularly it lowers an arbitrary value to a predefined key. + +macro_rules! lower_str { + ($key:ident, $value:ident $(, $validation_closure:expr)?) => { + match $value { + ast::ValueType::StrValue(st) => { + $($validation_closure(&st)?;)? + Ok(vec![dir::DirValue::$key(types::StrValue { value: st })]) + } + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::StrValue, + got: $value.get_type(), + }), + } + }; +} + +macro_rules! lower_metadata { + ($key:ident, $value:ident) => { + match $value { + ast::ValueType::MetadataVariant(md) => { + Ok(vec![dir::DirValue::$key(types::MetadataValue { + key: md.key, + value: md.value, + })]) + } + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::MetadataValue, + got: $value.get_type(), + }), + } + }; +} +/// lowers the comparison operators for different subtle value types present +/// by throwing required errors for comparisons that can't be performed for a certain value type +/// for example +/// can't have greater/less than operations on enum types + +fn lower_comparison_inner( + comp: ast::Comparison, +) -> Result, AnalysisErrorType> { + let key_enum = dir::DirKeyKind::from_str(comp.lhs.as_str()) + .map_err(|_| AnalysisErrorType::InvalidKey(comp.lhs.clone()))?; + + if !O::is_key_allowed(&key_enum) { + return Err(AnalysisErrorType::InvalidKey(key_enum.to_string())); + } + + match (&comp.comparison, &comp.value) { + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::EnumVariant(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::EnumVariant, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::NumberArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::Number, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::EnumVariantArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::EnumVariant, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::NumberComparisonArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::Number, + })?; + } + + _ => {} + } + + let value = comp.value; + let comparison = comp.comparison; + + match key_enum { + dir::DirKeyKind::PaymentMethod => lower_enum!(PaymentMethod, value), + + dir::DirKeyKind::CardType => lower_enum!(CardType, value), + + dir::DirKeyKind::CardNetwork => lower_enum!(CardNetwork, value), + + dir::DirKeyKind::PayLaterType => lower_enum!(PayLaterType, value), + + dir::DirKeyKind::WalletType => lower_enum!(WalletType, value), + + dir::DirKeyKind::BankDebitType => lower_enum!(BankDebitType, value), + + dir::DirKeyKind::BankRedirectType => lower_enum!(BankRedirectType, value), + + dir::DirKeyKind::CryptoType => lower_enum!(CryptoType, value), + + dir::DirKeyKind::PaymentType => lower_enum!(PaymentType, value), + + dir::DirKeyKind::MandateType => lower_enum!(MandateType, value), + + dir::DirKeyKind::MandateAcceptanceType => lower_enum!(MandateAcceptanceType, value), + + dir::DirKeyKind::RewardType => lower_enum!(RewardType, value), + + dir::DirKeyKind::PaymentCurrency => lower_enum!(PaymentCurrency, value), + + dir::DirKeyKind::AuthenticationType => lower_enum!(AuthenticationType, value), + + dir::DirKeyKind::CaptureMethod => lower_enum!(CaptureMethod, value), + + dir::DirKeyKind::BusinessCountry => lower_enum!(BusinessCountry, value), + + dir::DirKeyKind::BillingCountry => lower_enum!(BillingCountry, value), + + dir::DirKeyKind::SetupFutureUsage => lower_enum!(SetupFutureUsage, value), + + dir::DirKeyKind::UpiType => lower_enum!(UpiType, value), + + dir::DirKeyKind::VoucherType => lower_enum!(VoucherType, value), + + dir::DirKeyKind::GiftCardType => lower_enum!(GiftCardType, value), + + dir::DirKeyKind::BankTransferType => lower_enum!(BankTransferType, value), + + dir::DirKeyKind::CardRedirectType => lower_enum!(CardRedirectType, value), + + dir::DirKeyKind::CardBin => { + let validation_closure = |st: &String| -> Result<(), AnalysisErrorType> { + if st.len() == 6 && st.chars().all(|x| x.is_ascii_digit()) { + Ok(()) + } else { + Err(AnalysisErrorType::InvalidValue { + key: dir::DirKeyKind::CardBin, + value: st.clone(), + message: Some("Expected 6 digits".to_string()), + }) + } + }; + lower_str!(CardBin, value, validation_closure) + } + + dir::DirKeyKind::BusinessLabel => lower_str!(BusinessLabel, value), + + dir::DirKeyKind::MetaData => lower_metadata!(MetaData, value), + + dir::DirKeyKind::PaymentAmount => lower_number!(PaymentAmount, value, comparison), + + dir::DirKeyKind::Connector => Err(AnalysisErrorType::InvalidKey( + dir::DirKeyKind::Connector.to_string(), + )), + } +} + +/// returns all the comparison values by matching them appropriately to ComparisonTypes and in turn +/// calls the lower_comparison_inner function +fn lower_comparison( + comp: ast::Comparison, +) -> Result { + let metadata = comp.metadata.clone(); + let logic = match &comp.comparison { + ast::ComparisonType::Equal => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::NotEqual => dir::DirComparisonLogic::NegativeConjunction, + ast::ComparisonType::LessThan => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::LessThanEqual => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::GreaterThanEqual => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::GreaterThan => dir::DirComparisonLogic::PositiveDisjunction, + }; + let values = lower_comparison_inner::(comp).map_err(|etype| AnalysisError { + error_type: etype, + metadata: metadata.clone(), + })?; + + Ok(dir::DirComparison { + values, + logic, + metadata, + }) +} + +/// lowers the if statement accordingly with a condition and following nested if statements (if +/// present) +fn lower_if_statement( + stmt: ast::IfStatement, +) -> Result { + Ok(dir::DirIfStatement { + condition: stmt + .condition + .into_iter() + .map(lower_comparison::) + .collect::>()?, + nested: stmt + .nested + .map(|n| n.into_iter().map(lower_if_statement::).collect()) + .transpose()?, + }) +} + +/// lowers the rules supplied accordingly to DirRule struct by specifying the rule_name, +/// connector_selection and statements that are a bunch of if statements +pub fn lower_rule( + rule: ast::Rule, +) -> Result, AnalysisError> { + Ok(dir::DirRule { + name: rule.name, + connector_selection: rule.connector_selection, + statements: rule + .statements + .into_iter() + .map(lower_if_statement::) + .collect::>()?, + }) +} + +/// uses the above rules and lowers the whole ast Program into DirProgram by specifying +/// default_selection that is ast ConnectorSelection, a vector of DirRules and clones the metadata +/// whatever comes in the ast_program +pub fn lower_program( + program: ast::Program, +) -> Result, AnalysisError> { + Ok(dir::DirProgram { + default_selection: program.default_selection, + rules: program + .rules + .into_iter() + .map(lower_rule) + .collect::>()?, + metadata: program.metadata, + }) +} diff --git a/crates/euclid/src/frontend/ast/parser.rs b/crates/euclid/src/frontend/ast/parser.rs new file mode 100644 index 000000000000..8b2f717a8688 --- /dev/null +++ b/crates/euclid/src/frontend/ast/parser.rs @@ -0,0 +1,441 @@ +use nom::{ + branch, bytes::complete, character::complete as pchar, combinator, error, multi, sequence, +}; + +use crate::{frontend::ast, types::DummyOutput}; +pub type ParseResult = nom::IResult>; + +pub enum EuclidError { + InvalidPercentage(String), + InvalidConnector(String), + InvalidOperator(String), + InvalidNumber(String), +} + +pub trait EuclidParsable: Sized { + fn parse_output(input: &str) -> ParseResult<&str, Self>; +} + +impl EuclidParsable for DummyOutput { + fn parse_output(input: &str) -> ParseResult<&str, Self> { + let string_w = sequence::delimited( + skip_ws(complete::tag("\"")), + complete::take_while(|c| c != '"'), + skip_ws(complete::tag("\"")), + ); + let full_sequence = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + sequence::delimited( + skip_ws(complete::tag("\"")), + complete::take_while(|c| c != '"'), + skip_ws(complete::tag("\"")), + ), + )); + let sequence = sequence::pair(string_w, full_sequence); + error::context( + "dummy_strings", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("[")), + sequence, + skip_ws(complete::tag("]")), + ), + |out: (&str, Vec<&str>)| { + let mut first = out.1; + first.insert(0, out.0); + let v = first.iter().map(|s| s.to_string()).collect(); + Self { outputs: v } + }, + ), + )(input) + } +} +pub fn skip_ws<'a, F: 'a, O>(inner: F) -> impl FnMut(&'a str) -> ParseResult<&str, O> +where + F: FnMut(&'a str) -> ParseResult<&str, O>, +{ + sequence::preceded(pchar::multispace0, inner) +} + +pub fn num_i64(input: &str) -> ParseResult<&str, i64> { + error::context( + "num_i32", + combinator::map_res( + complete::take_while1(|c: char| c.is_ascii_digit()), + |o: &str| { + o.parse::() + .map_err(|_| EuclidError::InvalidNumber(o.to_string())) + }, + ), + )(input) +} + +pub fn string_str(input: &str) -> ParseResult<&str, String> { + error::context( + "String", + combinator::map( + sequence::delimited( + complete::tag("\""), + complete::take_while1(|c: char| c != '"'), + complete::tag("\""), + ), + |val: &str| val.to_string(), + ), + )(input) +} + +pub fn identifier(input: &str) -> ParseResult<&str, String> { + error::context( + "identifier", + combinator::map( + sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + ), + |out: (&str, &str)| out.0.to_string() + out.1, + ), + )(input) +} +pub fn percentage(input: &str) -> ParseResult<&str, u8> { + error::context( + "volume_split_percentage", + combinator::map_res( + sequence::terminated( + complete::take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), + complete::tag("%"), + ), + |o: &str| { + o.parse::() + .map_err(|_| EuclidError::InvalidPercentage(o.to_string())) + }, + ), + )(input) +} + +pub fn number_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "number_value", + combinator::map(num_i64, ast::ValueType::Number), + )(input) +} + +pub fn str_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "str_value", + combinator::map(string_str, ast::ValueType::StrValue), + )(input) +} +pub fn enum_value_string(input: &str) -> ParseResult<&str, String> { + combinator::map( + sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + ), + |out: (&str, &str)| out.0.to_string() + out.1, + )(input) +} + +pub fn enum_variant_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "enum_variant_value", + combinator::map(enum_value_string, ast::ValueType::EnumVariant), + )(input) +} + +pub fn number_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(num_i64), + )); + + let full_sequence = sequence::pair(skip_ws(num_i64), many_with_comma); + + error::context( + "number_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (i64, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::NumberArray(rest) + }, + ), + )(input) +} + +pub fn enum_variant_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(enum_value_string), + )); + + let full_sequence = sequence::pair(skip_ws(enum_value_string), many_with_comma); + + error::context( + "enum_variant_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (String, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::EnumVariantArray(rest) + }, + ), + )(input) +} + +pub fn number_comparison(input: &str) -> ParseResult<&str, ast::NumberComparison> { + let operator = combinator::map_res( + branch::alt(( + complete::tag(">="), + complete::tag("<="), + complete::tag(">"), + complete::tag("<"), + )), + |s: &str| match s { + ">=" => Ok(ast::ComparisonType::GreaterThanEqual), + "<=" => Ok(ast::ComparisonType::LessThanEqual), + ">" => Ok(ast::ComparisonType::GreaterThan), + "<" => Ok(ast::ComparisonType::LessThan), + _ => Err(EuclidError::InvalidOperator(s.to_string())), + }, + ); + + error::context( + "number_comparison", + combinator::map( + sequence::pair(operator, num_i64), + |tup: (ast::ComparisonType, i64)| ast::NumberComparison { + comparison_type: tup.0, + number: tup.1, + }, + ), + )(input) +} + +pub fn number_comparison_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(number_comparison), + )); + + let full_sequence = sequence::pair(skip_ws(number_comparison), many_with_comma); + + error::context( + "number_comparison_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (ast::NumberComparison, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::NumberComparisonArray(rest) + }, + ), + )(input) +} + +pub fn value_type(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "value_type", + branch::alt(( + number_value, + enum_variant_value, + enum_variant_array_value, + number_array_value, + number_comparison_array_value, + str_value, + )), + )(input) +} + +pub fn comparison_type(input: &str) -> ParseResult<&str, ast::ComparisonType> { + error::context( + "comparison_operator", + combinator::map_res( + branch::alt(( + complete::tag("/="), + complete::tag(">="), + complete::tag("<="), + complete::tag("="), + complete::tag(">"), + complete::tag("<"), + )), + |s: &str| match s { + "/=" => Ok(ast::ComparisonType::NotEqual), + ">=" => Ok(ast::ComparisonType::GreaterThanEqual), + "<=" => Ok(ast::ComparisonType::LessThanEqual), + "=" => Ok(ast::ComparisonType::Equal), + ">" => Ok(ast::ComparisonType::GreaterThan), + "<" => Ok(ast::ComparisonType::LessThan), + _ => Err(EuclidError::InvalidOperator(s.to_string())), + }, + ), + )(input) +} + +pub fn comparison(input: &str) -> ParseResult<&str, ast::Comparison> { + error::context( + "condition", + combinator::map( + sequence::tuple(( + skip_ws(complete::take_while1(|c: char| { + c.is_ascii_alphabetic() || c == '.' || c == '_' + })), + skip_ws(comparison_type), + skip_ws(value_type), + )), + |tup: (&str, ast::ComparisonType, ast::ValueType)| ast::Comparison { + lhs: tup.0.to_string(), + comparison: tup.1, + value: tup.2, + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} + +pub fn arbitrary_comparison(input: &str) -> ParseResult<&str, ast::Comparison> { + error::context( + "condition", + combinator::map( + sequence::tuple(( + skip_ws(string_str), + skip_ws(comparison_type), + skip_ws(string_str), + )), + |tup: (String, ast::ComparisonType, String)| ast::Comparison { + lhs: "metadata".to_string(), + comparison: tup.1, + value: ast::ValueType::MetadataVariant(ast::MetadataValue { + key: tup.0, + value: tup.2, + }), + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} + +pub fn comparison_array(input: &str) -> ParseResult<&str, Vec> { + let many_with_ampersand = error::context( + "many_with_amp", + multi::many0(sequence::preceded(skip_ws(complete::tag("&")), comparison)), + ); + + let full_sequence = sequence::pair( + skip_ws(branch::alt((comparison, arbitrary_comparison))), + many_with_ampersand, + ); + + error::context( + "comparison_array", + combinator::map( + full_sequence, + |tup: (ast::Comparison, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + rest + }, + ), + )(input) +} + +pub fn if_statement(input: &str) -> ParseResult<&str, ast::IfStatement> { + let nested_block = sequence::delimited( + skip_ws(complete::tag("{")), + multi::many0(if_statement), + skip_ws(complete::tag("}")), + ); + + error::context( + "if_statement", + combinator::map( + sequence::pair(comparison_array, combinator::opt(nested_block)), + |tup: (ast::IfCondition, Option>)| ast::IfStatement { + condition: tup.0, + nested: tup.1, + }, + ), + )(input) +} + +pub fn rule_conditions_array(input: &str) -> ParseResult<&str, Vec> { + error::context( + "rules_array", + sequence::delimited( + skip_ws(complete::tag("{")), + multi::many1(if_statement), + skip_ws(complete::tag("}")), + ), + )(input) +} + +pub fn rule(input: &str) -> ParseResult<&str, ast::Rule> { + let rule_name = error::context( + "rule_name", + combinator::map( + skip_ws(sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + )), + |out: (&str, &str)| out.0.to_string() + out.1, + ), + ); + + let connector_selection = error::context( + "parse_output", + sequence::preceded(skip_ws(complete::tag(":")), output), + ); + + error::context( + "rule", + combinator::map( + sequence::tuple((rule_name, connector_selection, rule_conditions_array)), + |tup: (String, O, Vec)| ast::Rule { + name: tup.0, + connector_selection: tup.1, + statements: tup.2, + }, + ), + )(input) +} + +pub fn output(input: &str) -> ParseResult<&str, O> { + O::parse_output(input) +} + +pub fn default_output(input: &str) -> ParseResult<&str, O> { + error::context( + "default_output", + sequence::preceded( + sequence::pair(skip_ws(complete::tag("default")), skip_ws(pchar::char(':'))), + skip_ws(output), + ), + )(input) +} + +pub fn program(input: &str) -> ParseResult<&str, ast::Program> { + error::context( + "program", + combinator::map( + sequence::pair(default_output, multi::many1(skip_ws(rule::))), + |tup: (O, Vec>)| ast::Program { + default_selection: tup.0, + rules: tup.1, + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs new file mode 100644 index 000000000000..7f2fc252d232 --- /dev/null +++ b/crates/euclid/src/frontend/dir.rs @@ -0,0 +1,803 @@ +//! Domain Intermediate Representation +pub mod enums; +pub mod lowering; +pub mod transformers; + +use strum::IntoEnumIterator; + +use crate::{enums as euclid_enums, frontend::ast, types}; + +#[macro_export] +#[cfg(feature = "connector_choice_mca_id")] +macro_rules! dirval { + (Connector = $name:ident) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + }, + )) + }; + + ($key:ident = $val:ident) => {{ + pub use $crate::frontend::dir::enums::*; + + $crate::frontend::dir::DirValue::$key($key::$val) + }}; + + ($key:ident = $num:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::NumValue { + number: $num, + refinement: None, + }) + }}; + + ($key:ident s= $str:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::StrValue { + value: $str.to_string(), + }) + }}; + + ($key:literal = $str:literal) => {{ + $crate::frontend::dir::DirValue::MetaData($crate::types::MetadataValue { + key: $key.to_string(), + value: $str.to_string(), + }) + }}; +} + +#[macro_export] +#[cfg(not(feature = "connector_choice_mca_id"))] +macro_rules! dirval { + (Connector = $name:ident) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + sub_label: None, + }, + )) + }; + + (Connector = ($name:ident, $sub_label:literal)) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + sub_label: Some($sub_label.to_string()), + }, + )) + }; + + ($key:ident = $val:ident) => {{ + pub use $crate::frontend::dir::enums::*; + + $crate::frontend::dir::DirValue::$key($key::$val) + }}; + + ($key:ident = $num:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::NumValue { + number: $num, + refinement: None, + }) + }}; + + ($key:ident s= $str:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::StrValue { + value: $str.to_string(), + }) + }}; + ($key:literal = $str:literal) => {{ + $crate::frontend::dir::DirValue::MetaData($crate::types::MetadataValue { + key: $key.to_string(), + value: $str.to_string(), + }) + }}; +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] +pub struct DirKey { + pub kind: DirKeyKind, + pub value: Option, +} + +impl DirKey { + pub fn new(kind: DirKeyKind, value: Option) -> Self { + Self { kind, value } + } +} + +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + serde::Serialize, + strum::Display, + strum::EnumIter, + strum::EnumVariantNames, + strum::EnumString, + strum::EnumMessage, + strum::EnumProperty, +)] +pub enum DirKeyKind { + #[strum( + serialize = "payment_method", + detailed_message = "Different modes of payment - eg. cards, wallets, banks", + props(Category = "Payment Methods") + )] + #[serde(rename = "payment_method")] + PaymentMethod, + #[strum( + serialize = "card_bin", + detailed_message = "First 4 to 6 digits of a payment card number", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_bin")] + CardBin, + #[strum( + serialize = "card_type", + detailed_message = "Type of the payment card - eg. credit, debit", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_type")] + CardType, + #[strum( + serialize = "card_network", + detailed_message = "Network that facilitates payment card transactions", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_network")] + CardNetwork, + #[strum( + serialize = "pay_later", + detailed_message = "Supported types of Pay Later payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "pay_later")] + PayLaterType, + #[strum( + serialize = "gift_card", + detailed_message = "Supported types of Gift Card payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "gift_card")] + GiftCardType, + #[strum( + serialize = "mandate_acceptance_type", + detailed_message = "Mode of customer acceptance for mandates - online and offline", + props(Category = "Payments") + )] + #[serde(rename = "mandate_acceptance_type")] + MandateAcceptanceType, + #[strum( + serialize = "mandate_type", + detailed_message = "Type of mandate acceptance - single use and multi use", + props(Category = "Payments") + )] + #[serde(rename = "mandate_type")] + MandateType, + #[strum( + serialize = "payment_type", + detailed_message = "Indicates if a payment is mandate or non-mandate", + props(Category = "Payments") + )] + #[serde(rename = "payment_type")] + PaymentType, + #[strum( + serialize = "wallet", + detailed_message = "Supported types of Wallet payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "wallet")] + WalletType, + #[strum( + serialize = "upi", + detailed_message = "Supported types of UPI payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "upi")] + UpiType, + #[strum( + serialize = "voucher", + detailed_message = "Supported types of Voucher payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "voucher")] + VoucherType, + #[strum( + serialize = "bank_transfer", + detailed_message = "Supported types of Bank Transfer payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_transfer")] + BankTransferType, + #[strum( + serialize = "bank_redirect", + detailed_message = "Supported types of Bank Redirect payment methods", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_redirect")] + BankRedirectType, + #[strum( + serialize = "bank_debit", + detailed_message = "Supported types of Bank Debit payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_debit")] + BankDebitType, + #[strum( + serialize = "crypto", + detailed_message = "Supported types of Crypto payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "crypto")] + CryptoType, + #[strum( + serialize = "metadata", + detailed_message = "Aribitrary Key and value pair", + props(Category = "Metadata") + )] + #[serde(rename = "metadata")] + MetaData, + #[strum( + serialize = "reward", + detailed_message = "Supported types of Reward payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "reward")] + RewardType, + #[strum( + serialize = "amount", + detailed_message = "Value of the transaction", + props(Category = "Payments") + )] + #[serde(rename = "amount")] + PaymentAmount, + #[strum( + serialize = "currency", + detailed_message = "Currency used for the payment", + props(Category = "Payments") + )] + #[serde(rename = "currency")] + PaymentCurrency, + #[strum( + serialize = "authentication_type", + detailed_message = "Type of authentication for the payment", + props(Category = "Payments") + )] + #[serde(rename = "authentication_type")] + AuthenticationType, + #[strum( + serialize = "capture_method", + detailed_message = "Modes of capturing a payment", + props(Category = "Payments") + )] + #[serde(rename = "capture_method")] + CaptureMethod, + #[strum( + serialize = "country", + serialize = "business_country", + detailed_message = "Country of the business unit", + props(Category = "Merchant") + )] + #[serde(rename = "business_country", alias = "country")] + BusinessCountry, + #[strum( + serialize = "billing_country", + detailed_message = "Country of the billing address of the customer", + props(Category = "Customer") + )] + #[serde(rename = "billing_country")] + BillingCountry, + #[serde(skip_deserializing, rename = "connector")] + #[strum(disabled)] + Connector, + #[strum( + serialize = "business_label", + detailed_message = "Identifier for business unit", + props(Category = "Merchant") + )] + #[serde(rename = "business_label")] + BusinessLabel, + #[strum( + serialize = "setup_future_usage", + detailed_message = "Identifier for recurring payments", + props(Category = "Payments") + )] + #[serde(rename = "setup_future_usage")] + SetupFutureUsage, + #[strum( + serialize = "card_redirect_type", + detailed_message = "Supported types of Card Redirect payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "card_redirect")] + CardRedirectType, +} + +pub trait EuclidDirFilter: Sized +where + Self: 'static, +{ + const ALLOWED: &'static [DirKeyKind]; + fn get_allowed_keys() -> &'static [DirKeyKind] { + Self::ALLOWED + } + + fn is_key_allowed(key: &DirKeyKind) -> bool { + Self::ALLOWED.contains(key) + } +} + +impl DirKeyKind { + pub fn get_type(&self) -> types::DataType { + match self { + Self::PaymentMethod => types::DataType::EnumVariant, + Self::CardBin => types::DataType::StrValue, + Self::CardType => types::DataType::EnumVariant, + Self::CardNetwork => types::DataType::EnumVariant, + Self::MetaData => types::DataType::MetadataValue, + Self::MandateType => types::DataType::EnumVariant, + Self::PaymentType => types::DataType::EnumVariant, + Self::MandateAcceptanceType => types::DataType::EnumVariant, + Self::PayLaterType => types::DataType::EnumVariant, + Self::WalletType => types::DataType::EnumVariant, + Self::UpiType => types::DataType::EnumVariant, + Self::VoucherType => types::DataType::EnumVariant, + Self::BankTransferType => types::DataType::EnumVariant, + Self::GiftCardType => types::DataType::EnumVariant, + Self::BankRedirectType => types::DataType::EnumVariant, + Self::CryptoType => types::DataType::EnumVariant, + Self::RewardType => types::DataType::EnumVariant, + Self::PaymentAmount => types::DataType::Number, + Self::PaymentCurrency => types::DataType::EnumVariant, + Self::AuthenticationType => types::DataType::EnumVariant, + Self::CaptureMethod => types::DataType::EnumVariant, + Self::BusinessCountry => types::DataType::EnumVariant, + Self::BillingCountry => types::DataType::EnumVariant, + Self::Connector => types::DataType::EnumVariant, + Self::BankDebitType => types::DataType::EnumVariant, + Self::BusinessLabel => types::DataType::StrValue, + Self::SetupFutureUsage => types::DataType::EnumVariant, + Self::CardRedirectType => types::DataType::EnumVariant, + } + } + pub fn get_value_set(&self) -> Option> { + match self { + Self::PaymentMethod => Some( + enums::PaymentMethod::iter() + .map(DirValue::PaymentMethod) + .collect(), + ), + Self::CardBin => None, + Self::CardType => Some(enums::CardType::iter().map(DirValue::CardType).collect()), + Self::MandateAcceptanceType => Some( + euclid_enums::MandateAcceptanceType::iter() + .map(DirValue::MandateAcceptanceType) + .collect(), + ), + Self::PaymentType => Some( + euclid_enums::PaymentType::iter() + .map(DirValue::PaymentType) + .collect(), + ), + Self::MandateType => Some( + euclid_enums::MandateType::iter() + .map(DirValue::MandateType) + .collect(), + ), + Self::CardNetwork => Some( + enums::CardNetwork::iter() + .map(DirValue::CardNetwork) + .collect(), + ), + Self::PayLaterType => Some( + enums::PayLaterType::iter() + .map(DirValue::PayLaterType) + .collect(), + ), + Self::MetaData => None, + Self::WalletType => Some( + enums::WalletType::iter() + .map(DirValue::WalletType) + .collect(), + ), + Self::UpiType => Some(enums::UpiType::iter().map(DirValue::UpiType).collect()), + Self::VoucherType => Some( + enums::VoucherType::iter() + .map(DirValue::VoucherType) + .collect(), + ), + Self::BankTransferType => Some( + enums::BankTransferType::iter() + .map(DirValue::BankTransferType) + .collect(), + ), + Self::GiftCardType => Some( + enums::GiftCardType::iter() + .map(DirValue::GiftCardType) + .collect(), + ), + Self::BankRedirectType => Some( + enums::BankRedirectType::iter() + .map(DirValue::BankRedirectType) + .collect(), + ), + Self::CryptoType => Some( + enums::CryptoType::iter() + .map(DirValue::CryptoType) + .collect(), + ), + Self::RewardType => Some( + enums::RewardType::iter() + .map(DirValue::RewardType) + .collect(), + ), + Self::PaymentAmount => None, + Self::PaymentCurrency => Some( + enums::PaymentCurrency::iter() + .map(DirValue::PaymentCurrency) + .collect(), + ), + Self::AuthenticationType => Some( + enums::AuthenticationType::iter() + .map(DirValue::AuthenticationType) + .collect(), + ), + Self::CaptureMethod => Some( + enums::CaptureMethod::iter() + .map(DirValue::CaptureMethod) + .collect(), + ), + Self::BankDebitType => Some( + enums::BankDebitType::iter() + .map(DirValue::BankDebitType) + .collect(), + ), + Self::BusinessCountry => Some( + enums::Country::iter() + .map(DirValue::BusinessCountry) + .collect(), + ), + Self::BillingCountry => Some( + enums::Country::iter() + .map(DirValue::BillingCountry) + .collect(), + ), + Self::Connector => Some( + enums::Connector::iter() + .map(|connector| { + DirValue::Connector(Box::new(ast::ConnectorChoice { + connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + })) + }) + .collect(), + ), + Self::BusinessLabel => None, + Self::SetupFutureUsage => Some( + enums::SetupFutureUsage::iter() + .map(DirValue::SetupFutureUsage) + .collect(), + ), + Self::CardRedirectType => Some( + enums::CardRedirectType::iter() + .map(DirValue::CardRedirectType) + .collect(), + ), + } + } +} + +#[derive( + Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, strum::Display, strum::EnumVariantNames, +)] +#[serde(tag = "key", content = "value")] +pub enum DirValue { + #[serde(rename = "payment_method")] + PaymentMethod(enums::PaymentMethod), + #[serde(rename = "card_bin")] + CardBin(types::StrValue), + #[serde(rename = "card_type")] + CardType(enums::CardType), + #[serde(rename = "card_network")] + CardNetwork(enums::CardNetwork), + #[serde(rename = "metadata")] + MetaData(types::MetadataValue), + #[serde(rename = "pay_later")] + PayLaterType(enums::PayLaterType), + #[serde(rename = "wallet")] + WalletType(enums::WalletType), + #[serde(rename = "acceptance_type")] + MandateAcceptanceType(euclid_enums::MandateAcceptanceType), + #[serde(rename = "mandate_type")] + MandateType(euclid_enums::MandateType), + #[serde(rename = "payment_type")] + PaymentType(euclid_enums::PaymentType), + #[serde(rename = "upi")] + UpiType(enums::UpiType), + #[serde(rename = "voucher")] + VoucherType(enums::VoucherType), + #[serde(rename = "bank_transfer")] + BankTransferType(enums::BankTransferType), + #[serde(rename = "bank_redirect")] + BankRedirectType(enums::BankRedirectType), + #[serde(rename = "bank_debit")] + BankDebitType(enums::BankDebitType), + #[serde(rename = "crypto")] + CryptoType(enums::CryptoType), + #[serde(rename = "reward")] + RewardType(enums::RewardType), + #[serde(rename = "gift_card")] + GiftCardType(enums::GiftCardType), + #[serde(rename = "amount")] + PaymentAmount(types::NumValue), + #[serde(rename = "currency")] + PaymentCurrency(enums::PaymentCurrency), + #[serde(rename = "authentication_type")] + AuthenticationType(enums::AuthenticationType), + #[serde(rename = "capture_method")] + CaptureMethod(enums::CaptureMethod), + #[serde(rename = "business_country", alias = "country")] + BusinessCountry(enums::Country), + #[serde(rename = "billing_country")] + BillingCountry(enums::Country), + #[serde(skip_deserializing, rename = "connector")] + Connector(Box), + #[serde(rename = "business_label")] + BusinessLabel(types::StrValue), + #[serde(rename = "setup_future_usage")] + SetupFutureUsage(enums::SetupFutureUsage), + #[serde(rename = "card_redirect")] + CardRedirectType(enums::CardRedirectType), +} + +impl DirValue { + pub fn get_key(&self) -> DirKey { + let (kind, data) = match self { + Self::PaymentMethod(_) => (DirKeyKind::PaymentMethod, None), + Self::CardBin(_) => (DirKeyKind::CardBin, None), + Self::RewardType(_) => (DirKeyKind::RewardType, None), + Self::BusinessCountry(_) => (DirKeyKind::BusinessCountry, None), + Self::BillingCountry(_) => (DirKeyKind::CardBin, None), + Self::BankTransferType(_) => (DirKeyKind::BankTransferType, None), + Self::UpiType(_) => (DirKeyKind::UpiType, None), + Self::CardType(_) => (DirKeyKind::CardType, None), + Self::CardNetwork(_) => (DirKeyKind::CardNetwork, None), + Self::MetaData(met) => (DirKeyKind::MetaData, Some(met.key.clone())), + Self::PayLaterType(_) => (DirKeyKind::PayLaterType, None), + Self::WalletType(_) => (DirKeyKind::WalletType, None), + Self::BankRedirectType(_) => (DirKeyKind::BankRedirectType, None), + Self::CryptoType(_) => (DirKeyKind::CryptoType, None), + Self::AuthenticationType(_) => (DirKeyKind::AuthenticationType, None), + Self::CaptureMethod(_) => (DirKeyKind::CaptureMethod, None), + Self::PaymentAmount(_) => (DirKeyKind::PaymentAmount, None), + Self::PaymentCurrency(_) => (DirKeyKind::PaymentCurrency, None), + Self::Connector(_) => (DirKeyKind::Connector, None), + Self::BankDebitType(_) => (DirKeyKind::BankDebitType, None), + Self::MandateAcceptanceType(_) => (DirKeyKind::MandateAcceptanceType, None), + Self::MandateType(_) => (DirKeyKind::MandateType, None), + Self::PaymentType(_) => (DirKeyKind::PaymentType, None), + Self::BusinessLabel(_) => (DirKeyKind::BusinessLabel, None), + Self::SetupFutureUsage(_) => (DirKeyKind::SetupFutureUsage, None), + Self::CardRedirectType(_) => (DirKeyKind::CardRedirectType, None), + Self::VoucherType(_) => (DirKeyKind::VoucherType, None), + Self::GiftCardType(_) => (DirKeyKind::GiftCardType, None), + }; + + DirKey::new(kind, data) + } + pub fn get_metadata_val(&self) -> Option { + match self { + Self::MetaData(val) => Some(val.clone()), + Self::PaymentMethod(_) => None, + Self::CardBin(_) => None, + Self::CardType(_) => None, + Self::CardNetwork(_) => None, + Self::PayLaterType(_) => None, + Self::WalletType(_) => None, + Self::BankRedirectType(_) => None, + Self::CryptoType(_) => None, + Self::AuthenticationType(_) => None, + Self::CaptureMethod(_) => None, + Self::GiftCardType(_) => None, + Self::PaymentAmount(_) => None, + Self::PaymentCurrency(_) => None, + Self::BusinessCountry(_) => None, + Self::BillingCountry(_) => None, + Self::Connector(_) => None, + Self::BankTransferType(_) => None, + Self::UpiType(_) => None, + Self::BankDebitType(_) => None, + Self::RewardType(_) => None, + Self::VoucherType(_) => None, + Self::MandateAcceptanceType(_) => None, + Self::MandateType(_) => None, + Self::PaymentType(_) => None, + Self::BusinessLabel(_) => None, + Self::SetupFutureUsage(_) => None, + Self::CardRedirectType(_) => None, + } + } + + pub fn get_str_val(&self) -> Option { + match self { + Self::CardBin(val) => Some(val.clone()), + _ => None, + } + } + + pub fn get_num_value(&self) -> Option { + match self { + Self::PaymentAmount(val) => Some(val.clone()), + _ => None, + } + } + + pub fn check_equality(v1: &Self, v2: &Self) -> bool { + match (v1, v2) { + (Self::PaymentMethod(pm1), Self::PaymentMethod(pm2)) => pm1 == pm2, + (Self::CardType(ct1), Self::CardType(ct2)) => ct1 == ct2, + (Self::CardNetwork(cn1), Self::CardNetwork(cn2)) => cn1 == cn2, + (Self::MetaData(md1), Self::MetaData(md2)) => md1 == md2, + (Self::PayLaterType(plt1), Self::PayLaterType(plt2)) => plt1 == plt2, + (Self::WalletType(wt1), Self::WalletType(wt2)) => wt1 == wt2, + (Self::BankDebitType(bdt1), Self::BankDebitType(bdt2)) => bdt1 == bdt2, + (Self::BankRedirectType(brt1), Self::BankRedirectType(brt2)) => brt1 == brt2, + (Self::BankTransferType(btt1), Self::BankTransferType(btt2)) => btt1 == btt2, + (Self::GiftCardType(gct1), Self::GiftCardType(gct2)) => gct1 == gct2, + (Self::CryptoType(ct1), Self::CryptoType(ct2)) => ct1 == ct2, + (Self::AuthenticationType(at1), Self::AuthenticationType(at2)) => at1 == at2, + (Self::CaptureMethod(cm1), Self::CaptureMethod(cm2)) => cm1 == cm2, + (Self::PaymentCurrency(pc1), Self::PaymentCurrency(pc2)) => pc1 == pc2, + (Self::BusinessCountry(c1), Self::BusinessCountry(c2)) => c1 == c2, + (Self::BillingCountry(c1), Self::BillingCountry(c2)) => c1 == c2, + (Self::PaymentType(pt1), Self::PaymentType(pt2)) => pt1 == pt2, + (Self::MandateType(mt1), Self::MandateType(mt2)) => mt1 == mt2, + (Self::MandateAcceptanceType(mat1), Self::MandateAcceptanceType(mat2)) => mat1 == mat2, + (Self::RewardType(rt1), Self::RewardType(rt2)) => rt1 == rt2, + (Self::Connector(c1), Self::Connector(c2)) => c1 == c2, + (Self::BusinessLabel(bl1), Self::BusinessLabel(bl2)) => bl1 == bl2, + (Self::SetupFutureUsage(sfu1), Self::SetupFutureUsage(sfu2)) => sfu1 == sfu2, + (Self::UpiType(ut1), Self::UpiType(ut2)) => ut1 == ut2, + (Self::VoucherType(vt1), Self::VoucherType(vt2)) => vt1 == vt2, + (Self::CardRedirectType(crt1), Self::CardRedirectType(crt2)) => crt1 == crt2, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum DirComparisonLogic { + NegativeConjunction, + PositiveDisjunction, +} + +#[derive(Debug, Clone)] +pub struct DirComparison { + pub values: Vec, + pub logic: DirComparisonLogic, + pub metadata: types::Metadata, +} + +pub type DirIfCondition = Vec; + +#[derive(Debug, Clone)] +pub struct DirIfStatement { + pub condition: DirIfCondition, + pub nested: Option>, +} + +#[derive(Debug, Clone)] +pub struct DirRule { + pub name: String, + pub connector_selection: O, + pub statements: Vec, +} + +#[derive(Debug, Clone)] +pub struct DirProgram { + pub default_selection: O, + pub rules: Vec>, + pub metadata: types::Metadata, +} + +#[cfg(test)] +mod test { + #![allow(clippy::expect_used)] + use rustc_hash::FxHashMap; + use strum::IntoEnumIterator; + + use super::*; + + #[test] + fn test_consistent_dir_key_naming() { + let mut key_names: FxHashMap = FxHashMap::default(); + + for key in DirKeyKind::iter() { + let json_str = if let DirKeyKind::MetaData = key { + r#""metadata""#.to_string() + } else { + serde_json::to_string(&key).expect("JSON Serialization") + }; + let display_str = key.to_string(); + + assert_eq!(&json_str[1..json_str.len() - 1], display_str); + key_names.insert(key, display_str); + } + + let values = vec![ + dirval!(PaymentMethod = Card), + dirval!(CardBin s= "123456"), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PayLaterType = Klarna), + dirval!(WalletType = Paypal), + dirval!(BankRedirectType = Sofort), + dirval!(BankDebitType = Bacs), + dirval!(CryptoType = CryptoCurrency), + dirval!("" = "metadata"), + dirval!(PaymentAmount = 100), + dirval!(PaymentCurrency = USD), + dirval!(CardRedirectType = Benefit), + dirval!(AuthenticationType = ThreeDs), + dirval!(CaptureMethod = Manual), + dirval!(BillingCountry = UnitedStatesOfAmerica), + dirval!(BusinessCountry = France), + ]; + + for val in values { + let json_val = serde_json::to_value(&val).expect("JSON Value Serialization"); + + let json_key = json_val + .as_object() + .expect("Serialized Object") + .get("key") + .expect("Object Key"); + + let value_str = json_key.as_str().expect("Value string"); + let dir_key = val.get_key(); + + let key_name = key_names.get(&dir_key.kind).expect("Key name"); + + assert_eq!(key_name, value_str); + } + } + + #[cfg(feature = "ast_parser")] + #[test] + fn test_allowed_dir_keys() { + use crate::types::DummyOutput; + + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + payment_method = card + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + + let out = ast::lowering::lower_program::(program); + assert!(out.is_ok()) + } + #[cfg(feature = "ast_parser")] + #[test] + fn test_not_allowed_dir_keys() { + use crate::types::DummyOutput; + + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + bank_debit = ach + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + + let out = ast::lowering::lower_program::(program); + assert!(out.is_err()) + } +} diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs new file mode 100644 index 000000000000..17699940363f --- /dev/null +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -0,0 +1,321 @@ +use strum::VariantNames; + +use crate::enums::collect_variants; +pub use crate::enums::{ + AuthenticationType, CaptureMethod, CardNetwork, Connector, Country, Country as BusinessCountry, + Country as BillingCountry, Currency as PaymentCurrency, MandateAcceptanceType, MandateType, + PaymentMethod, PaymentType, SetupFutureUsage, +}; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CardType { + Credit, + Debit, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PayLaterType { + Affirm, + AfterpayClearpay, + Alma, + Klarna, + PayBright, + Walley, + Atome, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum WalletType { + GooglePay, + ApplePay, + Paypal, + AliPay, + AliPayHk, + MbWay, + MobilePay, + WeChatPay, + SamsungPay, + GoPay, + KakaoPay, + Twint, + Gcash, + Vipps, + Momo, + Dana, + TouchNGo, + Swish, + Cashapp, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum VoucherType { + Boleto, + Efecty, + PagoEfectivo, + RedCompra, + RedPagos, + Alfamart, + Indomaret, + SevenEleven, + Lawson, + MiniStop, + FamilyMart, + Seicomart, + PayEasy, + Oxxo, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankRedirectType { + Bizum, + Giropay, + Ideal, + Sofort, + Eps, + BancontactCard, + Blik, + Interac, + OnlineBankingCzechRepublic, + OnlineBankingFinland, + OnlineBankingPoland, + OnlineBankingSlovakia, + OnlineBankingFpx, + OnlineBankingThailand, + OpenBankingUk, + Przelewy24, + Trustly, +} +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankTransferType { + Multibanco, + Ach, + Sepa, + Bacs, + BcaBankTransfer, + BniVa, + BriVa, + CimbVa, + DanamonVa, + MandiriVa, + PermataBankTransfer, + Pix, + Pse, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GiftCardType { + PaySafeCard, + Givex, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CardRedirectType { + Benefit, + Knet, + MomoAtm, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CryptoType { + CryptoCurrency, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum UpiType { + UpiCollect, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankDebitType { + Ach, + Sepa, + Bacs, + Becs, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RewardType { + ClassicReward, + Evoucher, +} + +collect_variants!(CardType); +collect_variants!(PayLaterType); +collect_variants!(WalletType); +collect_variants!(BankRedirectType); +collect_variants!(BankDebitType); +collect_variants!(CryptoType); +collect_variants!(RewardType); +collect_variants!(UpiType); +collect_variants!(VoucherType); +collect_variants!(GiftCardType); +collect_variants!(BankTransferType); +collect_variants!(CardRedirectType); diff --git a/crates/euclid/src/frontend/dir/lowering.rs b/crates/euclid/src/frontend/dir/lowering.rs new file mode 100644 index 000000000000..516e10e0389e --- /dev/null +++ b/crates/euclid/src/frontend/dir/lowering.rs @@ -0,0 +1,295 @@ +//! Analysis of the lowering logic for the DIR +//! +//! Consists of certain functions that supports the lowering logic from DIR to VIR. +//! These includes the lowering of the DIR program and vector of rules , and the lowering of ifstatements +//! ,and comparisonsLogic and also the lowering of the enums of value variants from DIR to VIR. +use super::enums; +use crate::{ + dssa::types::{AnalysisError, AnalysisErrorType}, + enums as global_enums, + frontend::{dir, vir}, + types::EuclidValue, +}; + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CardType) -> Self { + match value { + enums::CardType::Credit => Self::Credit, + enums::CardType::Debit => Self::Debit, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::PayLaterType) -> Self { + match value { + enums::PayLaterType::Affirm => Self::Affirm, + enums::PayLaterType::AfterpayClearpay => Self::AfterpayClearpay, + enums::PayLaterType::Alma => Self::Alma, + enums::PayLaterType::Klarna => Self::Klarna, + enums::PayLaterType::PayBright => Self::PayBright, + enums::PayLaterType::Walley => Self::Walley, + enums::PayLaterType::Atome => Self::Atome, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::WalletType) -> Self { + match value { + enums::WalletType::GooglePay => Self::GooglePay, + enums::WalletType::ApplePay => Self::ApplePay, + enums::WalletType::Paypal => Self::Paypal, + enums::WalletType::AliPay => Self::AliPay, + enums::WalletType::AliPayHk => Self::AliPayHk, + enums::WalletType::MbWay => Self::MbWay, + enums::WalletType::MobilePay => Self::MobilePay, + enums::WalletType::WeChatPay => Self::WeChatPay, + enums::WalletType::SamsungPay => Self::SamsungPay, + enums::WalletType::GoPay => Self::GoPay, + enums::WalletType::KakaoPay => Self::KakaoPay, + enums::WalletType::Twint => Self::Twint, + enums::WalletType::Gcash => Self::Gcash, + enums::WalletType::Vipps => Self::Vipps, + enums::WalletType::Momo => Self::Momo, + enums::WalletType::Dana => Self::Dana, + enums::WalletType::TouchNGo => Self::TouchNGo, + enums::WalletType::Swish => Self::Swish, + enums::WalletType::Cashapp => Self::Cashapp, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankDebitType) -> Self { + match value { + enums::BankDebitType::Ach => Self::Ach, + enums::BankDebitType::Sepa => Self::Sepa, + enums::BankDebitType::Bacs => Self::Bacs, + enums::BankDebitType::Becs => Self::Becs, + } + } +} +impl From for global_enums::PaymentMethodType { + fn from(value: enums::UpiType) -> Self { + match value { + enums::UpiType::UpiCollect => Self::UpiCollect, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::VoucherType) -> Self { + match value { + enums::VoucherType::Boleto => Self::Boleto, + enums::VoucherType::Efecty => Self::Efecty, + enums::VoucherType::PagoEfectivo => Self::PagoEfectivo, + enums::VoucherType::RedCompra => Self::RedCompra, + enums::VoucherType::RedPagos => Self::RedPagos, + enums::VoucherType::Alfamart => Self::Alfamart, + enums::VoucherType::Indomaret => Self::Indomaret, + enums::VoucherType::SevenEleven => Self::SevenEleven, + enums::VoucherType::Lawson => Self::Lawson, + enums::VoucherType::MiniStop => Self::MiniStop, + enums::VoucherType::FamilyMart => Self::FamilyMart, + enums::VoucherType::Seicomart => Self::Seicomart, + enums::VoucherType::PayEasy => Self::PayEasy, + enums::VoucherType::Oxxo => Self::Oxxo, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankTransferType) -> Self { + match value { + enums::BankTransferType::Multibanco => Self::Multibanco, + enums::BankTransferType::Pix => Self::Pix, + enums::BankTransferType::Pse => Self::Pse, + enums::BankTransferType::Ach => Self::Ach, + enums::BankTransferType::Sepa => Self::Sepa, + enums::BankTransferType::Bacs => Self::Bacs, + enums::BankTransferType::BcaBankTransfer => Self::BcaBankTransfer, + enums::BankTransferType::BniVa => Self::BniVa, + enums::BankTransferType::BriVa => Self::BriVa, + enums::BankTransferType::CimbVa => Self::CimbVa, + enums::BankTransferType::DanamonVa => Self::DanamonVa, + enums::BankTransferType::MandiriVa => Self::MandiriVa, + enums::BankTransferType::PermataBankTransfer => Self::PermataBankTransfer, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::GiftCardType) -> Self { + match value { + enums::GiftCardType::PaySafeCard => Self::PaySafeCard, + enums::GiftCardType::Givex => Self::Givex, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CardRedirectType) -> Self { + match value { + enums::CardRedirectType::Benefit => Self::Benefit, + enums::CardRedirectType::Knet => Self::Knet, + enums::CardRedirectType::MomoAtm => Self::MomoAtm, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankRedirectType) -> Self { + match value { + enums::BankRedirectType::Bizum => Self::Bizum, + enums::BankRedirectType::Giropay => Self::Giropay, + enums::BankRedirectType::Ideal => Self::Ideal, + enums::BankRedirectType::Sofort => Self::Sofort, + enums::BankRedirectType::Eps => Self::Eps, + enums::BankRedirectType::BancontactCard => Self::BancontactCard, + enums::BankRedirectType::Blik => Self::Blik, + enums::BankRedirectType::Interac => Self::Interac, + enums::BankRedirectType::OnlineBankingCzechRepublic => Self::OnlineBankingCzechRepublic, + enums::BankRedirectType::OnlineBankingFinland => Self::OnlineBankingFinland, + enums::BankRedirectType::OnlineBankingPoland => Self::OnlineBankingPoland, + enums::BankRedirectType::OnlineBankingSlovakia => Self::OnlineBankingSlovakia, + enums::BankRedirectType::OnlineBankingFpx => Self::OnlineBankingFpx, + enums::BankRedirectType::OnlineBankingThailand => Self::OnlineBankingThailand, + enums::BankRedirectType::OpenBankingUk => Self::OpenBankingUk, + enums::BankRedirectType::Przelewy24 => Self::Przelewy24, + enums::BankRedirectType::Trustly => Self::Trustly, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CryptoType) -> Self { + match value { + enums::CryptoType::CryptoCurrency => Self::CryptoCurrency, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::RewardType) -> Self { + match value { + enums::RewardType::ClassicReward => Self::ClassicReward, + enums::RewardType::Evoucher => Self::Evoucher, + } + } +} + +/// Analyses of the lowering of the DirValues to EuclidValues . +/// +/// For example, +/// ```notrust +/// DirValue::PaymentMethod::Cards -> EuclidValue::PaymentMethod::Cards +/// ```notrust +/// This is a function that lowers the Values of the DIR variants into the Value of the VIR variants. +/// The function for each DirValue variant creates a corresponding EuclidValue variants and if there +/// lacks any direct mapping, it return an Error. +fn lower_value(dir_value: dir::DirValue) -> Result { + Ok(match dir_value { + dir::DirValue::PaymentMethod(pm) => EuclidValue::PaymentMethod(pm), + dir::DirValue::CardBin(ci) => EuclidValue::CardBin(ci), + dir::DirValue::CardType(ct) => EuclidValue::PaymentMethodType(ct.into()), + dir::DirValue::CardNetwork(cn) => EuclidValue::CardNetwork(cn), + dir::DirValue::MetaData(md) => EuclidValue::Metadata(md), + dir::DirValue::PayLaterType(plt) => EuclidValue::PaymentMethodType(plt.into()), + dir::DirValue::WalletType(wt) => EuclidValue::PaymentMethodType(wt.into()), + dir::DirValue::UpiType(ut) => EuclidValue::PaymentMethodType(ut.into()), + dir::DirValue::VoucherType(vt) => EuclidValue::PaymentMethodType(vt.into()), + dir::DirValue::BankTransferType(btt) => EuclidValue::PaymentMethodType(btt.into()), + dir::DirValue::GiftCardType(gct) => EuclidValue::PaymentMethodType(gct.into()), + dir::DirValue::CardRedirectType(crt) => EuclidValue::PaymentMethodType(crt.into()), + dir::DirValue::BankRedirectType(brt) => EuclidValue::PaymentMethodType(brt.into()), + dir::DirValue::CryptoType(ct) => EuclidValue::PaymentMethodType(ct.into()), + dir::DirValue::AuthenticationType(at) => EuclidValue::AuthenticationType(at), + dir::DirValue::CaptureMethod(cm) => EuclidValue::CaptureMethod(cm), + dir::DirValue::PaymentAmount(pa) => EuclidValue::PaymentAmount(pa), + dir::DirValue::PaymentCurrency(pc) => EuclidValue::PaymentCurrency(pc), + dir::DirValue::BusinessCountry(buc) => EuclidValue::BusinessCountry(buc), + dir::DirValue::BillingCountry(bic) => EuclidValue::BillingCountry(bic), + dir::DirValue::MandateAcceptanceType(mat) => EuclidValue::MandateAcceptanceType(mat), + dir::DirValue::MandateType(mt) => EuclidValue::MandateType(mt), + dir::DirValue::PaymentType(pt) => EuclidValue::PaymentType(pt), + dir::DirValue::Connector(_) => Err(AnalysisErrorType::UnsupportedProgramKey( + dir::DirKeyKind::Connector, + ))?, + dir::DirValue::BankDebitType(bdt) => EuclidValue::PaymentMethodType(bdt.into()), + dir::DirValue::RewardType(rt) => EuclidValue::PaymentMethodType(rt.into()), + dir::DirValue::BusinessLabel(bl) => EuclidValue::BusinessLabel(bl), + dir::DirValue::SetupFutureUsage(sfu) => EuclidValue::SetupFutureUsage(sfu), + }) +} + +fn lower_comparison( + dir_comparison: dir::DirComparison, +) -> Result { + Ok(vir::ValuedComparison { + values: dir_comparison + .values + .into_iter() + .map(lower_value) + .collect::>()?, + logic: match dir_comparison.logic { + dir::DirComparisonLogic::NegativeConjunction => { + vir::ValuedComparisonLogic::NegativeConjunction + } + dir::DirComparisonLogic::PositiveDisjunction => { + vir::ValuedComparisonLogic::PositiveDisjunction + } + }, + metadata: dir_comparison.metadata, + }) +} + +fn lower_if_statement( + dir_if_statement: dir::DirIfStatement, +) -> Result { + Ok(vir::ValuedIfStatement { + condition: dir_if_statement + .condition + .into_iter() + .map(lower_comparison) + .collect::>()?, + nested: dir_if_statement + .nested + .map(|v| { + v.into_iter() + .map(lower_if_statement) + .collect::>() + }) + .transpose()?, + }) +} + +fn lower_rule(dir_rule: dir::DirRule) -> Result, AnalysisErrorType> { + Ok(vir::ValuedRule { + name: dir_rule.name, + connector_selection: dir_rule.connector_selection, + statements: dir_rule + .statements + .into_iter() + .map(lower_if_statement) + .collect::>()?, + }) +} + +pub fn lower_program( + dir_program: dir::DirProgram, +) -> Result, AnalysisError> { + Ok(vir::ValuedProgram { + default_selection: dir_program.default_selection, + rules: dir_program + .rules + .into_iter() + .map(lower_rule) + .collect::>() + .map_err(|e| AnalysisError { + error_type: e, + metadata: Default::default(), + })?, + metadata: dir_program.metadata, + }) +} diff --git a/crates/euclid/src/frontend/dir/transformers.rs b/crates/euclid/src/frontend/dir/transformers.rs new file mode 100644 index 000000000000..da413d380c0f --- /dev/null +++ b/crates/euclid/src/frontend/dir/transformers.rs @@ -0,0 +1,166 @@ +use crate::{dirval, dssa::types::AnalysisErrorType, enums as global_enums, frontend::dir}; + +pub trait IntoDirValue { + fn into_dir_value(self) -> Result; +} +impl IntoDirValue for (global_enums::PaymentMethodType, global_enums::PaymentMethod) { + fn into_dir_value(self) -> Result { + match self.0 { + global_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), + global_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), + global_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), + global_enums::PaymentMethodType::Ideal => Ok(dirval!(BankRedirectType = Ideal)), + global_enums::PaymentMethodType::Sofort => Ok(dirval!(BankRedirectType = Sofort)), + global_enums::PaymentMethodType::Eps => Ok(dirval!(BankRedirectType = Eps)), + global_enums::PaymentMethodType::Klarna => Ok(dirval!(PayLaterType = Klarna)), + global_enums::PaymentMethodType::Affirm => Ok(dirval!(PayLaterType = Affirm)), + global_enums::PaymentMethodType::AfterpayClearpay => { + Ok(dirval!(PayLaterType = AfterpayClearpay)) + } + global_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)), + global_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)), + global_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)), + global_enums::PaymentMethodType::CryptoCurrency => { + Ok(dirval!(CryptoType = CryptoCurrency)) + } + global_enums::PaymentMethodType::Ach => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Ach)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Ach)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::Bacs => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Bacs)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Bacs)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::Becs => Ok(dirval!(BankDebitType = Becs)), + global_enums::PaymentMethodType::Sepa => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Sepa)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Sepa)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::AliPay => Ok(dirval!(WalletType = AliPay)), + global_enums::PaymentMethodType::AliPayHk => Ok(dirval!(WalletType = AliPayHk)), + global_enums::PaymentMethodType::BancontactCard => { + Ok(dirval!(BankRedirectType = BancontactCard)) + } + global_enums::PaymentMethodType::Blik => Ok(dirval!(BankRedirectType = Blik)), + global_enums::PaymentMethodType::MbWay => Ok(dirval!(WalletType = MbWay)), + global_enums::PaymentMethodType::MobilePay => Ok(dirval!(WalletType = MobilePay)), + global_enums::PaymentMethodType::Cashapp => Ok(dirval!(WalletType = Cashapp)), + global_enums::PaymentMethodType::Multibanco => { + Ok(dirval!(BankTransferType = Multibanco)) + } + global_enums::PaymentMethodType::Pix => Ok(dirval!(BankTransferType = Pix)), + global_enums::PaymentMethodType::Pse => Ok(dirval!(BankTransferType = Pse)), + global_enums::PaymentMethodType::Interac => Ok(dirval!(BankRedirectType = Interac)), + global_enums::PaymentMethodType::OnlineBankingCzechRepublic => { + Ok(dirval!(BankRedirectType = OnlineBankingCzechRepublic)) + } + global_enums::PaymentMethodType::OnlineBankingFinland => { + Ok(dirval!(BankRedirectType = OnlineBankingFinland)) + } + global_enums::PaymentMethodType::OnlineBankingPoland => { + Ok(dirval!(BankRedirectType = OnlineBankingPoland)) + } + global_enums::PaymentMethodType::OnlineBankingSlovakia => { + Ok(dirval!(BankRedirectType = OnlineBankingSlovakia)) + } + global_enums::PaymentMethodType::Swish => Ok(dirval!(WalletType = Swish)), + global_enums::PaymentMethodType::Trustly => Ok(dirval!(BankRedirectType = Trustly)), + global_enums::PaymentMethodType::Bizum => Ok(dirval!(BankRedirectType = Bizum)), + + global_enums::PaymentMethodType::PayBright => Ok(dirval!(PayLaterType = PayBright)), + global_enums::PaymentMethodType::Walley => Ok(dirval!(PayLaterType = Walley)), + global_enums::PaymentMethodType::Przelewy24 => { + Ok(dirval!(BankRedirectType = Przelewy24)) + } + global_enums::PaymentMethodType::WeChatPay => Ok(dirval!(WalletType = WeChatPay)), + + global_enums::PaymentMethodType::ClassicReward => { + Ok(dirval!(RewardType = ClassicReward)) + } + global_enums::PaymentMethodType::Evoucher => Ok(dirval!(RewardType = Evoucher)), + global_enums::PaymentMethodType::UpiCollect => Ok(dirval!(UpiType = UpiCollect)), + global_enums::PaymentMethodType::SamsungPay => Ok(dirval!(WalletType = SamsungPay)), + global_enums::PaymentMethodType::GoPay => Ok(dirval!(WalletType = GoPay)), + global_enums::PaymentMethodType::KakaoPay => Ok(dirval!(WalletType = KakaoPay)), + global_enums::PaymentMethodType::Twint => Ok(dirval!(WalletType = Twint)), + global_enums::PaymentMethodType::Gcash => Ok(dirval!(WalletType = Gcash)), + global_enums::PaymentMethodType::Vipps => Ok(dirval!(WalletType = Vipps)), + global_enums::PaymentMethodType::Momo => Ok(dirval!(WalletType = Momo)), + global_enums::PaymentMethodType::Alma => Ok(dirval!(PayLaterType = Alma)), + global_enums::PaymentMethodType::Dana => Ok(dirval!(WalletType = Dana)), + global_enums::PaymentMethodType::OnlineBankingFpx => { + Ok(dirval!(BankRedirectType = OnlineBankingFpx)) + } + global_enums::PaymentMethodType::OnlineBankingThailand => { + Ok(dirval!(BankRedirectType = OnlineBankingThailand)) + } + global_enums::PaymentMethodType::TouchNGo => Ok(dirval!(WalletType = TouchNGo)), + global_enums::PaymentMethodType::Atome => Ok(dirval!(PayLaterType = Atome)), + global_enums::PaymentMethodType::Boleto => Ok(dirval!(VoucherType = Boleto)), + global_enums::PaymentMethodType::Efecty => Ok(dirval!(VoucherType = Efecty)), + global_enums::PaymentMethodType::PagoEfectivo => { + Ok(dirval!(VoucherType = PagoEfectivo)) + } + global_enums::PaymentMethodType::RedCompra => Ok(dirval!(VoucherType = RedCompra)), + global_enums::PaymentMethodType::RedPagos => Ok(dirval!(VoucherType = RedPagos)), + global_enums::PaymentMethodType::Alfamart => Ok(dirval!(VoucherType = Alfamart)), + global_enums::PaymentMethodType::BcaBankTransfer => { + Ok(dirval!(BankTransferType = BcaBankTransfer)) + } + global_enums::PaymentMethodType::BniVa => Ok(dirval!(BankTransferType = BniVa)), + global_enums::PaymentMethodType::BriVa => Ok(dirval!(BankTransferType = BriVa)), + global_enums::PaymentMethodType::CimbVa => Ok(dirval!(BankTransferType = CimbVa)), + global_enums::PaymentMethodType::DanamonVa => Ok(dirval!(BankTransferType = DanamonVa)), + global_enums::PaymentMethodType::Indomaret => Ok(dirval!(VoucherType = Indomaret)), + global_enums::PaymentMethodType::MandiriVa => Ok(dirval!(BankTransferType = MandiriVa)), + global_enums::PaymentMethodType::PermataBankTransfer => { + Ok(dirval!(BankTransferType = PermataBankTransfer)) + } + global_enums::PaymentMethodType::PaySafeCard => Ok(dirval!(GiftCardType = PaySafeCard)), + global_enums::PaymentMethodType::SevenEleven => Ok(dirval!(VoucherType = SevenEleven)), + global_enums::PaymentMethodType::Lawson => Ok(dirval!(VoucherType = Lawson)), + global_enums::PaymentMethodType::MiniStop => Ok(dirval!(VoucherType = MiniStop)), + global_enums::PaymentMethodType::FamilyMart => Ok(dirval!(VoucherType = FamilyMart)), + global_enums::PaymentMethodType::Seicomart => Ok(dirval!(VoucherType = Seicomart)), + global_enums::PaymentMethodType::PayEasy => Ok(dirval!(VoucherType = PayEasy)), + global_enums::PaymentMethodType::Givex => Ok(dirval!(GiftCardType = Givex)), + global_enums::PaymentMethodType::Benefit => Ok(dirval!(CardRedirectType = Benefit)), + global_enums::PaymentMethodType::Knet => Ok(dirval!(CardRedirectType = Knet)), + global_enums::PaymentMethodType::OpenBankingUk => { + Ok(dirval!(BankRedirectType = OpenBankingUk)) + } + global_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), + global_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + } + } +} diff --git a/crates/euclid/src/frontend/vir.rs b/crates/euclid/src/frontend/vir.rs new file mode 100644 index 000000000000..750ff4e61ff8 --- /dev/null +++ b/crates/euclid/src/frontend/vir.rs @@ -0,0 +1,37 @@ +//! Valued Intermediate Representation +use crate::types::{EuclidValue, Metadata}; + +#[derive(Debug, Clone)] +pub enum ValuedComparisonLogic { + NegativeConjunction, + PositiveDisjunction, +} + +#[derive(Clone, Debug)] +pub struct ValuedComparison { + pub values: Vec, + pub logic: ValuedComparisonLogic, + pub metadata: Metadata, +} + +pub type ValuedIfCondition = Vec; + +#[derive(Clone, Debug)] +pub struct ValuedIfStatement { + pub condition: ValuedIfCondition, + pub nested: Option>, +} + +#[derive(Clone, Debug)] +pub struct ValuedRule { + pub name: String, + pub connector_selection: O, + pub statements: Vec, +} + +#[derive(Clone, Debug)] +pub struct ValuedProgram { + pub default_selection: O, + pub rules: Vec>, + pub metadata: Metadata, +} diff --git a/crates/euclid/src/lib.rs b/crates/euclid/src/lib.rs new file mode 100644 index 000000000000..d64297437aeb --- /dev/null +++ b/crates/euclid/src/lib.rs @@ -0,0 +1,7 @@ +#![allow(clippy::result_large_err)] +pub mod backend; +pub mod dssa; +pub mod enums; +pub mod frontend; +pub mod types; +pub mod utils; diff --git a/crates/euclid/src/types.rs b/crates/euclid/src/types.rs new file mode 100644 index 000000000000..59736ae65125 --- /dev/null +++ b/crates/euclid/src/types.rs @@ -0,0 +1,318 @@ +pub mod transformers; + +use euclid_macros::EnumNums; +use serde::Serialize; +use strum::VariantNames; + +use crate::{ + dssa::types::EuclidAnalysable, + enums, + frontend::{ + ast, + dir::{DirKeyKind, DirValue, EuclidDirFilter}, + }, +}; + +pub type Metadata = std::collections::HashMap; + +#[derive( + Debug, + Clone, + EnumNums, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumString, +)] +pub enum EuclidKey { + #[strum(serialize = "payment_method")] + PaymentMethod, + #[strum(serialize = "card_bin")] + CardBin, + #[strum(serialize = "metadata")] + Metadata, + #[strum(serialize = "mandate_type")] + MandateType, + #[strum(serialize = "mandate_acceptance_type")] + MandateAcceptanceType, + #[strum(serialize = "payment_type")] + PaymentType, + #[strum(serialize = "payment_method_type")] + PaymentMethodType, + #[strum(serialize = "card_network")] + CardNetwork, + #[strum(serialize = "authentication_type")] + AuthenticationType, + #[strum(serialize = "capture_method")] + CaptureMethod, + #[strum(serialize = "amount")] + PaymentAmount, + #[strum(serialize = "currency")] + PaymentCurrency, + #[strum(serialize = "country", to_string = "business_country")] + BusinessCountry, + #[strum(serialize = "billing_country")] + BillingCountry, + #[strum(serialize = "business_label")] + BusinessLabel, + #[strum(serialize = "setup_future_usage")] + SetupFutureUsage, +} +impl EuclidDirFilter for DummyOutput { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::AuthenticationType, + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::AuthenticationType, + DirKeyKind::CardBin, + DirKeyKind::PayLaterType, + DirKeyKind::PaymentAmount, + DirKeyKind::MetaData, + DirKeyKind::MandateAcceptanceType, + DirKeyKind::MandateType, + DirKeyKind::PaymentType, + DirKeyKind::SetupFutureUsage, + ]; +} +impl EuclidAnalysable for DummyOutput { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { + self.outputs + .iter() + .map(|dummyc| { + let metadata_key = "MetadataKey".to_string(); + let metadata_value = dummyc; + ( + DirValue::MetaData(MetadataValue { + key: metadata_key.clone(), + value: metadata_value.clone(), + }), + std::collections::HashMap::from_iter([( + "DUMMY_OUTPUT".to_string(), + serde_json::json!({ + "rule_name":rule_name, + "Metadata_Key" :metadata_key, + "Metadata_Value" : metadata_value, + }), + )]), + ) + }) + .collect() + } +} +#[derive(Debug, Clone, Serialize)] +pub struct DummyOutput { + pub outputs: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DataType { + Number, + EnumVariant, + MetadataValue, + StrValue, +} + +impl EuclidKey { + pub fn key_type(&self) -> DataType { + match self { + Self::PaymentMethod => DataType::EnumVariant, + Self::CardBin => DataType::StrValue, + Self::Metadata => DataType::MetadataValue, + Self::PaymentMethodType => DataType::EnumVariant, + Self::CardNetwork => DataType::EnumVariant, + Self::AuthenticationType => DataType::EnumVariant, + Self::CaptureMethod => DataType::EnumVariant, + Self::PaymentAmount => DataType::Number, + Self::PaymentCurrency => DataType::EnumVariant, + Self::BusinessCountry => DataType::EnumVariant, + Self::BillingCountry => DataType::EnumVariant, + Self::MandateType => DataType::EnumVariant, + Self::MandateAcceptanceType => DataType::EnumVariant, + Self::PaymentType => DataType::EnumVariant, + Self::BusinessLabel => DataType::StrValue, + Self::SetupFutureUsage => DataType::EnumVariant, + } + } +} + +enums::collect_variants!(EuclidKey); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NumValueRefinement { + NotEqual, + GreaterThan, + LessThan, + GreaterThanEqual, + LessThanEqual, +} + +impl From for Option { + fn from(comp_type: ast::ComparisonType) -> Self { + match comp_type { + ast::ComparisonType::Equal => None, + ast::ComparisonType::NotEqual => Some(NumValueRefinement::NotEqual), + ast::ComparisonType::GreaterThan => Some(NumValueRefinement::GreaterThan), + ast::ComparisonType::LessThan => Some(NumValueRefinement::LessThan), + ast::ComparisonType::LessThanEqual => Some(NumValueRefinement::LessThanEqual), + ast::ComparisonType::GreaterThanEqual => Some(NumValueRefinement::GreaterThanEqual), + } + } +} + +impl From for ast::ComparisonType { + fn from(value: NumValueRefinement) -> Self { + match value { + NumValueRefinement::NotEqual => Self::NotEqual, + NumValueRefinement::LessThan => Self::LessThan, + NumValueRefinement::GreaterThan => Self::GreaterThan, + NumValueRefinement::GreaterThanEqual => Self::GreaterThanEqual, + NumValueRefinement::LessThanEqual => Self::LessThanEqual, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct StrValue { + pub value: String, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct MetadataValue { + pub key: String, + pub value: String, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct NumValue { + pub number: i64, + pub refinement: Option, +} + +impl NumValue { + pub fn fits(&self, other: &Self) -> bool { + let this_num = self.number; + let other_num = other.number; + + match (&self.refinement, &other.refinement) { + (None, None) => this_num == other_num, + + (Some(NumValueRefinement::GreaterThan), None) => other_num > this_num, + + (Some(NumValueRefinement::LessThan), None) => other_num < this_num, + + (Some(NumValueRefinement::NotEqual), Some(NumValueRefinement::NotEqual)) => { + other_num == this_num + } + + (Some(NumValueRefinement::GreaterThan), Some(NumValueRefinement::GreaterThan)) => { + other_num > this_num + } + (Some(NumValueRefinement::LessThan), Some(NumValueRefinement::LessThan)) => { + other_num < this_num + } + + (Some(NumValueRefinement::GreaterThanEqual), None) => other_num >= this_num, + (Some(NumValueRefinement::LessThanEqual), None) => other_num <= this_num, + ( + Some(NumValueRefinement::GreaterThanEqual), + Some(NumValueRefinement::GreaterThanEqual), + ) => other_num >= this_num, + + (Some(NumValueRefinement::LessThanEqual), Some(NumValueRefinement::LessThanEqual)) => { + other_num <= this_num + } + + _ => false, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum EuclidValue { + PaymentMethod(enums::PaymentMethod), + CardBin(StrValue), + Metadata(MetadataValue), + PaymentMethodType(enums::PaymentMethodType), + CardNetwork(enums::CardNetwork), + AuthenticationType(enums::AuthenticationType), + CaptureMethod(enums::CaptureMethod), + PaymentType(enums::PaymentType), + MandateAcceptanceType(enums::MandateAcceptanceType), + MandateType(enums::MandateType), + PaymentAmount(NumValue), + PaymentCurrency(enums::Currency), + BusinessCountry(enums::Country), + BillingCountry(enums::Country), + BusinessLabel(StrValue), + SetupFutureUsage(enums::SetupFutureUsage), +} + +impl EuclidValue { + pub fn get_num_value(&self) -> Option { + match self { + Self::PaymentAmount(val) => Some(val.clone()), + _ => None, + } + } + + pub fn get_key(&self) -> EuclidKey { + match self { + Self::PaymentMethod(_) => EuclidKey::PaymentMethod, + Self::CardBin(_) => EuclidKey::CardBin, + Self::Metadata(_) => EuclidKey::Metadata, + Self::PaymentMethodType(_) => EuclidKey::PaymentMethodType, + Self::MandateType(_) => EuclidKey::MandateType, + Self::PaymentType(_) => EuclidKey::PaymentType, + Self::MandateAcceptanceType(_) => EuclidKey::MandateAcceptanceType, + Self::CardNetwork(_) => EuclidKey::CardNetwork, + Self::AuthenticationType(_) => EuclidKey::AuthenticationType, + Self::CaptureMethod(_) => EuclidKey::CaptureMethod, + Self::PaymentAmount(_) => EuclidKey::PaymentAmount, + Self::PaymentCurrency(_) => EuclidKey::PaymentCurrency, + Self::BusinessCountry(_) => EuclidKey::BusinessCountry, + Self::BillingCountry(_) => EuclidKey::BillingCountry, + Self::BusinessLabel(_) => EuclidKey::BusinessLabel, + Self::SetupFutureUsage(_) => EuclidKey::SetupFutureUsage, + } + } +} + +#[cfg(test)] +mod global_type_tests { + use super::*; + + #[test] + fn test_num_value_fits_greater_than() { + let val1 = NumValue { + number: 10, + refinement: Some(NumValueRefinement::GreaterThan), + }; + let val2 = NumValue { + number: 30, + refinement: Some(NumValueRefinement::GreaterThan), + }; + + assert!(val1.fits(&val2)) + } + + #[test] + fn test_num_value_fits_less_than() { + let val1 = NumValue { + number: 30, + refinement: Some(NumValueRefinement::LessThan), + }; + let val2 = NumValue { + number: 10, + refinement: Some(NumValueRefinement::LessThan), + }; + + assert!(val1.fits(&val2)); + } +} diff --git a/crates/euclid/src/types/transformers.rs b/crates/euclid/src/types/transformers.rs new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/crates/euclid/src/types/transformers.rs @@ -0,0 +1 @@ + diff --git a/crates/euclid/src/utils.rs b/crates/euclid/src/utils.rs new file mode 100644 index 000000000000..e8cb7901f0d7 --- /dev/null +++ b/crates/euclid/src/utils.rs @@ -0,0 +1,3 @@ +pub mod dense_map; + +pub use dense_map::{DenseMap, EntityId}; diff --git a/crates/euclid/src/utils/dense_map.rs b/crates/euclid/src/utils/dense_map.rs new file mode 100644 index 000000000000..8bd4487c77b9 --- /dev/null +++ b/crates/euclid/src/utils/dense_map.rs @@ -0,0 +1,224 @@ +use std::{fmt, iter, marker::PhantomData, ops, slice, vec}; + +pub trait EntityId { + fn get_id(&self) -> usize; + fn with_id(id: usize) -> Self; +} + +pub struct DenseMap { + data: Vec, + _marker: PhantomData, +} + +impl DenseMap { + pub fn new() -> Self { + Self { + data: Vec::new(), + _marker: PhantomData, + } + } +} + +impl Default for DenseMap { + fn default() -> Self { + Self::new() + } +} + +impl DenseMap +where + K: EntityId, +{ + pub fn push(&mut self, elem: V) -> K { + let curr_len = self.data.len(); + self.data.push(elem); + K::with_id(curr_len) + } + + #[inline] + pub fn get(&self, idx: K) -> Option<&V> { + self.data.get(idx.get_id()) + } + + #[inline] + pub fn get_mut(&mut self, idx: K) -> Option<&mut V> { + self.data.get_mut(idx.get_id()) + } + + #[inline] + pub fn contains_key(&self, key: K) -> bool { + key.get_id() < self.data.len() + } + + #[inline] + pub fn keys(&self) -> Keys { + Keys::new(0..self.data.len()) + } + + #[inline] + pub fn into_keys(self) -> Keys { + Keys::new(0..self.data.len()) + } + + #[inline] + pub fn values(&self) -> slice::Iter<'_, V> { + self.data.iter() + } + + #[inline] + pub fn values_mut(&mut self) -> slice::IterMut<'_, V> { + self.data.iter_mut() + } + + #[inline] + pub fn into_values(self) -> vec::IntoIter { + self.data.into_iter() + } + + #[inline] + pub fn iter(&self) -> Iter<'_, K, V> { + Iter::new(self.data.iter()) + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_, K, V> { + IterMut::new(self.data.iter_mut()) + } +} + +impl fmt::Debug for DenseMap +where + K: EntityId + fmt::Debug, + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +pub struct Keys { + inner: ops::Range, + _marker: PhantomData, +} + +impl Keys { + fn new(range: ops::Range) -> Self { + Self { + inner: range, + _marker: PhantomData, + } + } +} + +impl Iterator for Keys +where + K: EntityId, +{ + type Item = K; + + fn next(&mut self) -> Option { + self.inner.next().map(K::with_id) + } +} + +pub struct Iter<'a, K, V> { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl<'a, K, V> Iter<'a, K, V> { + fn new(iter: slice::Iter<'a, V>) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl<'a, K, V> Iterator for Iter<'a, K, V> +where + K: EntityId, +{ + type Item = (K, &'a V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +pub struct IterMut<'a, K, V> { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl<'a, K, V> IterMut<'a, K, V> { + fn new(iter: slice::IterMut<'a, V>) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl<'a, K, V> Iterator for IterMut<'a, K, V> +where + K: EntityId, +{ + type Item = (K, &'a mut V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +pub struct IntoIter { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl IntoIter { + fn new(iter: vec::IntoIter) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl Iterator for IntoIter +where + K: EntityId, +{ + type Item = (K, V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +impl IntoIterator for DenseMap +where + K: EntityId, +{ + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self.data.into_iter()) + } +} + +impl FromIterator for DenseMap +where + K: EntityId, +{ + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Self { + data: Vec::from_iter(iter), + _marker: PhantomData, + } + } +} diff --git a/crates/euclid_macros/Cargo.toml b/crates/euclid_macros/Cargo.toml new file mode 100644 index 000000000000..2524887a8a0f --- /dev/null +++ b/crates/euclid_macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "euclid_macros" +description = "Macros for Euclid DSL" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.51" +quote = "1.0.23" +rustc-hash = "1.1.0" +strum = { version = "0.24", features = ["derive"] } +syn = "1.0.109" diff --git a/crates/euclid_macros/src/inner.rs b/crates/euclid_macros/src/inner.rs new file mode 100644 index 000000000000..979527560dd6 --- /dev/null +++ b/crates/euclid_macros/src/inner.rs @@ -0,0 +1,5 @@ +mod enum_nums; +mod knowledge; + +pub(crate) use enum_nums::enum_nums_inner; +pub(crate) use knowledge::knowledge_inner; diff --git a/crates/euclid_macros/src/inner/enum_nums.rs b/crates/euclid_macros/src/inner/enum_nums.rs new file mode 100644 index 000000000000..61f6765fce0e --- /dev/null +++ b/crates/euclid_macros/src/inner/enum_nums.rs @@ -0,0 +1,47 @@ +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; + +fn error() -> TokenStream2 { + syn::Error::new( + Span::call_site(), + "'EnumNums' can only be derived on enums with unit variants".to_string(), + ) + .to_compile_error() +} + +pub(crate) fn enum_nums_inner(ts: TokenStream) -> TokenStream { + let derive_input = syn::parse_macro_input!(ts as syn::DeriveInput); + + let enum_obj = match derive_input.data { + syn::Data::Enum(e) => e, + _ => return error().into(), + }; + + let enum_name = derive_input.ident; + + let mut match_arms = Vec::::with_capacity(enum_obj.variants.len()); + + for (i, variant) in enum_obj.variants.iter().enumerate() { + match variant.fields { + syn::Fields::Unit => {} + _ => return error().into(), + } + + let var_ident = &variant.ident; + + match_arms.push(quote! { Self::#var_ident => #i }); + } + + let impl_block = quote! { + impl #enum_name { + pub fn to_num(&self) -> usize { + match self { + #(#match_arms),* + } + } + } + }; + + impl_block.into() +} diff --git a/crates/euclid_macros/src/inner/knowledge.rs b/crates/euclid_macros/src/inner/knowledge.rs new file mode 100644 index 000000000000..73b94919c903 --- /dev/null +++ b/crates/euclid_macros/src/inner/knowledge.rs @@ -0,0 +1,680 @@ +use std::{hash::Hash, rc::Rc}; + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use rustc_hash::{FxHashMap, FxHashSet}; +use syn::{parse::Parse, Token}; + +mod strength { + syn::custom_punctuation!(Normal, ->); + syn::custom_punctuation!(Strong, ->>); +} + +mod kw { + syn::custom_keyword!(any); + syn::custom_keyword!(not); +} + +#[derive(Clone, PartialEq, Eq, Hash)] +enum Comparison { + LessThan, + Equal, + GreaterThan, + GreaterThanEqual, + LessThanEqual, +} + +impl ToString for Comparison { + fn to_string(&self) -> String { + match self { + Self::LessThan => "< ".to_string(), + Self::Equal => String::new(), + Self::GreaterThanEqual => ">= ".to_string(), + Self::LessThanEqual => "<= ".to_string(), + Self::GreaterThan => "> ".to_string(), + } + } +} + +impl Parse for Comparison { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + if input.peek(Token![>]) { + input.parse::]>()?; + Ok(Self::GreaterThan) + } else if input.peek(Token![<]) { + input.parse::()?; + Ok(Self::LessThan) + } else if input.peek(Token!(<=)) { + input.parse::()?; + Ok(Self::LessThanEqual) + } else if input.peek(Token!(>=)) { + input.parse::=]>()?; + Ok(Self::GreaterThanEqual) + } else { + Ok(Self::Equal) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +enum ValueType { + Any, + EnumVariant(String), + Number { number: i64, comparison: Comparison }, +} + +impl ValueType { + fn to_string(&self, key: &str) -> String { + match self { + Self::Any => format!("{key}(any)"), + Self::EnumVariant(s) => format!("{key}({s})"), + Self::Number { number, comparison } => { + format!("{}({}{})", key, comparison.to_string(), number) + } + } + } +} + +impl Parse for ValueType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Ident) { + let ident: syn::Ident = input.parse()?; + Ok(Self::EnumVariant(ident.to_string())) + } else if lookahead.peek(Token![>]) + || lookahead.peek(Token![<]) + || lookahead.peek(syn::LitInt) + { + let comparison: Comparison = input.parse()?; + let number: syn::LitInt = input.parse()?; + let num_val = number.base10_parse::()?; + Ok(Self::Number { + number: num_val, + comparison, + }) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct Atom { + key: String, + value: ValueType, +} + +impl ToString for Atom { + fn to_string(&self) -> String { + self.value.to_string(&self.key) + } +} + +impl Parse for Atom { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let maybe_any: syn::Ident = input.parse()?; + if maybe_any == "any" { + let actual_key: syn::Ident = input.parse()?; + Ok(Self { + key: actual_key.to_string(), + value: ValueType::Any, + }) + } else { + let content; + syn::parenthesized!(content in input); + let value: ValueType = content.parse()?; + Ok(Self { + key: maybe_any.to_string(), + value, + }) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash, strum::Display)] +enum Strength { + Normal, + Strong, +} + +impl Parse for Strength { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(strength::Strong) { + input.parse::()?; + Ok(Self::Strong) + } else if lookahead.peek(strength::Normal) { + input.parse::()?; + Ok(Self::Normal) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash, strum::Display)] +enum Relation { + Positive, + Negative, +} + +enum AtomType { + Value { + relation: Relation, + atom: Rc, + }, + + InAggregator { + key: String, + values: Vec, + relation: Relation, + }, +} + +fn parse_atom_type_inner( + input: syn::parse::ParseStream<'_>, + key: syn::Ident, + relation: Relation, +) -> syn::Result { + let result = if input.peek(Token![in]) { + input.parse::()?; + + let bracketed; + syn::bracketed!(bracketed in input); + + let mut values = Vec::::new(); + let first: syn::Ident = bracketed.parse()?; + values.push(first.to_string()); + while !bracketed.is_empty() { + bracketed.parse::()?; + let next: syn::Ident = bracketed.parse()?; + values.push(next.to_string()); + } + + AtomType::InAggregator { + key: key.to_string(), + values, + relation, + } + } else if input.peek(kw::any) { + input.parse::()?; + AtomType::Value { + relation, + atom: Rc::new(Atom { + key: key.to_string(), + value: ValueType::Any, + }), + } + } else { + let value: ValueType = input.parse()?; + AtomType::Value { + relation, + atom: Rc::new(Atom { + key: key.to_string(), + value, + }), + } + }; + + Ok(result) +} + +impl Parse for AtomType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let key: syn::Ident = input.parse()?; + let content; + syn::parenthesized!(content in input); + + let relation = if content.peek(kw::not) { + content.parse::()?; + Relation::Negative + } else { + Relation::Positive + }; + + let result = parse_atom_type_inner(&content, key, relation)?; + + if !content.is_empty() { + Err(content.error("Unexpected input received after atom value")) + } else { + Ok(result) + } + } +} + +fn parse_rhs_atom(input: syn::parse::ParseStream<'_>) -> syn::Result { + let key: syn::Ident = input.parse()?; + let content; + syn::parenthesized!(content in input); + + let lookahead = content.lookahead1(); + + let value_type = if lookahead.peek(kw::any) { + content.parse::()?; + ValueType::Any + } else if lookahead.peek(syn::Ident) { + let variant = content.parse::()?; + ValueType::EnumVariant(variant.to_string()) + } else { + return Err(lookahead.error()); + }; + + if !content.is_empty() { + Err(content.error("Unexpected input received after atom value")) + } else { + Ok(Atom { + key: key.to_string(), + value: value_type, + }) + } +} + +struct Rule { + lhs: Vec, + strength: Strength, + rhs: Rc, +} + +impl Parse for Rule { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let first_atom: AtomType = input.parse()?; + let mut lhs: Vec = vec![first_atom]; + + while input.peek(Token![&]) { + input.parse::()?; + let and_atom: AtomType = input.parse()?; + lhs.push(and_atom); + } + + let strength: Strength = input.parse()?; + + let rhs: Rc = Rc::new(parse_rhs_atom(input)?); + + input.parse::()?; + + Ok(Self { lhs, strength, rhs }) + } +} + +#[derive(Clone)] +enum Scope { + Crate, + Extern, +} + +impl Parse for Scope { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Token![crate]) { + input.parse::()?; + Ok(Self::Crate) + } else if lookahead.peek(Token![extern]) { + input.parse::()?; + Ok(Self::Extern) + } else { + Err(lookahead.error()) + } + } +} + +impl ToString for Scope { + fn to_string(&self) -> String { + match self { + Self::Crate => "crate".to_string(), + Self::Extern => "euclid".to_string(), + } + } +} + +#[derive(Clone)] +struct Program { + rules: Vec>, + scope: Scope, +} + +impl Parse for Program { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let scope: Scope = input.parse()?; + let mut rules: Vec> = Vec::new(); + + while !input.is_empty() { + rules.push(Rc::new(input.parse::()?)); + } + + Ok(Self { rules, scope }) + } +} + +struct GenContext { + next_idx: usize, + next_node_idx: usize, + idx2atom: FxHashMap>, + atom2idx: FxHashMap, usize>, + edges: FxHashMap>, + compiled_atoms: FxHashMap, proc_macro2::Ident>, +} + +impl GenContext { + fn new() -> Self { + Self { + next_idx: 1, + next_node_idx: 1, + idx2atom: FxHashMap::default(), + atom2idx: FxHashMap::default(), + edges: FxHashMap::default(), + compiled_atoms: FxHashMap::default(), + } + } + + fn register_node(&mut self, atom: Rc) -> usize { + if let Some(idx) = self.atom2idx.get(&atom) { + *idx + } else { + let this_idx = self.next_idx; + self.next_idx += 1; + + self.idx2atom.insert(this_idx, Rc::clone(&atom)); + self.atom2idx.insert(atom, this_idx); + + this_idx + } + } + + fn register_edge(&mut self, from: usize, to: usize) -> Result<(), String> { + let node_children = self.edges.entry(from).or_default(); + if node_children.contains(&to) { + Err("Duplicate edge detected".to_string()) + } else { + node_children.insert(to); + self.edges.entry(to).or_default(); + Ok(()) + } + } + + fn register_rule(&mut self, rule: &Rule) -> Result<(), String> { + let to_idx = self.register_node(Rc::clone(&rule.rhs)); + + for atom_type in &rule.lhs { + if let AtomType::Value { atom, .. } = atom_type { + let from_idx = self.register_node(Rc::clone(atom)); + self.register_edge(from_idx, to_idx)?; + } + } + + Ok(()) + } + + fn cycle_dfs( + &self, + node_id: usize, + explored: &mut FxHashSet, + visited: &mut FxHashSet, + order: &mut Vec, + ) -> Result>, String> { + if explored.contains(&node_id) { + let position = order + .iter() + .position(|v| *v == node_id) + .ok_or_else(|| "Error deciding cycle order".to_string())?; + + let cycle_order = order[position..].to_vec(); + Ok(Some(cycle_order)) + } else if visited.contains(&node_id) { + Ok(None) + } else { + visited.insert(node_id); + explored.insert(node_id); + order.push(node_id); + let dests = self + .edges + .get(&node_id) + .ok_or_else(|| "Error getting edges of node".to_string())?; + + for dest in dests.iter().copied() { + if let Some(cycle) = self.cycle_dfs(dest, explored, visited, order)? { + return Ok(Some(cycle)); + } + } + + order.pop(); + + Ok(None) + } + } + + fn detect_graph_cycles(&self) -> Result<(), String> { + let start_nodes = self.edges.keys().copied().collect::>(); + + let mut total_visited = FxHashSet::::default(); + + for node_id in start_nodes.iter().copied() { + let mut explored = FxHashSet::::default(); + let mut order = Vec::::new(); + + match self.cycle_dfs(node_id, &mut explored, &mut total_visited, &mut order)? { + None => {} + Some(order) => { + let mut display_strings = Vec::::with_capacity(order.len() + 1); + + for cycle_node_id in order { + let node = self.idx2atom.get(&cycle_node_id).ok_or_else(|| { + "Failed to find node during cycle display creation".to_string() + })?; + + display_strings.push(node.to_string()); + } + + let first = display_strings + .first() + .cloned() + .ok_or("Unable to fill cycle display array")?; + + display_strings.push(first); + + return Err(format!("Found cycle: {}", display_strings.join(" -> "))); + } + } + } + + Ok(()) + } + + fn next_node_ident(&mut self) -> (proc_macro2::Ident, usize) { + let this_idx = self.next_node_idx; + self.next_node_idx += 1; + (format_ident!("_node_{this_idx}"), this_idx) + } + + fn compile_atom( + &mut self, + atom: &Rc, + tokens: &mut TokenStream, + ) -> Result { + let maybe_ident = self.compiled_atoms.get(atom); + + if let Some(ident) = maybe_ident { + Ok(ident.clone()) + } else { + let (identifier, _) = self.next_node_ident(); + let key = format_ident!("{}", &atom.key); + let the_value = match &atom.value { + ValueType::Any => quote! { + NodeValue::Key(DirKey::new(DirKeyKind::#key,None)) + }, + ValueType::EnumVariant(variant) => { + let variant = format_ident!("{}", variant); + quote! { + NodeValue::Value(DirValue::#key(#key::#variant)) + } + } + ValueType::Number { number, comparison } => { + let comp_type = match comparison { + Comparison::Equal => quote! { + None + }, + Comparison::LessThan => quote! { + Some(NumValueRefinement::LessThan) + }, + Comparison::GreaterThan => quote! { + Some(NumValueRefinement::GreaterThan) + }, + Comparison::GreaterThanEqual => quote! { + Some(NumValueRefinement::GreaterThanEqual) + }, + Comparison::LessThanEqual => quote! { + Some(NumValueRefinement::LessThanEqual) + }, + }; + + quote! { + NodeValue::Value(DirValue::#key(NumValue { + number: #number, + refinement: #comp_type, + })) + } + } + }; + + let compiled = quote! { + let #identifier = graph.make_value_node(#the_value, None, Vec::new(), None::<()>).expect("NodeId derivation failed"); + }; + + tokens.extend(compiled); + self.compiled_atoms + .insert(Rc::clone(atom), identifier.clone()); + + Ok(identifier) + } + } + + fn compile_atom_type( + &mut self, + atom_type: &AtomType, + tokens: &mut TokenStream, + ) -> Result<(proc_macro2::Ident, Relation), String> { + match atom_type { + AtomType::Value { relation, atom } => { + let node_ident = self.compile_atom(atom, tokens)?; + + Ok((node_ident, relation.clone())) + } + + AtomType::InAggregator { + key, + values, + relation, + } => { + let key_ident = format_ident!("{key}"); + let mut values_tokens: Vec = Vec::new(); + + for value in values { + let value_ident = format_ident!("{value}"); + values_tokens.push(quote! { DirValue::#key_ident(#key_ident::#value_ident) }); + } + + let (node_ident, _) = self.next_node_ident(); + let node_code = quote! { + let #node_ident = graph.make_in_aggregator( + Vec::from_iter([#(#values_tokens),*]), + None, + None::<()>, + Vec::new(), + ).expect("Failed to make In aggregator"); + }; + + tokens.extend(node_code); + + Ok((node_ident, relation.clone())) + } + } + } + + fn compile_rule(&mut self, rule: &Rule, tokens: &mut TokenStream) -> Result<(), String> { + let rhs_ident = self.compile_atom(&rule.rhs, tokens)?; + let mut node_details: Vec<(proc_macro2::Ident, Relation)> = + Vec::with_capacity(rule.lhs.len()); + for lhs_atom_type in &rule.lhs { + let details = self.compile_atom_type(lhs_atom_type, tokens)?; + node_details.push(details); + } + + if node_details.len() <= 1 { + let strength = format_ident!("{}", rule.strength.to_string()); + for (from_node, relation) in &node_details { + let relation = format_ident!("{}", relation.to_string()); + tokens.extend(quote! { + graph.make_edge(#from_node, #rhs_ident, Strength::#strength, Relation::#relation) + .expect("Failed to make edge"); + }); + } + } else { + let mut all_agg_nodes: Vec = Vec::with_capacity(node_details.len()); + for (from_node, relation) in &node_details { + let relation = format_ident!("{}", relation.to_string()); + all_agg_nodes.push(quote! { (#from_node, Relation::#relation, Strength::Strong) }); + } + + let strength = format_ident!("{}", rule.strength.to_string()); + let (agg_node_ident, _) = self.next_node_ident(); + tokens.extend(quote! { + let #agg_node_ident = graph.make_all_aggregator(&[#(#all_agg_nodes),*], None, None::<()>, Vec::new()) + .expect("Failed to make all aggregator node"); + + graph.make_edge(#agg_node_ident, #rhs_ident, Strength::#strength, Relation::Positive) + .expect("Failed to create all aggregator edge"); + + }); + } + + Ok(()) + } + + fn compile(&mut self, program: Program) -> Result { + let mut tokens = TokenStream::new(); + for rule in &program.rules { + self.compile_rule(rule, &mut tokens)?; + } + + let scope = match &program.scope { + Scope::Crate => quote! { crate }, + Scope::Extern => quote! { euclid }, + }; + + let compiled = quote! {{ + use #scope::{ + dssa::graph::*, + types::*, + frontend::dir::{*, enums::*}, + }; + + use rustc_hash::{FxHashMap, FxHashSet}; + + let mut graph = KnowledgeGraphBuilder::new(); + + #tokens + + graph.build() + }}; + + Ok(compiled) + } +} + +pub(crate) fn knowledge_inner(ts: TokenStream) -> syn::Result { + let program = syn::parse::(ts.into())?; + let mut gen_context = GenContext::new(); + + for rule in &program.rules { + gen_context + .register_rule(rule) + .map_err(|msg| syn::Error::new(Span::call_site(), msg))?; + } + + gen_context + .detect_graph_cycles() + .map_err(|msg| syn::Error::new(Span::call_site(), msg))?; + + gen_context + .compile(program) + .map_err(|msg| syn::Error::new(Span::call_site(), msg)) +} diff --git a/crates/euclid_macros/src/lib.rs b/crates/euclid_macros/src/lib.rs new file mode 100644 index 000000000000..97b42aaa64c1 --- /dev/null +++ b/crates/euclid_macros/src/lib.rs @@ -0,0 +1,16 @@ +mod inner; + +use proc_macro::TokenStream; + +#[proc_macro_derive(EnumNums)] +pub fn enum_nums(ts: TokenStream) -> TokenStream { + inner::enum_nums_inner(ts) +} + +#[proc_macro] +pub fn knowledge(ts: TokenStream) -> TokenStream { + match inner::knowledge_inner(ts.into()) { + Ok(ts) => ts.into(), + Err(e) => e.into_compile_error().into(), + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml new file mode 100644 index 000000000000..90489eb78bf6 --- /dev/null +++ b/crates/euclid_wasm/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "euclid_wasm" +description = "WASM bindings for Euclid DSL" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib"] + +[features] +default = ["connector_choice_bcompat", "payouts"] +connector_choice_bcompat = [ + "euclid/connector_choice_bcompat", + "api_models/connector_choice_bcompat", + "kgraph_utils/backwards_compatibility" +] +connector_choice_mca_id = [ + "api_models/connector_choice_mca_id", + "euclid/connector_choice_mca_id", + "kgraph_utils/connector_choice_mca_id" +] +dummy_connector = ["kgraph_utils/dummy_connector"] +payouts = [] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +euclid = { path = "../euclid", features = [] } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +getrandom = { version = "0.2.10", features = ["js"] } +once_cell = "1.18.0" +serde = { version = "1.0", features = [] } +serde-wasm-bindgen = "0.5" +strum = { version = "0.25", features = ["derive"] } +wasm-bindgen = { version = "0.2.86" } +ron-parser = "0.1.4" diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs new file mode 100644 index 000000000000..e85a002544ff --- /dev/null +++ b/crates/euclid_wasm/src/lib.rs @@ -0,0 +1,227 @@ +#![allow(non_upper_case_globals)] +mod types; +mod utils; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; + +use api_models::{admin as admin_api, routing::ConnectorSelection}; +use euclid::{ + backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, + dssa::{ + self, analyzer, + graph::{self, Memoization}, + state_machine, truth, + }, + enums, + frontend::{ + ast, + dir::{self, enums as dir_enums}, + }, +}; +use once_cell::sync::OnceCell; +use strum::{EnumMessage, EnumProperty, VariantNames}; +use wasm_bindgen::prelude::*; + +use crate::utils::JsResultExt; +type JsResult = Result; + +struct SeedData<'a> { + kgraph: graph::KnowledgeGraph<'a>, + connectors: Vec, +} + +static SEED_DATA: OnceCell> = OnceCell::new(); + +/// This function can be used by the frontend to provide the WASM with information about +/// all the merchant's connector accounts. The input argument is a vector of all the merchant's +/// connector accounts from the API. +#[wasm_bindgen(js_name = seedKnowledgeGraph)] +pub fn seed_knowledge_graph(mcas: JsValue) -> JsResult { + let mcas: Vec = serde_wasm_bindgen::from_value(mcas)?; + let connectors: Vec = mcas + .iter() + .map(|mca| { + Ok::<_, strum::ParseError>(ast::ConnectorChoice { + connector: dir_enums::Connector::from_str(&mca.connector_name)?, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: mca.business_sub_label.clone(), + }) + }) + .collect::>() + .map_err(|_| "invalid connector name received") + .err_to_js()?; + + let mca_graph = kgraph_utils::mca::make_mca_graph(mcas).err_to_js()?; + let analysis_graph = + graph::KnowledgeGraph::combine(&mca_graph, &truth::ANALYSIS_GRAPH).err_to_js()?; + + SEED_DATA + .set(SeedData { + kgraph: analysis_graph, + connectors, + }) + .map_err(|_| "Knowledge Graph has been already seeded".to_string()) + .err_to_js()?; + + Ok(JsValue::NULL) +} + +/// This function allows the frontend to get all the merchant's configured +/// connectors that are valid for a rule based on the conditions specified in +/// the rule +#[wasm_bindgen(js_name = getValidConnectorsForRule)] +pub fn get_valid_connectors_for_rule(rule: JsValue) -> JsResult { + let seed_data = SEED_DATA.get().ok_or("Data not seeded").err_to_js()?; + + let rule: ast::Rule = serde_wasm_bindgen::from_value(rule)?; + let dir_rule = ast::lowering::lower_rule(rule).err_to_js()?; + let mut valid_connectors: Vec<(ast::ConnectorChoice, dir::DirValue)> = seed_data + .connectors + .iter() + .cloned() + .map(|choice| (choice.clone(), dir::DirValue::Connector(Box::new(choice)))) + .collect(); + let mut invalid_connectors: HashSet = HashSet::new(); + + let mut ctx_manager = state_machine::RuleContextManager::new(&dir_rule, &[]); + + let dummy_meta = HashMap::new(); + + // For every conjunctive context in the Rule, verify validity of all still-valid connectors + // using the knowledge graph + while let Some(ctx) = ctx_manager.advance_mut().err_to_js()? { + // Standalone conjunctive context analysis to ensure the context itself is valid before + // checking it against merchant's connectors + seed_data + .kgraph + .perform_context_analysis(ctx, &mut Memoization::new()) + .err_to_js()?; + + // Update conjunctive context and run analysis on all of merchant's connectors. + for (conn, choice) in &valid_connectors { + if invalid_connectors.contains(conn) { + continue; + } + + let ctx_val = dssa::types::ContextValue::assertion(choice, &dummy_meta); + ctx.push(ctx_val); + let analysis_result = seed_data + .kgraph + .perform_context_analysis(ctx, &mut Memoization::new()); + if analysis_result.is_err() { + invalid_connectors.insert(conn.clone()); + } + ctx.pop(); + } + } + + valid_connectors.retain(|(k, _)| !invalid_connectors.contains(k)); + + let valid_connectors: Vec = + valid_connectors.into_iter().map(|c| c.0).collect(); + + Ok(serde_wasm_bindgen::to_value(&valid_connectors)?) +} + +#[wasm_bindgen(js_name = analyzeProgram)] +pub fn analyze_program(js_program: JsValue) -> JsResult { + let program: ast::Program = serde_wasm_bindgen::from_value(js_program)?; + analyzer::analyze(program, SEED_DATA.get().map(|sd| &sd.kgraph)).err_to_js()?; + Ok(JsValue::NULL) +} + +#[wasm_bindgen(js_name = runProgram)] +pub fn run_program(program: JsValue, input: JsValue) -> JsResult { + let program: ast::Program = serde_wasm_bindgen::from_value(program)?; + let input: inputs::BackendInput = serde_wasm_bindgen::from_value(input)?; + + let backend = InterpreterBackend::with_program(program).err_to_js()?; + + let res: euclid::backend::BackendOutput = + backend.execute(input).err_to_js()?; + + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[wasm_bindgen(js_name = getAllConnectors)] +pub fn get_all_connectors() -> JsResult { + Ok(serde_wasm_bindgen::to_value(enums::Connector::VARIANTS)?) +} + +#[wasm_bindgen(js_name = getAllKeys)] +pub fn get_all_keys() -> JsResult { + let keys: Vec<&'static str> = dir::DirKeyKind::VARIANTS + .iter() + .copied() + .filter(|s| s != &"Connector") + .collect(); + Ok(serde_wasm_bindgen::to_value(&keys)?) +} + +#[wasm_bindgen(js_name = getKeyType)] +pub fn get_key_type(key: &str) -> Result { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + let key_str = key.get_type().to_string(); + Ok(key_str) +} + +#[wasm_bindgen(js_name=parseToString)] +pub fn parser(val: String) -> String { + ron_parser::my_parse(val) +} + +#[wasm_bindgen(js_name = getVariantValues)] +pub fn get_variant_values(key: &str) -> Result { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + + let variants: &[&str] = match key { + dir::DirKeyKind::PaymentMethod => dir_enums::PaymentMethod::VARIANTS, + dir::DirKeyKind::CardType => dir_enums::CardType::VARIANTS, + dir::DirKeyKind::CardNetwork => dir_enums::CardNetwork::VARIANTS, + dir::DirKeyKind::PayLaterType => dir_enums::PayLaterType::VARIANTS, + dir::DirKeyKind::WalletType => dir_enums::WalletType::VARIANTS, + dir::DirKeyKind::BankRedirectType => dir_enums::BankRedirectType::VARIANTS, + dir::DirKeyKind::CryptoType => dir_enums::CryptoType::VARIANTS, + dir::DirKeyKind::RewardType => dir_enums::RewardType::VARIANTS, + dir::DirKeyKind::AuthenticationType => dir_enums::AuthenticationType::VARIANTS, + dir::DirKeyKind::CaptureMethod => dir_enums::CaptureMethod::VARIANTS, + dir::DirKeyKind::PaymentCurrency => dir_enums::PaymentCurrency::VARIANTS, + dir::DirKeyKind::BusinessCountry => dir_enums::Country::VARIANTS, + dir::DirKeyKind::BillingCountry => dir_enums::Country::VARIANTS, + dir::DirKeyKind::BankTransferType => dir_enums::BankTransferType::VARIANTS, + dir::DirKeyKind::UpiType => dir_enums::UpiType::VARIANTS, + dir::DirKeyKind::SetupFutureUsage => dir_enums::SetupFutureUsage::VARIANTS, + dir::DirKeyKind::PaymentType => dir_enums::PaymentType::VARIANTS, + dir::DirKeyKind::MandateType => dir_enums::MandateType::VARIANTS, + dir::DirKeyKind::MandateAcceptanceType => dir_enums::MandateAcceptanceType::VARIANTS, + dir::DirKeyKind::CardRedirectType => dir_enums::CardRedirectType::VARIANTS, + dir::DirKeyKind::GiftCardType => dir_enums::GiftCardType::VARIANTS, + dir::DirKeyKind::VoucherType => dir_enums::VoucherType::VARIANTS, + dir::DirKeyKind::PaymentAmount + | dir::DirKeyKind::Connector + | dir::DirKeyKind::CardBin + | dir::DirKeyKind::BusinessLabel + | dir::DirKeyKind::MetaData => Err("Key does not have variants".to_string())?, + dir::DirKeyKind::BankDebitType => dir_enums::BankDebitType::VARIANTS, + }; + + Ok(serde_wasm_bindgen::to_value(variants)?) +} + +#[wasm_bindgen(js_name = addTwo)] +pub fn add_two(n1: i64, n2: i64) -> i64 { + n1 + n2 +} + +#[wasm_bindgen(js_name = getDescriptionCategory)] +pub fn get_description_category(key: &str) -> JsResult { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + + let result = types::Details { + description: key.get_detailed_message(), + category: key.get_str("Category"), + }; + Ok(serde_wasm_bindgen::to_value(&result)?) +} diff --git a/crates/euclid_wasm/src/types.rs b/crates/euclid_wasm/src/types.rs new file mode 100644 index 000000000000..ea40449971bc --- /dev/null +++ b/crates/euclid_wasm/src/types.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +#[derive(Serialize, Clone)] +pub struct Details<'a> { + pub description: Option<&'a str>, + pub category: Option<&'a str>, +} diff --git a/crates/euclid_wasm/src/utils.rs b/crates/euclid_wasm/src/utils.rs new file mode 100644 index 000000000000..c531dabd7e2a --- /dev/null +++ b/crates/euclid_wasm/src/utils.rs @@ -0,0 +1,17 @@ +use wasm_bindgen::prelude::*; + +pub trait JsResultExt { + fn err_to_js(self) -> Result; +} + +impl JsResultExt for Result +where + E: serde::Serialize, +{ + fn err_to_js(self) -> Result { + match self { + Ok(t) => Ok(t), + Err(e) => Err(serde_wasm_bindgen::to_value(&e)?), + } + } +} diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml new file mode 100644 index 000000000000..fa90b3974c20 --- /dev/null +++ b/crates/kgraph_utils/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "kgraph_utils" +description = "Utilities for constructing and working with Knowledge Graphs" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[features] +dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector"] +backwards_compatibility = ["euclid/backwards_compatibility", "euclid/backwards_compatibility"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id"] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +euclid = { version = "0.1.0", path = "../euclid" } +masking = { version = "0.1.0", path = "../masking/"} + +serde = "1.0.163" +serde_json = "1.0.96" +thiserror = "1.0.43" + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "evaluation" +harness = false diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs new file mode 100644 index 000000000000..ecea12203f8a --- /dev/null +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -0,0 +1,113 @@ +#![allow(unused, clippy::expect_used)] + +use std::str::FromStr; + +use api_models::{ + admin as admin_api, enums as api_enums, payment_methods::RequestPaymentMethodTypes, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use euclid::{ + dirval, + dssa::graph::{self, Memoization}, + frontend::dir, + types::{NumValue, NumValueRefinement}, +}; +use kgraph_utils::{error::KgraphError, transformers::IntoDirValue}; + +fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::KnowledgeGraph<'a> { + use api_models::{admin::*, payment_methods::*}; + + let mut pms_enabled: Vec = Vec::new(); + + for _ in (0..total_enabled) { + let mut pm_types: Vec = Vec::new(); + for _ in (0..total_pm_types) { + pm_types.push(RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Credit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Visa, + api_enums::CardNetwork::Mastercard, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::USD, + api_enums::Currency::INR, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }); + } + + pms_enabled.push(PaymentMethodsEnabled { + payment_method: api_enums::PaymentMethod::Card, + payment_method_types: Some(pm_types), + }); + } + + let stripe_account = MerchantConnectorResponse { + connector_type: api_enums::ConnectorType::FizOperations, + connector_name: "stripe".to_string(), + merchant_connector_id: "something".to_string(), + connector_account_details: masking::Secret::new(serde_json::json!({})), + test_mode: None, + disabled: None, + metadata: None, + payment_methods_enabled: Some(pms_enabled), + business_country: Some(api_enums::CountryAlpha2::US), + business_label: Some("hello".to_string()), + connector_label: Some("something".to_string()), + business_sub_label: Some("something".to_string()), + frm_configs: None, + connector_webhook_details: None, + profile_id: None, + applepay_verified_domains: None, + pm_auth_config: None, + }; + + kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") +} + +fn evaluation(c: &mut Criterion) { + let small_graph = build_test_data(3, 8); + let big_graph = build_test_data(20, 20); + + c.bench_function("MCA Small Graph Evaluation", |b| { + b.iter(|| { + small_graph.key_value_analysis( + dirval!(Connector = Stripe), + &graph::AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = BWP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + }); + }); + + c.bench_function("MCA Big Graph Evaluation", |b| { + b.iter(|| { + big_graph.key_value_analysis( + dirval!(Connector = Stripe), + &graph::AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = BWP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + }); + }); +} + +criterion_group!(benches, evaluation); +criterion_main!(benches); diff --git a/crates/kgraph_utils/src/error.rs b/crates/kgraph_utils/src/error.rs new file mode 100644 index 000000000000..5a16c6375b06 --- /dev/null +++ b/crates/kgraph_utils/src/error.rs @@ -0,0 +1,14 @@ +use euclid::dssa::{graph::GraphError, types::AnalysisErrorType}; + +#[derive(Debug, thiserror::Error, serde::Serialize)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum KgraphError { + #[error("Invalid connector name encountered: '{0}'")] + InvalidConnectorName(String), + #[error("There was an error constructing the graph: {0}")] + GraphConstructionError(GraphError), + #[error("There was an error constructing the context")] + ContextConstructionError(AnalysisErrorType), + #[error("there was an unprecedented indexing error")] + IndexingError, +} diff --git a/crates/kgraph_utils/src/lib.rs b/crates/kgraph_utils/src/lib.rs new file mode 100644 index 000000000000..eb8eef6dedb5 --- /dev/null +++ b/crates/kgraph_utils/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod mca; +pub mod transformers; diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs new file mode 100644 index 000000000000..34babd7a02bd --- /dev/null +++ b/crates/kgraph_utils/src/mca.rs @@ -0,0 +1,739 @@ +use std::str::FromStr; + +use api_models::{ + admin as admin_api, enums as api_enums, payment_methods::RequestPaymentMethodTypes, +}; +use euclid::{ + dssa::graph::{self, DomainIdentifier}, + frontend::{ + ast, + dir::{self, enums as dir_enums}, + }, + types::{NumValue, NumValueRefinement}, +}; + +use crate::{error::KgraphError, transformers::IntoDirValue}; + +pub const DOMAIN_IDENTIFIER: &str = "payment_methods_enabled_for_merchantconnectoraccount"; + +fn compile_request_pm_types( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + pm_types: RequestPaymentMethodTypes, + pm: api_enums::PaymentMethod, +) -> Result { + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation, graph::Strength)> = Vec::new(); + + let pmt_info = "PaymentMethodType"; + let pmt_id = builder + .make_value_node( + (pm_types.payment_method_type, pm) + .into_dir_value() + .map(Into::into)?, + Some(pmt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + agg_nodes.push(( + pmt_id, + graph::Relation::Positive, + match pm_types.payment_method_type { + api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit => { + graph::Strength::Weak + } + + _ => graph::Strength::Strong, + }, + )); + + if let Some(card_networks) = pm_types.card_networks { + if !card_networks.is_empty() { + let dir_vals: Vec = card_networks + .into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>()?; + + let card_network_info = "Card Networks"; + let card_network_id = builder + .make_in_aggregator(dir_vals, Some(card_network_info), None::<()>, Vec::new()) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push(( + card_network_id, + graph::Relation::Positive, + graph::Strength::Weak, + )); + } + } + + let currencies_data = pm_types + .accepted_currencies + .and_then(|accepted_currencies| match accepted_currencies { + admin_api::AcceptedCurrencies::EnableOnly(curr) if !curr.is_empty() => Some(( + curr.into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>() + .ok()?, + graph::Relation::Positive, + )), + + admin_api::AcceptedCurrencies::DisableOnly(curr) if !curr.is_empty() => Some(( + curr.into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>() + .ok()?, + graph::Relation::Negative, + )), + + _ => None, + }); + + if let Some((currencies, relation)) = currencies_data { + let accepted_currencies_info = "Accepted Currencies"; + let accepted_currencies_id = builder + .make_in_aggregator( + currencies, + Some(accepted_currencies_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push((accepted_currencies_id, relation, graph::Strength::Strong)); + } + + let mut amount_nodes = Vec::with_capacity(2); + + if let Some(min_amt) = pm_types.minimum_amount { + let num_val = NumValue { + number: min_amt.into(), + refinement: Some(NumValueRefinement::GreaterThanEqual), + }; + + let min_amt_info = "Minimum Amount"; + let min_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(num_val).into(), + Some(min_amt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + amount_nodes.push(min_amt_id); + } + + if let Some(max_amt) = pm_types.maximum_amount { + let num_val = NumValue { + number: max_amt.into(), + refinement: Some(NumValueRefinement::LessThanEqual), + }; + + let max_amt_info = "Maximum Amount"; + let max_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(num_val).into(), + Some(max_amt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + amount_nodes.push(max_amt_id); + } + + if !amount_nodes.is_empty() { + let zero_num_val = NumValue { + number: 0, + refinement: None, + }; + + let zero_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(zero_num_val).into(), + Some("zero_amount"), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + let or_node_neighbor_id = if amount_nodes.len() == 1 { + amount_nodes + .get(0) + .copied() + .ok_or(KgraphError::IndexingError)? + } else { + let nodes = amount_nodes + .iter() + .copied() + .map(|node_id| (node_id, graph::Relation::Positive, graph::Strength::Strong)) + .collect::>(); + + builder + .make_all_aggregator( + &nodes, + Some("amount_constraint_aggregator"), + None::<()>, + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + ) + .map_err(KgraphError::GraphConstructionError)? + }; + + let any_aggregator = builder + .make_any_aggregator( + &[ + (zero_amt_id, graph::Relation::Positive), + (or_node_neighbor_id, graph::Relation::Positive), + ], + Some("zero_plus_limits_amount_aggregator"), + None::<()>, + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + ) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push(( + any_aggregator, + graph::Relation::Positive, + graph::Strength::Strong, + )); + } + + let pmt_all_aggregator_info = "All Aggregator for PaymentMethodType"; + builder + .make_all_aggregator( + &agg_nodes, + Some(pmt_all_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError) +} + +fn compile_payment_method_enabled( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + enabled: admin_api::PaymentMethodsEnabled, +) -> Result, KgraphError> { + let agg_id = if !enabled + .payment_method_types + .as_ref() + .map(|v| v.is_empty()) + .unwrap_or(true) + { + let pm_info = "PaymentMethod"; + let pm_id = builder + .make_value_node( + enabled.payment_method.into_dir_value().map(Into::into)?, + Some(pm_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); + + if let Some(pm_types) = enabled.payment_method_types { + for pm_type in pm_types { + let node_id = compile_request_pm_types(builder, pm_type, enabled.payment_method)?; + agg_nodes.push((node_id, graph::Relation::Positive)); + } + } + + let any_aggregator_info = "Any aggregation for PaymentMethodsType"; + let pm_type_agg_id = builder + .make_any_aggregator( + &agg_nodes, + Some(any_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + let all_aggregator_info = "All aggregation for PaymentMethod"; + let enabled_pm_agg_id = builder + .make_all_aggregator( + &[ + (pm_id, graph::Relation::Positive, graph::Strength::Strong), + ( + pm_type_agg_id, + graph::Relation::Positive, + graph::Strength::Strong, + ), + ], + Some(all_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + Some(enabled_pm_agg_id) + } else { + None + }; + + Ok(agg_id) +} + +fn compile_merchant_connector_graph( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + mca: admin_api::MerchantConnectorResponse, +) -> Result<(), KgraphError> { + let connector = dir_enums::Connector::from_str(&mca.connector_name) + .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?; + + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); + + if let Some(pms_enabled) = mca.payment_methods_enabled { + for pm_enabled in pms_enabled { + let maybe_pm_enabled_id = compile_payment_method_enabled(builder, pm_enabled)?; + if let Some(pm_enabled_id) = maybe_pm_enabled_id { + agg_nodes.push((pm_enabled_id, graph::Relation::Positive)); + } + } + } + + let aggregator_info = "Available Payment methods for connector"; + let pms_enabled_agg_id = builder + .make_any_aggregator(&agg_nodes, Some(aggregator_info), None::<()>, Vec::new()) + .map_err(KgraphError::GraphConstructionError)?; + + let connector_dir_val = dir::DirValue::Connector(Box::new(ast::ConnectorChoice { + connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: mca.business_sub_label, + })); + + let connector_info = "Connector"; + let connector_node_id = builder + .make_value_node( + connector_dir_val.into(), + Some(connector_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + builder + .make_edge( + pms_enabled_agg_id, + connector_node_id, + graph::Strength::Normal, + graph::Relation::Positive, + ) + .map_err(KgraphError::GraphConstructionError)?; + + Ok(()) +} + +pub fn make_mca_graph<'a>( + accts: Vec, +) -> Result, KgraphError> { + let mut builder = graph::KnowledgeGraphBuilder::new(); + let _domain = builder.make_domain( + DomainIdentifier::new(DOMAIN_IDENTIFIER), + "Payment methods enabled for MerchantConnectorAccount".to_string(), + ); + for acct in accts { + compile_merchant_connector_graph(&mut builder, acct)?; + } + + Ok(builder.build()) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use api_models::enums as api_enums; + use euclid::{ + dirval, + dssa::graph::{AnalysisContext, Memoization}, + }; + + use super::*; + + fn build_test_data<'a>() -> graph::KnowledgeGraph<'a> { + use api_models::{admin::*, payment_methods::*}; + + let stripe_account = MerchantConnectorResponse { + connector_type: api_enums::ConnectorType::FizOperations, + connector_name: "stripe".to_string(), + merchant_connector_id: "something".to_string(), + business_country: Some(api_enums::CountryAlpha2::US), + connector_label: Some("something".to_string()), + business_label: Some("food".to_string()), + business_sub_label: None, + connector_account_details: masking::Secret::new(serde_json::json!({})), + test_mode: None, + disabled: None, + metadata: None, + payment_methods_enabled: Some(vec![PaymentMethodsEnabled { + payment_method: api_enums::PaymentMethod::Card, + payment_method_types: Some(vec![ + RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Credit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Visa, + api_enums::CardNetwork::Mastercard, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::USD, + api_enums::Currency::INR, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }, + RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Debit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Maestro, + api_enums::CardNetwork::JCB, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::GBP, + api_enums::Currency::PHP, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }, + ]), + }]), + frm_configs: None, + connector_webhook_details: None, + profile_id: None, + applepay_verified_domains: None, + pm_auth_config: None, + }; + + make_mca_graph(vec![stripe_account]).expect("Failed graph construction") + } + + #[test] + fn test_credit_card_success_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = USD), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_debit_card_success_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = Maestro), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_single_mismatch_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = DinersClub), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_err()); + } + + #[test] + fn test_amount_mismatch_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 7), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_err()); + } + + #[test] + fn test_incomplete_data_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 7), + ]), + &mut Memoization::new(), + ); + + //println!("{:#?}", result); + //println!("{}", serde_json::to_string_pretty(&result).expect("Hello")); + + assert!(result.is_err()); + } + + #[test] + fn test_incomplete_data_failure_case2() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(CardType = Debit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + //println!("{:#?}", result); + //println!("{}", serde_json::to_string_pretty(&result).expect("Hello")); + + assert!(result.is_err()); + } + + #[test] + fn test_sandbox_applepay_bug_usecase() { + let value = serde_json::json!([ + { + "connector_type": "payment_processor", + "connector_name": "bluesnap", + "merchant_connector_id": "REDACTED", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "REDACTED", + "key1": "REDACTED" + }, + "test_mode": true, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "Interac", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": {}, + "business_country": "US", + "business_label": "default", + "business_sub_label": null, + "frm_configs": null + }, + { + "connector_type": "payment_processor", + "connector_name": "stripe", + "merchant_connector_id": "REDACTED", + "connector_account_details": { + "auth_type": "HeaderKey", + "api_key": "REDACTED" + }, + "test_mode": true, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "Interac", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [] + } + ], + "metadata": {}, + "business_country": "US", + "business_label": "default", + "business_sub_label": null, + "frm_configs": null + } + ]); + + let data: Vec = + serde_json::from_value(value).expect("data"); + + let graph = make_mca_graph(data).expect("graph"); + let context = AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentAmount = 212), + dirval!(PaymentCurrency = ILS), + dirval!(PaymentMethod = Wallet), + dirval!(WalletType = ApplePay), + ]); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &context, + &mut Memoization::new(), + ); + + assert!(result.is_ok(), "stripe validation failed"); + + let result = graph.key_value_analysis( + dirval!(Connector = Bluesnap), + &context, + &mut Memoization::new(), + ); + assert!(result.is_err(), "bluesnap validation failed"); + } +} diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs new file mode 100644 index 000000000000..3d32cce38bd8 --- /dev/null +++ b/crates/kgraph_utils/src/transformers.rs @@ -0,0 +1,724 @@ +use api_models::enums as api_enums; +use euclid::{ + backend::BackendInput, + dirval, + dssa::types::AnalysisErrorType, + frontend::{ast, dir}, + types::{NumValue, StrValue}, +}; + +use crate::error::KgraphError; + +pub trait IntoContext { + fn into_context(self) -> Result, KgraphError>; +} + +impl IntoContext for BackendInput { + fn into_context(self) -> Result, KgraphError> { + let mut ctx: Vec = Vec::new(); + + ctx.push(dir::DirValue::PaymentAmount(NumValue { + number: self.payment.amount, + refinement: None, + })); + + ctx.push(dir::DirValue::PaymentCurrency(self.payment.currency)); + + if let Some(auth_type) = self.payment.authentication_type { + ctx.push(dir::DirValue::AuthenticationType(auth_type)); + } + + if let Some(capture_method) = self.payment.capture_method { + ctx.push(dir::DirValue::CaptureMethod(capture_method)); + } + + if let Some(business_country) = self.payment.business_country { + ctx.push(dir::DirValue::BusinessCountry(business_country)); + } + if let Some(business_label) = self.payment.business_label { + ctx.push(dir::DirValue::BusinessLabel(StrValue { + value: business_label, + })); + } + if let Some(billing_country) = self.payment.billing_country { + ctx.push(dir::DirValue::BillingCountry(billing_country)); + } + + if let Some(payment_method) = self.payment_method.payment_method { + ctx.push(dir::DirValue::PaymentMethod(payment_method)); + } + + if let (Some(pm_type), Some(payment_method)) = ( + self.payment_method.payment_method_type, + self.payment_method.payment_method, + ) { + ctx.push((pm_type, payment_method).into_dir_value()?) + } + + if let Some(card_network) = self.payment_method.card_network { + ctx.push(dir::DirValue::CardNetwork(card_network)); + } + if let Some(setup_future_usage) = self.payment.setup_future_usage { + ctx.push(dir::DirValue::SetupFutureUsage(setup_future_usage)); + } + if let Some(mandate_acceptance_type) = self.mandate.mandate_acceptance_type { + ctx.push(dir::DirValue::MandateAcceptanceType( + mandate_acceptance_type, + )); + } + if let Some(mandate_type) = self.mandate.mandate_type { + ctx.push(dir::DirValue::MandateType(mandate_type)); + } + if let Some(payment_type) = self.mandate.payment_type { + ctx.push(dir::DirValue::PaymentType(payment_type)); + } + + Ok(ctx) + } +} + +pub trait IntoDirValue { + fn into_dir_value(self) -> Result; +} + +impl IntoDirValue for ast::ConnectorChoice { + fn into_dir_value(self) -> Result { + Ok(dir::DirValue::Connector(Box::new(self))) + } +} + +impl IntoDirValue for api_enums::PaymentMethod { + fn into_dir_value(self) -> Result { + match self { + Self::Card => Ok(dirval!(PaymentMethod = Card)), + Self::Wallet => Ok(dirval!(PaymentMethod = Wallet)), + Self::PayLater => Ok(dirval!(PaymentMethod = PayLater)), + Self::BankRedirect => Ok(dirval!(PaymentMethod = BankRedirect)), + Self::Crypto => Ok(dirval!(PaymentMethod = Crypto)), + Self::BankDebit => Ok(dirval!(PaymentMethod = BankDebit)), + Self::BankTransfer => Ok(dirval!(PaymentMethod = BankTransfer)), + Self::Reward => Ok(dirval!(PaymentMethod = Reward)), + Self::Upi => Ok(dirval!(PaymentMethod = Upi)), + Self::Voucher => Ok(dirval!(PaymentMethod = Voucher)), + Self::GiftCard => Ok(dirval!(PaymentMethod = GiftCard)), + Self::CardRedirect => Ok(dirval!(PaymentMethod = CardRedirect)), + } + } +} + +impl IntoDirValue for api_enums::AuthenticationType { + fn into_dir_value(self) -> Result { + match self { + Self::ThreeDs => Ok(dirval!(AuthenticationType = ThreeDs)), + Self::NoThreeDs => Ok(dirval!(AuthenticationType = NoThreeDs)), + } + } +} + +impl IntoDirValue for api_enums::FutureUsage { + fn into_dir_value(self) -> Result { + match self { + Self::OnSession => Ok(dirval!(SetupFutureUsage = OnSession)), + Self::OffSession => Ok(dirval!(SetupFutureUsage = OffSession)), + } + } +} + +impl IntoDirValue for (api_enums::PaymentMethodType, api_enums::PaymentMethod) { + fn into_dir_value(self) -> Result { + match self.0 { + api_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), + api_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), + api_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), + api_enums::PaymentMethodType::Ideal => Ok(dirval!(BankRedirectType = Ideal)), + api_enums::PaymentMethodType::Sofort => Ok(dirval!(BankRedirectType = Sofort)), + api_enums::PaymentMethodType::Eps => Ok(dirval!(BankRedirectType = Eps)), + api_enums::PaymentMethodType::Klarna => Ok(dirval!(PayLaterType = Klarna)), + api_enums::PaymentMethodType::Affirm => Ok(dirval!(PayLaterType = Affirm)), + api_enums::PaymentMethodType::AfterpayClearpay => { + Ok(dirval!(PayLaterType = AfterpayClearpay)) + } + api_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)), + api_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)), + api_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)), + api_enums::PaymentMethodType::CryptoCurrency => { + Ok(dirval!(CryptoType = CryptoCurrency)) + } + api_enums::PaymentMethodType::Ach => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Ach)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Ach)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::Bacs => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Bacs)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Bacs)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::Becs => Ok(dirval!(BankDebitType = Becs)), + api_enums::PaymentMethodType::Sepa => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Sepa)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Sepa)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::AliPay => Ok(dirval!(WalletType = AliPay)), + api_enums::PaymentMethodType::AliPayHk => Ok(dirval!(WalletType = AliPayHk)), + api_enums::PaymentMethodType::BancontactCard => { + Ok(dirval!(BankRedirectType = BancontactCard)) + } + api_enums::PaymentMethodType::Blik => Ok(dirval!(BankRedirectType = Blik)), + api_enums::PaymentMethodType::MbWay => Ok(dirval!(WalletType = MbWay)), + api_enums::PaymentMethodType::MobilePay => Ok(dirval!(WalletType = MobilePay)), + api_enums::PaymentMethodType::Cashapp => Ok(dirval!(WalletType = Cashapp)), + api_enums::PaymentMethodType::Multibanco => Ok(dirval!(BankTransferType = Multibanco)), + api_enums::PaymentMethodType::Pix => Ok(dirval!(BankTransferType = Pix)), + api_enums::PaymentMethodType::Pse => Ok(dirval!(BankTransferType = Pse)), + api_enums::PaymentMethodType::Interac => Ok(dirval!(BankRedirectType = Interac)), + api_enums::PaymentMethodType::OnlineBankingCzechRepublic => { + Ok(dirval!(BankRedirectType = OnlineBankingCzechRepublic)) + } + api_enums::PaymentMethodType::OnlineBankingFinland => { + Ok(dirval!(BankRedirectType = OnlineBankingFinland)) + } + api_enums::PaymentMethodType::OnlineBankingPoland => { + Ok(dirval!(BankRedirectType = OnlineBankingPoland)) + } + api_enums::PaymentMethodType::OnlineBankingSlovakia => { + Ok(dirval!(BankRedirectType = OnlineBankingSlovakia)) + } + api_enums::PaymentMethodType::Swish => Ok(dirval!(WalletType = Swish)), + api_enums::PaymentMethodType::Trustly => Ok(dirval!(BankRedirectType = Trustly)), + api_enums::PaymentMethodType::Bizum => Ok(dirval!(BankRedirectType = Bizum)), + + api_enums::PaymentMethodType::PayBright => Ok(dirval!(PayLaterType = PayBright)), + api_enums::PaymentMethodType::Walley => Ok(dirval!(PayLaterType = Walley)), + api_enums::PaymentMethodType::Przelewy24 => Ok(dirval!(BankRedirectType = Przelewy24)), + api_enums::PaymentMethodType::WeChatPay => Ok(dirval!(WalletType = WeChatPay)), + + api_enums::PaymentMethodType::ClassicReward => Ok(dirval!(RewardType = ClassicReward)), + api_enums::PaymentMethodType::Evoucher => Ok(dirval!(RewardType = Evoucher)), + api_enums::PaymentMethodType::UpiCollect => Ok(dirval!(UpiType = UpiCollect)), + api_enums::PaymentMethodType::SamsungPay => Ok(dirval!(WalletType = SamsungPay)), + api_enums::PaymentMethodType::GoPay => Ok(dirval!(WalletType = GoPay)), + api_enums::PaymentMethodType::KakaoPay => Ok(dirval!(WalletType = KakaoPay)), + api_enums::PaymentMethodType::Twint => Ok(dirval!(WalletType = Twint)), + api_enums::PaymentMethodType::Gcash => Ok(dirval!(WalletType = Gcash)), + api_enums::PaymentMethodType::Vipps => Ok(dirval!(WalletType = Vipps)), + api_enums::PaymentMethodType::Momo => Ok(dirval!(WalletType = Momo)), + api_enums::PaymentMethodType::Alma => Ok(dirval!(PayLaterType = Alma)), + api_enums::PaymentMethodType::Dana => Ok(dirval!(WalletType = Dana)), + api_enums::PaymentMethodType::OnlineBankingFpx => { + Ok(dirval!(BankRedirectType = OnlineBankingFpx)) + } + api_enums::PaymentMethodType::OnlineBankingThailand => { + Ok(dirval!(BankRedirectType = OnlineBankingThailand)) + } + api_enums::PaymentMethodType::TouchNGo => Ok(dirval!(WalletType = TouchNGo)), + api_enums::PaymentMethodType::Atome => Ok(dirval!(PayLaterType = Atome)), + api_enums::PaymentMethodType::Boleto => Ok(dirval!(VoucherType = Boleto)), + api_enums::PaymentMethodType::Efecty => Ok(dirval!(VoucherType = Efecty)), + api_enums::PaymentMethodType::PagoEfectivo => Ok(dirval!(VoucherType = PagoEfectivo)), + api_enums::PaymentMethodType::RedCompra => Ok(dirval!(VoucherType = RedCompra)), + api_enums::PaymentMethodType::RedPagos => Ok(dirval!(VoucherType = RedPagos)), + api_enums::PaymentMethodType::Alfamart => Ok(dirval!(VoucherType = Alfamart)), + api_enums::PaymentMethodType::BcaBankTransfer => { + Ok(dirval!(BankTransferType = BcaBankTransfer)) + } + api_enums::PaymentMethodType::BniVa => Ok(dirval!(BankTransferType = BniVa)), + api_enums::PaymentMethodType::BriVa => Ok(dirval!(BankTransferType = BriVa)), + api_enums::PaymentMethodType::CimbVa => Ok(dirval!(BankTransferType = CimbVa)), + api_enums::PaymentMethodType::DanamonVa => Ok(dirval!(BankTransferType = DanamonVa)), + api_enums::PaymentMethodType::Indomaret => Ok(dirval!(VoucherType = Indomaret)), + api_enums::PaymentMethodType::MandiriVa => Ok(dirval!(BankTransferType = MandiriVa)), + api_enums::PaymentMethodType::PermataBankTransfer => { + Ok(dirval!(BankTransferType = PermataBankTransfer)) + } + api_enums::PaymentMethodType::PaySafeCard => Ok(dirval!(GiftCardType = PaySafeCard)), + api_enums::PaymentMethodType::SevenEleven => Ok(dirval!(VoucherType = SevenEleven)), + api_enums::PaymentMethodType::Lawson => Ok(dirval!(VoucherType = Lawson)), + api_enums::PaymentMethodType::MiniStop => Ok(dirval!(VoucherType = MiniStop)), + api_enums::PaymentMethodType::FamilyMart => Ok(dirval!(VoucherType = FamilyMart)), + api_enums::PaymentMethodType::Seicomart => Ok(dirval!(VoucherType = Seicomart)), + api_enums::PaymentMethodType::PayEasy => Ok(dirval!(VoucherType = PayEasy)), + api_enums::PaymentMethodType::Givex => Ok(dirval!(GiftCardType = Givex)), + api_enums::PaymentMethodType::Benefit => Ok(dirval!(CardRedirectType = Benefit)), + api_enums::PaymentMethodType::Knet => Ok(dirval!(CardRedirectType = Knet)), + api_enums::PaymentMethodType::OpenBankingUk => { + Ok(dirval!(BankRedirectType = OpenBankingUk)) + } + api_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), + api_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + } + } +} + +impl IntoDirValue for api_enums::CardNetwork { + fn into_dir_value(self) -> Result { + match self { + Self::Visa => Ok(dirval!(CardNetwork = Visa)), + Self::Mastercard => Ok(dirval!(CardNetwork = Mastercard)), + Self::AmericanExpress => Ok(dirval!(CardNetwork = AmericanExpress)), + Self::JCB => Ok(dirval!(CardNetwork = JCB)), + Self::DinersClub => Ok(dirval!(CardNetwork = DinersClub)), + Self::Discover => Ok(dirval!(CardNetwork = Discover)), + Self::CartesBancaires => Ok(dirval!(CardNetwork = CartesBancaires)), + Self::UnionPay => Ok(dirval!(CardNetwork = UnionPay)), + Self::Interac => Ok(dirval!(CardNetwork = Interac)), + Self::RuPay => Ok(dirval!(CardNetwork = RuPay)), + Self::Maestro => Ok(dirval!(CardNetwork = Maestro)), + } + } +} + +impl IntoDirValue for api_enums::Currency { + fn into_dir_value(self) -> Result { + match self { + Self::AED => Ok(dirval!(PaymentCurrency = AED)), + Self::ALL => Ok(dirval!(PaymentCurrency = ALL)), + Self::AMD => Ok(dirval!(PaymentCurrency = AMD)), + Self::ANG => Ok(dirval!(PaymentCurrency = ANG)), + Self::ARS => Ok(dirval!(PaymentCurrency = ARS)), + Self::AUD => Ok(dirval!(PaymentCurrency = AUD)), + Self::AWG => Ok(dirval!(PaymentCurrency = AWG)), + Self::AZN => Ok(dirval!(PaymentCurrency = AZN)), + Self::BBD => Ok(dirval!(PaymentCurrency = BBD)), + Self::BDT => Ok(dirval!(PaymentCurrency = BDT)), + Self::BHD => Ok(dirval!(PaymentCurrency = BHD)), + Self::BIF => Ok(dirval!(PaymentCurrency = BIF)), + Self::BMD => Ok(dirval!(PaymentCurrency = BMD)), + Self::BND => Ok(dirval!(PaymentCurrency = BND)), + Self::BOB => Ok(dirval!(PaymentCurrency = BOB)), + Self::BRL => Ok(dirval!(PaymentCurrency = BRL)), + Self::BSD => Ok(dirval!(PaymentCurrency = BSD)), + Self::BWP => Ok(dirval!(PaymentCurrency = BWP)), + Self::BZD => Ok(dirval!(PaymentCurrency = BZD)), + Self::CAD => Ok(dirval!(PaymentCurrency = CAD)), + Self::CHF => Ok(dirval!(PaymentCurrency = CHF)), + Self::CLP => Ok(dirval!(PaymentCurrency = CLP)), + Self::CNY => Ok(dirval!(PaymentCurrency = CNY)), + Self::COP => Ok(dirval!(PaymentCurrency = COP)), + Self::CRC => Ok(dirval!(PaymentCurrency = CRC)), + Self::CUP => Ok(dirval!(PaymentCurrency = CUP)), + Self::CZK => Ok(dirval!(PaymentCurrency = CZK)), + Self::DJF => Ok(dirval!(PaymentCurrency = DJF)), + Self::DKK => Ok(dirval!(PaymentCurrency = DKK)), + Self::DOP => Ok(dirval!(PaymentCurrency = DOP)), + Self::DZD => Ok(dirval!(PaymentCurrency = DZD)), + Self::EGP => Ok(dirval!(PaymentCurrency = EGP)), + Self::ETB => Ok(dirval!(PaymentCurrency = ETB)), + Self::EUR => Ok(dirval!(PaymentCurrency = EUR)), + Self::FJD => Ok(dirval!(PaymentCurrency = FJD)), + Self::GBP => Ok(dirval!(PaymentCurrency = GBP)), + Self::GHS => Ok(dirval!(PaymentCurrency = GHS)), + Self::GIP => Ok(dirval!(PaymentCurrency = GIP)), + Self::GMD => Ok(dirval!(PaymentCurrency = GMD)), + Self::GNF => Ok(dirval!(PaymentCurrency = GNF)), + Self::GTQ => Ok(dirval!(PaymentCurrency = GTQ)), + Self::GYD => Ok(dirval!(PaymentCurrency = GYD)), + Self::HKD => Ok(dirval!(PaymentCurrency = HKD)), + Self::HNL => Ok(dirval!(PaymentCurrency = HNL)), + Self::HRK => Ok(dirval!(PaymentCurrency = HRK)), + Self::HTG => Ok(dirval!(PaymentCurrency = HTG)), + Self::HUF => Ok(dirval!(PaymentCurrency = HUF)), + Self::IDR => Ok(dirval!(PaymentCurrency = IDR)), + Self::ILS => Ok(dirval!(PaymentCurrency = ILS)), + Self::INR => Ok(dirval!(PaymentCurrency = INR)), + Self::JMD => Ok(dirval!(PaymentCurrency = JMD)), + Self::JOD => Ok(dirval!(PaymentCurrency = JOD)), + Self::JPY => Ok(dirval!(PaymentCurrency = JPY)), + Self::KES => Ok(dirval!(PaymentCurrency = KES)), + Self::KGS => Ok(dirval!(PaymentCurrency = KGS)), + Self::KHR => Ok(dirval!(PaymentCurrency = KHR)), + Self::KMF => Ok(dirval!(PaymentCurrency = KMF)), + Self::KRW => Ok(dirval!(PaymentCurrency = KRW)), + Self::KWD => Ok(dirval!(PaymentCurrency = KWD)), + Self::KYD => Ok(dirval!(PaymentCurrency = KYD)), + Self::KZT => Ok(dirval!(PaymentCurrency = KZT)), + Self::LAK => Ok(dirval!(PaymentCurrency = LAK)), + Self::LBP => Ok(dirval!(PaymentCurrency = LBP)), + Self::LKR => Ok(dirval!(PaymentCurrency = LKR)), + Self::LRD => Ok(dirval!(PaymentCurrency = LRD)), + Self::LSL => Ok(dirval!(PaymentCurrency = LSL)), + Self::MAD => Ok(dirval!(PaymentCurrency = MAD)), + Self::MDL => Ok(dirval!(PaymentCurrency = MDL)), + Self::MGA => Ok(dirval!(PaymentCurrency = MGA)), + Self::MKD => Ok(dirval!(PaymentCurrency = MKD)), + Self::MMK => Ok(dirval!(PaymentCurrency = MMK)), + Self::MNT => Ok(dirval!(PaymentCurrency = MNT)), + Self::MOP => Ok(dirval!(PaymentCurrency = MOP)), + Self::MUR => Ok(dirval!(PaymentCurrency = MUR)), + Self::MVR => Ok(dirval!(PaymentCurrency = MVR)), + Self::MWK => Ok(dirval!(PaymentCurrency = MWK)), + Self::MXN => Ok(dirval!(PaymentCurrency = MXN)), + Self::MYR => Ok(dirval!(PaymentCurrency = MYR)), + Self::NAD => Ok(dirval!(PaymentCurrency = NAD)), + Self::NGN => Ok(dirval!(PaymentCurrency = NGN)), + Self::NIO => Ok(dirval!(PaymentCurrency = NIO)), + Self::NOK => Ok(dirval!(PaymentCurrency = NOK)), + Self::NPR => Ok(dirval!(PaymentCurrency = NPR)), + Self::NZD => Ok(dirval!(PaymentCurrency = NZD)), + Self::OMR => Ok(dirval!(PaymentCurrency = OMR)), + Self::PEN => Ok(dirval!(PaymentCurrency = PEN)), + Self::PGK => Ok(dirval!(PaymentCurrency = PGK)), + Self::PHP => Ok(dirval!(PaymentCurrency = PHP)), + Self::PKR => Ok(dirval!(PaymentCurrency = PKR)), + Self::PLN => Ok(dirval!(PaymentCurrency = PLN)), + Self::PYG => Ok(dirval!(PaymentCurrency = PYG)), + Self::QAR => Ok(dirval!(PaymentCurrency = QAR)), + Self::RON => Ok(dirval!(PaymentCurrency = RON)), + Self::RUB => Ok(dirval!(PaymentCurrency = RUB)), + Self::RWF => Ok(dirval!(PaymentCurrency = RWF)), + Self::SAR => Ok(dirval!(PaymentCurrency = SAR)), + Self::SCR => Ok(dirval!(PaymentCurrency = SCR)), + Self::SEK => Ok(dirval!(PaymentCurrency = SEK)), + Self::SGD => Ok(dirval!(PaymentCurrency = SGD)), + Self::SLL => Ok(dirval!(PaymentCurrency = SLL)), + Self::SOS => Ok(dirval!(PaymentCurrency = SOS)), + Self::SSP => Ok(dirval!(PaymentCurrency = SSP)), + Self::SVC => Ok(dirval!(PaymentCurrency = SVC)), + Self::SZL => Ok(dirval!(PaymentCurrency = SZL)), + Self::THB => Ok(dirval!(PaymentCurrency = THB)), + Self::TRY => Ok(dirval!(PaymentCurrency = TRY)), + Self::TTD => Ok(dirval!(PaymentCurrency = TTD)), + Self::TWD => Ok(dirval!(PaymentCurrency = TWD)), + Self::TZS => Ok(dirval!(PaymentCurrency = TZS)), + Self::UGX => Ok(dirval!(PaymentCurrency = UGX)), + Self::USD => Ok(dirval!(PaymentCurrency = USD)), + Self::UYU => Ok(dirval!(PaymentCurrency = UYU)), + Self::UZS => Ok(dirval!(PaymentCurrency = UZS)), + Self::VND => Ok(dirval!(PaymentCurrency = VND)), + Self::VUV => Ok(dirval!(PaymentCurrency = VUV)), + Self::XAF => Ok(dirval!(PaymentCurrency = XAF)), + Self::XOF => Ok(dirval!(PaymentCurrency = XOF)), + Self::XPF => Ok(dirval!(PaymentCurrency = XPF)), + Self::YER => Ok(dirval!(PaymentCurrency = YER)), + Self::ZAR => Ok(dirval!(PaymentCurrency = ZAR)), + } + } +} + +pub fn get_dir_country_dir_value(c: api_enums::Country) -> dir::enums::Country { + match c { + api_enums::Country::Afghanistan => dir::enums::Country::Afghanistan, + api_enums::Country::AlandIslands => dir::enums::Country::AlandIslands, + api_enums::Country::Albania => dir::enums::Country::Albania, + api_enums::Country::Algeria => dir::enums::Country::Algeria, + api_enums::Country::AmericanSamoa => dir::enums::Country::AmericanSamoa, + api_enums::Country::Andorra => dir::enums::Country::Andorra, + api_enums::Country::Angola => dir::enums::Country::Angola, + api_enums::Country::Anguilla => dir::enums::Country::Anguilla, + api_enums::Country::Antarctica => dir::enums::Country::Antarctica, + api_enums::Country::AntiguaAndBarbuda => dir::enums::Country::AntiguaAndBarbuda, + api_enums::Country::Argentina => dir::enums::Country::Argentina, + api_enums::Country::Armenia => dir::enums::Country::Armenia, + api_enums::Country::Aruba => dir::enums::Country::Aruba, + api_enums::Country::Australia => dir::enums::Country::Australia, + api_enums::Country::Austria => dir::enums::Country::Austria, + api_enums::Country::Azerbaijan => dir::enums::Country::Azerbaijan, + api_enums::Country::Bahamas => dir::enums::Country::Bahamas, + api_enums::Country::Bahrain => dir::enums::Country::Bahrain, + api_enums::Country::Bangladesh => dir::enums::Country::Bangladesh, + api_enums::Country::Barbados => dir::enums::Country::Barbados, + api_enums::Country::Belarus => dir::enums::Country::Belarus, + api_enums::Country::Belgium => dir::enums::Country::Belgium, + api_enums::Country::Belize => dir::enums::Country::Belize, + api_enums::Country::Benin => dir::enums::Country::Benin, + api_enums::Country::Bermuda => dir::enums::Country::Bermuda, + api_enums::Country::Bhutan => dir::enums::Country::Bhutan, + api_enums::Country::BoliviaPlurinationalState => { + dir::enums::Country::BoliviaPlurinationalState + } + api_enums::Country::BonaireSintEustatiusAndSaba => { + dir::enums::Country::BonaireSintEustatiusAndSaba + } + api_enums::Country::BosniaAndHerzegovina => dir::enums::Country::BosniaAndHerzegovina, + api_enums::Country::Botswana => dir::enums::Country::Botswana, + api_enums::Country::BouvetIsland => dir::enums::Country::BouvetIsland, + api_enums::Country::Brazil => dir::enums::Country::Brazil, + api_enums::Country::BritishIndianOceanTerritory => { + dir::enums::Country::BritishIndianOceanTerritory + } + api_enums::Country::BruneiDarussalam => dir::enums::Country::BruneiDarussalam, + api_enums::Country::Bulgaria => dir::enums::Country::Bulgaria, + api_enums::Country::BurkinaFaso => dir::enums::Country::BurkinaFaso, + api_enums::Country::Burundi => dir::enums::Country::Burundi, + api_enums::Country::CaboVerde => dir::enums::Country::CaboVerde, + api_enums::Country::Cambodia => dir::enums::Country::Cambodia, + api_enums::Country::Cameroon => dir::enums::Country::Cameroon, + api_enums::Country::Canada => dir::enums::Country::Canada, + api_enums::Country::CaymanIslands => dir::enums::Country::CaymanIslands, + api_enums::Country::CentralAfricanRepublic => dir::enums::Country::CentralAfricanRepublic, + api_enums::Country::Chad => dir::enums::Country::Chad, + api_enums::Country::Chile => dir::enums::Country::Chile, + api_enums::Country::China => dir::enums::Country::China, + api_enums::Country::ChristmasIsland => dir::enums::Country::ChristmasIsland, + api_enums::Country::CocosKeelingIslands => dir::enums::Country::CocosKeelingIslands, + api_enums::Country::Colombia => dir::enums::Country::Colombia, + api_enums::Country::Comoros => dir::enums::Country::Comoros, + api_enums::Country::Congo => dir::enums::Country::Congo, + api_enums::Country::CongoDemocraticRepublic => dir::enums::Country::CongoDemocraticRepublic, + api_enums::Country::CookIslands => dir::enums::Country::CookIslands, + api_enums::Country::CostaRica => dir::enums::Country::CostaRica, + api_enums::Country::CotedIvoire => dir::enums::Country::CotedIvoire, + api_enums::Country::Croatia => dir::enums::Country::Croatia, + api_enums::Country::Cuba => dir::enums::Country::Cuba, + api_enums::Country::Curacao => dir::enums::Country::Curacao, + api_enums::Country::Cyprus => dir::enums::Country::Cyprus, + api_enums::Country::Czechia => dir::enums::Country::Czechia, + api_enums::Country::Denmark => dir::enums::Country::Denmark, + api_enums::Country::Djibouti => dir::enums::Country::Djibouti, + api_enums::Country::Dominica => dir::enums::Country::Dominica, + api_enums::Country::DominicanRepublic => dir::enums::Country::DominicanRepublic, + api_enums::Country::Ecuador => dir::enums::Country::Ecuador, + api_enums::Country::Egypt => dir::enums::Country::Egypt, + api_enums::Country::ElSalvador => dir::enums::Country::ElSalvador, + api_enums::Country::EquatorialGuinea => dir::enums::Country::EquatorialGuinea, + api_enums::Country::Eritrea => dir::enums::Country::Eritrea, + api_enums::Country::Estonia => dir::enums::Country::Estonia, + api_enums::Country::Ethiopia => dir::enums::Country::Ethiopia, + api_enums::Country::FalklandIslandsMalvinas => dir::enums::Country::FalklandIslandsMalvinas, + api_enums::Country::FaroeIslands => dir::enums::Country::FaroeIslands, + api_enums::Country::Fiji => dir::enums::Country::Fiji, + api_enums::Country::Finland => dir::enums::Country::Finland, + api_enums::Country::France => dir::enums::Country::France, + api_enums::Country::FrenchGuiana => dir::enums::Country::FrenchGuiana, + api_enums::Country::FrenchPolynesia => dir::enums::Country::FrenchPolynesia, + api_enums::Country::FrenchSouthernTerritories => { + dir::enums::Country::FrenchSouthernTerritories + } + api_enums::Country::Gabon => dir::enums::Country::Gabon, + api_enums::Country::Gambia => dir::enums::Country::Gambia, + api_enums::Country::Georgia => dir::enums::Country::Georgia, + api_enums::Country::Germany => dir::enums::Country::Germany, + api_enums::Country::Ghana => dir::enums::Country::Ghana, + api_enums::Country::Gibraltar => dir::enums::Country::Gibraltar, + api_enums::Country::Greece => dir::enums::Country::Greece, + api_enums::Country::Greenland => dir::enums::Country::Greenland, + api_enums::Country::Grenada => dir::enums::Country::Grenada, + api_enums::Country::Guadeloupe => dir::enums::Country::Guadeloupe, + api_enums::Country::Guam => dir::enums::Country::Guam, + api_enums::Country::Guatemala => dir::enums::Country::Guatemala, + api_enums::Country::Guernsey => dir::enums::Country::Guernsey, + api_enums::Country::Guinea => dir::enums::Country::Guinea, + api_enums::Country::GuineaBissau => dir::enums::Country::GuineaBissau, + api_enums::Country::Guyana => dir::enums::Country::Guyana, + api_enums::Country::Haiti => dir::enums::Country::Haiti, + api_enums::Country::HeardIslandAndMcDonaldIslands => { + dir::enums::Country::HeardIslandAndMcDonaldIslands + } + api_enums::Country::HolySee => dir::enums::Country::HolySee, + api_enums::Country::Honduras => dir::enums::Country::Honduras, + api_enums::Country::HongKong => dir::enums::Country::HongKong, + api_enums::Country::Hungary => dir::enums::Country::Hungary, + api_enums::Country::Iceland => dir::enums::Country::Iceland, + api_enums::Country::India => dir::enums::Country::India, + api_enums::Country::Indonesia => dir::enums::Country::Indonesia, + api_enums::Country::IranIslamicRepublic => dir::enums::Country::IranIslamicRepublic, + api_enums::Country::Iraq => dir::enums::Country::Iraq, + api_enums::Country::Ireland => dir::enums::Country::Ireland, + api_enums::Country::IsleOfMan => dir::enums::Country::IsleOfMan, + api_enums::Country::Israel => dir::enums::Country::Israel, + api_enums::Country::Italy => dir::enums::Country::Italy, + api_enums::Country::Jamaica => dir::enums::Country::Jamaica, + api_enums::Country::Japan => dir::enums::Country::Japan, + api_enums::Country::Jersey => dir::enums::Country::Jersey, + api_enums::Country::Jordan => dir::enums::Country::Jordan, + api_enums::Country::Kazakhstan => dir::enums::Country::Kazakhstan, + api_enums::Country::Kenya => dir::enums::Country::Kenya, + api_enums::Country::Kiribati => dir::enums::Country::Kiribati, + api_enums::Country::KoreaDemocraticPeoplesRepublic => { + dir::enums::Country::KoreaDemocraticPeoplesRepublic + } + api_enums::Country::KoreaRepublic => dir::enums::Country::KoreaRepublic, + api_enums::Country::Kuwait => dir::enums::Country::Kuwait, + api_enums::Country::Kyrgyzstan => dir::enums::Country::Kyrgyzstan, + api_enums::Country::LaoPeoplesDemocraticRepublic => { + dir::enums::Country::LaoPeoplesDemocraticRepublic + } + api_enums::Country::Latvia => dir::enums::Country::Latvia, + api_enums::Country::Lebanon => dir::enums::Country::Lebanon, + api_enums::Country::Lesotho => dir::enums::Country::Lesotho, + api_enums::Country::Liberia => dir::enums::Country::Liberia, + api_enums::Country::Libya => dir::enums::Country::Libya, + api_enums::Country::Liechtenstein => dir::enums::Country::Liechtenstein, + api_enums::Country::Lithuania => dir::enums::Country::Lithuania, + api_enums::Country::Luxembourg => dir::enums::Country::Luxembourg, + api_enums::Country::Macao => dir::enums::Country::Macao, + api_enums::Country::MacedoniaTheFormerYugoslavRepublic => { + dir::enums::Country::MacedoniaTheFormerYugoslavRepublic + } + api_enums::Country::Madagascar => dir::enums::Country::Madagascar, + api_enums::Country::Malawi => dir::enums::Country::Malawi, + api_enums::Country::Malaysia => dir::enums::Country::Malaysia, + api_enums::Country::Maldives => dir::enums::Country::Maldives, + api_enums::Country::Mali => dir::enums::Country::Mali, + api_enums::Country::Malta => dir::enums::Country::Malta, + api_enums::Country::MarshallIslands => dir::enums::Country::MarshallIslands, + api_enums::Country::Martinique => dir::enums::Country::Martinique, + api_enums::Country::Mauritania => dir::enums::Country::Mauritania, + api_enums::Country::Mauritius => dir::enums::Country::Mauritius, + api_enums::Country::Mayotte => dir::enums::Country::Mayotte, + api_enums::Country::Mexico => dir::enums::Country::Mexico, + api_enums::Country::MicronesiaFederatedStates => { + dir::enums::Country::MicronesiaFederatedStates + } + api_enums::Country::MoldovaRepublic => dir::enums::Country::MoldovaRepublic, + api_enums::Country::Monaco => dir::enums::Country::Monaco, + api_enums::Country::Mongolia => dir::enums::Country::Mongolia, + api_enums::Country::Montenegro => dir::enums::Country::Montenegro, + api_enums::Country::Montserrat => dir::enums::Country::Montserrat, + api_enums::Country::Morocco => dir::enums::Country::Morocco, + api_enums::Country::Mozambique => dir::enums::Country::Mozambique, + api_enums::Country::Myanmar => dir::enums::Country::Myanmar, + api_enums::Country::Namibia => dir::enums::Country::Namibia, + api_enums::Country::Nauru => dir::enums::Country::Nauru, + api_enums::Country::Nepal => dir::enums::Country::Nepal, + api_enums::Country::Netherlands => dir::enums::Country::Netherlands, + api_enums::Country::NewCaledonia => dir::enums::Country::NewCaledonia, + api_enums::Country::NewZealand => dir::enums::Country::NewZealand, + api_enums::Country::Nicaragua => dir::enums::Country::Nicaragua, + api_enums::Country::Niger => dir::enums::Country::Niger, + api_enums::Country::Nigeria => dir::enums::Country::Nigeria, + api_enums::Country::Niue => dir::enums::Country::Niue, + api_enums::Country::NorfolkIsland => dir::enums::Country::NorfolkIsland, + api_enums::Country::NorthernMarianaIslands => dir::enums::Country::NorthernMarianaIslands, + api_enums::Country::Norway => dir::enums::Country::Norway, + api_enums::Country::Oman => dir::enums::Country::Oman, + api_enums::Country::Pakistan => dir::enums::Country::Pakistan, + api_enums::Country::Palau => dir::enums::Country::Palau, + api_enums::Country::PalestineState => dir::enums::Country::PalestineState, + api_enums::Country::Panama => dir::enums::Country::Panama, + api_enums::Country::PapuaNewGuinea => dir::enums::Country::PapuaNewGuinea, + api_enums::Country::Paraguay => dir::enums::Country::Paraguay, + api_enums::Country::Peru => dir::enums::Country::Peru, + api_enums::Country::Philippines => dir::enums::Country::Philippines, + api_enums::Country::Pitcairn => dir::enums::Country::Pitcairn, + + api_enums::Country::Poland => dir::enums::Country::Poland, + api_enums::Country::Portugal => dir::enums::Country::Portugal, + api_enums::Country::PuertoRico => dir::enums::Country::PuertoRico, + + api_enums::Country::Qatar => dir::enums::Country::Qatar, + api_enums::Country::Reunion => dir::enums::Country::Reunion, + api_enums::Country::Romania => dir::enums::Country::Romania, + api_enums::Country::RussianFederation => dir::enums::Country::RussianFederation, + api_enums::Country::Rwanda => dir::enums::Country::Rwanda, + api_enums::Country::SaintBarthelemy => dir::enums::Country::SaintBarthelemy, + api_enums::Country::SaintHelenaAscensionAndTristandaCunha => { + dir::enums::Country::SaintHelenaAscensionAndTristandaCunha + } + api_enums::Country::SaintKittsAndNevis => dir::enums::Country::SaintKittsAndNevis, + api_enums::Country::SaintLucia => dir::enums::Country::SaintLucia, + api_enums::Country::SaintMartinFrenchpart => dir::enums::Country::SaintMartinFrenchpart, + api_enums::Country::SaintPierreAndMiquelon => dir::enums::Country::SaintPierreAndMiquelon, + api_enums::Country::SaintVincentAndTheGrenadines => { + dir::enums::Country::SaintVincentAndTheGrenadines + } + api_enums::Country::Samoa => dir::enums::Country::Samoa, + api_enums::Country::SanMarino => dir::enums::Country::SanMarino, + api_enums::Country::SaoTomeAndPrincipe => dir::enums::Country::SaoTomeAndPrincipe, + api_enums::Country::SaudiArabia => dir::enums::Country::SaudiArabia, + api_enums::Country::Senegal => dir::enums::Country::Senegal, + api_enums::Country::Serbia => dir::enums::Country::Serbia, + api_enums::Country::Seychelles => dir::enums::Country::Seychelles, + api_enums::Country::SierraLeone => dir::enums::Country::SierraLeone, + api_enums::Country::Singapore => dir::enums::Country::Singapore, + api_enums::Country::SintMaartenDutchpart => dir::enums::Country::SintMaartenDutchpart, + api_enums::Country::Slovakia => dir::enums::Country::Slovakia, + api_enums::Country::Slovenia => dir::enums::Country::Slovenia, + api_enums::Country::SolomonIslands => dir::enums::Country::SolomonIslands, + api_enums::Country::Somalia => dir::enums::Country::Somalia, + api_enums::Country::SouthAfrica => dir::enums::Country::SouthAfrica, + api_enums::Country::SouthGeorgiaAndTheSouthSandwichIslands => { + dir::enums::Country::SouthGeorgiaAndTheSouthSandwichIslands + } + api_enums::Country::SouthSudan => dir::enums::Country::SouthSudan, + api_enums::Country::Spain => dir::enums::Country::Spain, + api_enums::Country::SriLanka => dir::enums::Country::SriLanka, + api_enums::Country::Sudan => dir::enums::Country::Sudan, + api_enums::Country::Suriname => dir::enums::Country::Suriname, + api_enums::Country::SvalbardAndJanMayen => dir::enums::Country::SvalbardAndJanMayen, + api_enums::Country::Swaziland => dir::enums::Country::Swaziland, + api_enums::Country::Sweden => dir::enums::Country::Sweden, + api_enums::Country::Switzerland => dir::enums::Country::Switzerland, + api_enums::Country::SyrianArabRepublic => dir::enums::Country::SyrianArabRepublic, + api_enums::Country::TaiwanProvinceOfChina => dir::enums::Country::TaiwanProvinceOfChina, + api_enums::Country::Tajikistan => dir::enums::Country::Tajikistan, + api_enums::Country::TanzaniaUnitedRepublic => dir::enums::Country::TanzaniaUnitedRepublic, + api_enums::Country::Thailand => dir::enums::Country::Thailand, + api_enums::Country::TimorLeste => dir::enums::Country::TimorLeste, + api_enums::Country::Togo => dir::enums::Country::Togo, + api_enums::Country::Tokelau => dir::enums::Country::Tokelau, + api_enums::Country::Tonga => dir::enums::Country::Tonga, + api_enums::Country::TrinidadAndTobago => dir::enums::Country::TrinidadAndTobago, + api_enums::Country::Tunisia => dir::enums::Country::Tunisia, + api_enums::Country::Turkey => dir::enums::Country::Turkey, + api_enums::Country::Turkmenistan => dir::enums::Country::Turkmenistan, + api_enums::Country::TurksAndCaicosIslands => dir::enums::Country::TurksAndCaicosIslands, + api_enums::Country::Tuvalu => dir::enums::Country::Tuvalu, + api_enums::Country::Uganda => dir::enums::Country::Uganda, + api_enums::Country::Ukraine => dir::enums::Country::Ukraine, + api_enums::Country::UnitedArabEmirates => dir::enums::Country::UnitedArabEmirates, + api_enums::Country::UnitedKingdomOfGreatBritainAndNorthernIreland => { + dir::enums::Country::UnitedKingdomOfGreatBritainAndNorthernIreland + } + api_enums::Country::UnitedStatesOfAmerica => dir::enums::Country::UnitedStatesOfAmerica, + api_enums::Country::UnitedStatesMinorOutlyingIslands => { + dir::enums::Country::UnitedStatesMinorOutlyingIslands + } + api_enums::Country::Uruguay => dir::enums::Country::Uruguay, + api_enums::Country::Uzbekistan => dir::enums::Country::Uzbekistan, + api_enums::Country::Vanuatu => dir::enums::Country::Vanuatu, + api_enums::Country::VenezuelaBolivarianRepublic => { + dir::enums::Country::VenezuelaBolivarianRepublic + } + api_enums::Country::Vietnam => dir::enums::Country::Vietnam, + api_enums::Country::VirginIslandsBritish => dir::enums::Country::VirginIslandsBritish, + api_enums::Country::VirginIslandsUS => dir::enums::Country::VirginIslandsUS, + api_enums::Country::WallisAndFutuna => dir::enums::Country::WallisAndFutuna, + api_enums::Country::WesternSahara => dir::enums::Country::WesternSahara, + api_enums::Country::Yemen => dir::enums::Country::Yemen, + api_enums::Country::Zambia => dir::enums::Country::Zambia, + api_enums::Country::Zimbabwe => dir::enums::Country::Zimbabwe, + } +} + +pub fn business_country_to_dir_value(c: api_enums::Country) -> dir::DirValue { + dir::DirValue::BusinessCountry(get_dir_country_dir_value(c)) +} + +pub fn billing_country_to_dir_value(c: api_enums::Country) -> dir::DirValue { + dir::DirValue::BillingCountry(get_dir_country_dir_value(c)) +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 81b23314ffb8..9ab955813336 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,20 +9,23 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache", "dummy_connector", "payouts"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe","basilisk","s3", "email","accounts_cache","kv_store"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] oltp = ["data_models/oltp", "storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] openapi = ["olap", "oltp", "payouts"] vergen = ["router_env/vergen"] -dummy_connector = ["api_models/dummy_connector"] +backwards_compatibility = ["api_models/backwards_compatibility", "euclid/backwards_compatibility", "kgraph_utils/backwards_compatibility"] +business_profile_routing=["api_models/business_profile_routing"] +dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgraph_utils/dummy_connector"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] @@ -66,10 +69,12 @@ num_cpus = "1.15.0" once_cell = "1.18.0" qrcode = "0.12.0" rand = "0.8.5" +rand_chacha = "0.3.1" regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = "0.16.20" roxmltree = "0.18.0" +rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" serde_path_to_error = "0.1.11" @@ -96,6 +101,7 @@ api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] cards = { version = "0.1.0", path = "../cards" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } external_services = { version = "0.1.0", path = "../external_services" } +euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_derive = { version = "0.1.0", path = "../router_derive" } @@ -103,6 +109,7 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false} data_models = { version = "0.1.0", path = "../data_models", default-features = false } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } [target.'cfg(not(target_os = "windows"))'.dependencies] diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index 1076dfe410fc..c237f21dde66 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -9,7 +9,7 @@ use crate::{ core::{api_locking::GetLockingInput, payment_methods::Oss, payments}, routes, services::{api, authentication as auth}, - types::api::{self as api_types}, + types::api as api_types, }; #[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate))] @@ -50,6 +50,7 @@ pub async fn payment_intents_create( &req, create_payment_req, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -58,6 +59,7 @@ pub async fn payment_intents_create( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -117,6 +119,7 @@ pub async fn payment_intents_retrieve( payload, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -180,6 +183,7 @@ pub async fn payment_intents_retrieve_with_gateway_creds( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -236,6 +240,7 @@ pub async fn payment_intents_update( &req, payload, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -244,6 +249,7 @@ pub async fn payment_intents_update( req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -302,6 +308,7 @@ pub async fn payment_intents_confirm( &req, payload, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -310,6 +317,7 @@ pub async fn payment_intents_confirm( req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -366,6 +374,7 @@ pub async fn payment_intents_capture( payload, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -426,6 +435,7 @@ pub async fn payment_intents_cancel( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 4d9632f8885e..c713011b80c8 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -282,9 +282,17 @@ impl TryFrom for payments::PaymentsRequest { let routing = routable_connector .map(|connector| { - crate::types::api::RoutingAlgorithm::Single( - api_models::admin::RoutableConnectorChoice::ConnectorName(connector), - ) + api_models::routing::RoutingAlgorithm::Single(Box::new( + api_models::routing::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: api_models::routing::RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + )) }) .map(|r| { serde_json::to_value(r) diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index 311498e1af58..515e41ec91fa 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -69,6 +69,7 @@ pub async fn setup_intents_create( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -128,6 +129,7 @@ pub async fn setup_intents_retrieve( payload, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -200,6 +202,7 @@ pub async fn setup_intents_update( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -273,6 +276,7 @@ pub async fn setup_intents_confirm( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 661a08e090e0..dde378e55925 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -185,9 +185,17 @@ impl TryFrom for payments::PaymentsRequest { let routing = routable_connector .map(|connector| { - crate::types::api::RoutingAlgorithm::Single( - api_models::admin::RoutableConnectorChoice::ConnectorName(connector), - ) + api_models::routing::RoutingAlgorithm::Single(Box::new( + api_models::routing::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: api_models::routing::RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + )) }) .map(|r| { serde_json::to_value(r) diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 02db8b1754ed..f76df7466581 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -46,3 +46,5 @@ pub(crate) const QR_IMAGE_DATA_SOURCE_STRING: &str = "data:image/png;base64"; pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32"; pub(crate) const METRICS_HOST_TAG_NAME: &str = "host"; +pub const MAX_ROUTING_CONFIGS_PER_MERCHANT: usize = 100; +pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a3bb3c78915c..d87ff64003b4 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -16,6 +16,7 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod routing; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5c9f44ffe575..5de273de0cef 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1,6 +1,8 @@ +use std::str::FromStr; + use api_models::{ admin::{self as admin_types}, - enums as api_enums, + enums as api_enums, routing as routing_types, }; use common_utils::{ crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue}, @@ -18,6 +20,7 @@ use crate::{ core::{ errors::{self, RouterResponse, RouterResult, StorageErrorExt}, payments::helpers, + routing::helpers as routing_helpers, utils as core_utils, }, db::StorageInterface, @@ -89,7 +92,7 @@ pub async fn create_merchant_account( .transpose()?; if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -178,7 +181,10 @@ pub async fn create_merchant_account( .await?, return_url: req.return_url.map(|a| a.to_string()), webhook_details, - routing_algorithm: req.routing_algorithm, + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), sub_merchants_enabled: req.sub_merchants_enabled, parent_merchant_id, enable_payment_response_hash, @@ -470,7 +476,7 @@ pub async fn merchant_account_update( } if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -756,6 +762,9 @@ pub async fn create_payment_connector( ) .await?; + let routable_connector = + api_enums::RoutableConnectors::from_str(&req.connector_name.to_string()).ok(); + let business_profile = state .store .find_business_profile_by_profile_id(&profile_id) @@ -828,6 +837,37 @@ pub async fn create_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + // The purpose of this merchant account update is just to update the + // merchant account `modified_at` field for KGraph cache invalidation + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + locker_id: None, + payment_response_hash_key: None, + primary_business_details: None, + metadata: None, + publishable_key: None, + redirect_to_merchant_with_http_post: None, + routing_algorithm: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + state + .store + .update_specific_fields_in_merchant(merchant_id, merchant_account_update, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error updating the merchant account when creating payment connector")?; + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -852,7 +892,7 @@ pub async fn create_payment_connector( connector_label: Some(connector_label), business_country: req.business_country, business_label: req.business_label.clone(), - business_sub_label: req.business_sub_label, + business_sub_label: req.business_sub_label.clone(), created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), id: None, @@ -873,6 +913,9 @@ pub async fn create_payment_connector( pm_auth_config: req.pm_auth_config.clone(), }; + let mut default_routing_config = + routing_helpers::get_merchant_default_config(&*state.store, merchant_id).await?; + let mca = state .store .insert_merchant_connector_account(merchant_connector_account, &key_store) @@ -884,6 +927,28 @@ pub async fn create_payment_connector( }, )?; + if let Some(routable_connector_val) = routable_connector { + let choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: routable_connector_val, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Some(mca.merchant_connector_id.clone()), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: req.business_sub_label, + }; + + if !default_routing_config.contains(&choice) { + default_routing_config.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + merchant_id, + default_routing_config, + ) + .await?; + } + } + metrics::MCA_CREATE.add( &metrics::CONTEXT, 1, @@ -1248,7 +1313,7 @@ pub async fn create_business_profile( .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; if let Some(ref routing_algorithm) = request.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -1360,7 +1425,7 @@ pub async fn update_business_profile( .transpose()?; if let Some(ref routing_algorithm) = request.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 1c062b7035af..dc1d56721e88 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -325,3 +325,49 @@ pub mod error_stack_parsing { } #[cfg(feature = "detailed_errors")] pub use error_stack_parsing::*; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum RoutingError { + #[error("Merchant routing algorithm not found in cache")] + CacheMiss, + #[error("Final connector selection failed")] + ConnectorSelectionFailed, + #[error("[DSL] Missing required field in payment data: '{field_name}'")] + DslMissingRequiredField { field_name: String }, + #[error("The lock on the DSL cache is most probably poisoned")] + DslCachePoisoned, + #[error("Expected DSL to be saved in DB but did not find")] + DslMissingInDb, + #[error("Unable to parse DSL from JSON")] + DslParsingError, + #[error("Failed to initialize DSL backend")] + DslBackendInitError, + #[error("Error updating merchant with latest dsl cache contents")] + DslMerchantUpdateError, + #[error("Error executing the DSL")] + DslExecutionError, + #[error("Final connector selection failed")] + DslFinalConnectorSelectionFailed, + #[error("[DSL] Received incorrect selection algorithm as DSL output")] + DslIncorrectSelectionAlgorithm, + #[error("there was an error saving/retrieving values from the kgraph cache")] + KgraphCacheFailure, + #[error("failed to refresh the kgraph cache")] + KgraphCacheRefreshFailed, + #[error("there was an error during the kgraph analysis phase")] + KgraphAnalysisError, + #[error("'profile_id' was not provided")] + ProfileIdMissing, + #[error("the profile was not found in the database")] + ProfileNotFound, + #[error("failed to fetch the fallback config for the merchant")] + FallbackConfigFetchFailed, + #[error("Invalid connector name received: '{0}'")] + InvalidConnectorName(String), + #[error("The routing algorithm in merchant account had invalid structure")] + InvalidRoutingAlgorithmStructure, + #[error("Volume split failed")] + VolumeSplitFailed, + #[error("Unable to parse metadata")] + MetadataParsingError, +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2161ab69222e..417b030f5494 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -31,7 +31,10 @@ use crate::{ transformers::{self as payment_methods}, vault, }, - payments::helpers, + payments::{ + helpers, + routing::{self, SessionFlowRoutingInput}, + }, }, db, logger, pii::prelude::*, @@ -42,7 +45,7 @@ use crate::{ }, services, types::{ - api::{self, PaymentMethodCreateExt}, + api::{self, routing as routing_types, PaymentMethodCreateExt}, domain::{ self, types::{decrypt, encrypt_optional, AsyncLift}, @@ -933,6 +936,135 @@ pub async fn list_payment_methods( .await?; } + if let Some((payment_attempt, payment_intent)) = + payment_attempt.as_ref().zip(payment_intent.as_ref()) + { + let routing_enabled_pms = HashSet::from([ + api_enums::PaymentMethod::BankTransfer, + api_enums::PaymentMethod::BankDebit, + api_enums::PaymentMethod::BankRedirect, + ]); + + let routing_enabled_pm_types = HashSet::from([ + api_enums::PaymentMethodType::GooglePay, + api_enums::PaymentMethodType::ApplePay, + api_enums::PaymentMethodType::Klarna, + api_enums::PaymentMethodType::Paypal, + ]); + + let mut chosen = Vec::::new(); + for intermediate in &response { + if routing_enabled_pm_types.contains(&intermediate.payment_method_type) + || routing_enabled_pms.contains(&intermediate.payment_method) + { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.clone().conf.connectors, + &intermediate.connector, + api::GetToken::from(intermediate.payment_method_type), + None, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("invalid connector name received")?; + + chosen.push(api::SessionConnectorData { + payment_method_type: intermediate.payment_method_type, + connector: connector_data, + business_sub_label: None, + }); + } + } + let sfr = SessionFlowRoutingInput { + state: &state, + country: shipping_address.clone().and_then(|ad| ad.country), + key_store: &key_store, + merchant_account: &merchant_account, + payment_attempt, + payment_intent, + chosen, + }; + let result = routing::perform_session_flow_routing(sfr) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing session flow routing")?; + + response.retain(|intermediate| { + if !routing_enabled_pm_types.contains(&intermediate.payment_method_type) + && !routing_enabled_pms.contains(&intermediate.payment_method) + { + return true; + } + + if let Some(choice) = result.get(&intermediate.payment_method_type) { + intermediate.connector == choice.connector.connector_name.to_string() + } else { + false + } + }); + + let mut routing_info: storage::PaymentRoutingInfo = payment_attempt + .straight_through_algorithm + .clone() + .map(|val| val.parse_value("PaymentRoutingInfo")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid PaymentRoutingInfo format found in payment attempt")? + .unwrap_or_else(|| storage::PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }); + + let mut pre_routing_results: HashMap< + api_enums::PaymentMethodType, + routing_types::RoutableConnectorChoice, + > = HashMap::new(); + + for (pm_type, choice) in result { + let routable_choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: choice + .connector + .connector_name + .to_string() + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("")?, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: choice.connector.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: choice.sub_label, + }; + + pre_routing_results.insert(pm_type, routable_choice); + } + + routing_info.pre_routing_results = Some(pre_routing_results); + + let encoded = utils::Encode::::encode_to_value(&routing_info) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize payment routing info to value")?; + + let attempt_update = storage::PaymentAttemptUpdate::UpdateTrackers { + payment_token: None, + connector: None, + straight_through_algorithm: Some(encoded), + amount_capturable: None, + updated_by: merchant_account.storage_scheme.to_string(), + merchant_connector_id: None, + }; + + state + .store + .update_payment_attempt_with_attempt_id( + payment_attempt.clone(), + attempt_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + let req = api_models::payments::PaymentsRequest::foreign_from(( payment_attempt.as_ref(), shipping_address.as_ref(), diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 9aa0e3c70d25..586126467e18 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3,11 +3,12 @@ pub mod customers; pub mod flows; pub mod helpers; pub mod operations; +pub mod routing; pub mod tokenization; pub mod transformers; pub mod types; -use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; +use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; use api_models::{ enums, @@ -35,6 +36,7 @@ pub use self::operations::{ use self::{ flows::{ConstructFlowSpecificData, Feature}, operations::{payment_complete_authorize, BoxedOperation, Operation}, + routing::{self as self_routing, SessionFlowRoutingInput}, }; use super::errors::StorageErrorExt; use crate::{ @@ -49,8 +51,11 @@ use crate::{ routes::{metrics, payment_methods::ParentPaymentMethodToken, AppState}, services::{self, api::Authenticate}, types::{ - self as router_types, api, domain, + self as router_types, + api::{self, ConnectorCallType}, + domain, storage::{self, enums as storage_enums}, + transformers::ForeignTryInto, }, utils::{ add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt, @@ -69,6 +74,7 @@ pub async fn payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResult<( PaymentData, @@ -136,28 +142,11 @@ where &merchant_account, &key_store, &mut payment_data, + eligible_connectors, ) .await?; - let schedule_time = match &connector { - Some(api::ConnectorCallType::Single(connector_data)) => { - if should_add_task_to_process_tracker(&payment_data) { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector_data.connector.id(), - &merchant_account.merchant_id, - 0, - ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - } - } - _ => None, - }; + let should_add_task_to_process_tracker = should_add_task_to_process_tracker(&payment_data); payment_data = tokenize_in_router_when_confirm_false( state, @@ -171,7 +160,21 @@ where let mut external_latency = None; if let Some(connector_details) = connector { payment_data = match connector_details { - api::ConnectorCallType::Single(connector) => { + api::ConnectorCallType::PreDetermined(connector) => { + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; let router_data = call_connector_service( state, &merchant_account, @@ -186,6 +189,57 @@ where header_payload, ) .await?; + let operation = Box::new(PaymentResponse); + let db = &*state.store; + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( + db, + &validate_result.payment_id, + payment_data, + router_data, + merchant_account.storage_scheme, + ) + .await? + } + + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); + + let connector_data = get_connector_data(&mut connectors)?; + + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector_data.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( + state, + &merchant_account, + &key_store, + connector_data, + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, + ) + .await?; let operation = Box::new(PaymentResponse); let db = &*state.store; @@ -205,7 +259,7 @@ where .await? } - api::ConnectorCallType::Multiple(connectors) => { + api::ConnectorCallType::SessionMultiple(connectors) => { call_multiple_connectors_service( state, &merchant_account, @@ -258,6 +312,17 @@ where )) } +#[inline] +pub fn get_connector_data( + connectors: &mut IntoIter, +) -> RouterResult { + connectors + .next() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Connector not found in connectors iterator") +} + #[allow(clippy::too_many_arguments)] pub async fn payments_core( state: AppState, @@ -267,6 +332,7 @@ pub async fn payments_core( req: Req, auth_flow: services::AuthFlow, call_connector_action: CallConnectorAction, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResponse where @@ -287,6 +353,12 @@ where // To perform router related operation for PaymentResponse PaymentResponse: Operation, { + let eligible_routable_connectors = eligible_connectors.map(|connectors| { + connectors + .into_iter() + .flat_map(|c| c.foreign_try_into()) + .collect() + }); let (payment_data, req, customer, connector_http_status_code, external_latency) = payments_operation_core::<_, _, _, _, Ctx>( &state, @@ -296,6 +368,7 @@ where req, call_connector_action, auth_flow, + eligible_routable_connectors, header_payload, ) .await?; @@ -470,6 +543,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom payment_confirm_req, services::api::AuthFlow::Merchant, connector_action, + None, HeaderPayload::default(), ) .await @@ -565,11 +639,11 @@ impl PaymentRedirectFlow for PaymentRedirectSyn payment_sync_req, services::api::AuthFlow::Merchant, connector_action, + None, HeaderPayload::default(), ) .await } - fn generate_response( &self, payments_response: api_models::payments::PaymentsResponse, @@ -1842,7 +1916,7 @@ pub fn update_straight_through_routing( where F: Send + Clone, { - let _: api::RoutingAlgorithm = request_straight_through + let _: api_models::routing::RoutingAlgorithm = request_straight_through .clone() .parse_value("RoutingAlgorithm") .attach_printable("Invalid straight through routing rules format")?; @@ -1859,7 +1933,8 @@ pub async fn get_connector_choice( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, -) -> RouterResult> + eligible_connectors: Option>, +) -> RouterResult> where F: Send + Clone, Ctx: PaymentMethodRetrieve, @@ -1868,7 +1943,7 @@ where .to_domain()? .get_connector( merchant_account, - state, + &state.clone(), req, &payment_data.payment_intent, key_store, @@ -1877,39 +1952,132 @@ where let connector = if should_call_connector(operation, payment_data) { Some(match connector_choice { - api::ConnectorChoice::SessionMultiple(session_connectors) => { - api::ConnectorCallType::Multiple(session_connectors) + api::ConnectorChoice::SessionMultiple(connectors) => { + let routing_output = perform_session_token_routing( + state.clone(), + merchant_account, + key_store, + payment_data, + connectors, + ) + .await?; + api::ConnectorCallType::SessionMultiple(routing_output) } - api::ConnectorChoice::StraightThrough(straight_through) => connector_selection( - state, - merchant_account, - payment_data, - Some(straight_through), - )?, + api::ConnectorChoice::StraightThrough(straight_through) => { + connector_selection( + state, + merchant_account, + key_store, + payment_data, + Some(straight_through), + eligible_connectors, + ) + .await? + } api::ConnectorChoice::Decide => { - connector_selection(state, merchant_account, payment_data, None)? + connector_selection( + state, + merchant_account, + key_store, + payment_data, + None, + eligible_connectors, + ) + .await? } }) - } else if let api::ConnectorChoice::StraightThrough(val) = connector_choice { - update_straight_through_routing(payment_data, val) + } else if let api::ConnectorChoice::StraightThrough(algorithm) = connector_choice { + update_straight_through_routing(payment_data, algorithm) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update straight through routing algorithm")?; + None } else { None }; - Ok(connector) } -pub fn connector_selection( +pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, -) -> RouterResult + eligible_connectors: Option>, +) -> RouterResult +where + F: Send + Clone, +{ + let request_straight_through: Option = + request_straight_through + .map(|val| val.parse_value("RoutingAlgorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid straight through routing rules format")?; + + let mut routing_data = storage::RoutingData { + routed_through: payment_data.payment_attempt.connector.clone(), + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: payment_data.payment_attempt.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + business_sub_label: payment_data.payment_attempt.business_sub_label.clone(), + algorithm: request_straight_through.clone(), + routing_info: payment_data + .payment_attempt + .straight_through_algorithm + .clone() + .map(|val| val.parse_value("PaymentRoutingInfo")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid straight through algorithm format found in payment attempt")? + .unwrap_or_else(|| storage::PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }), + }; + + let decided_connector = decide_connector( + state.clone(), + merchant_account, + key_store, + payment_data, + request_straight_through, + &mut routing_data, + eligible_connectors, + ) + .await?; + + let encoded_info = + Encode::::encode_to_value(&routing_data.routing_info) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error serializing payment routing info to serde value")?; + + payment_data.payment_attempt.connector = routing_data.routed_through; + #[cfg(feature = "connector_choice_mca_id")] + { + payment_data.payment_attempt.merchant_connector_id = routing_data.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + payment_data.payment_attempt.business_sub_label = routing_data.business_sub_label; + } + payment_data.payment_attempt.straight_through_algorithm = Some(encoded_info); + + Ok(decided_connector) +} + +pub async fn decide_connector( + state: AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + request_straight_through: Option, + routing_data: &mut storage::RoutingData, + eligible_connectors: Option>, +) -> RouterResult where F: Send + Clone, { @@ -1925,111 +2093,424 @@ where payment_data.payment_attempt.merchant_connector_id.clone(), ) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("invalid connector name received in payment attempt")?; + .attach_printable("Invalid connector name received in 'routed_through'")?; - return Ok(api::ConnectorCallType::Single(connector_data)); + routing_data.routed_through = Some(connector_name.clone()); + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); } - let request_straight_through = request_straight_through - .map(|val| val.parse_value::("StraightThroughAlgorithm")) - .transpose() + if let Some(mandate_connector_details) = payment_data.mandate_connector.as_ref() { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &mandate_connector_details.connector, + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + mandate_connector_details.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid straight through routing rules format") - .transpose(); + .attach_printable("Invalid connector name received in 'routed_through'")?; + + routing_data.routed_through = Some(mandate_connector_details.connector.clone()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = + mandate_connector_details.merchant_connector_id.clone(); + } + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); + } + + if let Some((pre_routing_results, storage_pm_type)) = routing_data + .routing_info + .pre_routing_results + .as_ref() + .zip(payment_data.payment_attempt.payment_method_type.as_ref()) + { + if let Some(choice) = pre_routing_results.get(storage_pm_type) { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &choice.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + choice.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = choice.merchant_connector_id.clone(); + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = choice.sub_label.clone(); + } + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); + } + } + + if let Some(routing_algorithm) = request_straight_through { + let (mut connectors, check_eligibility) = + routing::perform_straight_through_routing(&routing_algorithm, payment_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; + + if check_eligibility { + connectors = routing::perform_eligibility_analysis_with_fallback( + &state.clone(), + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed eligibility analysis and fallback")?; + } + + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); - let payment_routing_algorithm = request_straight_through.or(payment_data + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label.clone(); + } + routing_data.routing_info.algorithm = Some(routing_algorithm); + return Ok(api::ConnectorCallType::Retryable(connector_data)); + } + + if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm { + let (mut connectors, check_eligibility) = + routing::perform_straight_through_routing(routing_algorithm, payment_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; + + if check_eligibility { + connectors = routing::perform_eligibility_analysis_with_fallback( + &state, + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed eligibility analysis and fallback")?; + } + + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); + + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label; + } + return Ok(api::ConnectorCallType::Retryable(connector_data)); + } + + route_connector_v1( + &state, + merchant_account, + key_store, + payment_data, + routing_data, + eligible_connectors, + ) + .await +} + +pub fn should_add_task_to_process_tracker(payment_data: &PaymentData) -> bool { + let connector = payment_data.payment_attempt.connector.as_deref(); + + !matches!( + (payment_data.payment_attempt.payment_method, connector), + ( + Some(storage_enums::PaymentMethod::BankTransfer), + Some("stripe") + ) + ) +} + +pub async fn perform_session_token_routing( + state: AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + connectors: Vec, +) -> RouterResult> +where + F: Clone, +{ + let routing_info: Option = payment_data .payment_attempt .straight_through_algorithm .clone() - .map(|val| val.parse_value::("RoutingAlgorithm")) + .map(|val| val.parse_value("PaymentRoutingInfo")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid straight through algorithm format in payment attempt") - .transpose()); + .attach_printable("invalid payment routing info format found in payment attempt")?; - let routing_algorithm = payment_routing_algorithm - .or(merchant_account - .routing_algorithm - .clone() - .map(|merchant_routing_algorithm| { - merchant_routing_algorithm - .parse_value::("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::InternalServerError) // Deserialization failed - .attach_printable("Unable to deserialize merchant routing algorithm") - })) - .get_required_value("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::PreconditionFailed { - message: "no routing algorithm has been configured".to_string(), - })??; + if let Some(storage::PaymentRoutingInfo { + pre_routing_results: Some(pre_routing_results), + .. + }) = routing_info + { + let mut payment_methods: rustc_hash::FxHashMap< + (String, enums::PaymentMethodType), + api::SessionConnectorData, + > = rustc_hash::FxHashMap::from_iter(connectors.iter().map(|c| { + ( + ( + c.connector.connector_name.to_string(), + c.payment_method_type, + ), + c.clone(), + ) + })); - let mut routing_data = storage::RoutingData { - routed_through: payment_data.payment_attempt.connector.clone(), - algorithm: Some(routing_algorithm), - }; + let mut final_list: Vec = Vec::new(); + for (routed_pm_type, choice) in pre_routing_results.into_iter() { + if let Some(session_connector_data) = + payment_methods.remove(&(choice.to_string(), routed_pm_type)) + { + final_list.push(session_connector_data); + } + } - let (decided_connector, connector_id) = decide_connector(state, &mut routing_data)?; + if !final_list.is_empty() { + return Ok(final_list); + } + } - let encoded_algorithm = routing_data - .algorithm - .map(|algo| Encode::::encode_to_value(&algo)) - .transpose() + let routing_enabled_pms = std::collections::HashSet::from([ + enums::PaymentMethodType::GooglePay, + enums::PaymentMethodType::ApplePay, + enums::PaymentMethodType::Klarna, + enums::PaymentMethodType::Paypal, + ]); + + let mut chosen = Vec::::new(); + for connector_data in &connectors { + if routing_enabled_pms.contains(&connector_data.payment_method_type) { + chosen.push(connector_data.clone()); + } + } + let sfr = SessionFlowRoutingInput { + state: &state, + country: payment_data + .address + .billing + .as_ref() + .and_then(|address| address.address.as_ref()) + .and_then(|details| details.country), + key_store, + merchant_account, + payment_attempt: &payment_data.payment_attempt, + payment_intent: &payment_data.payment_intent, + + chosen, + }; + let result = self_routing::perform_session_flow_routing(sfr) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize routing algorithm to serde value")?; + .attach_printable("error performing session flow routing")?; + + let mut final_list: Vec = Vec::new(); + + #[cfg(not(feature = "connector_choice_mca_id"))] + for mut connector_data in connectors { + if !routing_enabled_pms.contains(&connector_data.payment_method_type) { + final_list.push(connector_data); + } else if let Some(choice) = result.get(&connector_data.payment_method_type) { + if connector_data.connector.connector_name == choice.connector.connector_name { + connector_data.business_sub_label = choice.sub_label.clone(); + final_list.push(connector_data); + } + } + } - payment_data.payment_attempt.connector = routing_data.routed_through; - payment_data.payment_attempt.straight_through_algorithm = encoded_algorithm; - payment_data.payment_attempt.merchant_connector_id = connector_id; + #[cfg(feature = "connector_choice_mca_id")] + for connector_data in connectors { + if !routing_enabled_pms.contains(&connector_data.payment_method_type) { + final_list.push(connector_data); + } else if let Some(choice) = result.get(&connector_data.payment_method_type) { + if connector_data.connector.connector_name == choice.connector.connector_name { + final_list.push(connector_data); + } + } + } - Ok(decided_connector) + Ok(final_list) } -pub fn decide_connector( +pub async fn route_connector_v1( state: &AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, -) -> RouterResult<(api::ConnectorCallType, Option)> { - let routing_algorithm = routing_data - .algorithm + eligible_connectors: Option>, +) -> RouterResult +where + F: Send + Clone, +{ + #[cfg(not(feature = "business_profile_routing"))] + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm .clone() - .get_required_value("Routing algorithm")?; + .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode merchant routing algorithm ref")? + .unwrap_or_default(); - let (connector_name, merchant_connector_id) = match routing_algorithm { - api::StraightThroughAlgorithm::Single(routable_connector_choice) => { - match routable_connector_choice { - api_models::admin::RoutableConnectorChoice::ConnectorName(routable_connector) => { - (routable_connector.to_string(), None) - } - api_models::admin::RoutableConnectorChoice::ConnectorId { - merchant_connector_id, - connector, - } => (connector.to_string(), Some(merchant_connector_id)), - } - } + #[cfg(feature = "business_profile_routing")] + let algorithm_ref: api::routing::RoutingAlgorithmRef = { + let profile_id = payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + business_profile + .routing_algorithm + .clone() + .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode merchant routing algorithm ref")? + .unwrap_or_default() }; - let connector_data = api::ConnectorData::get_connector_by_name( - &state.conf.connectors, - &connector_name, - api::GetToken::Connector, - merchant_connector_id.clone(), + let connectors = routing::perform_static_routing_v1( + state, + &merchant_account.merchant_id, + algorithm_ref, + payment_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let connectors = routing::perform_eligibility_analysis_with_fallback( + &state.clone(), + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), ) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid connector name received in routing algorithm")?; + .attach_printable("failed eligibility analysis and fallback")?; - routing_data.routed_through = Some(connector_name); - Ok(( - api::ConnectorCallType::Single(connector_data), - merchant_connector_id, - )) -} + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); -pub fn should_add_task_to_process_tracker(payment_data: &PaymentData) -> bool { - let connector = payment_data.payment_attempt.connector.as_deref(); + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); - !matches!( - (payment_data.payment_attempt.payment_method, connector), - ( - Some(storage_enums::PaymentMethod::BankTransfer), - Some("stripe") - ) - ) + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label; + } + + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + Ok(ConnectorCallType::Retryable(connector_data)) } diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs new file mode 100644 index 000000000000..4134ddf65ea0 --- /dev/null +++ b/crates/router/src/core/payments/routing.rs @@ -0,0 +1,950 @@ +mod transformers; + +use std::{ + collections::hash_map, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use api_models::{ + admin as admin_api, + enums::{self as api_enums, CountryAlpha2}, + routing::ConnectorSelection, +}; +use common_utils::static_cache::StaticCache; +use diesel_models::enums as storage_enums; +use error_stack::{IntoReport, ResultExt}; +use euclid::{ + backend::{self, inputs as dsl_inputs, EuclidBackend}, + dssa::graph::{self as euclid_graph, Memoization}, + enums as euclid_enums, + frontend::ast, +}; +use kgraph_utils::{ + mca as mca_graph, + transformers::{IntoContext, IntoDirValue}, +}; +use masking::PeekInterface; +use rand::{ + distributions::{self, Distribution}, + SeedableRng, +}; +use rustc_hash::FxHashMap; + +#[cfg(not(feature = "business_profile_routing"))] +use crate::utils::StringExt; +use crate::{ + core::{ + errors as oss_errors, errors, payments as payments_oss, routing::helpers as routing_helpers, + }, + logger, + types::{ + api, api::routing as routing_types, domain, storage as oss_storage, + transformers::ForeignInto, + }, + utils::{OptionExt, ValueExt}, + AppState, +}; + +pub(super) enum CachedAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(backend::VirInterpreterBackend), +} + +pub struct SessionFlowRoutingInput<'a> { + pub state: &'a AppState, + pub country: Option, + pub key_store: &'a domain::MerchantKeyStore, + pub merchant_account: &'a domain::MerchantAccount, + pub payment_attempt: &'a oss_storage::PaymentAttempt, + pub payment_intent: &'a oss_storage::PaymentIntent, + pub chosen: Vec, +} + +pub struct SessionRoutingPmTypeInput<'a> { + state: &'a AppState, + key_store: &'a domain::MerchantKeyStore, + merchant_last_modified: i64, + attempt_id: &'a str, + routing_algorithm: &'a MerchantAccountRoutingAlgorithm, + backend_input: dsl_inputs::BackendInput, + allowed_connectors: FxHashMap, + #[cfg(feature = "business_profile_routing")] + profile_id: Option, +} +static ROUTING_CACHE: StaticCache = StaticCache::new(); +static KGRAPH_CACHE: StaticCache> = StaticCache::new(); + +type RoutingResult = oss_errors::CustomResult; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum MerchantAccountRoutingAlgorithm { + V1(routing_types::RoutingAlgorithmRef), +} + +impl Default for MerchantAccountRoutingAlgorithm { + fn default() -> Self { + Self::V1(routing_types::RoutingAlgorithmRef::default()) + } +} + +pub fn make_dsl_input( + payment_data: &payments_oss::PaymentData, +) -> RoutingResult +where + F: Clone, +{ + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: payment_data + .setup_mandate + .as_ref() + .and_then(|mandate_data| { + mandate_data + .customer_acceptance + .clone() + .map(|cat| match cat.acceptance_type { + data_models::mandates::AcceptanceType::Online => { + euclid_enums::MandateAcceptanceType::Online + } + data_models::mandates::AcceptanceType::Offline => { + euclid_enums::MandateAcceptanceType::Offline + } + }) + }), + mandate_type: payment_data + .setup_mandate + .as_ref() + .and_then(|mandate_data| { + mandate_data.mandate_type.clone().map(|mt| match mt { + data_models::mandates::MandateDataType::SingleUse(_) => { + euclid_enums::MandateType::SingleUse + } + data_models::mandates::MandateDataType::MultiUse(_) => { + euclid_enums::MandateType::MultiUse + } + }) + }), + payment_type: Some(payment_data.setup_mandate.clone().map_or_else( + || euclid_enums::PaymentType::NonMandate, + |_| euclid_enums::PaymentType::SetupMandate, + )), + }; + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: payment_data.payment_attempt.payment_method, + payment_method_type: payment_data.payment_attempt.payment_method_type, + card_network: payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api::PaymentMethodData::Card(card) => card.card_network.clone(), + + _ => None, + }), + }; + + let payment_input = dsl_inputs::PaymentInput { + amount: payment_data.payment_intent.amount, + card_bin: payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api::PaymentMethodData::Card(card) => { + Some(card.card_number.peek().chars().take(6).collect()) + } + _ => None, + }), + currency: payment_data.currency, + authentication_type: payment_data.payment_attempt.authentication_type, + capture_method: payment_data + .payment_attempt + .capture_method + .and_then(|cm| cm.foreign_into()), + business_country: payment_data + .payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: payment_data + .address + .billing + .as_ref() + .and_then(|bic| bic.address.as_ref()) + .and_then(|add| add.country) + .map(api_enums::Country::from_alpha2), + business_label: payment_data.payment_intent.business_label.clone(), + setup_future_usage: payment_data.payment_intent.setup_future_usage, + }; + + let metadata = payment_data + .payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + Ok(dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }) +} + +pub async fn perform_static_routing_v1( + state: &AppState, + merchant_id: &str, + algorithm_ref: routing_types::RoutingAlgorithmRef, + payment_data: &mut payments_oss::PaymentData, +) -> RoutingResult> { + let algorithm_id = if let Some(id) = algorithm_ref.algorithm_id { + id + } else { + let fallback_config = + routing_helpers::get_merchant_default_config(&*state.clone().store, merchant_id) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + + return Ok(fallback_config); + }; + let key = ensure_algorithm_cached_v1( + state, + merchant_id, + algorithm_ref.timestamp, + &algorithm_id, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await?; + let cached_algorithm: Arc = ROUTING_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + + Ok(match cached_algorithm.as_ref() { + CachedAlgorithm::Single(conn) => vec![(**conn).clone()], + + CachedAlgorithm::Priority(plist) => plist.clone(), + + CachedAlgorithm::VolumeSplit(splits) => perform_volume_split(splits.to_vec(), None) + .change_context(errors::RoutingError::ConnectorSelectionFailed)?, + + CachedAlgorithm::Advanced(interpreter) => { + let backend_input = make_dsl_input(payment_data)?; + + execute_dsl_and_get_connector_v1(backend_input, interpreter)? + } + }) +} + +async fn ensure_algorithm_cached_v1( + state: &AppState, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult { + #[cfg(feature = "business_profile_routing")] + let key = { + let profile_id = profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + format!("routing_config_{merchant_id}_{profile_id}") + }; + + #[cfg(not(feature = "business_profile_routing"))] + let key = format!("dsl_{merchant_id}"); + + let present = ROUTING_CACHE + .present(&key) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + + let expired = ROUTING_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error checking expiry of DSL in cache")?; + + if !present || expired { + refresh_routing_cache_v1( + state, + key.clone(), + algorithm_id, + timestamp, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + }; + + Ok(key) +} + +pub fn perform_straight_through_routing( + algorithm: &routing_types::StraightThroughAlgorithm, + payment_data: &payments_oss::PaymentData, +) -> RoutingResult<(Vec, bool)> { + Ok(match algorithm { + routing_types::StraightThroughAlgorithm::Single(conn) => ( + vec![(**conn).clone()], + payment_data.creds_identifier.is_none(), + ), + + routing_types::StraightThroughAlgorithm::Priority(conns) => (conns.clone(), true), + + routing_types::StraightThroughAlgorithm::VolumeSplit(splits) => ( + perform_volume_split(splits.to_vec(), None) + .change_context(errors::RoutingError::ConnectorSelectionFailed) + .attach_printable( + "Volume Split connector selection error in straight through routing", + )?, + true, + ), + }) +} + +fn execute_dsl_and_get_connector_v1( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> RoutingResult> { + let routing_output: routing_types::RoutingAlgorithm = interpreter + .execute(backend_input) + .map(|out| out.connector_selection.foreign_into()) + .into_report() + .change_context(errors::RoutingError::DslExecutionError)?; + + Ok(match routing_output { + routing_types::RoutingAlgorithm::Priority(plist) => plist, + + routing_types::RoutingAlgorithm::VolumeSplit(splits) => perform_volume_split(splits, None) + .change_context(errors::RoutingError::DslFinalConnectorSelectionFailed)?, + + _ => Err(errors::RoutingError::DslIncorrectSelectionAlgorithm) + .into_report() + .attach_printable("Unsupported algorithm received as a result of static routing")?, + }) +} + +pub async fn refresh_routing_cache_v1( + state: &AppState, + key: String, + algorithm_id: &str, + timestamp: i64, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult<()> { + #[cfg(feature = "business_profile_routing")] + let algorithm = { + let algorithm = state + .store + .find_routing_algorithm_by_profile_id_algorithm_id( + &profile_id.unwrap_or_default(), + algorithm_id, + ) + .await + .change_context(errors::RoutingError::DslMissingInDb)?; + let algorithm: routing_types::RoutingAlgorithm = algorithm + .algorithm_data + .parse_value("RoutingAlgorithm") + .change_context(errors::RoutingError::DslParsingError)?; + algorithm + }; + + #[cfg(not(feature = "business_profile_routing"))] + let algorithm = { + let config = state + .store + .find_config_by_key(algorithm_id) + .await + .change_context(errors::RoutingError::DslMissingInDb) + .attach_printable("DSL not found in DB")?; + + let algorithm: routing_types::RoutingAlgorithm = config + .config + .parse_struct("Program") + .change_context(errors::RoutingError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + algorithm + }; + let cached_algorithm = match algorithm { + routing_types::RoutingAlgorithm::Single(conn) => CachedAlgorithm::Single(conn), + routing_types::RoutingAlgorithm::Priority(plist) => CachedAlgorithm::Priority(plist), + routing_types::RoutingAlgorithm::VolumeSplit(splits) => { + CachedAlgorithm::VolumeSplit(splits) + } + routing_types::RoutingAlgorithm::Advanced(program) => { + let interpreter = backend::VirInterpreterBackend::with_program(program) + .into_report() + .change_context(errors::RoutingError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + + CachedAlgorithm::Advanced(interpreter) + } + }; + + ROUTING_CACHE + .save(key, cached_algorithm, timestamp) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + + Ok(()) +} + +pub fn perform_volume_split( + mut splits: Vec, + rng_seed: Option<&str>, +) -> RoutingResult> { + let weights: Vec = splits.iter().map(|sp| sp.split).collect(); + let weighted_index = distributions::WeightedIndex::new(weights) + .into_report() + .change_context(errors::RoutingError::VolumeSplitFailed) + .attach_printable("Error creating weighted distribution for volume split")?; + + let idx = if let Some(seed) = rng_seed { + let mut hasher = hash_map::DefaultHasher::new(); + seed.hash(&mut hasher); + let hash = hasher.finish(); + + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(hash); + weighted_index.sample(&mut rng) + } else { + let mut rng = rand::thread_rng(); + weighted_index.sample(&mut rng) + }; + + splits + .get(idx) + .ok_or(errors::RoutingError::VolumeSplitFailed) + .into_report() + .attach_printable("Volume split index lookup failed")?; + + // Panic Safety: We have performed a `get(idx)` operation just above which will + // ensure that the index is always present, else throw an error. + let removed = splits.remove(idx); + splits.insert(0, removed); + + Ok(splits.into_iter().map(|sp| sp.connector).collect()) +} + +pub async fn get_merchant_kgraph<'a>( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult>> { + #[cfg(feature = "business_profile_routing")] + let key = { + let profile_id = profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + format!("kgraph_{}_{profile_id}", key_store.merchant_id) + }; + + #[cfg(not(feature = "business_profile_routing"))] + let key = format!("kgraph_{}", key_store.merchant_id); + + let kgraph_present = KGRAPH_CACHE + .present(&key) + .into_report() + .change_context(errors::RoutingError::KgraphCacheFailure) + .attach_printable("when checking kgraph presence")?; + + let kgraph_expired = KGRAPH_CACHE + .expired(&key, merchant_last_modified) + .into_report() + .change_context(errors::RoutingError::KgraphCacheFailure) + .attach_printable("when checking kgraph expiry")?; + + if !kgraph_present || kgraph_expired { + refresh_kgraph_cache( + state, + key_store, + merchant_last_modified, + key.clone(), + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + } + + let cached_kgraph = KGRAPH_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("when retrieving kgraph")?; + + Ok(cached_kgraph) +} + +pub async fn refresh_kgraph_cache( + state: &AppState, + key_store: &domain::MerchantKeyStore, + timestamp: i64, + key: String, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult<()> { + let mut merchant_connector_accounts = state + .store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + &key_store.merchant_id, + false, + key_store, + ) + .await + .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; + + merchant_connector_accounts + .retain(|mca| mca.connector_type != storage_enums::ConnectorType::PaymentVas); + + #[cfg(feature = "business_profile_routing")] + let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( + merchant_connector_accounts, + profile_id, + ); + + let api_mcas: Vec = merchant_connector_accounts + .into_iter() + .map(|acct| acct.try_into()) + .collect::>() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; + + let kgraph = mca_graph::make_mca_graph(api_mcas) + .into_report() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed) + .attach_printable("when construction kgraph")?; + + KGRAPH_CACHE + .save(key, kgraph, timestamp) + .into_report() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed) + .attach_printable("when saving kgraph to cache")?; + + Ok(()) +} + +async fn perform_kgraph_filtering( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + backend_input: dsl_inputs::BackendInput, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let context = euclid_graph::AnalysisContext::from_dir_values( + backend_input + .into_context() + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?, + ); + let cached_kgraph = get_merchant_kgraph( + state, + key_store, + merchant_last_modified, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + + let mut final_selection = Vec::::new(); + for choice in chosen { + let routable_connector = choice.connector; + let euclid_choice: ast::ConnectorChoice = choice.clone().foreign_into(); + let dir_val = euclid_choice + .into_dir_value() + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?; + let kgraph_eligible = cached_kgraph + .check_value_validity(dir_val, &context, &mut Memoization::new()) + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?; + + let filter_eligible = + eligible_connectors.map_or(true, |list| list.contains(&routable_connector)); + + if kgraph_eligible && filter_eligible { + final_selection.push(choice); + } + } + + Ok(final_selection) +} + +pub async fn perform_eligibility_analysis( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let backend_input = make_dsl_input(payment_data)?; + + perform_kgraph_filtering( + state, + key_store, + merchant_last_modified, + chosen, + backend_input, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await +} + +pub async fn perform_fallback_routing( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let fallback_config = + routing_helpers::get_merchant_default_config(&*state.store, &key_store.merchant_id) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let backend_input = make_dsl_input(payment_data)?; + + perform_kgraph_filtering( + state, + key_store, + merchant_last_modified, + fallback_config, + backend_input, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await +} + +pub async fn perform_eligibility_analysis_with_fallback( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let mut final_selection = perform_eligibility_analysis( + state, + key_store, + merchant_last_modified, + chosen, + payment_data, + eligible_connectors.as_ref(), + #[cfg(feature = "business_profile_routing")] + profile_id.clone(), + ) + .await?; + + let fallback_selection = perform_fallback_routing( + state, + key_store, + merchant_last_modified, + payment_data, + eligible_connectors.as_ref(), + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await; + + final_selection.append( + &mut fallback_selection + .unwrap_or_default() + .iter() + .filter(|&routable_connector_choice| { + !final_selection.contains(routable_connector_choice) + }) + .cloned() + .collect::>(), + ); + + let final_selected_connectors = final_selection + .iter() + .map(|item| item.connector) + .collect::>(); + logger::debug!(final_selected_connectors_for_routing=?final_selected_connectors, "List of final selected connectors for routing"); + + Ok(final_selection) +} + +pub async fn perform_session_flow_routing( + session_input: SessionFlowRoutingInput<'_>, +) -> RoutingResult> { + let mut pm_type_map: FxHashMap> = + FxHashMap::default(); + let merchant_last_modified = session_input + .merchant_account + .modified_at + .assume_utc() + .unix_timestamp(); + + #[cfg(feature = "business_profile_routing")] + let routing_algorithm: MerchantAccountRoutingAlgorithm = { + let profile_id = session_input + .payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + let business_profile = session_input + .state + .store + .find_business_profile_by_profile_id(&profile_id) + .await + .change_context(errors::RoutingError::ProfileNotFound)?; + + business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("MerchantAccountRoutingAlgorithm")) + .transpose() + .change_context(errors::RoutingError::InvalidRoutingAlgorithmStructure)? + .unwrap_or_default() + }; + + #[cfg(not(feature = "business_profile_routing"))] + let routing_algorithm: MerchantAccountRoutingAlgorithm = { + session_input + .merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("MerchantAccountRoutingAlgorithm")) + .transpose() + .change_context(errors::RoutingError::InvalidRoutingAlgorithmStructure)? + .unwrap_or_default() + }; + + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: None, + payment_method_type: None, + card_network: None, + }; + + let payment_input = dsl_inputs::PaymentInput { + amount: session_input.payment_intent.amount, + currency: session_input + .payment_intent + .currency + .get_required_value("Currency") + .change_context(errors::RoutingError::DslMissingRequiredField { + field_name: "currency".to_string(), + })?, + authentication_type: session_input.payment_attempt.authentication_type, + card_bin: None, + capture_method: session_input + .payment_attempt + .capture_method + .and_then(|cm| cm.foreign_into()), + business_country: session_input + .payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: session_input + .country + .map(storage_enums::Country::from_alpha2), + business_label: session_input.payment_intent.business_label.clone(), + setup_future_usage: session_input.payment_intent.setup_future_usage, + }; + + let metadata = session_input + .payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(?err); + None + }); + + let mut backend_input = dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + for connector_data in session_input.chosen.iter() { + pm_type_map + .entry(connector_data.payment_method_type) + .or_default() + .insert( + connector_data.connector.connector_name.to_string(), + connector_data.connector.get_token.clone(), + ); + } + + let mut result: FxHashMap = + FxHashMap::default(); + + for (pm_type, allowed_connectors) in pm_type_map { + let euclid_pmt: euclid_enums::PaymentMethodType = pm_type; + let euclid_pm: euclid_enums::PaymentMethod = euclid_pmt.into(); + + backend_input.payment_method.payment_method = Some(euclid_pm); + backend_input.payment_method.payment_method_type = Some(euclid_pmt); + + let session_pm_input = SessionRoutingPmTypeInput { + state: session_input.state, + key_store: session_input.key_store, + merchant_last_modified, + attempt_id: &session_input.payment_attempt.attempt_id, + routing_algorithm: &routing_algorithm, + backend_input: backend_input.clone(), + allowed_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id: session_input.payment_intent.clone().profile_id, + }; + let maybe_choice = perform_session_routing_for_pm_type(session_pm_input).await?; + + // (connector, sub_label) + if let Some(data) = maybe_choice { + result.insert( + pm_type, + routing_types::SessionRoutingChoice { + connector: data.0, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: data.1, + payment_method_type: pm_type, + }, + ); + } + } + + Ok(result) +} + +async fn perform_session_routing_for_pm_type( + session_pm_input: SessionRoutingPmTypeInput<'_>, +) -> RoutingResult)>> { + let merchant_id = &session_pm_input.key_store.merchant_id; + + let chosen_connectors = match session_pm_input.routing_algorithm { + MerchantAccountRoutingAlgorithm::V1(algorithm_ref) => { + if let Some(ref algorithm_id) = algorithm_ref.algorithm_id { + let key = ensure_algorithm_cached_v1( + &session_pm_input.state.clone(), + merchant_id, + algorithm_ref.timestamp, + algorithm_id, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + + let cached_algorithm = ROUTING_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("unable to retrieve cached routing algorithm")?; + + match cached_algorithm.as_ref() { + CachedAlgorithm::Single(conn) => vec![(**conn).clone()], + CachedAlgorithm::Priority(plist) => plist.clone(), + CachedAlgorithm::VolumeSplit(splits) => { + perform_volume_split(splits.to_vec(), Some(session_pm_input.attempt_id)) + .change_context(errors::RoutingError::ConnectorSelectionFailed)? + } + CachedAlgorithm::Advanced(interpreter) => execute_dsl_and_get_connector_v1( + session_pm_input.backend_input.clone(), + interpreter, + )?, + } + } else { + routing_helpers::get_merchant_default_config( + &*session_pm_input.state.clone().store, + merchant_id, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)? + } + } + }; + + let mut final_selection = perform_kgraph_filtering( + &session_pm_input.state.clone(), + session_pm_input.key_store, + session_pm_input.merchant_last_modified, + chosen_connectors, + session_pm_input.backend_input.clone(), + None, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + + if final_selection.is_empty() { + let fallback = routing_helpers::get_merchant_default_config( + &*session_pm_input.state.clone().store, + merchant_id, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + + final_selection = perform_kgraph_filtering( + &session_pm_input.state.clone(), + session_pm_input.key_store, + session_pm_input.merchant_last_modified, + fallback, + session_pm_input.backend_input, + None, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + } + + let mut final_choice: Option<(api::ConnectorData, Option)> = None; + + for selection in final_selection { + let connector_name = selection.connector.to_string(); + if let Some(get_token) = session_pm_input.allowed_connectors.get(&connector_name) { + let connector_data = api::ConnectorData::get_connector_by_name( + &session_pm_input.state.clone().conf.connectors, + &connector_name, + get_token.clone(), + #[cfg(feature = "connector_choice_mca_id")] + selection.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + .change_context(errors::RoutingError::InvalidConnectorName(connector_name))?; + #[cfg(not(feature = "connector_choice_mca_id"))] + let sub_label = selection.sub_label; + #[cfg(feature = "connector_choice_mca_id")] + let sub_label = None; + + final_choice = Some((connector_data, sub_label)); + break; + } + } + + Ok(final_choice) +} diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs new file mode 100644 index 000000000000..de94a36248ff --- /dev/null +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -0,0 +1,121 @@ +use api_models::{self, enums as api_enums, routing as routing_types}; +use diesel_models::enums as storage_enums; +use euclid::{enums as dsl_enums, frontend::ast as dsl_ast}; + +use crate::types::transformers::{ForeignFrom, ForeignInto}; + +impl ForeignFrom for dsl_ast::ConnectorChoice { + fn foreign_from(from: routing_types::RoutableConnectorChoice) -> Self { + Self { + // #[cfg(feature = "backwards_compatibility")] + // choice_kind: from.choice_kind.foreign_into(), + connector: from.connector.foreign_into(), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: from.sub_label, + } + } +} + +impl ForeignFrom for Option { + fn foreign_from(value: storage_enums::CaptureMethod) -> Self { + match value { + storage_enums::CaptureMethod::Automatic => Some(dsl_enums::CaptureMethod::Automatic), + storage_enums::CaptureMethod::Manual => Some(dsl_enums::CaptureMethod::Manual), + _ => None, + } + } +} + +impl ForeignFrom for dsl_enums::MandateAcceptanceType { + fn foreign_from(from: api_models::payments::AcceptanceType) -> Self { + match from { + api_models::payments::AcceptanceType::Online => Self::Online, + api_models::payments::AcceptanceType::Offline => Self::Offline, + } + } +} + +impl ForeignFrom for dsl_enums::MandateType { + fn foreign_from(from: api_models::payments::MandateType) -> Self { + match from { + api_models::payments::MandateType::MultiUse(_) => Self::MultiUse, + api_models::payments::MandateType::SingleUse(_) => Self::SingleUse, + } + } +} + +impl ForeignFrom for dsl_enums::MandateType { + fn foreign_from(from: storage_enums::MandateDataType) -> Self { + match from { + storage_enums::MandateDataType::MultiUse(_) => Self::MultiUse, + storage_enums::MandateDataType::SingleUse(_) => Self::SingleUse, + } + } +} + +impl ForeignFrom for dsl_enums::Connector { + fn foreign_from(from: api_enums::RoutableConnectors) -> Self { + match from { + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector7 => Self::DummyConnector7, + api_enums::RoutableConnectors::Aci => Self::Aci, + api_enums::RoutableConnectors::Adyen => Self::Adyen, + api_enums::RoutableConnectors::Airwallex => Self::Airwallex, + api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, + api_enums::RoutableConnectors::Bitpay => Self::Bitpay, + api_enums::RoutableConnectors::Bambora => Self::Bambora, + api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, + api_enums::RoutableConnectors::Boku => Self::Boku, + api_enums::RoutableConnectors::Braintree => Self::Braintree, + api_enums::RoutableConnectors::Cashtocode => Self::Cashtocode, + api_enums::RoutableConnectors::Checkout => Self::Checkout, + api_enums::RoutableConnectors::Coinbase => Self::Coinbase, + api_enums::RoutableConnectors::Cryptopay => Self::Cryptopay, + api_enums::RoutableConnectors::Cybersource => Self::Cybersource, + api_enums::RoutableConnectors::Dlocal => Self::Dlocal, + api_enums::RoutableConnectors::Fiserv => Self::Fiserv, + api_enums::RoutableConnectors::Forte => Self::Forte, + api_enums::RoutableConnectors::Globalpay => Self::Globalpay, + api_enums::RoutableConnectors::Globepay => Self::Globepay, + api_enums::RoutableConnectors::Gocardless => Self::Gocardless, + api_enums::RoutableConnectors::Helcim => Self::Helcim, + api_enums::RoutableConnectors::Iatapay => Self::Iatapay, + api_enums::RoutableConnectors::Klarna => Self::Klarna, + api_enums::RoutableConnectors::Mollie => Self::Mollie, + api_enums::RoutableConnectors::Multisafepay => Self::Multisafepay, + api_enums::RoutableConnectors::Nexinets => Self::Nexinets, + api_enums::RoutableConnectors::Nmi => Self::Nmi, + api_enums::RoutableConnectors::Noon => Self::Noon, + api_enums::RoutableConnectors::Nuvei => Self::Nuvei, + api_enums::RoutableConnectors::Opennode => Self::Opennode, + api_enums::RoutableConnectors::Payme => Self::Payme, + api_enums::RoutableConnectors::Paypal => Self::Paypal, + api_enums::RoutableConnectors::Payu => Self::Payu, + api_enums::RoutableConnectors::Powertranz => Self::Powertranz, + api_enums::RoutableConnectors::Rapyd => Self::Rapyd, + api_enums::RoutableConnectors::Shift4 => Self::Shift4, + api_enums::RoutableConnectors::Square => Self::Square, + api_enums::RoutableConnectors::Stax => Self::Stax, + api_enums::RoutableConnectors::Stripe => Self::Stripe, + api_enums::RoutableConnectors::Trustpay => Self::Trustpay, + api_enums::RoutableConnectors::Tsys => Self::Tsys, + api_enums::RoutableConnectors::Volt => Self::Volt, + api_enums::RoutableConnectors::Wise => Self::Wise, + api_enums::RoutableConnectors::Worldline => Self::Worldline, + api_enums::RoutableConnectors::Worldpay => Self::Worldpay, + api_enums::RoutableConnectors::Zen => Self::Zen, + } + } +} diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs new file mode 100644 index 000000000000..8033cc792b54 --- /dev/null +++ b/crates/router/src/core/routing.rs @@ -0,0 +1,713 @@ +pub mod helpers; +pub mod transformers; + +use api_models::routing as routing_types; +#[cfg(feature = "business_profile_routing")] +use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +#[cfg(not(feature = "business_profile_routing"))] +use common_utils::ext_traits::{Encode, StringExt}; +#[cfg(not(feature = "business_profile_routing"))] +use diesel_models::configs; +#[cfg(feature = "business_profile_routing")] +use diesel_models::routing_algorithm::RoutingAlgorithm; +use error_stack::{IntoReport, ResultExt}; +use rustc_hash::FxHashSet; + +#[cfg(feature = "business_profile_routing")] +use crate::core::utils::validate_and_get_business_profile; +#[cfg(feature = "business_profile_routing")] +use crate::types::transformers::{ForeignInto, ForeignTryInto}; +use crate::{ + consts, + core::errors::{RouterResponse, StorageErrorExt}, + routes::AppState, + types::domain, + utils::{self, OptionExt, ValueExt}, +}; +#[cfg(not(feature = "business_profile_routing"))] +use crate::{core::errors, services::api as service_api, types::storage}; +#[cfg(feature = "business_profile_routing")] +use crate::{errors, services::api as service_api}; + +pub async fn retrieve_merchant_routing_dictionary( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, +) -> RouterResponse { + #[cfg(feature = "business_profile_routing")] + { + let routing_metadata = state + .store + .list_routing_algorithm_metadata_by_merchant_id( + &merchant_account.merchant_id, + i64::from(query_params.limit.unwrap_or_default()), + i64::from(query_params.offset.unwrap_or_default()), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + let result = routing_metadata + .into_iter() + .map(ForeignInto::foreign_into) + .collect::>(); + + Ok(service_api::ApplicationResponse::Json( + routing_types::RoutingKind::RoutingAlgorithm(result), + )) + } + #[cfg(not(feature = "business_profile_routing"))] + Ok(service_api::ApplicationResponse::Json( + routing_types::RoutingKind::Config( + helpers::get_merchant_routing_dictionary( + state.store.as_ref(), + &merchant_account.merchant_id, + ) + .await?, + ), + )) +} + +pub async fn create_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + request: routing_types::RoutingConfigRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + + let name = request + .name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "name" }) + .attach_printable("Name of config not given")?; + + let description = request + .description + .get_required_value("description") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "description", + }) + .attach_printable("Description of config not given")?; + + let algorithm = request + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Algorithm of config not given")?; + + let algorithm_id = common_utils::generate_id( + consts::ROUTING_CONFIG_ID_LENGTH, + &format!("routing_{}", &merchant_account.merchant_id), + ); + + #[cfg(feature = "business_profile_routing")] + { + let profile_id = request + .profile_id + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }) + .attach_printable("Profile_id not provided")?; + + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await?; + + helpers::validate_connectors_in_routing_config( + db, + &key_store, + &merchant_account.merchant_id, + &profile_id, + &algorithm, + ) + .await?; + + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id: algorithm_id.clone(), + profile_id, + merchant_id: merchant_account.merchant_id, + name: name.clone(), + description: Some(description.clone()), + kind: algorithm.get_kind().foreign_into(), + algorithm_data: serde_json::json!(algorithm), + created_at: timestamp, + modified_at: timestamp, + }; + let record = db + .insert_routing_algorithm(algo) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let new_record = record.foreign_into(); + + Ok(service_api::ApplicationResponse::Json(new_record)) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let algorithm_str = + utils::Encode::::encode_to_string_of_json(&algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing algorithm to string")?; + + let mut algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + utils::when( + merchant_dictionary.records.len() >= consts::MAX_ROUTING_CONFIGS_PER_MERCHANT, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!("Reached the maximum number of routing configs ({}), please delete some to create new ones", consts::MAX_ROUTING_CONFIGS_PER_MERCHANT), + }) + .into_report() + }, + )?; + let timestamp = common_utils::date_time::now_unix_timestamp(); + let records_are_empty = merchant_dictionary.records.is_empty(); + + let new_record = routing_types::RoutingDictionaryRecord { + id: algorithm_id.clone(), + name: name.clone(), + kind: algorithm.get_kind(), + description: description.clone(), + created_at: timestamp, + modified_at: timestamp, + }; + merchant_dictionary.records.push(new_record.clone()); + + let new_algorithm_config = configs::ConfigNew { + key: algorithm_id.clone(), + config: algorithm_str, + }; + + db.insert_config(new_algorithm_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to save new routing algorithm config to DB")?; + + if records_are_empty { + merchant_dictionary.active_id = Some(algorithm_id.clone()); + algorithm_ref.update_algorithm_id(algorithm_id); + helpers::update_merchant_active_algorithm_ref(db, &key_store, algorithm_ref).await?; + } + + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + + Ok(service_api::ApplicationResponse::Json(new_record)) + } +} + +pub async fn link_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, + algorithm_id: String, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let routing_algorithm = db + .find_routing_algorithm_by_algorithm_id_merchant_id( + &algorithm_id, + &merchant_account.merchant_id, + ) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let business_profile = validate_and_get_business_profile( + db, + Some(&routing_algorithm.profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: routing_algorithm.profile_id.clone(), + })?; + + let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + + utils::when( + routing_ref.algorithm_id == Some(algorithm_id.clone()), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + .into_report() + }, + )?; + + routing_ref.update_algorithm_id(algorithm_id); + helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) + .await?; + + Ok(service_api::ApplicationResponse::Json( + routing_algorithm.foreign_into(), + )) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let mut routing_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + + utils::when( + routing_ref.algorithm_id == Some(algorithm_id.clone()), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + .into_report() + }, + )?; + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let modified_at = common_utils::date_time::now_unix_timestamp(); + let record = merchant_dictionary + .records + .iter_mut() + .find(|rec| rec.id == algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Record with given ID not found for routing config activation")?; + + record.modified_at = modified_at; + merchant_dictionary.active_id = Some(record.id.clone()); + let response = record.clone(); + routing_ref.update_algorithm_id(algorithm_id); + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} + +pub async fn retrieve_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + algorithm_id: String, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let routing_algorithm = db + .find_routing_algorithm_by_algorithm_id_merchant_id( + &algorithm_id, + &merchant_account.merchant_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + validate_and_get_business_profile( + db, + Some(&routing_algorithm.profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let response = routing_algorithm + .foreign_try_into() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse routing algorithm")?; + Ok(service_api::ApplicationResponse::Json(response)) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let record = merchant_dictionary + .records + .into_iter() + .find(|rec| rec.id == algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Algorithm with the given ID not found in the merchant dictionary")?; + + let algorithm_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("Routing config not found in DB")?; + + let algorithm: routing_types::RoutingAlgorithm = algorithm_config + .config + .parse_struct("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error deserializing routing algorithm config")?; + + let response = routing_types::MerchantRoutingAlgorithm { + id: record.id, + name: record.name, + description: record.description, + algorithm, + created_at: record.created_at, + modified_at: record.modified_at, + }; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} +pub async fn unlink_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, + #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let profile_id = request + .profile_id + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }) + .attach_printable("Profile_id not provided")?; + let business_profile = + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await?; + match business_profile { + Some(business_profile) => { + let routing_algo_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from merchant account", + )? + .unwrap_or_default(); + + let timestamp = common_utils::date_time::now_unix_timestamp(); + + match routing_algo_ref.algorithm_id { + Some(algorithm_id) => { + let routing_algorithm: routing_types::RoutingAlgorithmRef = + routing_types::RoutingAlgorithmRef { + algorithm_id: None, + timestamp, + config_algo_id: routing_algo_ref.config_algo_id.clone(), + surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id, + }; + + let record = db + .find_routing_algorithm_by_profile_id_algorithm_id( + &profile_id, + &algorithm_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + let response = record.foreign_into(); + helpers::update_business_profile_active_algorithm_ref( + db, + business_profile, + routing_algorithm, + ) + .await?; + Ok(service_api::ApplicationResponse::Json(response)) + } + None => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + }) + .into_report()?, + } + } + None => Err(errors::ApiErrorResponse::InvalidRequestData { + message: "The business_profile is not present".to_string(), + } + .into()), + } + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let routing_algo_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + let timestamp = common_utils::date_time::now_unix_timestamp(); + + utils::when(routing_algo_ref.algorithm_id.is_none(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + }) + .into_report() + })?; + let routing_algorithm: routing_types::RoutingAlgorithmRef = + routing_types::RoutingAlgorithmRef { + algorithm_id: None, + timestamp, + config_algo_id: routing_algo_ref.config_algo_id.clone(), + surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id, + }; + + let active_algorithm_id = merchant_dictionary + .active_id + .or(routing_algo_ref.algorithm_id.clone()) + .ok_or(errors::ApiErrorResponse::PreconditionFailed { + // When the merchant_dictionary doesn't have any active algorithm and merchant_account doesn't have any routing_algorithm configured + message: "Algorithm is already inactive".to_string(), + }) + .into_report()?; + + let record = merchant_dictionary + .records + .iter_mut() + .find(|rec| rec.id == active_algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Record with the given ID not found for de-activation")?; + + let response = record.clone(); + + merchant_dictionary.active_id = None; + + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + + let ref_value = + Encode::::encode_to_value(&routing_algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed converting routing algorithm ref to json value")?; + + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + locker_id: None, + metadata: None, + routing_algorithm: Some(ref_value), + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + db.update_specific_fields_in_merchant( + &key_store.merchant_id, + merchant_account_update, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in merchant account")?; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} + +pub async fn update_default_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + updated_config: Vec, +) -> RouterResponse> { + let db = state.store.as_ref(); + let default_config = + helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; + + utils::when(default_config.len() != updated_config.len(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "current config and updated config have different lengths".to_string(), + }) + .into_report() + })?; + + let existing_set: FxHashSet = + FxHashSet::from_iter(default_config.iter().map(|c| c.to_string())); + let updated_set: FxHashSet = + FxHashSet::from_iter(updated_config.iter().map(|c| c.to_string())); + + let symmetric_diff: Vec = existing_set + .symmetric_difference(&updated_set) + .cloned() + .collect(); + + utils::when(!symmetric_diff.is_empty(), || { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector mismatch between old and new configs ({})", + symmetric_diff.join(", ") + ), + }) + .into_report() + })?; + + helpers::update_merchant_default_config( + db, + &merchant_account.merchant_id, + updated_config.clone(), + ) + .await?; + + Ok(service_api::ApplicationResponse::Json(updated_config)) +} + +pub async fn retrieve_default_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse> { + let db = state.store.as_ref(); + + helpers::get_merchant_default_config(db, &merchant_account.merchant_id) + .await + .map(service_api::ApplicationResponse::Json) +} + +pub async fn retrieve_linked_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery, +) -> RouterResponse { + let db = state.store.as_ref(); + + #[cfg(feature = "business_profile_routing")] + { + let business_profiles = if let Some(profile_id) = query_params.profile_id { + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await? + .map(|profile| vec![profile]) + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id, + })? + } else { + db.list_business_profile_by_merchant_id(&merchant_account.merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)? + }; + + let mut active_algorithms = Vec::new(); + + for business_profile in business_profiles { + let routing_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from merchant account", + )? + .unwrap_or_default(); + + if let Some(algorithm_id) = routing_ref.algorithm_id { + let record = db + .find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &algorithm_id, + &business_profile.profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + active_algorithms.push(record.foreign_into()); + } + } + + Ok(service_api::ApplicationResponse::Json( + routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms), + )) + } + #[cfg(not(feature = "business_profile_routing"))] + { + let merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let algorithm = if let Some(algorithm_id) = merchant_dictionary.active_id { + let record = merchant_dictionary + .records + .into_iter() + .find(|rec| rec.id == algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("record for active algorithm not found in merchant dictionary")?; + + let config = db + .find_config_by_key(&algorithm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error finding routing config in db")?; + + let the_algorithm: routing_types::RoutingAlgorithm = config + .config + .parse_struct("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse routing algorithm")?; + + Some(routing_types::MerchantRoutingAlgorithm { + id: record.id, + name: record.name, + description: record.description, + algorithm: the_algorithm, + created_at: record.created_at, + modified_at: record.modified_at, + }) + } else { + None + }; + + let response = routing_types::LinkedRoutingConfigRetrieveResponse::MerchantAccountBased( + routing_types::RoutingRetrieveResponse { algorithm }, + ); + + Ok(service_api::ApplicationResponse::Json(response)) + } +} diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs new file mode 100644 index 000000000000..6eec39f53bc6 --- /dev/null +++ b/crates/router/src/core/routing/helpers.rs @@ -0,0 +1,479 @@ +//! Analysis for usage of all helper functions for use case of routing +//! +//! Functions that are used to perform the retrieval of merchant's +//! routing dict, configs, defaults +use api_models::routing as routing_types; +use common_utils::ext_traits::Encode; +use diesel_models::{ + business_profile::{BusinessProfile, BusinessProfileUpdateInternal}, + configs, +}; +use error_stack::ResultExt; +use rustc_hash::FxHashSet; + +use crate::{ + core::errors::{self, RouterResult}, + db::StorageInterface, + types::{domain, storage}, + utils::{self, StringExt}, +}; + +/// provides the complete merchant routing dictionary that is basically a list of all the routing +/// configs a merchant configured with an active_id field that specifies the current active routing +/// config +pub async fn get_merchant_routing_dictionary( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult { + let key = get_routing_dictionary_key(merchant_id); + let maybe_dict = db.find_config_by_key(&key).await; + + match maybe_dict { + Ok(config) => config + .config + .parse_struct("RoutingDictionary") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant routing dictionary has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_dictionary = routing_types::RoutingDictionary { + merchant_id: merchant_id.to_string(), + active_id: None, + records: Vec::new(), + }; + + let serialized = + utils::Encode::::encode_to_string_of_json( + &new_dictionary, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing newly created merchant dictionary")?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting new routing dictionary for merchant")?; + + Ok(new_dictionary) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching routing dictionary for merchant"), + } +} + +/// Provides us with all the configured configs of the Merchant in the ascending time configured +/// manner and chooses the first of them +pub async fn get_merchant_default_config( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult> { + let key = get_default_config_key(merchant_id); + let maybe_config = db.find_config_by_key(&key).await; + + match maybe_config { + Ok(config) => config + .config + .parse_struct("Vec") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant default config has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_config_conns = Vec::::new(); + let serialized = + utils::Encode::>::encode_to_string_of_json( + &new_config_conns, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Error while creating and serializing new merchant default config", + )?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting new default routing config into DB")?; + + Ok(new_config_conns) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching default config for merchant"), + } +} + +/// Merchant's already created config can be updated and this change will be reflected +/// in DB as well for the particular updated config +pub async fn update_merchant_default_config( + db: &dyn StorageInterface, + merchant_id: &str, + connectors: Vec, +) -> RouterResult<()> { + let key = get_default_config_key(merchant_id); + let config_str = + Encode::>::encode_to_string_of_json( + &connectors, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize merchant default routing config during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(config_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating the default routing config in DB")?; + + Ok(()) +} + +pub async fn update_merchant_routing_dictionary( + db: &dyn StorageInterface, + merchant_id: &str, + dictionary: routing_types::RoutingDictionary, +) -> RouterResult<()> { + let key = get_routing_dictionary_key(merchant_id); + let dictionary_str = + Encode::::encode_to_string_of_json(&dictionary) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing dictionary during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(dictionary_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error saving routing dictionary to DB")?; + + Ok(()) +} + +pub async fn update_routing_algorithm( + db: &dyn StorageInterface, + algorithm_id: String, + algorithm: routing_types::RoutingAlgorithm, +) -> RouterResult<()> { + let algorithm_str = + Encode::::encode_to_string_of_json(&algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing algorithm to string")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(algorithm_str), + }; + + db.update_config_by_key(&algorithm_id, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating the routing algorithm in DB")?; + + Ok(()) +} + +/// This will help make one of all configured algorithms to be in active state for a particular +/// merchant +pub async fn update_merchant_active_algorithm_ref( + db: &dyn StorageInterface, + key_store: &domain::MerchantKeyStore, + algorithm_id: routing_types::RoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_value = Encode::::encode_to_value(&algorithm_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed converting routing algorithm ref to json value")?; + + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + locker_id: None, + metadata: None, + routing_algorithm: Some(ref_value), + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + db.update_specific_fields_in_merchant( + &key_store.merchant_id, + merchant_account_update, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in merchant account")?; + + Ok(()) +} + +pub async fn update_business_profile_active_algorithm_ref( + db: &dyn StorageInterface, + current_business_profile: BusinessProfile, + algorithm_id: routing_types::RoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_val = Encode::::encode_to_value(&algorithm_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert routing ref to value")?; + + let business_profile_update = BusinessProfileUpdateInternal { + profile_name: None, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + routing_algorithm: Some(ref_val), + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + applepay_verified_domains: None, + modified_at: None, + is_recon_enabled: None, + }; + db.update_business_profile_by_profile_id(current_business_profile, business_profile_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in business profile")?; + Ok(()) +} + +pub async fn get_merchant_connector_agnostic_mandate_config( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult> { + let key = get_pg_agnostic_mandate_config_key(merchant_id); + let maybe_config = db.find_config_by_key(&key).await; + + match maybe_config { + Ok(config) => config + .config + .parse_struct("Vec") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("pg agnostic mandate config has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_mandate_config: Vec = Vec::new(); + + let serialized = + utils::Encode::>::encode_to_string_of_json( + &new_mandate_config, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error serializing newly created pg agnostic mandate config")?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting new pg agnostic mandate config in db")?; + + Ok(new_mandate_config) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching pg agnostic mandate config for merchant from db"), + } +} + +pub async fn update_merchant_connector_agnostic_mandate_config( + db: &dyn StorageInterface, + merchant_id: &str, + mandate_config: Vec, +) -> RouterResult> { + let key = get_pg_agnostic_mandate_config_key(merchant_id); + let mandate_config_str = + Encode::>::encode_to_string_of_json( + &mandate_config, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to serialize pg agnostic mandate config during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(mandate_config_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error saving pg agnostic mandate config to db")?; + + Ok(mandate_config) +} + +pub async fn validate_connectors_in_routing_config( + db: &dyn StorageInterface, + key_store: &domain::MerchantKeyStore, + merchant_id: &str, + profile_id: &str, + routing_algorithm: &routing_types::RoutingAlgorithm, +) -> RouterResult<()> { + let all_mcas = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_id.to_string(), + })?; + + #[cfg(feature = "connector_choice_mca_id")] + let name_mca_id_set = all_mcas + .iter() + .filter(|mca| mca.profile_id.as_deref() == Some(profile_id)) + .map(|mca| (&mca.connector_name, &mca.merchant_connector_id)) + .collect::>(); + + let name_set = all_mcas + .iter() + .filter(|mca| mca.profile_id.as_deref() == Some(profile_id)) + .map(|mca| &mca.connector_name) + .collect::>(); + + #[cfg(feature = "connector_choice_mca_id")] + let check_connector_choice = |choice: &routing_types::RoutableConnectorChoice| { + if let Some(ref mca_id) = choice.merchant_connector_id { + error_stack::ensure!( + name_mca_id_set.contains(&(&choice.connector.to_string(), mca_id)), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' and merchant connector account id '{}' not found for the given profile", + choice.connector, + mca_id, + ) + } + ); + } else { + error_stack::ensure!( + name_set.contains(&choice.connector.to_string()), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' not found for the given profile", + choice.connector, + ) + } + ); + } + + Ok(()) + }; + + #[cfg(not(feature = "connector_choice_mca_id"))] + let check_connector_choice = |choice: &routing_types::RoutableConnectorChoice| { + error_stack::ensure!( + name_set.contains(&choice.connector.to_string()), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' not found for the given profile", + choice.connector, + ) + } + ); + + Ok(()) + }; + + match routing_algorithm { + routing_types::RoutingAlgorithm::Single(choice) => { + check_connector_choice(choice)?; + } + + routing_types::RoutingAlgorithm::Priority(list) => { + for choice in list { + check_connector_choice(choice)?; + } + } + + routing_types::RoutingAlgorithm::VolumeSplit(splits) => { + for split in splits { + check_connector_choice(&split.connector)?; + } + } + + routing_types::RoutingAlgorithm::Advanced(program) => { + let check_connector_selection = + |selection: &routing_types::ConnectorSelection| -> RouterResult<()> { + match selection { + routing_types::ConnectorSelection::VolumeSplit(splits) => { + for split in splits { + check_connector_choice(&split.connector)?; + } + } + + routing_types::ConnectorSelection::Priority(list) => { + for choice in list { + check_connector_choice(choice)?; + } + } + } + + Ok(()) + }; + + check_connector_selection(&program.default_selection)?; + + for rule in &program.rules { + check_connector_selection(&rule.connector_selection)?; + } + } + } + + Ok(()) +} + +/// Provides the identifier for the specific merchant's routing_dictionary_key +#[inline(always)] +pub fn get_routing_dictionary_key(merchant_id: &str) -> String { + format!("routing_dict_{merchant_id}") +} + +/// Provides the identifier for the specific merchant's agnostic_mandate_config +#[inline(always)] +pub fn get_pg_agnostic_mandate_config_key(merchant_id: &str) -> String { + format!("pg_agnostic_mandate_{merchant_id}") +} + +/// Provides the identifier for the specific merchant's default_config +#[inline(always)] +pub fn get_default_config_key(merchant_id: &str) -> String { + format!("routing_default_{merchant_id}") +} +pub fn get_payment_config_routing_id(merchant_id: &str) -> String { + format!("payment_config_id_{merchant_id}") +} + +pub fn get_payment_method_surcharge_routing_id(merchant_id: &str) -> String { + format!("payment_method_surcharge_id_{merchant_id}") +} diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs new file mode 100644 index 000000000000..e5f1f1e1d5f0 --- /dev/null +++ b/crates/router/src/core/routing/transformers.rs @@ -0,0 +1,86 @@ +use api_models::routing::{ + MerchantRoutingAlgorithm, RoutingAlgorithm as Algorithm, RoutingAlgorithmKind, + RoutingDictionaryRecord, +}; +use common_utils::ext_traits::ValueExt; +use diesel_models::{ + enums as storage_enums, + routing_algorithm::{RoutingAlgorithm, RoutingProfileMetadata}, +}; + +use crate::{ + core::errors, + types::transformers::{ForeignFrom, ForeignInto, ForeignTryFrom}, +}; + +impl ForeignFrom for RoutingDictionaryRecord { + fn foreign_from(value: RoutingProfileMetadata) -> Self { + Self { + id: value.algorithm_id, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + name: value.name, + + kind: value.kind.foreign_into(), + description: value.description.unwrap_or_default(), + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + } + } +} + +impl ForeignFrom for RoutingDictionaryRecord { + fn foreign_from(value: RoutingAlgorithm) -> Self { + Self { + id: value.algorithm_id, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + name: value.name, + kind: value.kind.foreign_into(), + description: value.description.unwrap_or_default(), + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + } + } +} + +impl ForeignTryFrom for MerchantRoutingAlgorithm { + type Error = error_stack::Report; + + fn foreign_try_from(value: RoutingAlgorithm) -> Result { + Ok(Self { + id: value.algorithm_id, + name: value.name, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + description: value.description.unwrap_or_default(), + algorithm: value + .algorithm_data + .parse_value::("RoutingAlgorithm")?, + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + }) + } +} + +impl ForeignFrom for RoutingAlgorithmKind { + fn foreign_from(value: storage_enums::RoutingAlgorithmKind) -> Self { + match value { + storage_enums::RoutingAlgorithmKind::Single => Self::Single, + storage_enums::RoutingAlgorithmKind::Priority => Self::Priority, + storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, + storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced, + } + } +} + +impl ForeignFrom for storage_enums::RoutingAlgorithmKind { + fn foreign_from(value: RoutingAlgorithmKind) -> Self { + match value { + RoutingAlgorithmKind::Single => Self::Single, + RoutingAlgorithmKind::Priority => Self::Priority, + RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, + RoutingAlgorithmKind::Advanced => Self::Advanced, + } + } +} diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index eb2e19081ff3..8b7df2a14be7 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -98,6 +98,7 @@ pub async fn payments_incoming_webhook_flow< }, services::AuthFlow::Merchant, consume_or_trigger_flow, + None, HeaderPayload::default(), ) .await; @@ -579,6 +580,7 @@ async fn bank_transfer_webhook_flow Box; diff --git a/crates/router/src/db/routing_algorithm.rs b/crates/router/src/db/routing_algorithm.rs new file mode 100644 index 000000000000..58550b2f01fa --- /dev/null +++ b/crates/router/src/db/routing_algorithm.rs @@ -0,0 +1,199 @@ +use diesel_models::routing_algorithm as routing_storage; +use error_stack::IntoReport; +use storage_impl::mock_db::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +type StorageResult = CustomResult; + +#[async_trait::async_trait] +pub trait RoutingAlgorithmInterface { + async fn insert_routing_algorithm( + &self, + routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult; + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> StorageResult; + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult; + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult; + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult>; + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult>; +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for Store { + async fn insert_routing_algorithm( + &self, + routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_algorithm + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_by_algorithm_id_profile_id( + &conn, + algorithm_id, + profile_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_by_algorithm_id_merchant_id( + &conn, + algorithm_id, + merchant_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_metadata_by_algorithm_id_profile_id( + &conn, + algorithm_id, + profile_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::list_metadata_by_profile_id( + &conn, profile_id, limit, offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::list_metadata_by_merchant_id( + &conn, + merchant_id, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for MockDb { + async fn insert_routing_algorithm( + &self, + _routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + _profile_id: &str, + _algorithm_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + _algorithm_id: &str, + _merchant_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + _algorithm_id: &str, + _profile_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + _profile_id: &str, + _limit: i64, + _offset: i64, + ) -> StorageResult> { + Err(errors::StorageError::MockDbError)? + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + _merchant_id: &str, + _limit: i64, + _offset: i64, + ) -> StorageResult> { + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 11efec64055b..21ebfc06137b 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -141,6 +141,7 @@ pub fn mk_app( .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) + .service(routes::Routing::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 307797e8ac9d..38f95c4cdda8 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -20,6 +20,8 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +#[cfg(feature = "olap")] +pub mod routing; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; pub mod webhooks; @@ -28,6 +30,8 @@ pub mod webhooks; pub use self::app::DummyConnector; #[cfg(feature = "payouts")] pub use self::app::Payouts; +#[cfg(feature = "olap")] +pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5b16e93404ae..0369bb612668 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -14,6 +14,8 @@ use tokio::sync::oneshot; use super::dummy_connector::*; #[cfg(feature = "payouts")] use super::payouts::*; +#[cfg(feature = "olap")] +use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] @@ -274,6 +276,43 @@ impl Payments { } } +#[cfg(feature = "olap")] +pub struct Routing; + +#[cfg(feature = "olap")] +impl Routing { + pub fn server(state: AppState) -> Scope { + web::scope("/routing") + .app_data(web::Data::new(state.clone())) + .service( + web::resource("/active") + .route(web::get().to(cloud_routing::routing_retrieve_linked_config)), + ) + .service( + web::resource("") + .route(web::get().to(cloud_routing::routing_retrieve_dictionary)) + .route(web::post().to(cloud_routing::routing_create_config)), + ) + .service( + web::resource("/default") + .route(web::get().to(cloud_routing::routing_retrieve_default_config)) + .route(web::post().to(cloud_routing::routing_update_default_config)), + ) + .service( + web::resource("/deactivate") + .route(web::post().to(cloud_routing::routing_unlink_config)), + ) + .service( + web::resource("/{algorithm_id}") + .route(web::get().to(cloud_routing::routing_retrieve_config)), + ) + .service( + web::resource("/{algorithm_id}/activate") + .route(web::post().to(cloud_routing::routing_link_config)), + ) + } +} + pub struct Customers; #[cfg(any(feature = "olap", feature = "oltp"))] diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 5be361098bcc..14614268d79d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -22,6 +22,7 @@ pub enum ApiIdentifier { Verification, ApiKeys, PaymentLink, + Routing, } impl From for ApiIdentifier { @@ -33,6 +34,17 @@ impl From for ApiIdentifier { | Flow::MerchantsAccountDelete | Flow::MerchantAccountList => Self::MerchantAccount, + Flow::RoutingCreateConfig + | Flow::RoutingLinkConfig + | Flow::RoutingUnlinkConfig + | Flow::RoutingRetrieveConfig + | Flow::RoutingRetrieveActiveConfig + | Flow::RoutingRetrieveDefaultConfig + | Flow::RoutingRetrieveDictionary + | Flow::RoutingUpdateConfig + | Flow::RoutingUpdateDefaultConfig + | Flow::RoutingDeleteConfig => Self::Routing, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 4bc05826a3e4..5ed73df1c175 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -178,6 +178,7 @@ pub async fn payments_start( req, api::AuthFlow::Client, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -244,6 +245,7 @@ pub async fn payments_retrieve( req, auth_flow, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -305,6 +307,7 @@ pub async fn payments_retrieve_with_gateway_creds( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -509,6 +512,7 @@ pub async fn payments_capture( payload, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -564,6 +568,7 @@ pub async fn payments_connector_session( payload, api::AuthFlow::Client, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -774,6 +779,7 @@ pub async fn payments_cancel( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -897,6 +903,7 @@ where // the operation are flow agnostic, and the flow is only required in the post_update_tracker // Thus the flow can be generated just before calling the connector instead of explicitly passing it here. + let eligible_connectors = req.connector.clone(); match req.payment_type.unwrap_or_default() { api_models::enums::PaymentType::Normal | api_models::enums::PaymentType::RecurringMandate @@ -916,6 +923,7 @@ where req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, header_payload, ) .await @@ -936,6 +944,7 @@ where req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, header_payload, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs new file mode 100644 index 000000000000..1d5ccdf502fc --- /dev/null +++ b/crates/router/src/routes/routing.rs @@ -0,0 +1,298 @@ +//! Analysis for usage of Routing in Payment flows +//! +//! Functions that are used to perform the api level configuration, retrieval, updation +//! of Routing configs. +use actix_web::{web, HttpRequest, Responder}; +use api_models::routing as routing_types; +#[cfg(feature = "business_profile_routing")] +use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +use router_env::{ + tracing::{self, instrument}, + Flow, +}; + +use crate::{ + core::{api_locking, routing}, + routes::AppState, + services::{api as oss_api, authentication as oss_auth, authentication as auth}, +}; + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_create_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::RoutingCreateConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, payload| { + routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_link_config( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let flow = Flow::RoutingLinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + path.into_inner(), + |state, auth: oss_auth::AuthenticationData, algorithm_id| { + routing::link_routing_config( + state, + auth.merchant_account, + #[cfg(not(feature = "business_profile_routing"))] + auth.key_store, + algorithm_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[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: oss_auth::AuthenticationData, algorithm_id| { + routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_dictionary( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] query: web::Query, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + let flow = Flow::RoutingRetrieveDictionary; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + query.into_inner(), + |state, auth: oss_auth::AuthenticationData, query_params| { + routing::retrieve_merchant_routing_dictionary( + state, + auth.merchant_account, + query_params, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingRetrieveDictionary; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_unlink_config( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] payload: web::Json< + routing_types::RoutingConfigRequest, + >, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + let flow = Flow::RoutingUnlinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, payload_req| { + routing::unlink_routing_config(state, auth.merchant_account, payload_req) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingUnlinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_update_default_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json>, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingUpdateDefaultConfig, + state, + &req, + json_payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, updated_config| { + routing::update_default_routing_config(state, auth.merchant_account, updated_config) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_default_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingRetrieveDefaultConfig, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_default_routing_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_linked_config( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] query: web::Query, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + use crate::services::authentication::AuthenticationData; + let flow = Flow::RoutingRetrieveActiveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + query.into_inner(), + |state, auth: AuthenticationData, query_params| { + routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingRetrieveActiveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_linked_routing_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 8f5a0f8a59f2..69e7f8898d15 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -11,6 +11,7 @@ pub mod payment_methods; pub mod payments; pub mod payouts; pub mod refunds; +pub mod routing; pub mod webhooks; use std::{fmt::Debug, str::FromStr}; @@ -38,6 +39,13 @@ pub trait ConnectorAccessToken: { } +#[derive(Clone)] +pub enum ConnectorCallType { + PreDetermined(ConnectorData), + Retryable(Vec), + SessionMultiple(Vec), +} + #[derive(Clone, Debug)] pub struct VerifyWebhookSource; @@ -218,12 +226,6 @@ pub enum PayoutConnectorChoice { Decide, } -#[derive(Clone)] -pub enum ConnectorCallType { - Multiple(Vec), - Single(ConnectorData), -} - #[cfg(feature = "payouts")] #[derive(Clone)] pub enum PayoutConnectorCallType { @@ -231,12 +233,6 @@ pub enum PayoutConnectorCallType { Single(PayoutConnectorData), } -impl ConnectorCallType { - pub fn is_single(&self) -> bool { - matches!(self, Self::Single(_)) - } -} - #[cfg(feature = "payouts")] impl PayoutConnectorData { pub fn get_connector_by_name( diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 258a3d566dde..6bbe9149f4d7 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -4,8 +4,8 @@ pub use api_models::admin::{ MerchantAccountResponse, MerchantAccountUpdate, MerchantConnectorCreate, MerchantConnectorDeleteResponse, MerchantConnectorDetails, MerchantConnectorDetailsWrap, MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, MerchantId, - PaymentMethodsEnabled, PayoutRoutingAlgorithm, PayoutStraightThroughAlgorithm, - RoutingAlgorithm, StraightThroughAlgorithm, ToggleKVRequest, ToggleKVResponse, WebhookDetails, + PaymentMethodsEnabled, PayoutRoutingAlgorithm, PayoutStraightThroughAlgorithm, ToggleKVRequest, + ToggleKVResponse, WebhookDetails, }; use common_utils::ext_traits::ValueExt; use error_stack::ResultExt; diff --git a/crates/router/src/types/api/routing.rs b/crates/router/src/types/api/routing.rs new file mode 100644 index 000000000000..faafac76e3dc --- /dev/null +++ b/crates/router/src/types/api/routing.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "backwards_compatibility")] +pub use api_models::routing::RoutableChoiceKind; +pub use api_models::{ + enums as api_enums, + routing::{ + ConnectorVolumeSplit, DetailedConnectorChoice, RoutableConnectorChoice, RoutingAlgorithm, + RoutingAlgorithmKind, RoutingAlgorithmRef, RoutingConfigRequest, RoutingDictionary, + RoutingDictionaryRecord, StraightThroughAlgorithm, + }, +}; + +use super::types::api as api_oss; + +pub struct SessionRoutingChoice { + pub connector: api_oss::ConnectorData, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, + pub payment_method_type: api_enums::PaymentMethodType, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConnectorVolumeSplitV0 { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum RoutingAlgorithmV0 { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Custom { timestamp: i64 }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FrmRoutingAlgorithm { + pub data: String, + #[serde(rename = "type")] + pub algorithm_type: String, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 92ead76e9137..00a5e07a30e8 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -21,6 +21,9 @@ pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_link; pub mod payment_method; +pub mod routing_algorithm; +use std::collections::HashMap; + pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; pub use scheduler::db::process_tracker; pub mod reverse_lookup; @@ -41,11 +44,63 @@ pub use self::{ customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, + reverse_lookup::*, routing_algorithm::*, }; +use crate::types::api::routing; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RoutingData { pub routed_through: Option, - pub algorithm: Option, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub business_sub_label: Option, + pub routing_info: PaymentRoutingInfo, + pub algorithm: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(from = "PaymentRoutingInfoSerde", into = "PaymentRoutingInfoSerde")] +pub struct PaymentRoutingInfo { + pub algorithm: Option, + pub pre_routing_results: + Option>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentRoutingInfoInner { + pub algorithm: Option, + pub pre_routing_results: + Option>, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum PaymentRoutingInfoSerde { + OnlyAlgorithm(Box), + WithDetails(Box), +} + +impl From for PaymentRoutingInfo { + fn from(value: PaymentRoutingInfoSerde) -> Self { + match value { + PaymentRoutingInfoSerde::OnlyAlgorithm(algo) => Self { + algorithm: Some(*algo), + pre_routing_results: None, + }, + PaymentRoutingInfoSerde::WithDetails(details) => Self { + algorithm: details.algorithm, + pre_routing_results: details.pre_routing_results, + }, + } + } +} + +impl From for PaymentRoutingInfoSerde { + fn from(value: PaymentRoutingInfo) -> Self { + Self::WithDetails(Box::new(PaymentRoutingInfoInner { + algorithm: value.algorithm, + pre_routing_results: value.pre_routing_results, + })) + } } diff --git a/crates/router/src/types/storage/routing_algorithm.rs b/crates/router/src/types/storage/routing_algorithm.rs new file mode 100644 index 000000000000..8022ab075ec4 --- /dev/null +++ b/crates/router/src/types/storage/routing_algorithm.rs @@ -0,0 +1,3 @@ +pub use diesel_models::routing_algorithm::{ + RoutingAlgorithm, RoutingAlgorithmMetadata, RoutingProfileMetadata, +}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index d38497c7100a..83ca0d014dc8 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1,6 +1,6 @@ // use actix_web::HttpMessage; use actix_web::http::header::HeaderMap; -use api_models::{enums as api_enums, payments}; +use api_models::{enums as api_enums, payments, routing::ConnectorSelection}; use common_utils::{ consts::X_HS_LATENCY, crypto::Encryptable, @@ -8,14 +8,15 @@ use common_utils::{ pii, }; use diesel_models::enums as storage_enums; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; +use euclid::enums as dsl_enums; use masking::{ExposeInterface, PeekInterface}; use super::domain; use crate::{ core::errors, services::authentication::get_header_value_by_key, - types::{api as api_types, storage}, + types::{api as api_types, api::routing as routing_types, storage}, }; pub trait ForeignInto { @@ -169,6 +170,154 @@ impl ForeignFrom for api_models::payments::Manda } } +impl ForeignTryFrom for api_enums::RoutableConnectors { + type Error = error_stack::Report; + + fn foreign_try_from(from: api_enums::Connector) -> Result { + Ok(match from { + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + api_enums::Connector::Aci => Self::Aci, + api_enums::Connector::Adyen => Self::Adyen, + api_enums::Connector::Airwallex => Self::Airwallex, + api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, + api_enums::Connector::Bitpay => Self::Bitpay, + api_enums::Connector::Bambora => Self::Bambora, + api_enums::Connector::Bluesnap => Self::Bluesnap, + api_enums::Connector::Boku => Self::Boku, + api_enums::Connector::Braintree => Self::Braintree, + api_enums::Connector::Cashtocode => Self::Cashtocode, + api_enums::Connector::Checkout => Self::Checkout, + api_enums::Connector::Coinbase => Self::Coinbase, + api_enums::Connector::Cryptopay => Self::Cryptopay, + api_enums::Connector::Cybersource => Self::Cybersource, + api_enums::Connector::Dlocal => Self::Dlocal, + api_enums::Connector::Fiserv => Self::Fiserv, + api_enums::Connector::Forte => Self::Forte, + api_enums::Connector::Globalpay => Self::Globalpay, + api_enums::Connector::Globepay => Self::Globepay, + api_enums::Connector::Gocardless => Self::Gocardless, + api_enums::Connector::Helcim => Self::Helcim, + api_enums::Connector::Iatapay => Self::Iatapay, + api_enums::Connector::Klarna => Self::Klarna, + api_enums::Connector::Mollie => Self::Mollie, + api_enums::Connector::Multisafepay => Self::Multisafepay, + api_enums::Connector::Nexinets => Self::Nexinets, + api_enums::Connector::Nmi => Self::Nmi, + api_enums::Connector::Noon => Self::Noon, + api_enums::Connector::Nuvei => Self::Nuvei, + api_enums::Connector::Opennode => Self::Opennode, + api_enums::Connector::Payme => Self::Payme, + api_enums::Connector::Paypal => Self::Paypal, + api_enums::Connector::Payu => Self::Payu, + api_enums::Connector::Plaid => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "plaid is not a routable connector".to_string(), + }) + .into_report()? + } + api_enums::Connector::Powertranz => Self::Powertranz, + api_enums::Connector::Rapyd => Self::Rapyd, + api_enums::Connector::Shift4 => Self::Shift4, + api_enums::Connector::Signifyd => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "signifyd is not a routable connector".to_string(), + }) + .into_report()? + } + api_enums::Connector::Square => Self::Square, + api_enums::Connector::Stax => Self::Stax, + api_enums::Connector::Stripe => Self::Stripe, + api_enums::Connector::Trustpay => Self::Trustpay, + api_enums::Connector::Tsys => Self::Tsys, + api_enums::Connector::Volt => Self::Volt, + api_enums::Connector::Wise => Self::Wise, + api_enums::Connector::Worldline => Self::Worldline, + api_enums::Connector::Worldpay => Self::Worldpay, + api_enums::Connector::Zen => Self::Zen, + }) + } +} + +impl ForeignFrom for api_enums::RoutableConnectors { + fn foreign_from(from: dsl_enums::Connector) -> Self { + match from { + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector7 => Self::DummyConnector7, + dsl_enums::Connector::Aci => Self::Aci, + dsl_enums::Connector::Adyen => Self::Adyen, + dsl_enums::Connector::Airwallex => Self::Airwallex, + dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, + dsl_enums::Connector::Bitpay => Self::Bitpay, + dsl_enums::Connector::Bambora => Self::Bambora, + dsl_enums::Connector::Bluesnap => Self::Bluesnap, + dsl_enums::Connector::Boku => Self::Boku, + dsl_enums::Connector::Braintree => Self::Braintree, + dsl_enums::Connector::Cashtocode => Self::Cashtocode, + dsl_enums::Connector::Checkout => Self::Checkout, + dsl_enums::Connector::Coinbase => Self::Coinbase, + dsl_enums::Connector::Cryptopay => Self::Cryptopay, + dsl_enums::Connector::Cybersource => Self::Cybersource, + dsl_enums::Connector::Dlocal => Self::Dlocal, + dsl_enums::Connector::Fiserv => Self::Fiserv, + dsl_enums::Connector::Forte => Self::Forte, + dsl_enums::Connector::Globalpay => Self::Globalpay, + dsl_enums::Connector::Globepay => Self::Globepay, + dsl_enums::Connector::Gocardless => Self::Gocardless, + dsl_enums::Connector::Helcim => Self::Helcim, + dsl_enums::Connector::Iatapay => Self::Iatapay, + dsl_enums::Connector::Klarna => Self::Klarna, + dsl_enums::Connector::Mollie => Self::Mollie, + dsl_enums::Connector::Multisafepay => Self::Multisafepay, + dsl_enums::Connector::Nexinets => Self::Nexinets, + dsl_enums::Connector::Nmi => Self::Nmi, + dsl_enums::Connector::Noon => Self::Noon, + dsl_enums::Connector::Nuvei => Self::Nuvei, + dsl_enums::Connector::Opennode => Self::Opennode, + dsl_enums::Connector::Payme => Self::Payme, + dsl_enums::Connector::Paypal => Self::Paypal, + dsl_enums::Connector::Payu => Self::Payu, + dsl_enums::Connector::Powertranz => Self::Powertranz, + dsl_enums::Connector::Rapyd => Self::Rapyd, + dsl_enums::Connector::Shift4 => Self::Shift4, + dsl_enums::Connector::Square => Self::Square, + dsl_enums::Connector::Stax => Self::Stax, + dsl_enums::Connector::Stripe => Self::Stripe, + dsl_enums::Connector::Trustpay => Self::Trustpay, + dsl_enums::Connector::Tsys => Self::Tsys, + dsl_enums::Connector::Volt => Self::Volt, + dsl_enums::Connector::Wise => Self::Wise, + dsl_enums::Connector::Worldline => Self::Worldline, + dsl_enums::Connector::Worldpay => Self::Worldpay, + dsl_enums::Connector::Zen => Self::Zen, + } + } +} + impl ForeignFrom for api_models::payments::MandateAmountData { fn foreign_from(from: storage_enums::MandateAmountData) -> Self { Self { @@ -862,6 +1011,16 @@ impl From for payments::AddressDetails { } } +impl ForeignFrom for routing_types::RoutingAlgorithm { + fn foreign_from(value: ConnectorSelection) -> Self { + match value { + ConnectorSelection::Priority(connectors) => Self::Priority(connectors), + + ConnectorSelection::VolumeSplit(splits) => Self::VolumeSplit(splits), + } + } +} + impl ForeignFrom for diesel_models::organization::OrganizationNew { diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 540f2d68dd61..f41b300c5127 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -69,6 +69,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { tracking_data.clone(), payment_flows::CallConnectorAction::Trigger, services::AuthFlow::Client, + None, api::HeaderPayload::default(), ) .await?; diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 551960ac1380..d2d6c48507e5 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -369,6 +369,7 @@ async fn payments_create_core() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await @@ -539,6 +540,7 @@ async fn payments_create_core_adyen_no_redirect() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 96ed131dc6f8..ed8827a910be 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -135,6 +135,7 @@ async fn payments_create_core() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await @@ -313,6 +314,7 @@ async fn payments_create_core_adyen_no_redirect() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d63ddce58f30..9822432115b0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,26 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + /// Routing create flow, + RoutingCreateConfig, + /// Routing link config + RoutingLinkConfig, + /// Routing link config + RoutingUnlinkConfig, + /// Routing retrieve config + RoutingRetrieveConfig, + /// Routing retrieve active config + RoutingRetrieveActiveConfig, + /// Routing retrieve default config + RoutingRetrieveDefaultConfig, + /// Routing retrieve dictionary + RoutingRetrieveDictionary, + /// Routing update config + RoutingUpdateConfig, + /// Routing update default config + RoutingUpdateDefaultConfig, + /// Routing delete config + RoutingDeleteConfig, /// Incoming Webhook Receive IncomingWebhookReceive, /// Validate payment method flow diff --git a/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql b/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql new file mode 100644 index 000000000000..2cace88297db --- /dev/null +++ b/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE routing_algorithm; +DROP TYPE "RoutingAlgorithmKind"; diff --git a/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql b/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql new file mode 100644 index 000000000000..361194561227 --- /dev/null +++ b/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here + +CREATE TYPE "RoutingAlgorithmKind" AS ENUM ('single', 'priority', 'volume_split', 'advanced'); + +CREATE TABLE routing_algorithm ( + algorithm_id VARCHAR(64) PRIMARY KEY, + profile_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + name VARCHAR(64) NOT NULL, + description VARCHAR(256), + kind "RoutingAlgorithmKind" NOT NULL, + algorithm_data JSONB NOT NULL, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP NOT NULL +); + +CREATE INDEX routing_algorithm_profile_id_modified_at ON routing_algorithm (profile_id, modified_at DESC); + +CREATE INDEX routing_algorithm_merchant_id_modified_at ON routing_algorithm (merchant_id, modified_at DESC); From 585937204d9071baa37d402f73159f8f650d0a07 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Fri, 3 Nov 2023 18:57:11 +0530 Subject: [PATCH 12/57] fix: response spelling (#2779) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cf72d19085..41caed32f59a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ All notable changes to HyperSwitch will be documented here. ### Bug Fixes - **router:** Make customer_id optional when billing and shipping address is passed in payments create, update ([#2762](https://github.com/juspay/hyperswitch/pull/2762)) ([`e40a293`](https://github.com/juspay/hyperswitch/commit/e40a29351c7aa7b86a5684959a84f0236104cafd)) -- Null fields in payments respose ([#2745](https://github.com/juspay/hyperswitch/pull/2745)) ([`42261a5`](https://github.com/juspay/hyperswitch/commit/42261a5306bb99d3e20eb3aa734a895e589b1d94)) +- Null fields in payments response ([#2745](https://github.com/juspay/hyperswitch/pull/2745)) ([`42261a5`](https://github.com/juspay/hyperswitch/commit/42261a5306bb99d3e20eb3aa734a895e589b1d94)) ### Testing From 9314d1446326fd8a69f1f69657a976bbe7c27901 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:57:20 +0530 Subject: [PATCH 13/57] fix(connector): [Bluesnap] fix psync status to failure when it is '403' (#2772) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/router/src/connector/bluesnap.rs | 66 ++++++++++++------- crates/router/src/consts.rs | 1 + .../payments/operations/payment_response.rs | 2 +- crates/router/src/services/api.rs | 18 ++++- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 24d5787aa8d8..6c39fc41b721 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -37,6 +37,8 @@ use crate::{ utils::{self, BytesExt}, }; +pub const BLUESNAP_TRANSACTION_NOT_FOUND: &str = "is not authorized to view merchant-transaction:"; + #[derive(Debug, Clone)] pub struct Bluesnap; @@ -132,12 +134,24 @@ impl ConnectorCommon for Bluesnap { message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), }, - bluesnap::BluesnapErrors::General(error_response) => ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: error_response.clone(), - reason: Some(error_response), - }, + bluesnap::BluesnapErrors::General(error_response) => { + let error_res = if res.status_code == 403 + && error_response.contains(BLUESNAP_TRANSACTION_NOT_FOUND) + { + format!( + "{} in bluesnap dashboard", + consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + ) + } else { + error_response.clone() + }; + ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: error_response, + reason: Some(error_res), + } + } }; Ok(response_error_message) } @@ -322,21 +336,26 @@ impl ConnectorIntegration CustomResult { - let meta_data: CustomResult = - connector_utils::to_connector_meta_from_secret(req.connector_meta_data.clone()); - - match meta_data { - // if merchant_id is present, psync can be made using merchant_transaction_id - Ok(data) => get_url_with_merchant_transaction_id( - self.base_url(connectors).to_string(), - data.merchant_id, - req.attempt_id.to_owned(), - ), - // otherwise psync is made using connector_transaction_id - Err(_) => get_psync_url_with_connector_transaction_id( - &req.request.connector_transaction_id, - self.base_url(connectors).to_string(), - ), + let connector_transaction_id = req.request.connector_transaction_id.clone(); + match connector_transaction_id { + // if connector_transaction_id is present, we always sync with connector_transaction_id + types::ResponseId::ConnectorTransactionId(trans_id) => { + get_psync_url_with_connector_transaction_id( + trans_id, + self.base_url(connectors).to_string(), + ) + } + _ => { + // if connector_transaction_id is not present, we sync with merchant_transaction_id + let meta_data: bluesnap::BluesnapConnectorMetaData = + connector_utils::to_connector_meta_from_secret(req.connector_meta_data.clone()) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + get_url_with_merchant_transaction_id( + self.base_url(connectors).to_string(), + meta_data.merchant_id, + req.attempt_id.to_owned(), + ) + } } } @@ -1269,12 +1288,9 @@ fn get_url_with_merchant_transaction_id( } fn get_psync_url_with_connector_transaction_id( - connector_transaction_id: &types::ResponseId, + connector_transaction_id: String, base_url: String, ) -> CustomResult { - let connector_transaction_id = connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(format!( "{}{}{}", base_url, "services/2/transactions/", connector_transaction_id diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index f76df7466581..2f2563ee3976 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -13,6 +13,7 @@ pub(crate) const ALPHABETS: [char; 62] = [ pub const REQUEST_TIME_OUT: u64 = 30; pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; +pub const REQUEST_TIMEOUT_PAYMENT_NOT_FOUND: &str = "Timed out ,payment not found"; pub const REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC: &str = "This Payment has been moved to failed as there is no response from the connector"; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 1467da7f816d..60d3bc165a97 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -326,7 +326,7 @@ async fn payment_response_update_tracker( match err.status_code { // marking failure for 2xx because this is genuine payment failure 200..=299 => storage::enums::AttemptStatus::Failure, - _ => payment_data.payment_attempt.status, + _ => router_data.status, } } else { match err.status_code { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index a1e657c7d92f..3d618d047eac 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError}; -use api_models::enums::CaptureMethod; +use api_models::enums::{AttemptStatus, CaptureMethod}; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ @@ -403,7 +403,21 @@ where 500..=511 => { connector_integration.get_5xx_error_response(body)? } - _ => connector_integration.get_error_response(body)?, + _ => { + let error_res = + connector_integration.get_error_response(body)?; + if router_data.connector == "bluesnap" + && error_res.status_code == 403 + && error_res.reason + == Some(format!( + "{} in bluesnap dashboard", + consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + )) + { + router_data.status = AttemptStatus::Failure; + }; + error_res + } }; router_data.response = Err(error); From 21e8a105f9b47ded232b457a0420ad71ec2414ed Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:13:18 +0000 Subject: [PATCH 14/57] test(postman): update postman collection files --- .../stripe.postman_collection.json | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index b10bcd2a3b83..5d308dd0fe53 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -8669,6 +8669,266 @@ } ] }, + { + "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ From cdca284b2a7a77cb22074fa8b3b380a088c10f00 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:13:18 +0000 Subject: [PATCH 15/57] chore(version): v1.71.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41caed32f59a..7c492d22bd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.71.0 (2023-11-03) + +### Features + +- **merchant_connector_account:** Add cache for querying by `merchant_connector_id` ([#2738](https://github.com/juspay/hyperswitch/pull/2738)) ([`1ba6282`](https://github.com/juspay/hyperswitch/commit/1ba6282699b7dff5e6e95c9a14e51c0f8bf749cd)) +- **router:** Add Smart Routing to route payments efficiently ([#2665](https://github.com/juspay/hyperswitch/pull/2665)) ([`9b618d2`](https://github.com/juspay/hyperswitch/commit/9b618d24476967d364835d04010d9076a80aeb9c)) + +### Bug Fixes + +- **connector:** + - [Cryptopay]Remove default case handling for Cryptopay ([#2699](https://github.com/juspay/hyperswitch/pull/2699)) ([`255a4f8`](https://github.com/juspay/hyperswitch/commit/255a4f89a8e0124310d42bb63ad459bd8cde2cba)) + - [Bluesnap] fix psync status to failure when it is '403' ([#2772](https://github.com/juspay/hyperswitch/pull/2772)) ([`9314d14`](https://github.com/juspay/hyperswitch/commit/9314d1446326fd8a69f1f69657a976bbe7c27901)) +- Response spelling ([#2779](https://github.com/juspay/hyperswitch/pull/2779)) ([`5859372`](https://github.com/juspay/hyperswitch/commit/585937204d9071baa37d402f73159f8f650d0a07)) + +### Testing + +- **postman:** Update postman collection files ([`21e8a10`](https://github.com/juspay/hyperswitch/commit/21e8a105f9b47ded232b457a0420ad71ec2414ed)) + +**Full Changelog:** [`v1.70.1...v1.71.0`](https://github.com/juspay/hyperswitch/compare/v1.70.1...v1.71.0) + +- - - + + ## 1.70.1 (2023-11-03) ### Revert From fd6280a79d539da6c0b380fe400ebd17acfa890a Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:09:56 +0530 Subject: [PATCH 16/57] ci(postman): Rotate Paypal test cards to address collection failures (#2784) --- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Confirm/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../QuickStart/Payments - Create/request.json | 2 +- .../request.json | 2 +- .../request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../collection-dir/paypal/event.prerequest.js | 27 ++++++++++++++++++ .../paypal.postman_collection.json | 28 +++++++++---------- 16 files changed, 55 insertions(+), 28 deletions(-) diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 144a35f773aa..09772bd13de5 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index a6a8150c2404..8fca41333799 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index b9e2faa143c9..be7b29473334 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -42,7 +42,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json index 50cb0663b403..b9b658979cf2 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index b080ff1a6b95..8cf69c5039f6 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json index 07ffc4eedefc..d54ac18d3c50 100644 --- a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json index 6e9db26a339d..ad8aa7b2ae06 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "13", "card_exp_year": "2023", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json index 0b35b7a4e92b..ab5943ac13ac 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index 144a35f773aa..09772bd13de5 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index 09987daa71ec..d7582d82ddea 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/event.prerequest.js b/postman/collection-dir/paypal/event.prerequest.js index e69de29bb2d1..f4c9a7648646 100644 --- a/postman/collection-dir/paypal/event.prerequest.js +++ b/postman/collection-dir/paypal/event.prerequest.js @@ -0,0 +1,27 @@ +const path = pm.request.url.toString(); +const isPostRequest = pm.request.method.toString() === "POST"; +const isPaymentCreation = path.match(/\/payments$/) && isPostRequest; + +if (isPaymentCreation) { + try { + const request = JSON.parse(pm.request.body.toJSON().raw); + + // Attach routing + const routing = { type: "single", data: "paypal" }; + request["routing"] = routing; + + let updatedRequest = { + mode: "raw", + raw: JSON.stringify(request), + options: { + raw: { + language: "json", + }, + }, + }; + pm.request.body.update(updatedRequest); + } catch (error) { + console.error("Failed to inject routing in the request"); + console.error(error); + } +} \ No newline at end of file diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index bd6459e5c708..5a92253a9ba0 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -523,7 +523,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -782,7 +782,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1459,7 +1459,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1751,7 +1751,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2389,7 +2389,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2695,7 +2695,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3356,7 +3356,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3833,7 +3833,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4310,7 +4310,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5613,7 +5613,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5879,7 +5879,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6340,7 +6340,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6732,7 +6732,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6993,7 +6993,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", From cdead78ea6a1f2dce92187f499f54498ba4bb173 Mon Sep 17 00:00:00 2001 From: Kumar Harshwardhan <113055707+Harshwardhan9431@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:18:32 +0530 Subject: [PATCH 17/57] feat(connector): [ACI] Currency Unit Conversion (#2750) --- crates/router/src/connector/aci.rs | 20 ++- .../router/src/connector/aci/transformers.rs | 115 ++++++++++++------ 2 files changed, 96 insertions(+), 39 deletions(-) diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 0a6e0d8a6099..0e325a04ddb0 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -30,7 +30,9 @@ impl ConnectorCommon for Aci { fn id(&self) -> &'static str { "aci" } - + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } fn common_get_content_type(&self) -> &'static str { "application/x-www-form-urlencoded" } @@ -279,7 +281,13 @@ impl req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { // encode only for for urlencoded things. - let connector_req = aci::AciPaymentsRequest::try_from(req)?; + let connector_router_data = aci::AciRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = aci::AciPaymentsRequest::try_from(&connector_router_data)?; let aci_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -471,7 +479,13 @@ impl services::ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_req = aci::AciRefundRequest::try_from(req)?; + let connector_router_data = aci::AciRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_req = aci::AciRefundRequest::try_from(&connector_router_data)?; let body = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f6c1daffe4d8..f56369ed31ab 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -17,6 +17,38 @@ use crate::{ type Error = error_stack::Report; +#[derive(Debug, Serialize)] +pub struct AciRouterData { + amount: String, + router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for AciRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} + pub struct AciAuthType { pub api_key: Secret, pub entity_id: Secret, @@ -101,14 +133,14 @@ impl TryFrom<&api_models::payments::WalletData> for PaymentDetails { impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, )> for PaymentDetails { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, ), ) -> Result { @@ -202,9 +234,9 @@ impl bank_account_bic: None, bank_account_iban: None, billing_country: Some(country.to_owned()), - merchant_customer_id: Some(Secret::new(item.get_customer_id()?)), + merchant_customer_id: Some(Secret::new(item.router_data.get_customer_id()?)), merchant_transaction_id: Some(Secret::new( - item.connector_request_reference_id.clone(), + item.router_data.connector_request_reference_id.clone(), )), customer_email: None, })) @@ -348,10 +380,12 @@ pub enum AciPaymentType { Refund, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { +impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.request.payment_method_data.clone() { + fn try_from( + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ref card_data) => Self::try_from((item, card_data)), api::PaymentMethodData::Wallet(ref wallet_data) => Self::try_from((item, wallet_data)), api::PaymentMethodData::PayLater(ref pay_later_data) => { @@ -361,7 +395,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { Self::try_from((item, bank_redirect_data)) } api::PaymentMethodData::MandatePayment => { - let mandate_id = item.request.mandate_id.clone().ok_or( + let mandate_id = item.router_data.request.mandate_id.clone().ok_or( errors::ConnectorError::MissingRequiredField { field_name: "mandate_id", }, @@ -376,7 +410,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.payment_method), + message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, } @@ -385,14 +419,14 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::WalletData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::WalletData, ), ) -> Result { @@ -404,21 +438,21 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, ), ) -> Result { @@ -430,21 +464,21 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::PayLaterData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::PayLaterData, ), ) -> Result { @@ -456,15 +490,23 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } -impl TryFrom<(&types::PaymentsAuthorizeRouterData, &api::Card)> for AciPaymentsRequest { +impl + TryFrom<( + &AciRouterData<&types::PaymentsAuthorizeRouterData>, + &api::Card, + )> for AciPaymentsRequest +{ type Error = Error; fn try_from( - value: (&types::PaymentsAuthorizeRouterData, &api::Card), + value: ( + &AciRouterData<&types::PaymentsAuthorizeRouterData>, + &api::Card, + ), ) -> Result { let (item, card_data) = value; let txn_details = get_transaction_details(item)?; @@ -482,14 +524,14 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, &api::Card)> for AciPaymentsR impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, api_models::payments::MandateIds, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, api_models::payments::MandateIds, ), ) -> Result { @@ -501,32 +543,34 @@ impl txn_details, payment_method: PaymentDetails::Mandate, instruction, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } fn get_transaction_details( - item: &types::PaymentsAuthorizeRouterData, + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - let auth = AciAuthType::try_from(&item.connector_auth_type)?; + let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(TransactionDetails { entity_id: auth.entity_id, - amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, - currency: item.request.currency.to_string(), + amount: item.amount.to_owned(), + currency: item.router_data.request.currency.to_string(), payment_type: AciPaymentType::Debit, }) } -fn get_instruction_details(item: &types::PaymentsAuthorizeRouterData) -> Option { - if item.request.setup_mandate_details.is_some() { +fn get_instruction_details( + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, +) -> Option { + if item.router_data.request.setup_mandate_details.is_some() { return Some(Instruction { mode: InstructionMode::Initial, transaction_type: InstructionType::Unscheduled, source: InstructionSource::CardholderInitiatedTransaction, create_registration: Some(true), }); - } else if item.request.mandate_id.is_some() { + } else if item.router_data.request.mandate_id.is_some() { return Some(Instruction { mode: InstructionMode::Repeated, transaction_type: InstructionType::Unscheduled, @@ -703,14 +747,13 @@ pub struct AciRefundRequest { pub entity_id: Secret, } -impl TryFrom<&types::RefundsRouterData> for AciRefundRequest { +impl TryFrom<&AciRouterData<&types::RefundsRouterData>> for AciRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let amount = - utils::to_currency_base_unit(item.request.refund_amount, item.request.currency)?; - let currency = item.request.currency; + fn try_from(item: &AciRouterData<&types::RefundsRouterData>) -> Result { + let amount = item.amount.to_owned(); + let currency = item.router_data.request.currency; let payment_type = AciPaymentType::Refund; - let auth = AciAuthType::try_from(&item.connector_auth_type)?; + let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { amount, From b6b9e4f912e1c61cd31ab91be587ffb08c9f3a5b Mon Sep 17 00:00:00 2001 From: Seemebadnekai <51400137+SagarDevAchar@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:21:32 +0530 Subject: [PATCH 18/57] feat(connector): [Fiserv] Currency Unit Conversion (#2715) --- crates/router/src/connector/fiserv.rs | 28 +++++- .../src/connector/fiserv/transformers.rs | 97 +++++++++++++------ 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 70f58ffe6eb0..35d40f1a3fb6 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -104,6 +104,10 @@ impl ConnectorCommon for Fiserv { "fiserv" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -400,7 +404,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservCaptureRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = fiserv::FiservCaptureRequest::try_from(&router_obj)?; let fiserv_payments_capture_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -505,7 +515,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservPaymentsRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_request = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; let fiserv_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -592,7 +608,13 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservRefundRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = fiserv::FiservRefundRequest::try_from(&router_obj)?; let fiserv_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index ae8eed0af314..2d07da7f47a4 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -9,6 +9,38 @@ use crate::{ types::{self, api, storage::enums}, }; +#[derive(Debug, Serialize)] +pub struct FiservRouterData { + pub amount: String, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for FiservRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data, + }) + } +} + #[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { @@ -99,23 +131,25 @@ pub enum TransactionInteractionPosConditionCode { CardNotPresentEcom, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest { +impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let amount = Amount { - total: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }; let transaction_details = TransactionDetails { capture_flag: Some(matches!( - item.request.capture_method, + item.router_data.request.capture_method, Some(enums::CaptureMethod::Automatic) | None )), reversal_reason_code: None, - merchant_transaction_id: item.connector_request_reference_id.clone(), + merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; - let metadata = item.get_connector_meta()?; + let metadata = item.router_data.get_connector_meta()?; let session: SessionObject = metadata .parse_value("SessionObject") .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -133,7 +167,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest { //card not present in online transaction pos_condition_code: TransactionInteractionPosConditionCode::CardNotPresentEcom, }; - let source = match item.request.payment_method_data.clone() { + let source = match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ref ccard) => { let card = CardData { card_data: ccard.card_number.clone(), @@ -389,35 +423,40 @@ pub struct SessionObject { pub terminal_id: String, } -impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest { +impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let metadata = item + .router_data .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let session: SessionObject = metadata .parse_value("SessionObject") .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let amount = - utils::to_currency_base_unit(item.request.amount_to_capture, item.request.currency)?; Ok(Self { amount: Amount { - total: amount, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, transaction_details: TransactionDetails { capture_flag: Some(true), reversal_reason_code: None, - merchant_transaction_id: item.connector_request_reference_id.clone(), + merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }, merchant_details: MerchantDetails { merchant_id: auth.merchant_account, terminal_id: Some(session.terminal_id), }, reference_transaction_details: ReferenceTransactionDetails { - reference_transaction_id: item.request.connector_transaction_id.to_string(), + reference_transaction_id: item + .router_data + .request + .connector_transaction_id + .to_string(), }, }) } @@ -477,11 +516,14 @@ pub struct FiservRefundRequest { reference_transaction_details: ReferenceTransactionDetails, } -impl TryFrom<&types::RefundsRouterData> for FiservRefundRequest { +impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::RefundsRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let metadata = item + .router_data .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; @@ -490,18 +532,19 @@ impl TryFrom<&types::RefundsRouterData> for FiservRefundRequest { .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { amount: Amount { - total: utils::to_currency_base_unit( - item.request.refund_amount, - item.request.currency, - )?, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, merchant_details: MerchantDetails { merchant_id: auth.merchant_account, terminal_id: Some(session.terminal_id), }, reference_transaction_details: ReferenceTransactionDetails { - reference_transaction_id: item.request.connector_transaction_id.to_string(), + reference_transaction_id: item + .router_data + .request + .connector_transaction_id + .to_string(), }, }) } From 7141b89d231bae0c3b1c10095b88df16129b1665 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Sun, 5 Nov 2023 16:23:00 +0530 Subject: [PATCH 19/57] feat(connector): [Bitpay] Use `connector_request_reference_id` as reference to the connector (#2697) --- crates/router/src/connector/bitpay/transformers.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/router/src/connector/bitpay/transformers.rs b/crates/router/src/connector/bitpay/transformers.rs index c5c20608a754..5af20d6423fd 100644 --- a/crates/router/src/connector/bitpay/transformers.rs +++ b/crates/router/src/connector/bitpay/transformers.rs @@ -60,6 +60,7 @@ pub struct BitpayPaymentsRequest { notification_url: String, transaction_speed: TransactionSpeed, token: Secret, + order_id: String, } impl TryFrom<&BitpayRouterData<&types::PaymentsAuthorizeRouterData>> for BitpayPaymentsRequest { @@ -279,6 +280,7 @@ fn get_crypto_specific_payment_data( ConnectorAuthType::HeaderKey { api_key } => api_key, _ => String::default().into(), }; + let order_id = item.router_data.connector_request_reference_id.clone(); Ok(BitpayPaymentsRequest { price, @@ -287,6 +289,7 @@ fn get_crypto_specific_payment_data( notification_url, transaction_speed, token, + order_id, }) } From 1b45a302630ed8affc5abff0de1325fb5c6f870e Mon Sep 17 00:00:00 2001 From: Seemebadnekai <51400137+SagarDevAchar@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:03:45 +0530 Subject: [PATCH 20/57] feat(connector): [NMI] Currency Unit Conversion (#2707) --- crates/router/src/connector/nmi.rs | 28 ++++++- .../router/src/connector/nmi/transformers.rs | 79 +++++++++++++------ 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index cdeb9c99d5ea..4f7ee15d7302 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -58,6 +58,10 @@ impl ConnectorCommon for Nmi { "nmi" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.nmi.base_url.as_ref() } @@ -210,7 +214,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = nmi::NmiPaymentsRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -351,7 +361,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiCaptureRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = nmi::NmiCaptureRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -491,7 +507,13 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiRefundRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_req = nmi::NmiRefundRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 582bb9f73675..995341fefd96 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -40,6 +40,37 @@ impl TryFrom<&ConnectorAuthType> for NmiAuthType { } } +#[derive(Debug, Serialize)] +pub struct NmiRouterData { + pub amount: f64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for NmiRouterData +{ + type Error = Report; + + fn try_from( + (_currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount: utils::to_currency_base_unit_asf64(amount, currency)?, + router_data, + }) + } +} + #[derive(Debug, Serialize)] pub struct NmiPaymentsRequest { #[serde(rename = "type")] @@ -77,25 +108,27 @@ pub struct ApplePayData { applepay_payment_data: Secret, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for NmiPaymentsRequest { +impl TryFrom<&NmiRouterData<&types::PaymentsAuthorizeRouterData>> for NmiPaymentsRequest { type Error = Error; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let transaction_type = match item.request.is_auto_capture()? { + fn try_from( + item: &NmiRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let transaction_type = match item.router_data.request.is_auto_capture()? { true => TransactionType::Sale, false => TransactionType::Auth, }; - let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; - let amount = - utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; - let payment_method = PaymentMethod::try_from(&item.request.payment_method_data)?; + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; + let amount = item.amount; + let payment_method = + PaymentMethod::try_from(&item.router_data.request.payment_method_data)?; Ok(Self { transaction_type, security_key: auth_type.api_key, amount, - currency: item.request.currency, + currency: item.router_data.request.currency, payment_method, - orderid: item.connector_request_reference_id.clone(), + orderid: item.router_data.connector_request_reference_id.clone(), }) } } @@ -243,18 +276,17 @@ pub struct NmiCaptureRequest { pub amount: Option, } -impl TryFrom<&types::PaymentsCaptureRouterData> for NmiCaptureRequest { +impl TryFrom<&NmiRouterData<&types::PaymentsCaptureRouterData>> for NmiCaptureRequest { type Error = Error; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let auth = NmiAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &NmiRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let auth = NmiAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { transaction_type: TransactionType::Capture, security_key: auth.api_key, - transactionid: item.request.connector_transaction_id.clone(), - amount: Some(utils::to_currency_base_unit_asf64( - item.request.amount_to_capture, - item.request.currency, - )?), + transactionid: item.router_data.request.connector_transaction_id.clone(), + amount: Some(item.amount), }) } } @@ -577,18 +609,15 @@ pub struct NmiRefundRequest { amount: f64, } -impl TryFrom<&types::RefundsRouterData> for NmiRefundRequest { +impl TryFrom<&NmiRouterData<&types::RefundsRouterData>> for NmiRefundRequest { type Error = Error; - fn try_from(item: &types::RefundsRouterData) -> Result { - let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; + fn try_from(item: &NmiRouterData<&types::RefundsRouterData>) -> Result { + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; Ok(Self { transaction_type: TransactionType::Refund, security_key: auth_type.api_key, - transactionid: item.request.connector_transaction_id.clone(), - amount: utils::to_currency_base_unit_asf64( - item.request.refund_amount, - item.request.currency, - )?, + transactionid: item.router_data.request.connector_transaction_id.clone(), + amount: item.amount, }) } } From 278292322c7c06f4239dd73861469e436bd941fa Mon Sep 17 00:00:00 2001 From: Shivansh Bhatnagar Date: Sun, 5 Nov 2023 17:15:56 +0530 Subject: [PATCH 21/57] refactor(connector): [Stax] Currency Unit Conversion (#2711) Co-authored-by: Shivansh Bhatnagar Co-authored-by: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> --- crates/router/src/connector/stax.rs | 28 ++++++- .../router/src/connector/stax/transformers.rs | 79 +++++++++++++------ 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 82a4c7ff3233..7f5fde719389 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -70,6 +70,10 @@ impl ConnectorCommon for Stax { "stax" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -347,7 +351,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = stax::StaxPaymentsRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &req_obj, @@ -503,7 +513,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = stax::StaxCaptureRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = stax::StaxCaptureRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::encode_to_string_of_json, @@ -657,7 +673,13 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let req_obj = stax::StaxRefundRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = stax::StaxRefundRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &req_obj, utils::Encode::::encode_to_string_of_json, diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 4ee28be19375..f2aae442ddd6 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -11,6 +11,37 @@ use crate::{ types::{self, api, storage::enums}, }; +#[derive(Debug, Serialize)] +pub struct StaxRouterData { + pub amount: f64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for StaxRouterData +{ + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_f64(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} + #[derive(Debug, Serialize)] pub struct StaxPaymentsRequestMetaData { tax: i64, @@ -26,21 +57,23 @@ pub struct StaxPaymentsRequest { idempotency_id: Option, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { +impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - if item.request.currency != enums::Currency::USD { + fn try_from( + item: &StaxRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + if item.router_data.request.currency != enums::Currency::USD { Err(errors::ConnectorError::NotSupported { - message: item.request.currency.to_string(), + message: item.router_data.request.currency.to_string(), connector: "Stax", })? } - let total = utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; + let total = item.amount; - match item.request.payment_method_data.clone() { + match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(_) => { - let pm_token = item.get_payment_method_token()?; - let pre_auth = !item.request.is_auto_capture()?; + let pm_token = item.router_data.get_payment_method_token()?; + let pre_auth = !item.router_data.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, @@ -52,14 +85,14 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { Err(errors::ConnectorError::InvalidWalletToken)? } }), - idempotency_id: Some(item.connector_request_reference_id.clone()), + idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) } api::PaymentMethodData::BankDebit( api_models::payments::BankDebitData::AchBankDebit { .. }, ) => { - let pm_token = item.get_payment_method_token()?; - let pre_auth = !item.request.is_auto_capture()?; + let pm_token = item.router_data.get_payment_method_token()?; + let pre_auth = !item.router_data.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, @@ -71,7 +104,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { Err(errors::ConnectorError::InvalidWalletToken)? } }), - idempotency_id: Some(item.connector_request_reference_id.clone()), + idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) } api::PaymentMethodData::BankDebit(_) @@ -347,13 +380,12 @@ pub struct StaxCaptureRequest { total: Option, } -impl TryFrom<&types::PaymentsCaptureRouterData> for StaxCaptureRequest { +impl TryFrom<&StaxRouterData<&types::PaymentsCaptureRouterData>> for StaxCaptureRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let total = utils::to_currency_base_unit_asf64( - item.request.amount_to_capture, - item.request.currency, - )?; + fn try_from( + item: &StaxRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let total = item.amount; Ok(Self { total: Some(total) }) } } @@ -365,15 +397,10 @@ pub struct StaxRefundRequest { pub total: f64, } -impl TryFrom<&types::RefundsRouterData> for StaxRefundRequest { +impl TryFrom<&StaxRouterData<&types::RefundsRouterData>> for StaxRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - Ok(Self { - total: utils::to_currency_base_unit_asf64( - item.request.refund_amount, - item.request.currency, - )?, - }) + fn try_from(item: &StaxRouterData<&types::RefundsRouterData>) -> Result { + Ok(Self { total: item.amount }) } } From 25245b965371d93449f4584667adeb38ab7e0e59 Mon Sep 17 00:00:00 2001 From: Adarsh Jha <132337675+adarsh-jha-dev@users.noreply.github.com> Date: Sun, 5 Nov 2023 18:52:35 +0530 Subject: [PATCH 22/57] feat(connector): [Payeezy] Currency Unit Conversion (#2710) Co-authored-by: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Co-authored-by: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> --- crates/router/src/connector/payeezy.rs | 50 ++++++++--- .../src/connector/payeezy/transformers.rs | 85 ++++++++++++++----- 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index da7126054378..03e76af907ce 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -90,6 +90,10 @@ impl ConnectorCommon for Payeezy { "payeezy" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -292,12 +296,19 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let req_obj = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, + &req_obj, utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_req)) } @@ -380,9 +391,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyPaymentsRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; + let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, + &req_obj, utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -469,10 +487,16 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyRefundRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, + &req_obj, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(payeezy_req)) @@ -499,16 +523,22 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { + // Parse the response into a payeezy::RefundResponse let response: payeezy::RefundResponse = res .response .parse_struct("payeezy RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RefundsRouterData::try_from(types::ResponseRouterData { + + // Create a new instance of types::RefundsRouterData based on the response, input data, and HTTP code + let response_data = types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + }; + let router_data = types::RefundsRouterData::try_from(response_data) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + + Ok(router_data) } fn get_error_response( diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index efcd1b36d5bb..3a859b325300 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -9,6 +9,37 @@ use crate::{ core::errors, types::{self, api, storage::enums, transformers::ForeignFrom}, }; +#[derive(Debug, Serialize)] +pub struct PayeezyRouterData { + pub amount: String, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PayeezyRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data, + }) + } +} #[derive(Serialize, Debug)] pub struct PayeezyCard { @@ -66,7 +97,7 @@ pub struct PayeezyPaymentsRequest { pub merchant_ref: String, pub transaction_type: PayeezyTransactionType, pub method: PayeezyPaymentMethodType, - pub amount: i64, + pub amount: String, pub currency_code: String, pub credit_card: PayeezyPaymentMethod, pub stored_credentials: Option, @@ -95,10 +126,12 @@ pub enum Initiator { CardHolder, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayeezyPaymentsRequest { +impl TryFrom<&PayeezyRouterData<&types::PaymentsAuthorizeRouterData>> for PayeezyPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.payment_method { + fn try_from( + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.payment_method { diesel_models::enums::PaymentMethod::Card => get_card_specific_payment_data(item), diesel_models::enums::PaymentMethod::CardRedirect @@ -119,14 +152,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayeezyPaymentsRequest { } fn get_card_specific_payment_data( - item: &types::PaymentsAuthorizeRouterData, + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - let merchant_ref = item.attempt_id.to_string(); + let merchant_ref = item.router_data.attempt_id.to_string(); let method = PayeezyPaymentMethodType::CreditCard; - let amount = item.request.amount; - let currency_code = item.request.currency.to_string(); + let amount = item.amount.clone(); + let currency_code = item.router_data.request.currency.to_string(); let credit_card = get_payment_method_data(item)?; - let (transaction_type, stored_credentials) = get_transaction_type_and_stored_creds(item)?; + let (transaction_type, stored_credentials) = + get_transaction_type_and_stored_creds(item.router_data)?; Ok(PayeezyPaymentsRequest { merchant_ref, transaction_type, @@ -135,7 +169,7 @@ fn get_card_specific_payment_data( currency_code, credit_card, stored_credentials, - reference: item.connector_request_reference_id.clone(), + reference: item.router_data.connector_request_reference_id.clone(), }) } fn get_transaction_type_and_stored_creds( @@ -201,9 +235,9 @@ fn is_mandate_payment( } fn get_payment_method_data( - item: &types::PaymentsAuthorizeRouterData, + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - match item.request.payment_method_data { + match item.router_data.request.payment_method_data { api::PaymentMethodData::Card(ref card) => { let card_type = PayeezyCardType::try_from(card.get_card_issuer()?)?; let payeezy_card = PayeezyCard { @@ -305,16 +339,20 @@ pub struct PayeezyCaptureOrVoidRequest { currency_code: String, } -impl TryFrom<&types::PaymentsCaptureRouterData> for PayeezyCaptureOrVoidRequest { +impl TryFrom<&PayeezyRouterData<&types::PaymentsCaptureRouterData>> + for PayeezyCaptureOrVoidRequest +{ type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + fn try_from( + item: &PayeezyRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { let metadata: PayeezyPaymentsMetadata = - utils::to_connector_meta(item.request.connector_meta.clone()) + utils::to_connector_meta(item.router_data.request.connector_meta.clone()) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { transaction_type: PayeezyTransactionType::Capture, - amount: item.request.amount_to_capture.to_string(), - currency_code: item.request.currency.to_string(), + amount: item.amount.clone(), + currency_code: item.router_data.request.currency.to_string(), transaction_tag: metadata.transaction_tag, }) } @@ -338,6 +376,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for PayeezyCaptureOrVoidRequest { }) } } + #[derive(Debug, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] pub enum PayeezyTransactionType { @@ -442,16 +481,18 @@ pub struct PayeezyRefundRequest { currency_code: String, } -impl TryFrom<&types::RefundsRouterData> for PayeezyRefundRequest { +impl TryFrom<&PayeezyRouterData<&types::RefundsRouterData>> for PayeezyRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { + fn try_from( + item: &PayeezyRouterData<&types::RefundsRouterData>, + ) -> Result { let metadata: PayeezyPaymentsMetadata = - utils::to_connector_meta(item.request.connector_metadata.clone()) + utils::to_connector_meta(item.router_data.request.connector_metadata.clone()) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { transaction_type: PayeezyTransactionType::Refund, - amount: item.request.refund_amount.to_string(), - currency_code: item.request.currency.to_string(), + amount: item.amount.clone(), + currency_code: item.router_data.request.currency.to_string(), transaction_tag: metadata.transaction_tag, }) } From d11e7fd5642efe7da4b5021d87cf40f16d9eeded Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:31:49 +0000 Subject: [PATCH 23/57] test(postman): update postman collection files --- .../paypal.postman_collection.json | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index 5a92253a9ba0..4849a27fe051 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -4,7 +4,33 @@ "listen": "prerequest", "script": { "exec": [ - "" + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "", + "if (isPaymentCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"paypal\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject routing in the request\");", + " console.error(error);", + " }", + "}" ], "type": "text/javascript" } From d7b1673b85bf5994d0b8a0f1e51b3624b678341a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:31:50 +0000 Subject: [PATCH 24/57] chore(version): v1.72.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c492d22bd5e..207595f42828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.72.0 (2023-11-05) + +### Features + +- **connector:** + - [ACI] Currency Unit Conversion ([#2750](https://github.com/juspay/hyperswitch/pull/2750)) ([`cdead78`](https://github.com/juspay/hyperswitch/commit/cdead78ea6a1f2dce92187f499f54498ba4bb173)) + - [Fiserv] Currency Unit Conversion ([#2715](https://github.com/juspay/hyperswitch/pull/2715)) ([`b6b9e4f`](https://github.com/juspay/hyperswitch/commit/b6b9e4f912e1c61cd31ab91be587ffb08c9f3a5b)) + - [Bitpay] Use `connector_request_reference_id` as reference to the connector ([#2697](https://github.com/juspay/hyperswitch/pull/2697)) ([`7141b89`](https://github.com/juspay/hyperswitch/commit/7141b89d231bae0c3b1c10095b88df16129b1665)) + - [NMI] Currency Unit Conversion ([#2707](https://github.com/juspay/hyperswitch/pull/2707)) ([`1b45a30`](https://github.com/juspay/hyperswitch/commit/1b45a302630ed8affc5abff0de1325fb5c6f870e)) + - [Payeezy] Currency Unit Conversion ([#2710](https://github.com/juspay/hyperswitch/pull/2710)) ([`25245b9`](https://github.com/juspay/hyperswitch/commit/25245b965371d93449f4584667adeb38ab7e0e59)) + +### Refactors + +- **connector:** [Stax] Currency Unit Conversion ([#2711](https://github.com/juspay/hyperswitch/pull/2711)) ([`2782923`](https://github.com/juspay/hyperswitch/commit/278292322c7c06f4239dd73861469e436bd941fa)) + +### Testing + +- **postman:** Update postman collection files ([`d11e7fd`](https://github.com/juspay/hyperswitch/commit/d11e7fd5642efe7da4b5021d87cf40f16d9eeded)) + +**Full Changelog:** [`v1.71.0...v1.72.0`](https://github.com/juspay/hyperswitch/compare/v1.71.0...v1.72.0) + +- - - + + ## 1.71.0 (2023-11-03) ### Features From d335879f9289b57a90a76c6587a58a0b3e12c9ad Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:40:10 +0530 Subject: [PATCH 25/57] feat(router): make webhook events config disabled only and by default enable all the events (#2770) --- crates/router/src/core/webhooks.rs | 11 +++++- crates/router/src/core/webhooks/utils.rs | 48 ++++++++++-------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 8b7df2a14be7..ba4d7f6549e7 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -963,8 +963,13 @@ pub async fn webhooks_core api::MerchantWebhookConfig { - std::collections::HashSet::from([ - api::IncomingWebhookEvent::PaymentIntentSuccess, - api::IncomingWebhookEvent::PaymentIntentFailure, - api::IncomingWebhookEvent::PaymentIntentProcessing, - api::IncomingWebhookEvent::PaymentIntentCancelled, - api::IncomingWebhookEvent::PaymentActionRequired, - api::IncomingWebhookEvent::RefundSuccess, - ]) -} - const IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = "irrelevant_payment_id_in_source_verification_flow"; const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = @@ -30,38 +20,40 @@ const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str = "irrelevant_connector_request_reference_id_in_source_verification_flow"; -/// Check whether the merchant has configured to process the webhook `event` for the `connector` +/// Check whether the merchant has configured to disable the webhook `event` for the `connector` /// First check for the key "whconf_{merchant_id}_{connector_id}" in redis, -/// if not found, fetch from configs table in database, if not found use default -pub async fn lookup_webhook_event( +/// if not found, fetch from configs table in database +pub async fn is_webhook_event_disabled( db: &dyn StorageInterface, connector_id: &str, merchant_id: &str, event: &api::IncomingWebhookEvent, ) -> bool { - let redis_key = format!("whconf_{merchant_id}_{connector_id}"); - let merchant_webhook_config_result = - get_and_deserialize_key(db, &redis_key, "MerchantWebhookConfig") - .await - .map(|h| &h | &default_webhook_config()); + let redis_key = format!("whconf_disabled_events_{merchant_id}_{connector_id}"); + let merchant_webhook_disable_config_result: CustomResult< + api::MerchantWebhookConfig, + redis_interface::errors::RedisError, + > = get_and_deserialize_key(db, &redis_key, "MerchantWebhookConfig").await; - match merchant_webhook_config_result { + match merchant_webhook_disable_config_result { Ok(merchant_webhook_config) => merchant_webhook_config.contains(event), Err(..) => { //if failed to fetch from redis. fetch from db and populate redis db.find_config_by_key(&redis_key) .await .map(|config| { - if let Ok(set) = - serde_json::from_str::(&config.config) - { - &set | &default_webhook_config() - } else { - default_webhook_config() + match serde_json::from_str::(&config.config) { + Ok(set) => set.contains(event), + Err(err) => { + logger::warn!(?err, "error while parsing merchant webhook config"); + false + } } }) - .unwrap_or_else(|_| default_webhook_config()) - .contains(event) + .unwrap_or_else(|err| { + logger::warn!(?err, "error while fetching merchant webhook config"); + false + }) } } } From ff73aba8e72d8e072027881760335c0c818df665 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 6 Nov 2023 13:45:15 +0530 Subject: [PATCH 26/57] feat: make drainer logs queryable with request_id and global_id (#2771) --- crates/diesel_models/src/kv.rs | 22 +++++++++++++++------- crates/drainer/src/lib.rs | 16 ++++++++++++++++ crates/drainer/src/services.rs | 2 ++ crates/router/src/db.rs | 20 ++++++++++++++++++++ crates/router/src/routes/app.rs | 2 ++ crates/router_env/src/logger/formatter.rs | 4 ++++ crates/storage_impl/src/lib.rs | 12 +++++++++++- 7 files changed, 70 insertions(+), 8 deletions(-) diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index f1145a4b6e1f..81fa7a88ee3b 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -27,13 +27,21 @@ pub struct TypedSql { } impl TypedSql { - pub fn to_field_value_pairs(&self) -> crate::StorageResult> { - Ok(vec![( - "typed_sql", - serde_json::to_string(self) - .into_report() - .change_context(errors::DatabaseError::QueryGenerationFailed)?, - )]) + pub fn to_field_value_pairs( + &self, + request_id: String, + global_id: String, + ) -> crate::StorageResult> { + Ok(vec![ + ( + "typed_sql", + serde_json::to_string(self) + .into_report() + .change_context(errors::DatabaseError::QueryGenerationFailed)?, + ), + ("global_id", global_id), + ("request_id", request_id), + ]) } } diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7dcbc2c518cf..19abe9ba3aad 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -10,6 +10,7 @@ use std::sync::{atomic, Arc}; use common_utils::signals::get_allowed_signals; use diesel_models::kv; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use tokio::sync::{mpsc, oneshot}; use crate::{connection::pg_connection, services::Store}; @@ -122,6 +123,7 @@ async fn drainer_handler( active_tasks.fetch_add(1, atomic::Ordering::Release); let stream_name = utils::get_drainer_stream_name(store.clone(), stream_index); + let drainer_result = Box::pin(drainer(store.clone(), max_read_count, stream_name.as_str())).await; @@ -130,6 +132,7 @@ async fn drainer_handler( } let flag_stream_name = utils::get_stream_key_flag(store.clone(), stream_index); + //TODO: USE THE RESULT FOR LOGGING let output = utils::make_stream_available(flag_stream_name.as_str(), store.redis_conn.as_ref()).await; @@ -137,6 +140,7 @@ async fn drainer_handler( output } +#[instrument(skip_all, fields(global_id, request_id, session_id))] async fn drainer( store: Arc, max_read_count: u64, @@ -174,9 +178,21 @@ async fn drainer( }], ); + let session_id = common_utils::generate_id_with_default_len("drainer_session"); + // TODO: Handle errors when deserialization fails and when DB error occurs for entry in entries { let typed_sql = entry.1.get("typed_sql").map_or(String::new(), Clone::clone); + let request_id = entry + .1 + .get("request_id") + .map_or(String::new(), Clone::clone); + let global_id = entry.1.get("global_id").map_or(String::new(), Clone::clone); + + tracing::Span::current().record("request_id", request_id); + tracing::Span::current().record("global_id", global_id); + tracing::Span::current().record("session_id", &session_id); + let result = serde_json::from_str::(&typed_sql); let db_op = match result { Ok(f) => f, diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index 6edec31f26d7..73f66f27dbf5 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -7,6 +7,7 @@ pub struct Store { pub master_pool: PgPool, pub redis_conn: Arc, pub config: StoreConfig, + pub request_id: Option, } #[derive(Clone)] @@ -30,6 +31,7 @@ impl Store { drainer_stream_name: config.drainer.stream_name.clone(), drainer_num_partitions: config.drainer.num_partitions, }, + request_id: None, } } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 210f3d21e8cc..b62ffd2c530f 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -76,6 +76,7 @@ pub trait StorageInterface: + MasterKeyInterface + payment_link::PaymentLinkInterface + RedisConnInterface + + RequestIdStore + business_profile::BusinessProfileInterface + organization::OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface @@ -118,6 +119,25 @@ impl StorageInterface for MockDb { } } +pub trait RequestIdStore { + fn add_request_id(&mut self, _request_id: String) {} + fn get_request_id(&self) -> Option { + None + } +} + +impl RequestIdStore for MockDb {} + +impl RequestIdStore for Store { + fn add_request_id(&mut self, request_id: String) { + self.request_id = Some(request_id) + } + + fn get_request_id(&self) -> Option { + self.request_id.clone() + } +} + pub async fn get_and_deserialize_key( db: &dyn StorageInterface, key: &str, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0369bb612668..268b2ed703bf 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -80,7 +80,9 @@ impl AppStateInfo for AppState { } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); + self.store.add_request_id(request_id.to_string()) } + fn add_merchant_id(&mut self, merchant_id: Option) { self.api_client.add_merchant_id(merchant_id); } diff --git a/crates/router_env/src/logger/formatter.rs b/crates/router_env/src/logger/formatter.rs index ce2fd74e0e87..4fd94c221637 100644 --- a/crates/router_env/src/logger/formatter.rs +++ b/crates/router_env/src/logger/formatter.rs @@ -51,6 +51,8 @@ const REQUEST_METHOD: &str = "request_method"; const REQUEST_URL_PATH: &str = "request_url_path"; const REQUEST_ID: &str = "request_id"; const WORKFLOW_ID: &str = "workflow_id"; +const GLOBAL_ID: &str = "global_id"; +const SESSION_ID: &str = "session_id"; /// Set of predefined implicit keys. pub static IMPLICIT_KEYS: Lazy> = Lazy::new(|| { @@ -85,6 +87,8 @@ pub static EXTRA_IMPLICIT_KEYS: Lazy> = Lazy::new(|| set.insert(REQUEST_METHOD); set.insert(REQUEST_URL_PATH); set.insert(REQUEST_ID); + set.insert(GLOBAL_ID); + set.insert(SESSION_ID); set.insert(WORKFLOW_ID); set diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 17d432c7932b..cef4a8981a43 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -31,6 +31,7 @@ pub struct RouterStore { db_store: T, cache_store: RedisStore, master_encryption_key: StrongSecret>, + pub request_id: Option, } #[async_trait::async_trait] @@ -103,6 +104,7 @@ impl RouterStore { db_store, cache_store, master_encryption_key: encryption_key, + request_id: None, }) } @@ -128,6 +130,7 @@ impl RouterStore { db_store, cache_store, master_encryption_key: encryption_key, + request_id: None, }) } } @@ -138,6 +141,7 @@ pub struct KVRouterStore { drainer_stream_name: String, drainer_num_partitions: u8, ttl_for_kv: u32, + pub request_id: Option, } #[async_trait::async_trait] @@ -179,11 +183,14 @@ impl KVRouterStore { drainer_num_partitions: u8, ttl_for_kv: u32, ) -> Self { + let request_id = store.request_id.clone(); + Self { router_store: store, drainer_stream_name, drainer_num_partitions, ttl_for_kv, + request_id, } } @@ -203,6 +210,9 @@ impl KVRouterStore { where R: crate::redis::kv_store::KvStorePartition, { + let global_id = format!("{}", partition_key); + let request_id = self.request_id.clone().unwrap_or_default(); + let shard_key = R::shard_key(partition_key, self.drainer_num_partitions); let stream_name = self.get_drainer_stream_name(&shard_key); self.router_store @@ -212,7 +222,7 @@ impl KVRouterStore { &stream_name, &redis_interface::RedisEntryId::AutoGeneratedID, redis_entry - .to_field_value_pairs() + .to_field_value_pairs(request_id, global_id) .change_context(RedisError::JsonSerializationFailed)?, ) .await From 4563935372d2cdff3f746fa86a47f1166ffd32ac Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:46:12 +0530 Subject: [PATCH 27/57] feat(connector): [BANKOFAMERICA] Add Connector Template Code (#2764) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- config/config.example.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/enums.rs | 4 +- crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 20 +- crates/router/src/connector/bankofamerica.rs | 536 ++++++++++++++++++ .../connector/bankofamerica/transformers.rs | 250 ++++++++ crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 19 + crates/router/src/types/api.rs | 1 + .../router/tests/connectors/bankofamerica.rs | 420 ++++++++++++++ crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 7 +- crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + openapi/openapi_spec.json | 2 +- scripts/add_connector.sh | 2 +- 18 files changed, 1262 insertions(+), 13 deletions(-) create mode 100644 crates/router/src/connector/bankofamerica.rs create mode 100644 crates/router/src/connector/bankofamerica/transformers.rs create mode 100644 crates/router/tests/connectors/bankofamerica.rs diff --git a/config/config.example.toml b/config/config.example.toml index 59083d6c71d3..ed9cf9698984 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -163,6 +163,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" diff --git a/config/development.toml b/config/development.toml index 5e74eafcb467..34fbdbc9e078 100644 --- a/config/development.toml +++ b/config/development.toml @@ -71,6 +71,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", @@ -136,6 +137,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 20ca175ceb84..282894b56d43 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -78,6 +78,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -145,6 +146,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ee67c1187e6b..b27e71b9e8f5 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -75,8 +75,9 @@ pub enum Connector { Adyen, Airwallex, Authorizedotnet, - Bitpay, Bambora, + // Bankofamerica, Added as template code for future usage + Bitpay, Bluesnap, Boku, Braintree, @@ -195,6 +196,7 @@ pub enum RoutableConnectors { Adyen, Airwallex, Authorizedotnet, + // Bankofamerica, Added as template code for future usage Bitpay, Bambora, Bluesnap, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 204060b37aa0..df87c8a460ac 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -531,6 +531,7 @@ pub struct Connectors { pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, pub bambora: ConnectorParams, + pub bankofamerica: ConnectorParams, pub bitpay: ConnectorParams, pub bluesnap: ConnectorParamsWithSecondaryBaseUrl, pub boku: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 7849fd98a4d1..3a83fea0d910 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -3,6 +3,7 @@ pub mod adyen; pub mod airwallex; pub mod authorizedotnet; pub mod bambora; +pub mod bankofamerica; pub mod bitpay; pub mod bluesnap; pub mod boku; @@ -55,13 +56,14 @@ pub mod zen; pub use self::dummyconnector::DummyConnector; pub use self::{ aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet, - bambora::Bambora, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, braintree::Braintree, - cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay, - cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, forte::Forte, globalpay::Globalpay, - globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, - mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, - nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, - payu::Payu, powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, - square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, - wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + bambora::Bambora, bankofamerica::Bankofamerica, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, + braintree::Braintree, cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, + cryptopay::Cryptopay, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, forte::Forte, + globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, helcim::Helcim, + iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, + nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, + payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, + prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, + stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, + worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs new file mode 100644 index 000000000000..e25d99f9af3d --- /dev/null +++ b/crates/router/src/connector/bankofamerica.rs @@ -0,0 +1,536 @@ +pub mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use transformers as bankofamerica; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Bankofamerica; + +impl api::Payment for Bankofamerica {} +impl api::PaymentSession for Bankofamerica {} +impl api::ConnectorAccessToken for Bankofamerica {} +impl api::MandateSetup for Bankofamerica {} +impl api::PaymentAuthorize for Bankofamerica {} +impl api::PaymentSync for Bankofamerica {} +impl api::PaymentCapture for Bankofamerica {} +impl api::PaymentVoid for Bankofamerica {} +impl api::Refund for Bankofamerica {} +impl api::RefundExecute for Bankofamerica {} +impl api::RefundSync for Bankofamerica {} +impl api::PaymentToken for Bankofamerica {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Bankofamerica +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Bankofamerica +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::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 Bankofamerica { + fn id(&self) -> &'static str { + "bankofamerica" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.bankofamerica.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = bankofamerica::BankofamericaAuthType::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, + ) -> CustomResult { + let response: bankofamerica::BankofamericaErrorResponse = res + .response + .parse_struct("BankofamericaErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + }) + } +} + +impl ConnectorValidation for Bankofamerica { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration + for Bankofamerica +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Bankofamerica +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Bankofamerica +{ +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::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: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = + bankofamerica::BankofamericaPaymentsRequest::try_from(&connector_router_data)?; + let bankofamerica_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("Bankofamerica PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::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: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::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: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("bankofamerica PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::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: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("Bankofamerica PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::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: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = bankofamerica::BankofamericaRefundRequest::try_from(&connector_router_data)?; + let bankofamerica_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: bankofamerica::RefundResponse = res + .response + .parse_struct("bankofamerica RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::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: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::RefundResponse = res + .response + .parse_struct("bankofamerica RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Bankofamerica { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs new file mode 100644 index 000000000000..a396c47a4ced --- /dev/null +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -0,0 +1,250 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +//TODO: Fill the struct with respective fields +pub struct BankofamericaRouterData { + pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for BankofamericaRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Ok(Self { + amount, + router_data: item, + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct BankofamericaPaymentsRequest { + amount: i64, + card: BankofamericaCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct BankofamericaCard { + name: Secret, + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankofamericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = BankofamericaCard { + name: req_card.card_holder_name, + 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.to_owned(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct BankofamericaAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for BankofamericaAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::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 BankofamericaPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: BankofamericaPaymentStatus) -> Self { + match item { + BankofamericaPaymentStatus::Succeeded => Self::Charged, + BankofamericaPaymentStatus::Failed => Self::Failure, + BankofamericaPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BankofamericaPaymentsResponse { + status: BankofamericaPaymentStatus, + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankofamericaPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct BankofamericaRefundRequest { + pub amount: i64, +} + +impl TryFrom<&BankofamericaRouterData<&types::RefundsRouterData>> + for BankofamericaRefundRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankofamericaRouterData<&types::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 types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::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 BankofamericaErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5de273de0cef..e1e5ea744e2f 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1499,6 +1499,10 @@ pub(crate) fn validate_auth_and_metadata_type( authorizedotnet::transformers::AuthorizedotnetAuthType::try_from(val)?; Ok(()) } + // api_enums::Connector::Bankofamerica => { + // bankofamerica::transformers::BankofamericaAuthType::try_from(val)?; + // Ok(()) + // } Added as template code for future usage api_enums::Connector::Bitpay => { bitpay::transformers::BitpayAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c55df8e35d6e..0b253cdc6079 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -144,6 +144,7 @@ impl default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, + connector::Bankofamerica, connector::Bitpay, connector::Boku, connector::Cashtocode, @@ -212,6 +213,7 @@ default_imp_for_webhook_source_verification!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Braintree, @@ -290,6 +292,7 @@ default_imp_for_create_customer!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -366,6 +369,7 @@ default_imp_for_connector_redirect_response!( connector::Aci, connector::Adyen, connector::Bitpay, + connector::Bankofamerica, connector::Boku, connector::Cashtocode, connector::Coinbase, @@ -416,6 +420,7 @@ default_imp_for_connector_request_id!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -496,6 +501,7 @@ default_imp_for_accept_dispute!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -596,6 +602,7 @@ default_imp_for_file_upload!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -673,6 +680,7 @@ default_imp_for_submit_evidence!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -750,6 +758,7 @@ default_imp_for_defend_dispute!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -828,6 +837,7 @@ default_imp_for_pre_processing_steps!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -886,6 +896,7 @@ default_imp_for_payouts!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -964,6 +975,7 @@ default_imp_for_payouts_create!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1045,6 +1057,7 @@ default_imp_for_payouts_eligibility!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1123,6 +1136,7 @@ default_imp_for_payouts_fulfill!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1201,6 +1215,7 @@ default_imp_for_payouts_cancel!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1280,6 +1295,7 @@ default_imp_for_payouts_quote!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1359,6 +1375,7 @@ default_imp_for_payouts_recipient!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1437,6 +1454,7 @@ default_imp_for_approve!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1516,6 +1534,7 @@ default_imp_for_reject!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 69e7f8898d15..2179b4bde180 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -303,6 +303,7 @@ impl ConnectorData { enums::Connector::Airwallex => Ok(Box::new(&connector::Airwallex)), enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), + // enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), Added as template code for future usage enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)), enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)), enums::Connector::Boku => Ok(Box::new(&connector::Boku)), diff --git a/crates/router/tests/connectors/bankofamerica.rs b/crates/router/tests/connectors/bankofamerica.rs new file mode 100644 index 000000000000..ce264cbccc86 --- /dev/null +++ b/crates/router/tests/connectors/bankofamerica.rs @@ -0,0 +1,420 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct BankofamericaTest; +impl ConnectorActions for BankofamericaTest {} +impl utils::Connector for BankofamericaTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Bankofamerica; + types::api::ConnectorData { + connector: Box::new(&Bankofamerica), + connector_name: types::Connector::DummyConnector1, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .bankofamerica + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "bankofamerica".to_string() + } +} + +static CONNECTOR: BankofamericaTest = BankofamericaTest {}; + +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: router::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: router::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 scenerios +// 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: types::api::PaymentMethodData::Card(api::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: types::api::PaymentMethodData::Card(api::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: types::api::PaymentMethodData::Card(api::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 ed06312b77ac..03b6181b8a89 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -11,6 +11,7 @@ mod adyen; mod airwallex; mod authorizedotnet; mod bambora; +mod bankofamerica; mod bitpay; mod bluesnap; mod boku; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 0966db95a42f..f8f6039d6d36 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -183,4 +183,9 @@ api_key="API Key" api_key="API Key" [prophetpay] -api_key="API Key" \ No newline at end of file +api_key="API Key" + +[bankofamerica] +api_key = "MyApiKey" +key1 = "Merchant id" +api_secret = "Secret key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index d774e2530e9d..9562972c126e 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -17,6 +17,7 @@ pub struct ConnectorAuthentication { pub airwallex: Option, pub authorizedotnet: Option, pub bambora: Option, + pub bankofamerica: Option, pub bitpay: Option, pub bluesnap: Option, pub boku: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 7cdbc8dd6fdd..352c4ff551bc 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -64,6 +64,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -130,6 +131,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 45d0bde9d323..822b1aacee96 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -3898,8 +3898,8 @@ "adyen", "airwallex", "authorizedotnet", - "bitpay", "bambora", + "bitpay", "bluesnap", "boku", "braintree", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index bcd02f6cbd68..9fdc57bf3c81 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 airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From 7d33ab32db4d9511298597c455034380202183e8 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:23:42 +0530 Subject: [PATCH 28/57] ci(postman): Fix auto-update collection files (#2758) Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- .github/workflows/release-new-version.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index f489a3f8de2a..872c207e8aa3 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -74,7 +74,11 @@ jobs: connector=$(basename ${connector_dir}) newman dir-import ${POSTMAN_DIR}/${connector} -o ${POSTMAN_JSON_DIR}/${connector}.postman_collection.json done - (git diff --quiet && git diff --staged --quiet) || (git commit -am 'test(postman): update postman collection files' && echo "Committed changes") || (echo "Unable to commit the following changes:" && git diff) + + if git add postman && ! git diff --staged --quiet postman; then + git commit --message 'test(postman): update postman collection files' + echo "Changes detected and commited." + fi - name: Obtain previous and new tag information shell: bash From fd67657f46060bb402681a0ad2383f5b23570ad3 Mon Sep 17 00:00:00 2001 From: AnandKGanesh <118448330+AnandKGanesh@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:30:55 +0530 Subject: [PATCH 29/57] Add files via upload --- docs/imgs/aws_button.png | Bin 0 -> 2427 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/imgs/aws_button.png diff --git a/docs/imgs/aws_button.png b/docs/imgs/aws_button.png new file mode 100644 index 0000000000000000000000000000000000000000..4c8c2c20e09729f83f528882be4c3082b0264c0c GIT binary patch literal 2427 zcma);=OY^k1I9xsB|(W9MbL^I5}OiftV*s`)QDE?S&e!xW@6N;D_Sv2sWXdmXScSq zcO5;VXb_aNh}uNl_5J*Qc|SbA=lKJk55H$tmN!9sQhWda0ED$LwfWK0KZ4}F_=8V} z(e(hpC1b29#x{&&E5E}5>>x3~+{8VB8B0|t-foPweZyBkF?s7M(&m~GGlIIuyl+-L?4`;=39f;UVrbH zno9op0(zyv4sHiCV@SiZeN2pM&daUAWgeXC`@zb`p);_gbU|moKUT>YYh&w7rybOu z9kwbWDP1APC@ZBH)>4csgXx$2pCI-HrA>CHOQL9RsndkTYE(^b05Wp-SI3;Mnvm&M zeKNaAALyjVPLtd`82kDW)zF%TdYl?{-FtCjF~Vb}AXsNmM9KL3r!edHduFSx?zbEX zRR&RXy+HIz?^?FeU(CLZ%bK&|jh{6RTm7gTVh`-z=GMs&!;5tf!n_EBDzwFCs=ZD^B3jU6W z=uiHcL2|ZX*yYddK?|Mf@kl|r{oMxJsV1M`wl{!+MK8Lk{wc5~qj2dFPk1d(0;c@I ztjmhtKTTluM6Mh+5%rvP%nGXtl5RY6^1rGo((Y(bG@dwuNvy;}Ds8R!c(qX6y`j~g zk1gajzO~gI)~ia$D)-OmY$Cw7_b+kxYD=b%4tae)54pRnmM^gCS;B&np_%YYmU2pu z+}Hz`x83WW5rsUCRjv?brC9s2$fqF-nI-p!wlGYszOl=G2+T-Uf-q(>P z+t8r%ONP$kOpS;uvI0h|o5HLmw(Gf&cTr7f?YIWjF{HpHryu9XVjW3g*5$>jgURGgQ%=`+9?^UojX5Hw+; zUwm0w4Gru?ENJ%XNpZubdu)4=TJ69>^*^QUWzd2&-TS;XoZd}nL0EhaFa zpC0ZkIJkK%yvvm$xo&);6flHS;uctPqSdesg(pn(bk+7{CXenZ+TOT^KahQ4u!D&VsCO4$#!q#q133_4_K_i5{4@E>6v%rMXrPfHFKt07n>VcrSr;9Jo8mKCL9;0{q zRyn1pXXX{>)*%)ACifx1H$_LY)YuynR2>oc?b|2D1V~jW8^K$~2^mFqV3lT+LBr0WIrmKZBOVMJN?A4*2m^^1H^MVa6%~;96 zMkpmfekzrZZeXE6ag?itY2weeR##`fy5tS@L7p`So!t3y(Pn<~RYoIMer)bFi+1NZbLBn3cZoI%PVp2Z zkLQLGH}whxB?EWc5ZUUbV`iW7QKj$Trf6!@gbbtrx&dVDDc9rgem;A-JjS9%|OuM#;KLFdd!9Da$_>Jgyc8`Ws!>dDbs27`6e zlz^I6A(ju(k#85K3PgXl9p1@}tv?GTB_x$GtdR5~Py9P3!M0eN8ie57%*IgK z+$|-=KFaO%FDh!mA~fPhEDFX1W}}J>0|M}-e7Scq%0kAUSIQMDeX_K z=v%%lp?el*ymW@oQr=OtR?Ae`5dy#)$f)@!6*yv4N9DD|L#deJh_F#@NX}Hk_=rW+=MNQmJ}1v|t1` z-*TlKtb(rSc4QgQ7dlzwYE^*5s3zvU5xU-{{_PWUyOaAx$-{VJp!B*rx{f=Uhao03 ztd1M^mHl$P37-XLkZLP->h~LM80nNE;ZyVA+ng~3r4&UV=CT+E3qQdoG`Qf$tEC`2XhqD!Bi8QL+7dnK!3@a_CH#`YEg57T`lGJsA z4(0g5_>LYt`qVyky#)`mZ{I8yyh(<>Yb4(_HiYsKnmP5P0ym$ffOL zY__Wiue)=&IKQggH>1^p>3yHHj_jjYo7QBO!=-KQRZ>ax7>Y`%j~!XU7=^t~&~3(V zb#`q6?6Yf;*o>qdewutlu^bRumPn!ocH1wRkJOEiYt^R4`j?^HiZ%ruUm=p@v3xFU zGK6(9UuAi2KYF|PBPIIo-!FWu=gKQaOa(e@L@$xdu+8s_aR<;M+(E&OYQfYY%H5Md zy?yH@y%V_W&Zzzt~TnOkQ@@a|Wxp3su4!B@o%7tic8qPG6KWzSA+{D|in zq7xE!DrO9!i5eHbjk`mCxd#vHdjIf?-49yehaaM{d7g@$zuy0O?MB@hb|nv50>fQ@ kH?;nH*ukp Date: Mon, 6 Nov 2023 23:17:53 +0530 Subject: [PATCH 30/57] feat(connector): [Bitpay] Add order id as the reference id (#2591) Co-authored-by: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> --- crates/router/src/connector/bitpay/transformers.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/bitpay/transformers.rs b/crates/router/src/connector/bitpay/transformers.rs index 5af20d6423fd..89dd2368b2b7 100644 --- a/crates/router/src/connector/bitpay/transformers.rs +++ b/crates/router/src/connector/bitpay/transformers.rs @@ -134,6 +134,7 @@ pub struct BitpayPaymentResponseData { pub expiration_time: Option, pub current_time: Option, pub id: String, + pub order_id: Option, pub low_fee_detected: Option, pub display_amount_paid: Option, pub exception_status: ExceptionStatus, @@ -162,7 +163,7 @@ impl .data .url .map(|x| services::RedirectForm::from((x, services::Method::Get))); - let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id); + let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id.clone()); let attempt_status = item.response.data.status; Ok(Self { status: enums::AttemptStatus::from(attempt_status), @@ -172,7 +173,11 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item + .response + .data + .order_id + .or(Some(item.response.data.id)), }), ..item.data }) From 3199cd6f357d2599f4f3f7f00e49229f4d9cb249 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:25:58 +0530 Subject: [PATCH 31/57] ci: use `env` input to fix the command injection vulnerability (#2797) --- .github/workflows/pr-title-check.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 8c15246f0e6c..167be295443b 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -41,7 +41,9 @@ jobs: steps: - name: Store PR title in a file shell: bash - run: echo '${{ github.event.pull_request.title }}' > pr_title.txt + env: + TITLE: ${{ github.event.pull_request.title }} + run: echo $TITLE > pr_title.txt - name: Spell check uses: crate-ci/typos@master @@ -66,8 +68,10 @@ jobs: id: pr_title_check if: ${{ github.event_name == 'pull_request_target' }} shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} continue-on-error: true - run: cog verify '${{ github.event.pull_request.title }}' + run: cog verify "$TITLE" - name: Verify commit message follows conventional commit standards id: commit_message_check From bb39cd4081fdcaf68b2b5de2234e93493dbd84b6 Mon Sep 17 00:00:00 2001 From: AnandKGanesh <118448330+AnandKGanesh@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:02:30 +0530 Subject: [PATCH 32/57] docs(README): add one-click deployment information using CDK (#2798) --- README.md | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cc19670a8fc3..ae46fff20f03 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,6 @@ The single API to access payment ecosystems across 130+ countries

-

🎉 Hacktoberfest is here! 🎉

- -New to Rust? Hyperswitch is the perfect place to start this hacktoberfest! 😁 - -> ⭐️ If you're new to Hacktoberfest, you can learn more and register to participate [here](https://hacktoberfest.com/participation/). Registration is from **September 26th - October 31st**. -
@@ -58,8 +52,6 @@ Using Hyperswitch, you can: - 🎨 **Customize payment flows** with full visibility and control - 🌐 **Increase business reach** with local/alternate payment methods -> Hyperswitch is **wire-compatible** with top processors like Stripe, making it easy to integrate. -
Hyperswitch-Product @@ -67,24 +59,18 @@ Using Hyperswitch, you can:

⚡️ Quick Start Guide

+

One-click deployment on AWS cloud

- - -Ways to get started with Hyperswitch: - -1. Try it in our Sandbox Environment: Fast and easy to - start. - No code or setup is required in your system, [learn more](/docs/try_sandbox.md) - +The fastest and easiest way to try hyperswitch is via our CDK scripts - +1. Click on the following button to deploy a Production-ready Kubernetes setup inside your AWS stack. + No code or setup is required in your system -2. A simple demo of integrating Hyperswitch with your React App, Try our React [Demo App](https://github.com/aashu331998/hyperswitch-react-demo-app/archive/refs/heads/main.zip). + +2. Sign-in to your AWS console. -3. Install in your local system: Configurations and - setup required in your system. - Suitable if you like to customise the core offering, [setup guide](/docs/try_local_system.md) +3. Follow the instructions provided on the console to successfully deploy Hyperswitch

🔌 Fast Integration for Stripe Users

From 34f52260d3fa68b54e5b46207afaf2ad07a8d8ba Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:04:10 +0530 Subject: [PATCH 33/57] fix(connector): fix amount conversion incase of minor unit (#2793) Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- crates/router/src/connector/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 6f1566a35b0d..3a8cae3a631e 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1077,7 +1077,7 @@ pub fn get_amount_as_string( currency: diesel_models::enums::Currency, ) -> Result> { let amount = match currency_unit { - types::api::CurrencyUnit::Minor => to_currency_lower_unit(amount.to_string(), currency)?, + types::api::CurrencyUnit::Minor => amount.to_string(), types::api::CurrencyUnit::Base => to_currency_base_unit(amount, currency)?, }; Ok(amount) From e7375d0e26099a7e0e6efd1b83b8eb9c7b1c5411 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:02:23 +0530 Subject: [PATCH 34/57] refactor(payment_methods): Added support for account subtype in pmd (#2651) --- crates/api_models/src/payment_methods.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b30590bfd6f2..dcbdb56bf7b7 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -167,6 +167,8 @@ pub struct CardDetailsPaymentMethod { pub struct PaymentMethodDataBankCreds { pub mask: String, pub hash: String, + pub account_type: Option, + pub account_name: Option, pub payment_method_type: api_enums::PaymentMethodType, pub connector_details: Vec, } From 784170225dc5aa8dcad69befe46b8f04653916a5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:36:26 +0000 Subject: [PATCH 35/57] chore(version): v1.73.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 207595f42828..4f33956eddf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.73.0 (2023-11-07) + +### Features + +- **connector:** + - [BANKOFAMERICA] Add Connector Template Code ([#2764](https://github.com/juspay/hyperswitch/pull/2764)) ([`4563935`](https://github.com/juspay/hyperswitch/commit/4563935372d2cdff3f746fa86a47f1166ffd32ac)) + - [Bitpay] Add order id as the reference id ([#2591](https://github.com/juspay/hyperswitch/pull/2591)) ([`d47d4ac`](https://github.com/juspay/hyperswitch/commit/d47d4ac682705d6ac692f9381149bbf08ad71264)) +- **router:** Make webhook events config disabled only and by default enable all the events ([#2770](https://github.com/juspay/hyperswitch/pull/2770)) ([`d335879`](https://github.com/juspay/hyperswitch/commit/d335879f9289b57a90a76c6587a58a0b3e12c9ad)) +- Make drainer logs queryable with request_id and global_id ([#2771](https://github.com/juspay/hyperswitch/pull/2771)) ([`ff73aba`](https://github.com/juspay/hyperswitch/commit/ff73aba8e72d8e072027881760335c0c818df665)) + +### Bug Fixes + +- **connector:** Fix amount conversion incase of minor unit ([#2793](https://github.com/juspay/hyperswitch/pull/2793)) ([`34f5226`](https://github.com/juspay/hyperswitch/commit/34f52260d3fa68b54e5b46207afaf2ad07a8d8ba)) + +### Refactors + +- **payment_methods:** Added support for account subtype in pmd ([#2651](https://github.com/juspay/hyperswitch/pull/2651)) ([`e7375d0`](https://github.com/juspay/hyperswitch/commit/e7375d0e26099a7e0e6efd1b83b8eb9c7b1c5411)) + +### Documentation + +- **README:** Add one-click deployment information using CDK ([#2798](https://github.com/juspay/hyperswitch/pull/2798)) ([`bb39cd4`](https://github.com/juspay/hyperswitch/commit/bb39cd4081fdcaf68b2b5de2234e93493dbd84b6)) + +**Full Changelog:** [`v1.72.0...v1.73.0`](https://github.com/juspay/hyperswitch/compare/v1.72.0...v1.73.0) + +- - - + + ## 1.72.0 (2023-11-05) ### Features From bef0a04edc6323b3b7a2e0dd7eeb7954915ba7cf Mon Sep 17 00:00:00 2001 From: AnandKGanesh <118448330+AnandKGanesh@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:28:44 +0530 Subject: [PATCH 36/57] docs(README): update README (#2800) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae46fff20f03..e6e9baa07f7d 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,7 @@ The single API to access payment ecosystems across 130+ countries
-Hyperswitch is an open source payments switch to make payments fast, reliable, and, affordable. -It lets you connect with multiple payment processors and route traffic effortlessly, all with a single API integration. - +Hyperswitch is a community-led, open payments switch to enable access to the best payments infrastructure for every digital business. Using Hyperswitch, you can: @@ -63,8 +61,8 @@ Using Hyperswitch, you can: The fastest and easiest way to try hyperswitch is via our CDK scripts -1. Click on the following button to deploy a Production-ready Kubernetes setup inside your AWS stack. - No code or setup is required in your system +1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. + No code or setup is required in your system and the deployment is covered within the AWS free-tier setup.
@@ -72,6 +70,8 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts 3. Follow the instructions provided on the console to successfully deploy Hyperswitch +For an early access to the production-ready setup fill this Early Access Form +

🔌 Fast Integration for Stripe Users

From 1effddd0a0d3985d6df03c4ae9be28712befc05e Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:00:21 +0530 Subject: [PATCH 37/57] feat(test_utils): Add custom-headers and custom delay support to rustman (#2636) --- .../workflows/postman-collection-runner.yml | 2 +- crates/test_utils/README.md | 7 ++ crates/test_utils/src/main.rs | 28 ++++++- crates/test_utils/src/newman_runner.rs | 78 +++++++++++++++++-- 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index 6b0911d1b456..3291755b56cf 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -143,7 +143,7 @@ jobs: for i in $(echo "$CONNECTORS" | tr "," "\n"); do echo $i - if ! cargo run --bin test_utils -- --connector_name="$i" --base_url="$BASE_URL" --admin_api_key="$ADMIN_API_KEY"; then + if ! cargo run --bin test_utils -- --connector-name="$i" --base-url="$BASE_URL" --admin-api-key="$ADMIN_API_KEY"; then failed_connectors+=("$i") fi done diff --git a/crates/test_utils/README.md b/crates/test_utils/README.md index 1e92174b3337..2edbc7104c25 100644 --- a/crates/test_utils/README.md +++ b/crates/test_utils/README.md @@ -28,9 +28,16 @@ Required fields: Optional fields: +- `--delay` -- To add a delay between requests in milliseconds. + - Maximum delay is 4294967295 milliseconds or 4294967.295 seconds or 71616 minutes or 1193.6 hours or 49.733 days + - Example: `--delay 1000` (for 1 second delay) - `--folder` -- To run individual folders in the collection - Use double quotes to specify folder name. If you wish to run multiple folders, separate them with a comma (`,`) - Example: `--folder "QuickStart"` or `--folder "Health check,QuickStart"` +- `--header` -- If you wish to add custom headers to the requests, you can pass them as a string + - Example: `--header "key:value"` + - If you want to pass multiple custom headers, you can pass multiple `--header` flags + - Example: `--header "key1:value1" --header "key2:value2"` - `--verbose` -- A boolean to print detailed logs (requests and responses) **Note:** Passing `--verbose` will also print the connector as well as admin API keys in the logs. So, make sure you don't push the commands with `--verbose` to any public repository. diff --git a/crates/test_utils/src/main.rs b/crates/test_utils/src/main.rs index 637122e468e6..22c91e063d8f 100644 --- a/crates/test_utils/src/main.rs +++ b/crates/test_utils/src/main.rs @@ -3,10 +3,10 @@ use std::process::{exit, Command}; use test_utils::newman_runner; fn main() { - let mut newman_command: Command = newman_runner::command_generate(); + let mut runner = newman_runner::generate_newman_command(); // Execute the newman command - let output = newman_command.spawn(); + let output = runner.newman_command.spawn(); let mut child = match output { Ok(child) => child, Err(err) => { @@ -16,6 +16,30 @@ fn main() { }; let status = child.wait(); + if runner.file_modified_flag { + let git_status = Command::new("git") + .args([ + "restore", + format!("{}/event.prerequest.js", runner.collection_path).as_str(), + ]) + .output(); + + match git_status { + Ok(output) => { + if output.status.success() { + let stdout_str = String::from_utf8_lossy(&output.stdout); + println!("Git command executed successfully: {stdout_str}"); + } else { + let stderr_str = String::from_utf8_lossy(&output.stderr); + eprintln!("Git command failed with error: {stderr_str}"); + } + } + Err(e) => { + eprintln!("Error running Git: {e}"); + } + } + } + let exit_code = match status { Ok(exit_status) => { if exit_status.success() { diff --git a/crates/test_utils/src/newman_runner.rs b/crates/test_utils/src/newman_runner.rs index c51556f8f255..af7fb5592813 100644 --- a/crates/test_utils/src/newman_runner.rs +++ b/crates/test_utils/src/newman_runner.rs @@ -1,22 +1,34 @@ -use std::{env, process::Command}; +use std::{ + env, + fs::OpenOptions, + io::{self, Write}, + path::Path, + process::Command, +}; use clap::{arg, command, Parser}; use masking::PeekInterface; use crate::connector_auth::{ConnectorAuthType, ConnectorAuthenticationMap}; - #[derive(Parser)] #[command(version, about = "Postman collection runner using newman!", long_about = None)] struct Args { /// Admin API Key of the environment - #[arg(short, long = "admin_api_key")] + #[arg(short, long)] admin_api_key: String, /// Base URL of the Hyperswitch environment - #[arg(short, long = "base_url")] + #[arg(short, long)] base_url: String, /// Name of the connector - #[arg(short, long = "connector_name")] + #[arg(short, long)] connector_name: String, + /// Custom headers + #[arg(short = 'H', long = "header")] + custom_headers: Option>, + /// Minimum delay in milliseconds to be added before sending a request + /// By default, 7 milliseconds will be the delay + #[arg(short, long, default_value_t = 7)] + delay_request: u32, /// Folder name of specific tests #[arg(short, long = "folder")] folders: Option, @@ -25,6 +37,12 @@ struct Args { verbose: bool, } +pub struct ReturnArgs { + pub newman_command: Command, + pub file_modified_flag: bool, + pub collection_path: String, +} + // Just by the name of the connector, this function generates the name of the collection dir // Example: CONNECTOR_NAME="stripe" -> OUTPUT: postman/collection-dir/stripe #[inline] @@ -32,7 +50,29 @@ fn get_path(name: impl AsRef) -> String { format!("postman/collection-dir/{}", name.as_ref()) } -pub fn command_generate() -> Command { +// This function currently allows you to add only custom headers. +// In future, as we scale, this can be modified based on the need +fn insert_content(dir: T, content_to_insert: U) -> io::Result<()> +where + T: AsRef + std::fmt::Debug, + U: AsRef + std::fmt::Debug, +{ + let file_name = "event.prerequest.js"; + let file_path = dir.as_ref().join(file_name); + + // Open the file in write mode or create it if it doesn't exist + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(file_path)?; + + write!(file, "\n{:#?}", content_to_insert)?; + + Ok(()) +} + +pub fn generate_newman_command() -> ReturnArgs { let args = Args::parse(); let connector_name = args.connector_name; @@ -129,7 +169,10 @@ pub fn command_generate() -> Command { ]); } - newman_command.arg("--delay-request").arg("7"); // 7 milli seconds delay + newman_command.args([ + "--delay-request", + format!("{}", &args.delay_request).as_str(), + ]); newman_command.arg("--color").arg("on"); @@ -151,5 +194,24 @@ pub fn command_generate() -> Command { newman_command.arg("--verbose"); } - newman_command + let mut modified = false; + if let Some(headers) = &args.custom_headers { + for header in headers { + if let Some((key, value)) = header.split_once(':') { + let content_to_insert = + format!(r#"pm.request.headers.add({{key: "{key}", value: "{value}"}});"#); + if insert_content(&collection_path, &content_to_insert).is_ok() { + modified = true; + } + } else { + eprintln!("Invalid header format: {}", header); + } + } + } + + ReturnArgs { + newman_command, + file_modified_flag: modified, + collection_path, + } } From 9cc8b93070e111c7a4b5beb1ee3bd2e51d96f2ca Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:56:41 +0530 Subject: [PATCH 38/57] CI: checkout repo before spell checking pr title (#2776) --- ...heck.yml => conventional-commit-check.yml} | 17 +----------- .github/workflows/pr-title-spell-check.yml | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 16 deletions(-) rename .github/workflows/{pr-title-check.yml => conventional-commit-check.yml} (89%) create mode 100644 .github/workflows/pr-title-spell-check.yml diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/conventional-commit-check.yml similarity index 89% rename from .github/workflows/pr-title-check.yml rename to .github/workflows/conventional-commit-check.yml index 167be295443b..5fd25e9332d1 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/conventional-commit-check.yml @@ -1,4 +1,4 @@ -name: PR Title Checks +name: Conventional Commit Message Check on: # This is a dangerous event trigger as it causes the workflow to run in the @@ -35,21 +35,6 @@ env: CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: - typos: - name: Spell check PR title - runs-on: ubuntu-latest - steps: - - name: Store PR title in a file - shell: bash - env: - TITLE: ${{ github.event.pull_request.title }} - run: echo $TITLE > pr_title.txt - - - name: Spell check - uses: crate-ci/typos@master - with: - files: ./pr_title.txt - pr_title_check: name: Verify PR title follows conventional commit standards runs-on: ubuntu-latest diff --git a/.github/workflows/pr-title-spell-check.yml b/.github/workflows/pr-title-spell-check.yml new file mode 100644 index 000000000000..6ab6f184739d --- /dev/null +++ b/.github/workflows/pr-title-spell-check.yml @@ -0,0 +1,27 @@ +name: PR Title Spell Check + +on: + pull_request: + types: + - opened + - edited + - synchronize + +jobs: + typos: + name: Spell check PR title + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Store PR title in a file + shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} + run: echo $TITLE > pr_title.txt + + - name: Spell check + uses: crate-ci/typos@master + with: + files: ./pr_title.txt From 6678689265ae9a4fbb7a43c1938237d349c5a68e Mon Sep 17 00:00:00 2001 From: Abhishek Marrivagu <68317979+Abhicodes-crypto@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:12:28 +0530 Subject: [PATCH 39/57] feat(core): use redis as temp locker instead of basilisk (#2789) --- crates/router/src/consts.rs | 3 + crates/router/src/core/payment_methods.rs | 11 +- .../router/src/core/payment_methods/cards.rs | 131 +--- .../router/src/core/payment_methods/vault.rs | 723 +++++++++--------- crates/router/src/core/payments.rs | 25 +- .../src/core/payments/flows/authorize_flow.rs | 77 +- crates/router/src/core/payments/helpers.rs | 22 +- crates/router/src/core/payments/operations.rs | 7 +- .../payments/operations/payment_approve.rs | 4 +- .../operations/payment_complete_authorize.rs | 4 +- .../payments/operations/payment_confirm.rs | 4 +- .../payments/operations/payment_create.rs | 4 +- .../operations/payment_method_validate.rs | 3 +- .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 3 +- .../payments/operations/payment_status.rs | 3 +- .../payments/operations/payment_update.rs | 4 +- .../router/src/core/payments/tokenization.rs | 2 + crates/router/src/core/payouts.rs | 4 +- crates/router/src/core/payouts/helpers.rs | 21 +- crates/router/src/core/payouts/validator.rs | 2 + .../router/src/types/api/payment_methods.rs | 11 +- crates/router/src/workflows/tokenized_data.rs | 16 +- 23 files changed, 554 insertions(+), 531 deletions(-) diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 2f2563ee3976..7b20c3865d15 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -49,3 +49,6 @@ pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32 pub(crate) const METRICS_HOST_TAG_NAME: &str = "host"; pub const MAX_ROUTING_CONFIGS_PER_MERCHANT: usize = 100; pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; + +pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; +pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 422c3fa19881..b19b381af507 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -13,7 +13,10 @@ use diesel_models::enums; use crate::{ core::{errors::RouterResult, payments::helpers}, routes::AppState, - types::api::{self, payments}, + types::{ + api::{self, payments}, + domain, + }, }; pub struct Oss; @@ -25,6 +28,7 @@ pub trait PaymentMethodRetrieve { state: &AppState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; } @@ -35,6 +39,7 @@ impl PaymentMethodRetrieve for Oss { state: &AppState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)> { match pm_data { pm_opt @ Some(pm @ api::PaymentMethodData::Card(_)) => { @@ -44,6 +49,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::Card, pm, + merchant_key_store, ) .await?; @@ -64,6 +70,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::BankTransfer, pm, + merchant_key_store, ) .await?; @@ -76,6 +83,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::Wallet, pm, + merchant_key_store, ) .await?; @@ -88,6 +96,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::BankRedirect, pm, + merchant_key_store, ) .await?; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 417b030f5494..234323f0179a 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2009,7 +2009,7 @@ pub async fn list_customer_payment_method( let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token).await? + get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? } else { None }; @@ -2104,6 +2104,7 @@ async fn get_card_details( key: &[u8], state: &routes::AppState, hyperswitch_token: &str, + key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult> { let mut _card_decrypted = decrypt::(pm.payment_method_data.clone(), key) @@ -2120,7 +2121,7 @@ async fn get_card_details( }); Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm).await?, + get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, )) } @@ -2128,6 +2129,7 @@ pub async fn get_lookup_key_from_locker( state: &routes::AppState, payment_token: &str, pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2142,9 +2144,15 @@ pub async fn get_lookup_key_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Get Card Details Failed")?; let card = card_detail.clone(); - let resp = - BasiliskCardSupport::create_payment_method_data_in_locker(state, payment_token, card, pm) - .await?; + + let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( + state, + payment_token, + card, + pm, + merchant_key_store, + ) + .await?; Ok(resp) } @@ -2177,6 +2185,7 @@ pub async fn get_lookup_key_for_payout_method( Some(payout_token.to_string()), &pm_parsed, Some(pm.customer_id.to_owned()), + key_store, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -2190,110 +2199,16 @@ pub async fn get_lookup_key_for_payout_method( } } -pub struct BasiliskCardSupport; +pub struct TempLockerCardSupport; -#[cfg(not(feature = "basilisk"))] -impl BasiliskCardSupport { - async fn create_payment_method_data_in_locker( - state: &routes::AppState, - payment_token: &str, - card: api::CardDetailFromLocker, - pm: &storage::PaymentMethod, - ) -> errors::RouterResult { - let card_number = card.card_number.clone().get_required_value("card_number")?; - let card_exp_month = card - .expiry_month - .clone() - .expose_option() - .get_required_value("expiry_month")?; - let card_exp_year = card - .expiry_year - .clone() - .expose_option() - .get_required_value("expiry_year")?; - let card_holder_name = card - .card_holder_name - .clone() - .expose_option() - .unwrap_or_default(); - let value1 = payment_methods::mk_card_value1( - card_number, - card_exp_year, - card_exp_month, - Some(card_holder_name), - None, - None, - None, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - let value2 = payment_methods::mk_card_value2( - None, - None, - None, - Some(pm.customer_id.to_string()), - Some(pm.payment_method_id.to_string()), - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let value1 = vault::VaultPaymentMethod::Card(value1); - let value2 = vault::VaultPaymentMethod::Card(value2); - - let value1 = utils::Encode::::encode_to_string_of_json(&value1) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value1 construction failed when saving card to locker")?; - - let value2 = utils::Encode::::encode_to_string_of_json(&value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value2 construction failed when saving card to locker")?; - - let db_value = vault::MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Mock tokenize value construction failed when saving card to locker", - )?; - - let db = &*state.store; - - let already_present = db.find_config_by_key(payment_token).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: payment_token.to_string(), - config: value_string, - }; - - db.insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - - db.update_config_by_key(payment_token, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization db update failed")?; - } - - Ok(card) - } -} - -#[cfg(feature = "basilisk")] -impl BasiliskCardSupport { +impl TempLockerCardSupport { #[instrument(skip_all)] - async fn create_payment_method_data_in_locker( + async fn create_payment_method_data_in_temp_locker( state: &routes::AppState, payment_token: &str, card: api::CardDetailFromLocker, pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card_number = card.card_number.clone().get_required_value("card_number")?; let card_exp_month = card @@ -2343,8 +2258,14 @@ impl BasiliskCardSupport { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Wrapped value2 construction failed when saving card to locker")?; - let lookup_key = - vault::create_tokenize(state, value1, Some(value2), payment_token.to_string()).await?; + let lookup_key = vault::create_tokenize( + state, + value1, + Some(value2), + payment_token.to_string(), + merchant_key_store.key.get_inner(), + ) + .await?; vault::add_delete_tokenized_data_task( &*state.store, &lookup_key, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index d16269deb9b2..5ad78c9d730e 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1,34 +1,30 @@ -use common_utils::generate_id_with_default_len; -#[cfg(feature = "basilisk")] -use error_stack::report; -use error_stack::{IntoReport, ResultExt}; +use common_utils::{ + crypto::{DecodeMessage, EncodeMessage, GcmAes256}, + ext_traits::BytesExt, + generate_id_with_default_len, +}; +use error_stack::{report, IntoReport, ResultExt}; #[cfg(feature = "basilisk")] use josekit::jwe; use masking::PeekInterface; use router_env::{instrument, tracing}; -#[cfg(feature = "basilisk")] use scheduler::{types::process_data, utils as process_tracker_utils}; -#[cfg(feature = "basilisk")] -use crate::routes::metrics; #[cfg(feature = "payouts")] use crate::types::api::payouts; use crate::{ - configs::settings, + consts, core::errors::{self, CustomResult, RouterResult}, - logger, routes, + db, logger, routes, + routes::metrics, types::{ - api, - storage::{self, enums}, + api, domain, + storage::{self, enums, ProcessTrackerExt}, }, utils::{self, StringExt}, }; #[cfg(feature = "basilisk")] -use crate::{core::payment_methods::transformers as payment_methods, services, utils::BytesExt}; -#[cfg(feature = "basilisk")] -use crate::{db, types::storage::ProcessTrackerExt}; - -#[cfg(feature = "basilisk")] +use crate::{core::payment_methods::transformers as payment_methods, services, settings}; const VAULT_SERVICE_NAME: &str = "CARD"; #[cfg(feature = "basilisk")] const VAULT_VERSION: &str = "0"; @@ -622,196 +618,15 @@ pub struct MockTokenizeDBValue { pub struct Vault; -#[cfg(not(feature = "basilisk"))] impl Vault { #[instrument(skip_all)] pub async fn get_payment_method_data_from_locker( state: &routes::AppState, lookup_key: &str, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payment_method, supp_data) = - api::PaymentMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payment Method from Values")?; - - Ok((Some(payment_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn get_payout_method_data_from_temporary_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payout_method, supp_data) = - api::PayoutMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payout Method from Values")?; - - Ok((Some(payout_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn store_payout_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payout_method: &api::PayoutMethodData, - customer_id: Option, - ) -> RouterResult { - let value1 = payout_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payout_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payout method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn store_payment_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payment_method: &api::PaymentMethodData, - customer_id: Option, - _pm: enums::PaymentMethod, - ) -> RouterResult { - let value1 = payment_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payment_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value12 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payment method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn delete_locker_payment_method_by_lookup_key( - state: &routes::AppState, - lookup_key: &Option, - ) { - let db = &*state.store; - if let Some(id) = lookup_key { - match db.delete_config_by_key(id).await { - Ok(_) => logger::info!("Card Deleted from locker mock up"), - Err(err) => logger::error!("Err: Card Delete from locker Failed : {}", err), - } - } - } -} - -#[cfg(feature = "basilisk")] -impl Vault { - #[instrument(skip_all)] - pub async fn get_payment_method_data_from_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let de_tokenize = get_tokenized_data(state, lookup_key, true).await?; + let de_tokenize = + get_tokenized_data(state, lookup_key, true, merchant_key_store.key.get_inner()).await?; let (payment_method, customer_id) = api::PaymentMethodData::from_values(de_tokenize.value1, de_tokenize.value2) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -827,6 +642,7 @@ impl Vault { payment_method: &api::PaymentMethodData, customer_id: Option, pm: enums::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let value1 = payment_method .get_value1(customer_id.clone()) @@ -840,7 +656,14 @@ impl Vault { let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - let lookup_key = create_tokenize(state, value1, Some(value2), lookup_key).await?; + let lookup_key = create_tokenize( + state, + value1, + Some(value2), + lookup_key, + merchant_key_store.key.get_inner(), + ) + .await?; add_delete_tokenized_data_task(&*state.store, &lookup_key, pm).await?; metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); Ok(lookup_key) @@ -851,8 +674,10 @@ impl Vault { pub async fn get_payout_method_data_from_temporary_locker( state: &routes::AppState, lookup_key: &str, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, SupplementaryVaultData)> { - let de_tokenize = get_tokenized_data(state, lookup_key, true).await?; + let de_tokenize = + get_tokenized_data(state, lookup_key, true, merchant_key_store.key.get_inner()).await?; let (payout_method, supp_data) = api::PayoutMethodData::from_values(de_tokenize.value1, de_tokenize.value2) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -868,6 +693,7 @@ impl Vault { token_id: Option, payout_method: &api::PayoutMethodData, customer_id: Option, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let value1 = payout_method .get_value1(customer_id.clone()) @@ -881,7 +707,14 @@ impl Vault { let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - let lookup_key = create_tokenize(state, value1, Some(value2), lookup_key).await?; + let lookup_key = create_tokenize( + state, + value1, + Some(value2), + lookup_key, + merchant_key_store.key.get_inner(), + ) + .await?; // add_delete_tokenized_data_task(&*state.store, &lookup_key, pm).await?; // scheduler_metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); Ok(lookup_key) @@ -893,31 +726,334 @@ impl Vault { lookup_key: &Option, ) { if let Some(lookup_key) = lookup_key { - let delete_resp = delete_tokenized_data(state, lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully") - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp) - } - } - Err(err) => logger::error!("Err: Deleting Card From Locker : {:?}", err), - } + delete_tokenized_data(state, lookup_key) + .await + .map(|_| logger::info!("Card From locker deleted Successfully")) + .map_err(|err| logger::error!("Error: Deleting Card From Redis Locker : {:?}", err)) + .ok(); } } } //------------------------------------------------TokenizeService------------------------------------------------ -pub fn get_key_id(keys: &settings::Jwekey) -> &str { - let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources - if key_identifier == "1" { - &keys.locker_key_identifier1 - } else { - &keys.locker_key_identifier2 + +#[inline(always)] +fn get_redis_locker_key(lookup_key: &str) -> String { + format!("{}_{}", consts::LOCKER_REDIS_PREFIX, lookup_key) +} + +#[instrument(skip(state, value1, value2))] +pub async fn create_tokenize( + state: &routes::AppState, + value1: String, + value2: Option, + lookup_key: String, + encryption_key: &masking::Secret>, +) -> RouterResult { + let redis_key = get_redis_locker_key(lookup_key.as_str()); + let func = || async { + metrics::CREATED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let payload_to_be_encrypted = api::TokenizePayloadRequest { + value1: value1.clone(), + value2: value2.clone().unwrap_or_default(), + lookup_key: lookup_key.clone(), + service_name: VAULT_SERVICE_NAME.to_string(), + }; + + let payload = utils::Encode::::encode_to_string_of_json( + &payload_to_be_encrypted, + ) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let encrypted_payload = GcmAes256 + .encode_message(encryption_key.peek().as_ref(), payload.as_bytes()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode redis temp locker data")?; + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + redis_conn + .set_key_if_not_exists_with_expiry( + redis_key.as_str(), + bytes::Bytes::from(encrypted_payload), + Some(i64::from(consts::LOCKER_REDIS_EXPIRY_SECONDS)), + ) + .await + .map(|_| lookup_key.clone()) + .map_err(|err| { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + err + }) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error from redis locker") + }; + + match func().await { + Ok(s) => { + logger::info!( + "Insert payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_create_tokenize(state, value1, value2, lookup_key).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +#[instrument(skip(state))] +pub async fn get_tokenized_data( + state: &routes::AppState, + lookup_key: &str, + _should_get_value2: bool, + encryption_key: &masking::Secret>, +) -> RouterResult { + let redis_key = get_redis_locker_key(lookup_key); + let func = || async { + metrics::GET_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.get_key::(redis_key.as_str()).await; + + match response { + Ok(resp) => { + let decrypted_payload = GcmAes256 + .decode_message( + encryption_key.peek().as_ref(), + masking::Secret::new(resp.into()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to decode redis temp locker data")?; + + let get_response: api::TokenizePayloadRequest = + bytes::Bytes::from(decrypted_payload) + .parse_struct("TokenizePayloadRequest") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Error getting TokenizePayloadRequest from tokenize response", + )?; + + Ok(get_response) + } + Err(err) => { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + Err(err).change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".into(), + }) + } + } + }; + + match func().await { + Ok(s) => { + logger::info!( + "Fetch payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_get_tokenized_data(state, lookup_key, _should_get_value2).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +#[instrument(skip(state))] +pub async fn delete_tokenized_data(state: &routes::AppState, lookup_key: &str) -> RouterResult<()> { + let redis_key = get_redis_locker_key(lookup_key); + let func = || async { + metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.delete_key(redis_key.as_str()).await; + + match response { + Ok(redis_interface::DelReply::KeyDeleted) => Ok(()), + Ok(redis_interface::DelReply::KeyNotDeleted) => { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Token invalid or expired") + } + Err(err) => { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable_lazy(|| { + format!("Failed to delete from redis locker: {err:?}") + }) + } + } + }; + match func().await { + Ok(s) => { + logger::info!( + "Delete payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_delete_tokenized_data(state, lookup_key).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +// ********************************************** PROCESS TRACKER ********************************************** + +pub async fn add_delete_tokenized_data_task( + db: &dyn db::StorageInterface, + lookup_key: &str, + pm: enums::PaymentMethod, +) -> RouterResult<()> { + let runner = "DELETE_TOKENIZE_DATA_WORKFLOW"; + let current_time = common_utils::date_time::now(); + let tracking_data = serde_json::to_value(storage::TokenizeCoreWorkflow { + lookup_key: lookup_key.to_owned(), + pm, + }) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("unable to convert into value {lookup_key:?}"))?; + + let schedule_time = get_delete_tokenize_schedule_time(db, &pm, 0).await; + + let process_tracker_entry = storage::ProcessTrackerNew { + id: format!("{runner}_{lookup_key}"), + name: Some(String::from(runner)), + tag: vec![String::from("BASILISK-V3")], + runner: Some(String::from(runner)), + retry_count: 0, + schedule_time, + rule: String::new(), + tracking_data, + business_status: String::from("Pending"), + status: enums::ProcessTrackerStatus::New, + event: vec![], + created_at: current_time, + updated_at: current_time, + }; + let response = db.insert_process(process_tracker_entry).await; + response.map(|_| ()).or_else(|err| { + if err.current_context().is_db_unique_violation() { + Ok(()) + } else { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + } + }) +} + +pub async fn start_tokenize_data_workflow( + state: &routes::AppState, + tokenize_tracker: &storage::ProcessTracker, +) -> Result<(), errors::ProcessTrackerError> { + let db = &*state.store; + let delete_tokenize_data = serde_json::from_value::( + tokenize_tracker.tracking_data.clone(), + ) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "unable to convert into DeleteTokenizeByTokenRequest {:?}", + tokenize_tracker.tracking_data + ) + })?; + + match delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await { + Ok(()) => { + logger::info!("Card From locker deleted Successfully"); + //mark task as finished + let id = tokenize_tracker.id.clone(); + tokenize_tracker + .clone() + .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) + .await?; + } + Err(err) => { + logger::error!("Err: Deleting Card From Locker : {:?}", err); + retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) + .await?; + metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); + } } + Ok(()) +} + +pub async fn get_delete_tokenize_schedule_time( + db: &dyn db::StorageInterface, + pm: &enums::PaymentMethod, + retry_count: i32, +) -> Option { + let redis_mapping = db::get_and_deserialize_key( + db, + &format!("pt_mapping_delete_{pm}_tokenize_data"), + "PaymentMethodsPTMapping", + ) + .await; + let mapping = match redis_mapping { + Ok(x) => x, + Err(err) => { + logger::info!("Redis Mapping Error: {}", err); + process_data::PaymentMethodsPTMapping::default() + } + }; + let time_delta = process_tracker_utils::get_pm_schedule_time(mapping, pm, retry_count + 1); + + process_tracker_utils::get_time_from_delta(time_delta) } +pub async fn retry_delete_tokenize( + db: &dyn db::StorageInterface, + pm: &enums::PaymentMethod, + pt: storage::ProcessTracker, +) -> Result<(), errors::ProcessTrackerError> { + let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; + + match schedule_time { + Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + None => { + pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) + .await + } + } +} + +// Fallback logic of old temp locker needs to be removed later + #[cfg(feature = "basilisk")] async fn get_locker_jwe_keys( keys: &settings::ActiveKmsSecrets, @@ -936,13 +1072,13 @@ async fn get_locker_jwe_keys( } #[cfg(feature = "basilisk")] -pub async fn create_tokenize( +#[instrument(skip(state, value1, value2))] +pub async fn old_create_tokenize( state: &routes::AppState, value1: String, value2: Option, lookup_key: String, ) -> RouterResult { - metrics::CREATED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); let payload_to_be_encrypted = api::TokenizePayloadRequest { value1, value2: value2.unwrap_or_default(), @@ -1017,7 +1153,7 @@ pub async fn create_tokenize( } #[cfg(feature = "basilisk")] -pub async fn get_tokenized_data( +pub async fn old_get_tokenized_data( state: &routes::AppState, lookup_key: &str, should_get_value2: bool, @@ -1096,10 +1232,10 @@ pub async fn get_tokenized_data( } #[cfg(feature = "basilisk")] -pub async fn delete_tokenized_data( +pub async fn old_delete_tokenized_data( state: &routes::AppState, lookup_key: &str, -) -> RouterResult { +) -> RouterResult<()> { metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); let payload_to_be_encrypted = api::DeleteTokenizeByTokenRequest { lookup_key: lookup_key.to_string(), @@ -1136,11 +1272,11 @@ pub async fn delete_tokenized_data( .attach_printable("Error while making /tokenize/delete/token call to the locker")?; match response { Ok(r) => { - let delete_response = std::str::from_utf8(&r.response) + let _delete_response = std::str::from_utf8(&r.response) .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Decoding Failed for basilisk delete response")?; - Ok(delete_response.to_string()) + Ok(()) } Err(err) => { metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); @@ -1151,133 +1287,12 @@ pub async fn delete_tokenized_data( } } -// ********************************************** PROCESS TRACKER ********************************************** #[cfg(feature = "basilisk")] -pub async fn add_delete_tokenized_data_task( - db: &dyn db::StorageInterface, - lookup_key: &str, - pm: enums::PaymentMethod, -) -> RouterResult<()> { - let runner = "DELETE_TOKENIZE_DATA_WORKFLOW"; - let current_time = common_utils::date_time::now(); - let tracking_data = serde_json::to_value(storage::TokenizeCoreWorkflow { - lookup_key: lookup_key.to_owned(), - pm, - }) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("unable to convert into value {lookup_key:?}"))?; - - let schedule_time = get_delete_tokenize_schedule_time(db, &pm, 0).await; - - let process_tracker_entry = storage::ProcessTrackerNew { - id: format!("{runner}_{lookup_key}"), - name: Some(String::from(runner)), - tag: vec![String::from("BASILISK-V3")], - runner: Some(String::from(runner)), - retry_count: 0, - schedule_time, - rule: String::new(), - tracking_data, - business_status: String::from("Pending"), - status: enums::ProcessTrackerStatus::New, - event: vec![], - created_at: current_time, - updated_at: current_time, - }; - let response = db.insert_process(process_tracker_entry).await; - response.map(|_| ()).or_else(|err| { - if err.current_context().is_db_unique_violation() { - Ok(()) - } else { - Err(report!(errors::ApiErrorResponse::InternalServerError)) - } - }) -} - -#[cfg(feature = "basilisk")] -pub async fn start_tokenize_data_workflow( - state: &routes::AppState, - tokenize_tracker: &storage::ProcessTracker, -) -> Result<(), errors::ProcessTrackerError> { - let db = &*state.store; - let delete_tokenize_data = serde_json::from_value::( - tokenize_tracker.tracking_data.clone(), - ) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!( - "unable to convert into DeleteTokenizeByTokenRequest {:?}", - tokenize_tracker.tracking_data - ) - })?; - - let delete_resp = delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully"); - //mark task as finished - let id = tokenize_tracker.id.clone(); - tokenize_tracker - .clone() - .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) - .await?; - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp); - retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) - .await?; - metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); - } - } - Err(err) => { - logger::error!("Err: Deleting Card From Locker : {:?}", err); - retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) - .await?; - metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); - } - } - Ok(()) -} - -#[cfg(feature = "basilisk")] -pub async fn get_delete_tokenize_schedule_time( - db: &dyn db::StorageInterface, - pm: &enums::PaymentMethod, - retry_count: i32, -) -> Option { - let redis_mapping = db::get_and_deserialize_key( - db, - &format!("pt_mapping_delete_{pm}_tokenize_data"), - "PaymentMethodsPTMapping", - ) - .await; - let mapping = match redis_mapping { - Ok(x) => x, - Err(err) => { - logger::info!("Redis Mapping Error: {}", err); - process_data::PaymentMethodsPTMapping::default() - } - }; - let time_delta = process_tracker_utils::get_pm_schedule_time(mapping, pm, retry_count + 1); - - process_tracker_utils::get_time_from_delta(time_delta) -} - -#[cfg(feature = "basilisk")] -pub async fn retry_delete_tokenize( - db: &dyn db::StorageInterface, - pm: &enums::PaymentMethod, - pt: storage::ProcessTracker, -) -> Result<(), errors::ProcessTrackerError> { - let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; - - match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, - None => { - pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) - .await - } +pub fn get_key_id(keys: &settings::Jwekey) -> &str { + let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources + if key_identifier == "1" { + &keys.locker_key_identifier1 + } else { + &keys.locker_key_identifier2 } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 586126467e18..98ab158e7935 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -153,6 +153,7 @@ where &operation, &mut payment_data, &validate_result, + &key_store, ) .await?; @@ -717,6 +718,7 @@ where payment_data, validate_result, &merchant_connector_account, + key_store, ) .await?; @@ -1399,6 +1401,7 @@ pub async fn get_connector_tokenization_action_when_confirm_true( payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(PaymentData, TokenizationAction)> where F: Send + Clone, @@ -1461,7 +1464,12 @@ where TokenizationAction::TokenizeInRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; TokenizationAction::SkipConnectorTokenization @@ -1471,7 +1479,12 @@ where TokenizationAction::TokenizeInConnectorAndRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; @@ -1507,6 +1520,7 @@ pub async fn tokenize_in_router_when_confirm_false( operation: &BoxedOperation<'_, F, Req, Ctx>, payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> where F: Send + Clone, @@ -1516,7 +1530,12 @@ where let payment_data = if !is_operation_confirm(operation) { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; payment_data diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 2c77184b9bd4..e27fe54c0ed0 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -95,37 +95,56 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - let save_payment_result = tokenization::save_payment_method( - state, - connector, - resp.to_owned(), - maybe_customer, - merchant_account, - self.request.payment_method_type, - key_store, - ) - .await; - - let pm_id = match save_payment_result { - Ok(payment_method_id) => Ok(payment_method_id), - Err(error) => { - if resp.request.setup_mandate_details.clone().is_some() { - Err(error) - } else { - logger::error!(save_payment_method_error=?error); - Ok(None) + if resp.request.setup_mandate_details.clone().is_some() { + let payment_method_id = tokenization::save_payment_method( + state, + connector, + resp.to_owned(), + maybe_customer, + merchant_account, + self.request.payment_method_type, + key_store, + ) + .await?; + Ok(mandate::mandate_procedure( + state, + resp, + maybe_customer, + payment_method_id, + connector.merchant_connector_id.clone(), + ) + .await?) + } else { + let connector = connector.clone(); + let response = resp.clone(); + let maybe_customer = maybe_customer.clone(); + let merchant_account = merchant_account.clone(); + let key_store = key_store.clone(); + let state = state.clone(); + + logger::info!("Initiating async call to save_payment_method in locker"); + + tokio::spawn(async move { + logger::info!("Starting async call to save_payment_method in locker"); + + let result = tokenization::save_payment_method( + &state, + &connector, + response, + &maybe_customer, + &merchant_account, + self.request.payment_method_type, + &key_store, + ) + .await; + + if let Err(err) = result { + logger::error!("Asynchronously saving card in locker failed : {:?}", err); } - } - }?; + }); - Ok(mandate::mandate_procedure( - state, - resp, - maybe_customer, - pm_id, - connector.merchant_connector_id.clone(), - ) - .await?) + Ok(resp) + } } else { Ok(self.clone()) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f42e4985380c..fd9fd7361da3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -399,6 +399,7 @@ pub async fn get_token_pm_type_mandate_details( request: &api::PaymentsRequest, mandate_type: Option, merchant_account: &domain::MerchantAccount, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( Option, Option, @@ -427,7 +428,13 @@ pub async fn get_token_pm_type_mandate_details( recurring_mandate_payment_data, payment_method_type_, mandate_connector, - ) = get_token_for_recurring_mandate(state, request, merchant_account).await?; + ) = get_token_for_recurring_mandate( + state, + request, + merchant_account, + merchant_key_store, + ) + .await?; Ok(( token_, payment_method_, @@ -452,6 +459,7 @@ pub async fn get_token_for_recurring_mandate( state: &AppState, req: &api::PaymentsRequest, merchant_account: &domain::MerchantAccount, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( Option, Option, @@ -501,7 +509,9 @@ pub async fn get_token_for_recurring_mandate( }; if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method { - let _ = cards::get_lookup_key_from_locker(state, &token, &payment_method).await?; + let _ = + cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store) + .await?; if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if pm != payment_method.payment_method { @@ -1320,6 +1330,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, payment_data: &mut PaymentData, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, @@ -1373,6 +1384,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( state, &hyperswitch_token, + merchant_key_store, ) .await .attach_printable( @@ -1402,6 +1414,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( &updated_pm, payment_data.payment_intent.customer_id.to_owned(), enums::PaymentMethod::Card, + merchant_key_store, ) .await?; Some(updated_pm) @@ -1442,6 +1455,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( state, &payment_data.payment_intent, &payment_data.payment_attempt, + merchant_key_store, ) .await?; @@ -1461,6 +1475,7 @@ pub async fn store_in_vault_and_generate_ppmt( payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, payment_method: enums::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let router_token = vault::Vault::store_payment_method_data_in_locker( state, @@ -1468,6 +1483,7 @@ pub async fn store_in_vault_and_generate_ppmt( payment_method_data, payment_intent.customer_id.to_owned(), payment_method, + merchant_key_store, ) .await?; let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); @@ -1491,6 +1507,7 @@ pub async fn store_payment_method_data_in_vault( payment_intent: &PaymentIntent, payment_method: enums::PaymentMethod, payment_method_data: &api::PaymentMethodData, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { if should_store_payment_method_data_in_vault( &state.conf.temp_locker_enable_config, @@ -1503,6 +1520,7 @@ pub async fn store_payment_method_data_in_vault( payment_intent, payment_attempt, payment_method, + merchant_key_store, ) .await?; diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index d198cd562a79..ad747ac2792a 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -123,6 +123,7 @@ pub trait Domain: Send + Sync { state: &'a AppState, payment_data: &mut PaymentData, storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, @@ -233,11 +234,12 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } } @@ -282,6 +284,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, Option, @@ -343,6 +346,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, Option, @@ -394,6 +398,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRejectRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 16bb84f69ddb..4cd1bae04dee 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -88,6 +88,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -299,12 +300,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index a2f5292a37f7..4e87b3869431 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -87,6 +87,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -294,12 +295,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; Ok((op, payment_method_data)) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 8842963990b6..5de281a5e63c 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -69,6 +69,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ); let (mut payment_intent, mandate_details) = @@ -423,12 +424,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, key_store).await?; utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index f3b777534bf7..909f4d456530 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -107,6 +107,7 @@ impl request, mandate_type, merchant_account, + merchant_key_store, ) .await?; @@ -353,11 +354,12 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 6d97f7b66cd1..33f6c23c8363 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -297,11 +297,12 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::VerifyRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } async fn get_connector<'a>( diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index cf16a053592b..354c62648bb3 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -318,6 +318,7 @@ where _state: &'b AppState, _payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsSessionRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 227e7e2f90db..e9fa301bf07b 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -282,6 +282,7 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, Option, @@ -293,7 +294,7 @@ where .map(|connector_name| connector_name == *"bluesnap".to_string()) .unwrap_or(false) { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } else { Ok((Box::new(self), None)) } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d20830d9bc6b..96aac6c9d79b 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -95,11 +95,12 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index d0b17b5d460d..622d09754396 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -106,6 +106,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -394,11 +395,12 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f7831465e1ce..794180e2112e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1,6 +1,7 @@ use common_utils::{ext_traits::ValueExt, pii}; use error_stack::{report, ResultExt}; use masking::ExposeInterface; +use router_env::{instrument, tracing}; use super::helpers; use crate::{ @@ -20,6 +21,7 @@ use crate::{ utils::OptionExt, }; +#[instrument(skip_all)] pub async fn save_payment_method( state: &AppState, connector: &api::ConnectorData, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index ddb2a017e35a..f1136a35a65a 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -112,7 +112,7 @@ where // Validate create request let (payout_id, payout_method_data) = - validator::validate_create_request(&state, &merchant_account, &req).await?; + validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries let mut payout_data = payout_create_db_entries( @@ -403,6 +403,7 @@ pub async fn payouts_fulfill_core( &payout_attempt.merchant_id, &payout_attempt.payout_id, Some(&payout_data.payouts.payout_type), + &key_store, ) .await? .get_required_value("payout_method_data")?, @@ -458,6 +459,7 @@ pub async fn call_connector_payout( &payout_attempt.merchant_id, &payout_attempt.payout_id, Some(&payouts.payout_type), + key_store, ) .await? .get_required_value("payout_method_data")?, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9890cd9d5efd..39079ea36cd6 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -28,6 +28,7 @@ use crate::{ utils::{self, OptionExt}, }; +#[allow(clippy::too_many_arguments)] pub async fn make_payout_method_data<'a>( state: &'a AppState, payout_method_data: Option<&api::PayoutMethodData>, @@ -36,6 +37,7 @@ pub async fn make_payout_method_data<'a>( merchant_id: &str, payout_id: &str, payout_type: Option<&api_enums::PayoutType>, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { let db = &*state.store; let hyperswitch_token = if let Some(payout_token) = payout_token { @@ -67,14 +69,16 @@ pub async fn make_payout_method_data<'a>( match (payout_method_data.to_owned(), hyperswitch_token) { // Get operation (None, Some(payout_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payout_method_data_from_temporary_locker( - state, - &payout_token, - ) - .await - .attach_printable( - "Payout method for given token not found or there was a problem fetching it", - )?; + let (pm, supplementary_data) = + vault::Vault::get_payout_method_data_from_temporary_locker( + state, + &payout_token, + merchant_key_store, + ) + .await + .attach_printable( + "Payout method for given token not found or there was a problem fetching it", + )?; utils::when( supplementary_data .customer_id @@ -93,6 +97,7 @@ pub async fn make_payout_method_data<'a>( payout_token.to_owned(), payout_method, Some(customer_id.to_owned()), + merchant_key_store, ) .await?; diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index c815d91e41dd..3793ee523dc3 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -57,6 +57,7 @@ pub async fn validate_create_request( state: &AppState, merchant_account: &domain::MerchantAccount, req: &payouts::PayoutCreateRequest, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(String, Option)> { let merchant_id = &merchant_account.merchant_id; @@ -103,6 +104,7 @@ pub async fn validate_create_request( &merchant_account.merchant_id, payout_id.as_ref(), req.payout_type.as_ref(), + merchant_key_store, ) .await? } diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index e5bf1d8dd1bf..5acb66b5068e 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,12 +1,11 @@ use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByDateRequest, DeleteTokenizeByTokenRequest, - GetTokenizePayloadRequest, GetTokenizePayloadResponse, PaymentMethodCreate, - PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, PaymentMethodListRequest, - PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, - TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, - TokenizedWalletValue1, TokenizedWalletValue2, + CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, + GetTokenizePayloadResponse, PaymentMethodCreate, PaymentMethodDeleteResponse, PaymentMethodId, + PaymentMethodList, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, + PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, + TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, }; use error_stack::report; diff --git a/crates/router/src/workflows/tokenized_data.rs b/crates/router/src/workflows/tokenized_data.rs index 2f5d33173276..0674982f92fe 100644 --- a/crates/router/src/workflows/tokenized_data.rs +++ b/crates/router/src/workflows/tokenized_data.rs @@ -1,14 +1,13 @@ use scheduler::consumer::workflows::ProcessTrackerWorkflow; -#[cfg(feature = "basilisk")] -use crate::core::payment_methods::vault; -use crate::{errors, logger::error, routes::AppState, types::storage}; +use crate::{ + core::payment_methods::vault, errors, logger::error, routes::AppState, types::storage, +}; pub struct DeleteTokenizeDataWorkflow; #[async_trait::async_trait] impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { - #[cfg(feature = "basilisk")] async fn execute_workflow<'a>( &'a self, state: &'a AppState, @@ -17,15 +16,6 @@ impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { Ok(vault::start_tokenize_data_workflow(state, &process).await?) } - #[cfg(not(feature = "basilisk"))] - async fn execute_workflow<'a>( - &'a self, - _state: &'a AppState, - _process: storage::ProcessTracker, - ) -> Result<(), errors::ProcessTrackerError> { - Ok(()) - } - async fn error_handler<'a>( &'a self, _state: &'a AppState, From 5642fef52a6d591d12c5745ed381f41a1593f183 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:44:51 +0530 Subject: [PATCH 40/57] fix(connector): Add attempt_status in field in error_response (#2794) --- connector-template/mod.rs | 1 + crates/router/src/connector/aci.rs | 1 + crates/router/src/connector/adyen.rs | 7 +++++++ .../router/src/connector/adyen/transformers.rs | 5 +++++ crates/router/src/connector/airwallex.rs | 1 + crates/router/src/connector/authorizedotnet.rs | 3 +++ .../connector/authorizedotnet/transformers.rs | 4 ++++ crates/router/src/connector/bambora.rs | 1 + crates/router/src/connector/bankofamerica.rs | 1 + crates/router/src/connector/bitpay.rs | 1 + crates/router/src/connector/bluesnap.rs | 16 +++++++++++----- crates/router/src/connector/boku.rs | 2 ++ crates/router/src/connector/braintree.rs | 2 ++ .../braintree/braintree_graphql_transformers.rs | 1 + crates/router/src/connector/cashtocode.rs | 1 + .../src/connector/cashtocode/transformers.rs | 1 + crates/router/src/connector/checkout.rs | 1 + .../src/connector/checkout/transformers.rs | 2 ++ crates/router/src/connector/coinbase.rs | 1 + crates/router/src/connector/cryptopay.rs | 1 + crates/router/src/connector/cybersource.rs | 1 + .../src/connector/cybersource/transformers.rs | 1 + crates/router/src/connector/dlocal.rs | 1 + crates/router/src/connector/dummyconnector.rs | 1 + crates/router/src/connector/fiserv.rs | 2 ++ crates/router/src/connector/forte.rs | 1 + crates/router/src/connector/globalpay.rs | 2 ++ crates/router/src/connector/globepay.rs | 1 + .../src/connector/globepay/transformers.rs | 1 + crates/router/src/connector/gocardless.rs | 1 + crates/router/src/connector/helcim.rs | 1 + crates/router/src/connector/iatapay.rs | 2 ++ crates/router/src/connector/klarna.rs | 1 + crates/router/src/connector/mollie.rs | 1 + crates/router/src/connector/multisafepay.rs | 1 + .../src/connector/multisafepay/transformers.rs | 3 +++ crates/router/src/connector/nexinets.rs | 1 + crates/router/src/connector/nmi/transformers.rs | 1 + crates/router/src/connector/noon.rs | 1 + crates/router/src/connector/noon/transformers.rs | 1 + .../router/src/connector/nuvei/transformers.rs | 1 + crates/router/src/connector/opayo.rs | 1 + crates/router/src/connector/opennode.rs | 1 + crates/router/src/connector/payeezy.rs | 1 + crates/router/src/connector/payme.rs | 1 + .../router/src/connector/payme/transformers.rs | 2 ++ crates/router/src/connector/paypal.rs | 3 +++ crates/router/src/connector/payu.rs | 2 ++ crates/router/src/connector/powertranz.rs | 1 + .../src/connector/powertranz/transformers.rs | 2 ++ crates/router/src/connector/prophetpay.rs | 1 + crates/router/src/connector/rapyd.rs | 1 + .../router/src/connector/rapyd/transformers.rs | 2 ++ crates/router/src/connector/shift4.rs | 1 + crates/router/src/connector/square.rs | 1 + crates/router/src/connector/stax.rs | 1 + crates/router/src/connector/stripe.rs | 13 +++++++++++++ .../router/src/connector/stripe/transformers.rs | 1 + crates/router/src/connector/trustpay.rs | 3 +++ .../src/connector/trustpay/transformers.rs | 8 ++++++++ crates/router/src/connector/tsys/transformers.rs | 1 + crates/router/src/connector/volt.rs | 1 + crates/router/src/connector/wise.rs | 4 ++++ crates/router/src/connector/worldpay.rs | 1 + crates/router/src/connector/zen.rs | 1 + crates/router/src/core/payments/access_token.rs | 1 + crates/router/src/services/api.rs | 16 ++++++---------- crates/router/src/types.rs | 3 +++ crates/router/src/types/api.rs | 1 + crates/router/src/utils.rs | 1 + 70 files changed, 140 insertions(+), 15 deletions(-) diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 05f527d24662..b27a0774e714 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -105,6 +105,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 0e325a04ddb0..7dbe2a0cd9a2 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -78,6 +78,7 @@ impl ConnectorCommon for Aci { .collect::>() .join("; ") }), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 18a575b509cb..30b06d1ccf41 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -73,6 +73,7 @@ impl ConnectorCommon for Adyen { code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -251,6 +252,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -366,6 +368,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -533,6 +536,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } @@ -699,6 +703,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -896,6 +901,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -1399,6 +1405,7 @@ impl services::ConnectorIntegration { @@ -909,6 +911,7 @@ fn get_error_response( message: message.to_string(), reason: Some(message.to_string()), status_code, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 561723be46cf..884504154e8f 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -573,6 +573,7 @@ impl message: error.error_text.clone(), reason: None, status_code: item.http_code, + attempt_status: None, }) }); let metadata = transaction_response @@ -647,6 +648,7 @@ impl message: error.error_text.clone(), reason: None, status_code: item.http_code, + attempt_status: None, }) }); let metadata = transaction_response @@ -789,6 +791,7 @@ impl TryFrom types::Error message: message.message[0].text.clone(), reason: None, status_code, + attempt_status: None, } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index d5e8119b66c8..d0ed9929a77b 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -95,6 +95,7 @@ impl ConnectorCommon for Bambora { code: response.code.to_string(), message: response.message, reason: Some(serde_json::to_string(&response.details).unwrap_or_default()), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index e25d99f9af3d..a51fcc0ad626 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -111,6 +111,7 @@ impl ConnectorCommon for Bankofamerica { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index e8826e933905..63b6e41feaf7 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -120,6 +120,7 @@ impl ConnectorCommon for Bitpay { .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), message: response.error, reason: response.message, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 6c39fc41b721..73eeee8ca5a4 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -126,6 +126,7 @@ impl ConnectorCommon for Bluesnap { .map(|error_code_message| error_code_message.error_message) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), + attempt_status: None, } } bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { @@ -133,23 +134,28 @@ impl ConnectorCommon for Bluesnap { code: error_res.error_code.clone(), message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), + attempt_status: None, }, bluesnap::BluesnapErrors::General(error_response) => { - let error_res = if res.status_code == 403 + let (error_res, attempt_status) = if res.status_code == 403 && error_response.contains(BLUESNAP_TRANSACTION_NOT_FOUND) { - format!( - "{} in bluesnap dashboard", - consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + ( + format!( + "{} in bluesnap dashboard", + consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + ), + Some(enums::AttemptStatus::Failure), // when bluesnap throws 403 for payment not found, we update the payment status to failure. ) } else { - error_response.clone() + (error_response.clone(), None) }; ErrorResponse { status_code: res.status_code, code: consts::NO_ERROR_CODE.to_string(), message: error_response, reason: Some(error_res), + attempt_status, } } }; diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 826d218cd56e..710bcaf3842b 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -130,6 +130,7 @@ impl ConnectorCommon for Boku { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }), Err(_) => get_xml_deserialized(res), } @@ -651,6 +652,7 @@ fn get_xml_deserialized(res: Response) -> CustomResult Ok(ErrorResponse { @@ -139,6 +140,7 @@ impl ConnectorCommon for Braintree { code: consts::NO_ERROR_CODE.to_string(), message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.errors), + attempt_status: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index b622e041915d..bf51973237c5 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -316,6 +316,7 @@ fn get_error_response( message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: error_reason, status_code: http_code, + attempt_status: None, }) } diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index ed994dca31fc..75433cfd0f13 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -119,6 +119,7 @@ impl ConnectorCommon for Cashtocode { code: response.error.to_string(), message: response.error_description, reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 4db1bef7e3f2..2caef69db92c 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -217,6 +217,7 @@ impl status_code: item.http_code, message: error_data.error_description, reason: None, + attempt_status: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f4cc4ac9640e..6904a440b147 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Checkout { .error_codes .map(|errors| errors.join(" & ")) .or(response.error_type), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 53182e65ed5b..6ad040da2842 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -576,6 +576,7 @@ impl TryFrom> .clone() .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, + attempt_status: None, }) } else { None @@ -623,6 +624,7 @@ impl TryFrom> .clone() .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, + attempt_status: None, }) } else { None diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index d50e490cfc30..f26100fcc8cf 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -108,6 +108,7 @@ impl ConnectorCommon for Coinbase { code: response.error.error_type, message: response.error.message, reason: response.error.code, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 8abe84a93532..4dff4206033c 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -167,6 +167,7 @@ impl ConnectorCommon for Cryptopay { code: response.error.code, message: response.error.message, reason: response.error.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 0a13aa0cf141..f038bc2c91da 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Cybersource { code, message, reason: Some(connector_reason), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 55507e4f4903..9233a95d7dd7 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -367,6 +367,7 @@ impl message: error.message, reason: Some(error.reason), status_code: item.http_code, + attempt_status: None, }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index b706d694a3d5..9bf4bc546cd3 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -135,6 +135,7 @@ impl ConnectorCommon for Dlocal { code: response.code.to_string(), message: response.message, reason: response.param, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index af87029a682c..0346e452ea30 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -111,6 +111,7 @@ impl ConnectorCommon for DummyConnector { code: response.error.code, message: response.error.message, reason: response.error.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 35d40f1a3fb6..e31217908f25 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -151,6 +151,7 @@ impl ConnectorCommon for Fiserv { message: first_error.message.to_owned(), reason: first_error.field.to_owned(), status_code: res.status_code, + attempt_status: None, }) }) .unwrap_or(types::ErrorResponse { @@ -158,6 +159,7 @@ impl ConnectorCommon for Fiserv { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, status_code: res.status_code, + attempt_status: None, })) } } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 6f20e93e8c8d..af838649031e 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -130,6 +130,7 @@ impl ConnectorCommon for Forte { code, message, reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index dfcddae777e8..57c81cccae3f 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -104,6 +104,7 @@ impl ConnectorCommon for Globalpay { code: response.error_code, message: response.detailed_error_description, reason: None, + attempt_status: None, }) } } @@ -313,6 +314,7 @@ impl ConnectorIntegration message: error_response.error_info.clone(), reason: Some(error_response.error_info), status_code: item.http_code, + attempt_status: None, }), ..item.data }), @@ -808,6 +809,7 @@ impl TryFrom for types::ErrorResponse { message: response.responsetext, reason: None, status_code: http_code, + attempt_status: None, } } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 156e10928d3e..6302fc0f27c2 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Noon { code: response.result_code.to_string(), message: response.class_description, reason: Some(response.message), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 4a2128f7ec64..27a874930bcc 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -511,6 +511,7 @@ impl message: error_message.clone(), reason: Some(error_message), status_code: item.http_code, + attempt_status: None, }), _ => { let connector_response_reference_id = diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 88ebe1d8dbea..c23114e2a96b 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1579,6 +1579,7 @@ fn get_error_response( .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: None, status_code: http_code, + attempt_status: None, }) } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index 89e16416d27f..9fc1ad2931af 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -107,6 +107,7 @@ impl ConnectorCommon for Opayo { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 07d33382a21e..9e8283ff6376 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Opennode { code: consts::NO_ERROR_CODE.to_string(), message: response.message, reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 03e76af907ce..20504f91d5b6 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -123,6 +123,7 @@ impl ConnectorCommon for Payeezy { code: response.transaction_status, message: error_messages.join(", "), reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index e0d6229c004c..3790bcb66da6 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -97,6 +97,7 @@ impl ConnectorCommon for Payme { "{}, additional info: {}", response.status_error_details, response.status_additional_info )), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 1b7ce27439b3..24b7f2b3a0bd 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -226,6 +226,7 @@ impl From<(&PaymePaySaleResponse, u16)> for types::ErrorResponse { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: pay_sale_response.status_error_details.to_owned(), status_code: http_code, + attempt_status: None, } } } @@ -308,6 +309,7 @@ impl From<(&SaleQuery, u16)> for types::ErrorResponse { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: sale_query_response.sale_error_text.clone(), status_code: http_code, + attempt_status: None, } } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 854d48dcaadc..af0707070e05 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -91,6 +91,7 @@ impl Paypal { code: response.name, message: response.message.clone(), reason: error_reason.or(Some(response.message)), + attempt_status: None, }) } } @@ -203,6 +204,7 @@ impl ConnectorCommon for Paypal { code: response.name, message: response.message.clone(), reason, + attempt_status: None, }) } } @@ -340,6 +342,7 @@ impl ConnectorIntegration>() .join(", "), ), + attempt_status: None, } }) } else if !ISO_SUCCESS_CODES.contains(&item.iso_response_code.as_str()) { @@ -452,6 +453,7 @@ fn build_error_response( code: item.iso_response_code.clone(), message: item.response_message.clone(), reason: Some(item.response_message.clone()), + attempt_status: None, }) } else { None diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 0e8d5100ea35..e3860eb70989 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Prophetpay { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 29f21f37381d..42ba8197e74e 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -98,6 +98,7 @@ impl ConnectorCommon for Rapyd { code: response_data.status.error_code, message: response_data.status.status.unwrap_or_default(), reason: response_data.status.message, + attempt_status: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 9df699b938bb..08985ba022fc 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -457,6 +457,7 @@ impl status_code: item.http_code, message: item.response.status.status.unwrap_or_default(), reason: data.failure_message.to_owned(), + attempt_status: None, }), ), _ => { @@ -497,6 +498,7 @@ impl status_code: item.http_code, message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, + attempt_status: None, }), ), }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index a17546711f14..1d1ea36fae16 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -99,6 +99,7 @@ impl ConnectorCommon for Shift4 { .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), message: response.error.message, reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index a048b0f5433b..5c52728d879f 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -123,6 +123,7 @@ impl ConnectorCommon for Square { .and_then(|error| error.category.clone()) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 7f5fde719389..ba9642b4f875 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -109,6 +109,7 @@ impl ConnectorCommon for Stax { .change_context(errors::ConnectorError::ResponseDeserializationFailed)? .to_owned(), ), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index e3551306e673..98e544105fda 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -225,6 +225,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -351,6 +352,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -473,6 +475,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -603,6 +606,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -743,6 +747,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -897,6 +902,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -1016,6 +1022,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -1170,6 +1177,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -1287,6 +1295,7 @@ impl services::ConnectorIntegration }) .or(Some(error.message.clone())), status_code: item.http_code, + attempt_status: None, }); let connector_metadata = diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 912f1575e1e0..903952dc8eb4 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -138,6 +138,7 @@ impl ConnectorCommon for Trustpay { .map(|error_code_message| error_code_message.error_code) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: reason.or(response_data.description), + attempt_status: None, }) } Err(error_msg) => { @@ -293,6 +294,7 @@ impl ConnectorIntegration TryFrom: ConnectorIntegrationAny { let error_res = connector_integration.get_error_response(body)?; - if router_data.connector == "bluesnap" - && error_res.status_code == 403 - && error_res.reason - == Some(format!( - "{} in bluesnap dashboard", - consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND - )) - { - router_data.status = AttemptStatus::Failure; + if let Some(status) = error_res.attempt_status { + router_data.status = status; }; error_res } @@ -434,6 +429,7 @@ where message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, + attempt_status: None, }; router_data.response = Err(error_response); router_data.connector_http_status_code = Some(504); diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 261195d166cb..8f08ce062560 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -923,6 +923,7 @@ pub struct ErrorResponse { pub message: String, pub reason: Option, pub status_code: u16, + pub attempt_status: Option, } impl ErrorResponse { @@ -938,6 +939,7 @@ impl ErrorResponse { .error_message(), reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + attempt_status: None, } } } @@ -980,6 +982,7 @@ impl From for ErrorResponse { errors::ApiErrorResponse::ExternalConnectorError { status_code, .. } => status_code, _ => 500, }, + attempt_status: None, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 2179b4bde180..e815740cac48 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -111,6 +111,7 @@ pub trait ConnectorCommon { code: consts::NO_ERROR_CODE.to_string(), message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, + attempt_status: None, }) } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 386bd02ae94b..558044028f7a 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -401,6 +401,7 @@ pub fn handle_json_response_deserialization_failure( code: consts::NO_ERROR_CODE.to_string(), message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), reason: Some(response_data), + attempt_status: None, }) } } From 7623ea93bee61b0bb22b68e86f44de17f04f876b Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:49:49 +0530 Subject: [PATCH 41/57] refactor(router): add parameter connectors to get_request_body function (#2708) --- connector-template/mod.rs | 13 +++-- crates/router/src/connector/aci.rs | 19 +++++-- crates/router/src/connector/adyen.rs | 57 +++++++++++++++---- crates/router/src/connector/airwallex.rs | 36 +++++++++--- .../router/src/connector/authorizedotnet.rs | 33 ++++++++--- crates/router/src/connector/bambora.rs | 23 ++++++-- crates/router/src/connector/bankofamerica.rs | 19 +++++-- crates/router/src/connector/bitpay.rs | 5 +- crates/router/src/connector/bluesnap.rs | 28 +++++++-- crates/router/src/connector/boku.rs | 25 ++++++-- crates/router/src/connector/braintree.rs | 47 +++++++++++---- crates/router/src/connector/cashtocode.rs | 5 +- crates/router/src/connector/checkout.rs | 38 ++++++++++--- crates/router/src/connector/coinbase.rs | 5 +- crates/router/src/connector/cryptopay.rs | 7 ++- crates/router/src/connector/cybersource.rs | 21 +++++-- crates/router/src/connector/dlocal.rs | 19 +++++-- crates/router/src/connector/dummyconnector.rs | 15 ++++- crates/router/src/connector/fiserv.rs | 34 ++++++++--- crates/router/src/connector/forte.rs | 20 +++++-- crates/router/src/connector/globalpay.rs | 32 ++++++++--- crates/router/src/connector/globepay.rs | 10 +++- crates/router/src/connector/gocardless.rs | 25 ++++++-- crates/router/src/connector/helcim.rs | 29 ++++++++-- crates/router/src/connector/iatapay.rs | 19 +++++-- crates/router/src/connector/klarna.rs | 10 +++- crates/router/src/connector/mollie.rs | 15 ++++- crates/router/src/connector/multisafepay.rs | 14 ++++- crates/router/src/connector/nexinets.rs | 20 +++++-- crates/router/src/connector/nmi.rs | 35 +++++++++--- crates/router/src/connector/noon.rs | 24 ++++++-- crates/router/src/connector/nuvei.rs | 36 +++++++++--- crates/router/src/connector/opayo.rs | 19 +++++-- crates/router/src/connector/opennode.rs | 5 +- crates/router/src/connector/payeezy.rs | 24 ++++++-- crates/router/src/connector/payme.rs | 36 +++++++++--- crates/router/src/connector/paypal.rs | 27 +++++++-- crates/router/src/connector/payu.rs | 20 +++++-- crates/router/src/connector/powertranz.rs | 23 ++++++-- crates/router/src/connector/prophetpay.rs | 19 +++++-- crates/router/src/connector/rapyd.rs | 21 +++++-- crates/router/src/connector/shift4.rs | 22 +++++-- crates/router/src/connector/square.rs | 15 ++++- crates/router/src/connector/stax.rs | 25 ++++++-- crates/router/src/connector/stripe.rs | 49 ++++++++++++---- crates/router/src/connector/trustpay.rs | 18 ++++-- crates/router/src/connector/tsys.rs | 30 ++++++++-- crates/router/src/connector/volt.rs | 20 +++++-- crates/router/src/connector/wise.rs | 20 +++++-- crates/router/src/connector/worldline.rs | 15 ++++- crates/router/src/connector/worldpay.rs | 18 ++++-- crates/router/src/connector/zen.rs | 14 ++++- crates/router/src/services/api.rs | 1 + 53 files changed, 920 insertions(+), 259 deletions(-) diff --git a/connector-template/mod.rs b/connector-template/mod.rs index b27a0774e714..7f21962109de 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -157,7 +157,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -186,7 +186,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) .build(), )) } @@ -302,6 +302,7 @@ impl fn get_request_body( &self, _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -319,7 +320,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) .build(), )) } @@ -374,7 +375,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::RefundsRouterData) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -394,7 +395,7 @@ impl .url(&types::RefundExecuteType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundExecuteType::get_headers(self, req, connectors)?) - .body(types::RefundExecuteType::get_request_body(self, req)?) + .body(types::RefundExecuteType::get_request_body(self, req, connectors)?) .build(); Ok(Some(request)) } @@ -442,7 +443,7 @@ impl .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .body(types::RefundSyncType::get_request_body(self, req)?) + .body(types::RefundSyncType::get_request_body(self, req, connectors)?) .build(), )) } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 7dbe2a0cd9a2..f6389c802f9e 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -201,7 +201,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -280,6 +282,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // encode only for for urlencoded things. let connector_router_data = aci::AciRouterData::try_from(( @@ -317,7 +320,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -386,6 +391,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = aci::AciCancelRequest::try_from(req)?; let aci_req = types::RequestBody::log_and_get_request_body( @@ -406,7 +412,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -479,6 +487,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = aci::AciRouterData::try_from(( &self.get_currency_unit(), @@ -508,7 +517,9 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let authorize_req = types::PaymentsAuthorizeRouterData::from(( req, @@ -203,7 +204,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -306,6 +309,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -334,7 +338,9 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -398,6 +404,7 @@ impl fn get_request_body( &self, req: &types::RouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, @@ -482,7 +489,7 @@ impl req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let request_body = self.get_request_body(req)?; + let request_body = self.get_request_body(req, connectors)?; match request_body { Some(_) => Ok(Some( services::RequestBuilder::new() @@ -490,7 +497,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )), None => Ok(None), @@ -632,6 +641,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -664,7 +674,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -752,6 +764,7 @@ impl fn get_request_body( &self, req: &types::PaymentsBalanceRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -776,7 +789,9 @@ impl .headers(types::PaymentsBalanceType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body(self, req)?) + .body(types::PaymentsBalanceType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -845,6 +860,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenCancelRequest::try_from(req)?; @@ -866,7 +882,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -955,6 +973,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenPayoutCancelRequest::try_from(req)?; let adyen_req = types::RequestBody::log_and_get_request_body( @@ -975,7 +994,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1066,7 +1088,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1162,7 +1187,9 @@ impl .headers(types::PayoutEligibilityType::get_headers( self, req, connectors, )?) - .body(types::PayoutEligibilityType::get_request_body(self, req)?) + .body(types::PayoutEligibilityType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) @@ -1241,6 +1268,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1269,7 +1297,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1369,7 +1400,9 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = airwallex::AirwallexIntentRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -274,7 +277,9 @@ impl .url(&types::PaymentsInitType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body(self, req)?) + .body(types::PaymentsInitType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -373,6 +378,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), @@ -404,7 +410,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?; @@ -572,7 +581,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -635,6 +644,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?; @@ -660,7 +670,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?; @@ -773,7 +786,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), @@ -848,7 +864,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -176,7 +177,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = authorizedotnet::AuthorizedotnetCreateSyncRequest::try_from(req)?; let sync_request = types::RequestBody::log_and_get_request_body( @@ -261,7 +265,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -362,7 +369,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; @@ -445,7 +455,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -542,7 +555,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -634,7 +650,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -735,7 +754,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index d0ed9929a77b..802be26408df 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -173,6 +173,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraPaymentsRequest::try_from(req)?; @@ -195,7 +196,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bambora::BamboraPaymentsCaptureRequest::try_from(req)?; let bambora_req = types::RequestBody::log_and_get_request_body( @@ -368,7 +370,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraPaymentsRequest::try_from(req)?; @@ -464,7 +467,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = bambora::BamboraRefundRequest::try_from(req)?; let bambora_req = types::RequestBody::log_and_get_request_body( @@ -557,7 +563,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; @@ -766,7 +777,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(); Ok(Some(request)) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index a51fcc0ad626..84870f7407fb 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -166,6 +166,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( &self.get_currency_unit(), @@ -198,7 +199,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -332,7 +336,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( &self.get_currency_unit(), @@ -420,7 +427,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bitpay::BitpayRouterData::try_from(( &self.get_currency_unit(), @@ -205,7 +206,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bluesnap::BluesnapVoidRequest::try_from(req)?; let bluesnap_req = types::RequestBody::log_and_get_request_body( @@ -283,7 +284,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -465,7 +469,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; let bluesnap_req = types::RequestBody::log_and_get_request_body( @@ -552,7 +559,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -668,7 +678,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -789,7 +802,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -855,6 +868,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -883,7 +897,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuPaymentsRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -232,7 +233,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuPsyncRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -318,7 +322,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -395,7 +402,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = boku::BokuRefundRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -485,7 +495,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuRsyncRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -564,7 +577,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = braintree::BraintreeSessionRequest::try_from(req)?; let braintree_session_request = types::RequestBody::log_and_get_request_body( @@ -324,6 +327,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; @@ -349,7 +353,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )), false => Ok(None), @@ -439,6 +445,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version.clone(); let connector_router_data = @@ -484,7 +491,9 @@ impl ConnectorIntegration Err(errors::ConnectorError::NotImplemented( @@ -589,6 +598,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -620,7 +630,9 @@ impl ConnectorIntegration Ok(Some( @@ -629,7 +641,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; let connector_router_data = @@ -920,7 +937,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -1057,6 +1077,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; let connector_router_data = @@ -1103,7 +1124,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -1221,7 +1245,9 @@ impl ConnectorIntegration Ok(None), @@ -1586,6 +1612,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -1631,7 +1658,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )), diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 75433cfd0f13..12a52e485396 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -203,6 +203,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?; let cashtocode_req = types::RequestBody::log_and_get_request_body( @@ -228,7 +229,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = checkout::TokenRequest::try_from(req)?; let checkout_req = types::RequestBody::log_and_get_request_body( @@ -229,7 +230,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -313,6 +316,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -342,7 +346,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -532,7 +541,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = checkout::PaymentVoidRequest::try_from(req)?; let checkout_req = types::RequestBody::log_and_get_request_body( @@ -609,7 +621,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -704,7 +719,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let checkout_req = checkout::Evidence::try_from(req)?; let checkout_req_string = types::RequestBody::log_and_get_request_body( @@ -1065,7 +1085,9 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body(self, req)?) + .body(types::SubmitEvidenceType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index f26100fcc8cf..5704ea15b005 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -184,6 +184,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = coinbase::CoinbasePaymentsRequest::try_from(req)?; let coinbase_payment_request = types::RequestBody::log_and_get_request_body( @@ -208,7 +209,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { let api_method; - let payload = match self.get_request_body(req)? { + let payload = match self.get_request_body(req, connectors)? { Some(val) => { let body = types::RequestBody::get_inner_value(val).peek().to_owned(); api_method = "POST".to_string(); @@ -217,6 +217,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = cryptopay::CryptopayRouterData::try_from(( &self.get_currency_unit(), @@ -249,7 +250,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { let date = OffsetDateTime::now_utc(); - let cybersource_req = self.get_request_body(req)?; + let cybersource_req = self.get_request_body(req, connectors)?; let auth = cybersource::CybersourceAuthType::try_from(&req.connector_auth_type)?; let merchant_account = auth.merchant_account.clone(); let base_url = connectors.cybersource.base_url.as_str(); @@ -298,6 +298,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; let cybersource_payments_request = types::RequestBody::log_and_get_request_body( @@ -320,7 +321,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) @@ -472,6 +476,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), @@ -503,7 +508,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) @@ -588,7 +594,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; let cybersource_refund_request = types::RequestBody::log_and_get_request_body( @@ -679,7 +686,7 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let dlocal_req = match self.get_request_body(req)? { + let dlocal_req = match self.get_request_body(req, connectors)? { Some(val) => val, None => types::RequestBody::log_and_get_request_body("".to_string(), Ok) .change_context(errors::ConnectorError::RequestEncodingFailed)?, @@ -211,6 +211,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), @@ -242,7 +243,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; let dlocal_payments_capture_request = types::RequestBody::log_and_get_request_body( @@ -392,7 +396,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), @@ -546,7 +553,9 @@ impl ConnectorIntegration fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; let dummmy_payments_request = types::RequestBody::log_and_get_request_body( @@ -213,7 +214,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -348,6 +351,7 @@ impl fn get_request_body( &self, _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -430,6 +434,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = transformers::DummyConnectorRefundRequest::try_from(req)?; let dummmy_refund_request = types::RequestBody::log_and_get_request_body( @@ -452,7 +457,9 @@ impl ConnectorIntegration ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; let auth: fiserv::FiservAuthType = @@ -70,7 +70,7 @@ where let mut auth_header = self.get_auth_header(&req.connector_auth_type)?; let fiserv_req = self - .get_request_body(req)? + .get_request_body(req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let client_request_id = Uuid::new_v4().to_string(); @@ -246,6 +246,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservCancelRequest::try_from(req)?; let fiserv_payments_cancel_request = types::RequestBody::log_and_get_request_body( @@ -267,7 +268,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservSyncRequest::try_from(req)?; let fiserv_payments_sync_request = types::RequestBody::log_and_get_request_body( @@ -353,7 +357,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), @@ -434,7 +441,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), @@ -547,7 +557,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), @@ -636,7 +649,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservSyncRequest::try_from(req)?; let fiserv_sync_request = types::RequestBody::log_and_get_request_body( @@ -716,7 +732,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::FortePaymentsRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -226,7 +227,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::ForteCaptureRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -380,7 +384,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::ForteCancelRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -461,7 +468,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = forte::ForteRefundRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -542,7 +552,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let globalpay_req = types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -187,7 +188,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -263,7 +264,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = GlobalpayRefreshTokenRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -383,7 +387,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = requests::GlobalpayCancelRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -541,6 +548,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = requests::GlobalpayCaptureRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -564,7 +572,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = GlobalpayPaymentsRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -655,7 +666,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = requests::GlobalpayRefundRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -742,7 +756,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = globepay::GlobepayPaymentsRequest::try_from(req)?; let globepay_req = types::RequestBody::log_and_get_request_body( @@ -213,7 +214,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = globepay::GlobepayRefundRequest::try_from(req)?; let globepay_req = types::RequestBody::log_and_get_request_body( @@ -387,7 +391,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessCustomerRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -178,7 +179,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -248,6 +251,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessBankAccountRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -269,7 +273,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -366,6 +372,7 @@ impl fn get_request_body( &self, req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessMandateRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -389,7 +396,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } else { @@ -447,6 +456,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), @@ -478,7 +488,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), @@ -640,7 +653,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = helcim::HelcimVerifyRequest::try_from(req)?; @@ -214,7 +215,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -270,6 +273,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -301,7 +305,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -470,7 +477,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = helcim::HelcimVoidRequest::try_from(req)?; let helcim_req = types::RequestBody::log_and_get_request_body( @@ -546,7 +556,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -628,7 +641,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = iatapay::IatapayAuthUpdateRequest::try_from(req)?; let iatapay_req = types::RequestBody::log_and_get_request_body( @@ -199,7 +200,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), @@ -307,7 +311,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), @@ -490,7 +497,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = klarna::KlarnaSessionRequest::try_from(req)?; // encode only for for urlencoded things. @@ -178,7 +179,9 @@ impl .headers(types::PaymentsSessionType::get_headers( self, req, connectors, )?) - .body(types::PaymentsSessionType::get_request_body(self, req)?) + .body(types::PaymentsSessionType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -410,6 +413,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = klarna::KlarnaRouterData::try_from(( &self.get_currency_unit(), @@ -441,7 +445,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index 94da85b19d2d..ef3eb6a3e7b3 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -150,6 +150,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = mollie::MollieCardTokenRequest::try_from(req)?; let mollie_req = types::RequestBody::log_and_get_request_body( @@ -170,7 +171,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -233,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), @@ -264,7 +268,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), @@ -455,7 +462,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), @@ -293,7 +294,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), @@ -392,7 +396,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nexinets::NexinetsPaymentsRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -225,7 +226,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -385,7 +389,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -466,7 +473,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = nexinets::NexinetsRefundRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -551,7 +561,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -163,7 +164,9 @@ impl .method(services::Method::Post) .url(&types::SetupMandateType::get_url(self, req, connectors)?) .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -213,6 +216,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), @@ -243,7 +247,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiSyncRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -313,7 +320,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), @@ -388,7 +398,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiCancelRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -458,7 +471,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), @@ -534,7 +550,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiSyncRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -602,7 +621,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -227,7 +228,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -371,7 +375,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = noon::NoonPaymentsCancelRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -446,7 +453,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -521,7 +531,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; @@ -176,7 +177,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -235,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -255,7 +257,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -339,7 +344,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -421,7 +429,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; let req = types::RequestBody::log_and_get_request_body( @@ -581,7 +592,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -670,7 +684,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -727,6 +741,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; let req = types::RequestBody::log_and_get_request_body( @@ -749,7 +764,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -828,7 +846,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = opayo::OpayoPaymentsRequest::try_from(req)?; let opayo_req = types::RequestBody::log_and_get_request_body( @@ -198,7 +199,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -332,7 +336,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = opayo::OpayoRefundRequest::try_from(req)?; @@ -413,7 +420,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = opennode::OpennodeRouterData::try_from(( &self.get_currency_unit(), @@ -203,7 +204,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { let auth = payeezy::PayeezyAuthType::try_from(&req.connector_auth_type)?; - let option_request_payload = self.get_request_body(req)?; + let option_request_payload = self.get_request_body(req, connectors)?; let request_payload = option_request_payload.map_or("{}".to_string(), |payload| { types::RequestBody::get_inner_value(payload).expose() }); @@ -200,6 +200,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; let payeezy_req = types::RequestBody::log_and_get_request_body( @@ -220,7 +221,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), @@ -325,7 +329,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), @@ -422,7 +429,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), @@ -514,7 +524,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = payme::CaptureBuyerRequest::try_from(req)?; @@ -173,7 +174,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), ), AuthenticationType::NoThreeDs => None, @@ -253,6 +256,7 @@ impl fn get_request_body( &self, req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let amount = req.request.get_amount()?; let currency = req.request.get_currency()?; @@ -283,7 +287,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), ); @@ -378,6 +382,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::Pay3dsRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -403,7 +408,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -480,6 +485,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -511,7 +517,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::PaymeQuerySaleRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -595,7 +604,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -688,7 +700,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -793,7 +808,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::PaymeQueryTransactionRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -874,7 +892,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = paypal::PaypalAuthUpdateRequest::try_from(req)?; let paypal_req = types::RequestBody::log_and_get_request_body( @@ -304,7 +305,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -415,7 +419,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -717,7 +724,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -877,7 +887,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; let paypal_req = types::RequestBody::log_and_get_request_body( diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 305e49fea06d..9a8d4734f837 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -244,6 +244,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = payu::PayuAuthUpdateRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -266,7 +267,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payu::PayuPaymentsCaptureRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -441,7 +445,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payu::PayuPaymentsRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -541,7 +548,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = payu::PayuRefundRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -628,7 +638,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzPaymentsRequest::try_from(req)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -219,7 +220,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let redirect_payload: powertranz::RedirectResponsePayload = req .request @@ -309,7 +313,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -371,6 +375,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -394,7 +399,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -482,7 +490,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(req)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -543,7 +554,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), @@ -196,7 +197,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -330,7 +334,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), @@ -418,7 +425,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -212,7 +213,7 @@ impl let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?; - let body = types::PaymentsAuthorizeType::get_request_body(self, req)? + let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = @@ -233,7 +234,9 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -493,6 +496,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -522,7 +526,7 @@ impl "/v1/payments/{}/capture", req.request.connector_transaction_id ); - let body = types::PaymentsCaptureType::get_request_body(self, req)? + let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = @@ -541,7 +545,9 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -631,6 +637,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -656,7 +663,7 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -249,7 +250,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -496,7 +500,9 @@ impl .content_type(request::ContentType::FormUrlEncoded) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body(self, req)?) + .body(types::PaymentsInitType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -556,6 +562,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -581,7 +588,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -636,6 +643,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = shift4::Shift4RefundRequest::try_from(req)?; let shift4_req = types::RequestBody::log_and_get_request_body( @@ -658,7 +666,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = square::SquareTokenRequest::try_from(req)?; @@ -268,7 +269,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -413,6 +416,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = square::SquarePaymentsRequest::try_from(req)?; @@ -439,7 +443,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = square::SquareRefundRequest::try_from(req)?; let square_req = types::RequestBody::log_and_get_request_body( @@ -725,7 +732,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = stax::StaxCustomerRequest::try_from(req)?; @@ -187,7 +188,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -250,6 +253,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stax::StaxTokenRequest::try_from(req)?; @@ -272,7 +276,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -351,6 +357,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), @@ -383,7 +390,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), @@ -542,7 +552,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), @@ -701,7 +714,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; let pre_processing_request = types::RequestBody::log_and_get_request_body( @@ -171,7 +172,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -270,6 +271,7 @@ impl fn get_request_body( &self, req: &types::ConnectorCustomerRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CustomerRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -295,7 +297,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -397,6 +401,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::TokenRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -418,7 +423,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -526,6 +533,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CaptureRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -549,7 +557,9 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -671,7 +681,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -810,6 +822,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { match &req.request.payment_method_data { api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { @@ -842,7 +855,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -950,6 +965,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CancelRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -970,7 +986,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -1084,6 +1102,7 @@ impl types::SetupMandateRequestData, types::PaymentsResponseData, >, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req = stripe::SetupIntentRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -1109,7 +1128,7 @@ impl .url(&Verify::get_url(self, req, connectors)?) .attach_default_headers() .headers(Verify::get_headers(self, req, connectors)?) - .body(Verify::get_request_body(self, req)?) + .body(Verify::get_request_body(self, req, connectors)?) .build(), )) } @@ -1220,6 +1239,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::RefundRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -1242,7 +1262,9 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let stripe_req = stripe::Evidence::try_from(req)?; let stripe_req_string = types::RequestBody::log_and_get_request_body( @@ -1719,7 +1744,9 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body(self, req)?) + .body(types::SubmitEvidenceType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 903952dc8eb4..7509131afeef 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -236,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = trustpay::TrustpayAuthUpdateRequest::try_from(req)?; let trustpay_req = types::RequestBody::log_and_get_request_body( @@ -257,7 +258,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let currency = req.request.get_currency()?; let amount = req @@ -476,7 +480,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), ); @@ -553,6 +557,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let amount = req .request @@ -599,7 +604,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), @@ -706,7 +714,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysPaymentsRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -168,7 +169,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysSyncRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -247,7 +251,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = tsys::TsysPaymentsCaptureRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -328,7 +335,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysPaymentsCancelRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -404,7 +414,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysRefundRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -482,7 +495,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysSyncRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -559,7 +575,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = volt::VoltAuthUpdateRequest::try_from(req)?; let volt_req = types::RequestBody::log_and_get_request_body( @@ -196,7 +197,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), @@ -293,7 +297,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -435,7 +442,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), @@ -525,7 +535,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -346,7 +347,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -423,7 +427,9 @@ impl .headers(types::PayoutRecipientType::get_headers( self, req, connectors, )?) - .body(types::PayoutRecipientType::get_request_body(self, req)?) + .body(types::PayoutRecipientType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) @@ -516,6 +522,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutCreateRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -536,7 +543,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutFulfillRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -634,7 +644,9 @@ impl services::ConnectorIntegration, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = worldline::ApproveRequest::try_from(req)?; @@ -406,7 +407,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = worldline::WorldlineRouterData::try_from(( &self.get_currency_unit(), @@ -524,7 +528,9 @@ impl ConnectorIntegration, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = worldline::WorldlineRefundRequest::try_from(req)?; let refund_req = types::RequestBody::log_and_get_request_body( @@ -610,7 +617,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = worldpay::WorldpayRouterData::try_from(( &self.get_currency_unit(), @@ -463,7 +466,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = WorldpayRefundRequest::try_from(req)?; let fiserv_refund_request = types::RequestBody::log_and_get_request_body( @@ -550,7 +556,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), @@ -247,7 +248,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), @@ -439,7 +443,9 @@ impl ConnectorIntegration: ConnectorIntegrationAny, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(None) } From a429b23c7f21c9d08a79895c0b770b35aab725f7 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:54:50 +0530 Subject: [PATCH 42/57] feat(router): add `gateway_status_map` interface (#2804) --- crates/diesel_models/src/gsm.rs | 97 ++++++++++ crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/query.rs | 1 + crates/diesel_models/src/query/gsm.rs | 100 ++++++++++ crates/diesel_models/src/schema.rs | 28 +++ crates/router/src/db.rs | 2 + crates/router/src/db/gsm.rs | 180 ++++++++++++++++++ crates/router/src/types/storage.rs | 9 +- crates/router/src/types/storage/gsm.rs | 4 + .../2023-11-07-110139_add_gsm_table/down.sql | 2 + .../2023-11-07-110139_add_gsm_table/up.sql | 16 ++ 11 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 crates/diesel_models/src/gsm.rs create mode 100644 crates/diesel_models/src/query/gsm.rs create mode 100644 crates/router/src/db/gsm.rs create mode 100644 crates/router/src/types/storage/gsm.rs create mode 100644 migrations/2023-11-07-110139_add_gsm_table/down.sql create mode 100644 migrations/2023-11-07-110139_add_gsm_table/up.sql diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs new file mode 100644 index 000000000000..d5b3122c7806 --- /dev/null +++ b/crates/diesel_models/src/gsm.rs @@ -0,0 +1,97 @@ +//! Gateway status mapping + +use common_utils::custom_serde; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::schema::gateway_status_map; + +#[derive( + Clone, + Debug, + Eq, + PartialEq, + router_derive::DebugAsDisplay, + Identifiable, + Queryable, + serde::Serialize, +)] +#[diesel(table_name = gateway_status_map, primary_key(connector, flow, sub_flow, code, message))] +pub struct GatewayStatusMap { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + #[serde(with = "custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "custom_serde::iso8601")] + pub last_modified: PrimitiveDateTime, + pub step_up_possible: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Insertable)] +#[diesel(table_name = gateway_status_map)] +pub struct GatewayStatusMappingNew { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + pub step_up_possible: bool, +} + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + AsChangeset, + router_derive::DebugAsDisplay, + Default, + serde::Deserialize, +)] +#[diesel(table_name = gateway_status_map)] +pub struct GatewayStatusMapperUpdateInternal { + pub connector: Option, + pub flow: Option, + pub sub_flow: Option, + pub code: Option, + pub message: Option, + pub status: Option, + pub router_error: Option>, + pub decision: Option, + pub step_up_possible: Option, +} + +#[derive(Debug)] +pub struct GatewayStatusMappingUpdate { + pub status: Option, + pub router_error: Option>, + pub decision: Option, + pub step_up_possible: Option, +} + +impl From for GatewayStatusMapperUpdateInternal { + fn from(value: GatewayStatusMappingUpdate) -> Self { + let GatewayStatusMappingUpdate { + decision, + status, + router_error, + step_up_possible, + } = value; + Self { + status, + router_error, + decision, + step_up_possible, + ..Default::default() + } + } +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 2d459499a1bd..08d74fb8fd37 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -15,6 +15,7 @@ pub mod events; pub mod file; #[allow(unused)] pub mod fraud_check; +pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; pub mod locker_mock_up; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index aeb09b969f13..ac3eeba44359 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -11,6 +11,7 @@ pub mod events; pub mod file; pub mod fraud_check; pub mod generics; +pub mod gsm; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; diff --git a/crates/diesel_models/src/query/gsm.rs b/crates/diesel_models/src/query/gsm.rs new file mode 100644 index 000000000000..bd44ce4dc378 --- /dev/null +++ b/crates/diesel_models/src/query/gsm.rs @@ -0,0 +1,100 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use error_stack::report; + +use crate::{ + errors, gsm::*, query::generics, schema::gateway_status_map::dsl, PgPooledConn, StorageResult, +}; + +impl GatewayStatusMappingNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl GatewayStatusMap { + pub async fn find( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + ) + .await + } + + pub async fn retrieve_decision( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + Self::find(conn, connector, flow, sub_flow, code, message) + .await + .map(|item| item.decision) + } + + pub async fn update( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + gsm: GatewayStatusMappingUpdate, + ) -> StorageResult { + generics::generic_update_with_results::< + ::Table, + GatewayStatusMapperUpdateInternal, + _, + _, + >( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + gsm.into(), + ) + .await? + .first() + .cloned() + .ok_or_else(|| { + report!(errors::DatabaseError::NotFound) + .attach_printable("Error while updating gsm entry") + }) + } + + pub async fn delete( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2923c719c8f7..50531e432adc 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -332,6 +332,33 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + gateway_status_map (connector, flow, sub_flow, code, message) { + #[max_length = 64] + connector -> Varchar, + #[max_length = 64] + flow -> Varchar, + #[max_length = 64] + sub_flow -> Varchar, + #[max_length = 255] + code -> Varchar, + #[max_length = 1024] + message -> Varchar, + #[max_length = 64] + status -> Varchar, + #[max_length = 64] + router_error -> Nullable, + #[max_length = 64] + decision -> Varchar, + created_at -> Timestamp, + last_modified -> Timestamp, + step_up_possible -> Bool, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -909,6 +936,7 @@ diesel::allow_tables_to_appear_in_same_query!( events, file_metadata, fraud_check, + gateway_status_map, locker_mock_up, mandate, merchant_account, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index b62ffd2c530f..3efef2c40f29 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -12,6 +12,7 @@ pub mod ephemeral_key; pub mod events; pub mod file; pub mod fraud_check; +pub mod gsm; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -80,6 +81,7 @@ pub trait StorageInterface: + business_profile::BusinessProfileInterface + organization::OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface + + gsm::GsmInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/gsm.rs b/crates/router/src/db/gsm.rs new file mode 100644 index 000000000000..b623bdc2bcf5 --- /dev/null +++ b/crates/router/src/db/gsm.rs @@ -0,0 +1,180 @@ +use diesel_models::gsm as storage; +use error_stack::IntoReport; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait GsmInterface { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult; + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult; + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl GsmInterface for Store { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + rule.insert(&conn).await.map_err(Into::into).into_report() + } + + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::GatewayStatusMap::retrieve_decision( + &conn, connector, flow, sub_flow, code, message, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::GatewayStatusMap::find(&conn, connector, flow, sub_flow, code, message) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::GatewayStatusMap::update(&conn, connector, flow, sub_flow, code, message, data) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::GatewayStatusMap::delete(&conn, connector, flow, sub_flow, code, message) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl GsmInterface for MockDb { + async fn add_gsm_rule( + &self, + _rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_gsm_decision( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + _data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 00a5e07a30e8..1e7c34a420b1 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -11,6 +11,7 @@ pub mod enums; pub mod ephemeral_key; pub mod events; pub mod file; +pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; pub mod locker_mock_up; @@ -41,10 +42,10 @@ pub use data_models::payments::{ pub use self::{ address::*, api_keys::*, capture::*, cards_info::*, configs::*, connector_response::*, - customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, - payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, + customers::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, + mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, + payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, + refund::*, reverse_lookup::*, routing_algorithm::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/gsm.rs b/crates/router/src/types/storage/gsm.rs new file mode 100644 index 000000000000..bcea00e90910 --- /dev/null +++ b/crates/router/src/types/storage/gsm.rs @@ -0,0 +1,4 @@ +pub use diesel_models::gsm::{ + GatewayStatusMap, GatewayStatusMapperUpdateInternal, GatewayStatusMappingNew, + GatewayStatusMappingUpdate, +}; diff --git a/migrations/2023-11-07-110139_add_gsm_table/down.sql b/migrations/2023-11-07-110139_add_gsm_table/down.sql new file mode 100644 index 000000000000..e1cdd5d4133d --- /dev/null +++ b/migrations/2023-11-07-110139_add_gsm_table/down.sql @@ -0,0 +1,2 @@ +-- Tables +DROP TABLE gateway_status_map; diff --git a/migrations/2023-11-07-110139_add_gsm_table/up.sql b/migrations/2023-11-07-110139_add_gsm_table/up.sql new file mode 100644 index 000000000000..9dfa68b01af9 --- /dev/null +++ b/migrations/2023-11-07-110139_add_gsm_table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here +-- Tables +CREATE TABLE IF NOT EXISTS gateway_status_map ( + connector VARCHAR(64) NOT NULL, + flow VARCHAR(64) NOT NULL, + sub_flow VARCHAR(64) NOT NULL, + code VARCHAR(255) NOT NULL, + message VARCHAR(1024), + status VARCHAR(64) NOT NULL, + router_error VARCHAR(64), + decision VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + last_modified TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + step_up_possible BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (connector, flow, sub_flow, code, message) +); From 164d1c66fbcb84104db07412496114db2f8c5c0c Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 8 Nov 2023 15:59:13 +0530 Subject: [PATCH 43/57] feat(events): add request details to api events (#2769) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/router/src/events/api_logs.rs | 15 +++++++++++++++ crates/router/src/services/api.rs | 1 + 2 files changed, 16 insertions(+) diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 35eaf1edae7f..5a66ba3e0bf9 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -1,3 +1,4 @@ +use actix_web::HttpRequest; use router_env::{tracing_actix_web::RequestId, types::FlowMetric}; use serde::Serialize; use time::OffsetDateTime; @@ -15,10 +16,14 @@ pub struct ApiEvent { #[serde(flatten)] auth_type: AuthenticationType, request: serde_json::Value, + user_agent: Option, + ip_addr: Option, + url_path: String, response: Option, } impl ApiEvent { + #[allow(clippy::too_many_arguments)] pub fn new( api_flow: &impl FlowMetric, request_id: &RequestId, @@ -27,6 +32,7 @@ impl ApiEvent { request: serde_json::Value, response: Option, auth_type: AuthenticationType, + http_req: &HttpRequest, ) -> Self { Self { api_flow: api_flow.to_string(), @@ -37,6 +43,15 @@ impl ApiEvent { request, response, auth_type, + ip_addr: http_req + .connection_info() + .realip_remote_addr() + .map(ToOwned::to_owned), + user_agent: http_req + .headers() + .get("user-agent") + .and_then(|user_agent_value| user_agent_value.to_str().ok().map(ToOwned::to_owned)), + url_path: http_req.path().to_string(), } } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index c942f09f96aa..362644906971 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -852,6 +852,7 @@ where serialized_request, serialized_response, auth_type, + request, ); match api_event.clone().try_into() { Ok(event) => { From 21ce8079f4cb11d70c5eaae78f83773141c67d0c Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:50:44 +0530 Subject: [PATCH 44/57] refactor(config): update payment method filter of Klarna in Stripe (#2807) --- config/development.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/development.toml b/config/development.toml index 34fbdbc9e078..63c1f045d94f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -248,7 +248,7 @@ ideal = { country = "NL", currency = "EUR" } [pm_filters.stripe] google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" } -klarna = { country = "US", currency = "USD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } affirm = { country = "US", currency = "USD" } afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } giropay = { country = "DE", currency = "EUR" } From 9ea84912bdc972d9edd5e565522013fcf1c2987a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:31:58 +0000 Subject: [PATCH 45/57] chore(version): v1.74.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f33956eddf9..0aa00381bf62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.74.0 (2023-11-08) + +### Features + +- **core:** Use redis as temp locker instead of basilisk ([#2789](https://github.com/juspay/hyperswitch/pull/2789)) ([`6678689`](https://github.com/juspay/hyperswitch/commit/6678689265ae9a4fbb7a43c1938237d349c5a68e)) +- **events:** Add request details to api events ([#2769](https://github.com/juspay/hyperswitch/pull/2769)) ([`164d1c6`](https://github.com/juspay/hyperswitch/commit/164d1c66fbcb84104db07412496114db2f8c5c0c)) +- **router:** Add `gateway_status_map` interface ([#2804](https://github.com/juspay/hyperswitch/pull/2804)) ([`a429b23`](https://github.com/juspay/hyperswitch/commit/a429b23c7f21c9d08a79895c0b770b35aab725f7)) +- **test_utils:** Add custom-headers and custom delay support to rustman ([#2636](https://github.com/juspay/hyperswitch/pull/2636)) ([`1effddd`](https://github.com/juspay/hyperswitch/commit/1effddd0a0d3985d6df03c4ae9be28712befc05e)) + +### Bug Fixes + +- **connector:** Add attempt_status in field in error_response ([#2794](https://github.com/juspay/hyperswitch/pull/2794)) ([`5642fef`](https://github.com/juspay/hyperswitch/commit/5642fef52a6d591d12c5745ed381f41a1593f183)) + +### Refactors + +- **config:** Update payment method filter of Klarna in Stripe ([#2807](https://github.com/juspay/hyperswitch/pull/2807)) ([`21ce807`](https://github.com/juspay/hyperswitch/commit/21ce8079f4cb11d70c5eaae78f83773141c67d0c)) +- **router:** Add parameter connectors to get_request_body function ([#2708](https://github.com/juspay/hyperswitch/pull/2708)) ([`7623ea9`](https://github.com/juspay/hyperswitch/commit/7623ea93bee61b0bb22b68e86f44de17f04f876b)) + +### Documentation + +- **README:** Update README ([#2800](https://github.com/juspay/hyperswitch/pull/2800)) ([`bef0a04`](https://github.com/juspay/hyperswitch/commit/bef0a04edc6323b3b7a2e0dd7eeb7954915ba7cf)) + +**Full Changelog:** [`v1.73.0...v1.74.0`](https://github.com/juspay/hyperswitch/compare/v1.73.0...v1.74.0) + +- - - + + ## 1.73.0 (2023-11-07) ### Features From 25a73c29a4c4715a54862dd6a28c875fd3752f63 Mon Sep 17 00:00:00 2001 From: Arjun Karthik Date: Wed, 8 Nov 2023 20:31:07 +0530 Subject: [PATCH 46/57] fix: [mollie] locale validation irrespective of auth type (#2814) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/connector/mollie/transformers.rs | 55 +++++++------------ crates/router/src/connector/utils.rs | 12 ++++ 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index 3c23c9f1d39b..b77077ae709f 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -9,8 +9,8 @@ use url::Url; use crate::{ connector::utils::{ - self, AddressDetailsData, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, - RouterData, + self, AddressDetailsData, BrowserInformationData, CardData, + PaymentMethodTokenizationRequestData, PaymentsAuthorizeRequestData, RouterData, }, core::errors, services, types, @@ -62,7 +62,7 @@ pub struct MolliePaymentsRequest { locale: Option, #[serde(flatten)] payment_method_data: PaymentMethodData, - metadata: Option, + metadata: Option, sequence_type: SequenceType, mandate_id: Option, } @@ -148,8 +148,10 @@ pub struct Address { pub country: api_models::enums::CountryAlpha2, } -pub struct MollieBrowserInfo { - language: String, +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MollieMetadata { + pub order_id: String, } impl TryFrom<&MollieRouterData<&types::PaymentsAuthorizeRouterData>> for MolliePaymentsRequest { @@ -216,7 +218,9 @@ impl TryFrom<&MollieRouterData<&types::PaymentsAuthorizeRouterData>> for MollieP webhook_url: "".to_string(), locale: None, payment_method_data, - metadata: None, + metadata: Some(MollieMetadata { + order_id: item.router_data.connector_request_reference_id.clone(), + }), sequence_type: SequenceType::Oneoff, mandate_id: None, }) @@ -287,12 +291,7 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { let card_expiry_date = ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); let card_cvv = ccard.card_cvc; - let browser_info = get_browser_info(item)?; - let locale = browser_info - .ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "browser_info.language", - })? - .language; + let locale = item.request.get_browser_info()?.get_language()?; let testmode = item.test_mode .ok_or(errors::ConnectorError::MissingRequiredField { @@ -386,24 +385,6 @@ fn get_address_details( Ok(address_details) } -fn get_browser_info( - item: &types::TokenizationRouterData, -) -> Result, error_stack::Report> { - if matches!(item.auth_type, enums::AuthenticationType::ThreeDs) { - item.request - .browser_info - .as_ref() - .map(|info| { - Ok(MollieBrowserInfo { - language: info.get_language()?, - }) - }) - .transpose() - } else { - Ok(None) - } -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MolliePaymentsResponse { @@ -411,7 +392,7 @@ pub struct MolliePaymentsResponse { pub id: String, pub amount: Amount, pub description: Option, - pub metadata: Option, + pub metadata: Option, pub status: MolliePaymentStatus, pub is_cancelable: Option, pub sequence_type: SequenceType, @@ -544,12 +525,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: url, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.id), }), ..item.data }) @@ -561,6 +542,7 @@ impl pub struct MollieRefundRequest { amount: Amount, description: Option, + metadata: Option, } impl TryFrom<&MollieRouterData<&types::RefundsRouterData>> for MollieRefundRequest { @@ -575,6 +557,9 @@ impl TryFrom<&MollieRouterData<&types::RefundsRouterData>> for MollieRefun Ok(Self { amount, description: item.router_data.request.reason.to_owned(), + metadata: Some(MollieMetadata { + order_id: item.router_data.request.refund_id.clone(), + }), }) } } @@ -589,7 +574,7 @@ pub struct RefundResponse { settlement_amount: Option, status: MollieRefundStatus, description: Option, - metadata: serde_json::Value, + metadata: Option, payment_id: String, #[serde(rename = "_links")] links: Links, @@ -642,6 +627,4 @@ pub struct ErrorResponse { pub title: Option, pub detail: String, pub field: Option, - #[serde(rename = "_links")] - pub links: Option, } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 3a8cae3a631e..8600fe802195 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -292,6 +292,18 @@ pub trait PaymentsAuthorizeRequestData { fn get_ip_address_as_optional(&self) -> Option>; } +pub trait PaymentMethodTokenizationRequestData { + fn get_browser_info(&self) -> Result; +} + +impl PaymentMethodTokenizationRequestData for types::PaymentMethodTokenizationData { + fn get_browser_info(&self) -> Result { + self.browser_info + .clone() + .ok_or_else(missing_field_err("browser_info")) + } +} + impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn is_auto_capture(&self) -> Result { match self.capture_method { From 89857941b09c5fbe0f3e7d5b4f908bb144ae162d Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 8 Nov 2023 21:31:53 +0530 Subject: [PATCH 47/57] feat(events): add extracted fields based on req/res types (#2795) --- Cargo.lock | 2 +- crates/api_models/src/admin.rs | 2 + crates/api_models/src/api_keys.rs | 6 + crates/api_models/src/events.rs | 74 +++++++++ crates/api_models/src/events/customer.rs | 35 ++++ crates/api_models/src/events/payment.rs | 151 ++++++++++++++++++ crates/api_models/src/events/payouts.rs | 29 ++++ crates/api_models/src/events/refund.rs | 63 ++++++++ crates/api_models/src/events/routing.rs | 58 +++++++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/payment_methods.rs | 6 +- crates/api_models/src/payments.rs | 4 +- crates/api_models/src/refunds.rs | 13 +- crates/api_models/src/routing.rs | 5 + crates/common_enums/Cargo.toml | 1 - crates/common_utils/Cargo.toml | 1 + crates/common_utils/src/events.rs | 91 +++++++++++ crates/common_utils/src/lib.rs | 2 + crates/diesel_models/src/ephemeral_key.rs | 6 + crates/diesel_models/src/refund.rs | 9 ++ .../src/compatibility/stripe/refunds.rs | 8 +- .../src/compatibility/stripe/refunds/types.rs | 3 + crates/router/src/compatibility/wrap.rs | 5 +- crates/router/src/core/api_keys.rs | 8 +- crates/router/src/core/refunds.rs | 9 +- crates/router/src/core/routing.rs | 10 +- crates/router/src/core/verification.rs | 3 +- crates/router/src/db/refund.rs | 8 +- crates/router/src/events/api_logs.rs | 53 +++++- crates/router/src/openapi.rs | 2 +- crates/router/src/routes/admin.rs | 8 +- crates/router/src/routes/api_keys.rs | 10 +- crates/router/src/routes/dummy_connector.rs | 2 +- crates/router/src/routes/refunds.rs | 9 +- crates/router/src/routes/routing.rs | 6 +- crates/router/src/routes/verification.rs | 2 +- crates/router/src/services/api.rs | 14 +- crates/router/src/types/api/customers.rs | 6 + crates/router/src/types/storage/refund.rs | 4 +- openapi/openapi_spec.json | 4 +- 40 files changed, 664 insertions(+), 69 deletions(-) create mode 100644 crates/api_models/src/events.rs create mode 100644 crates/api_models/src/events/customer.rs create mode 100644 crates/api_models/src/events/payment.rs create mode 100644 crates/api_models/src/events/payouts.rs create mode 100644 crates/api_models/src/events/refund.rs create mode 100644 crates/api_models/src/events/routing.rs create mode 100644 crates/common_utils/src/events.rs diff --git a/Cargo.lock b/Cargo.lock index 886a8b50acc8..ac7fde55d8e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1503,7 +1503,6 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" name = "common_enums" version = "0.1.0" dependencies = [ - "common_utils", "diesel", "router_derive", "serde", @@ -1519,6 +1518,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bytes", + "common_enums", "diesel", "error-stack", "fake", diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 037d223754a0..e844d1900a1a 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -893,6 +893,8 @@ pub struct ToggleKVResponse { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ToggleKVRequest { + #[serde(skip_deserializing)] + pub merchant_id: String, /// Status of KV for the specific merchant #[schema(example = true)] pub kv_enabled: bool, diff --git a/crates/api_models/src/api_keys.rs b/crates/api_models/src/api_keys.rs index f0ab403d9c65..805c5616c2a0 100644 --- a/crates/api_models/src/api_keys.rs +++ b/crates/api_models/src/api_keys.rs @@ -129,6 +129,12 @@ pub struct UpdateApiKeyRequest { /// rotating your keys once every 6 months. #[schema(example = "2022-09-10T10:11:12Z")] pub expiration: Option, + + #[serde(skip_deserializing)] + pub key_id: String, + + #[serde(skip_deserializing)] + pub merchant_id: String, } /// The response body for revoking an API Key. diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs new file mode 100644 index 000000000000..78f34b4b87fa --- /dev/null +++ b/crates/api_models/src/events.rs @@ -0,0 +1,74 @@ +pub mod customer; +pub mod payment; +#[cfg(feature = "payouts")] +pub mod payouts; +pub mod refund; +pub mod routing; + +use common_utils::{ + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, +}; + +use crate::{ + admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, + payments::*, verifications::*, +}; + +impl ApiEventMetric for TimeRange {} + +impl_misc_api_event_type!( + PaymentMethodId, + PaymentsSessionResponse, + PaymentMethodListResponse, + PaymentMethodCreate, + PaymentLinkInitiateRequest, + RetrievePaymentLinkResponse, + MandateListConstraints, + CreateFileResponse, + DisputeResponse, + SubmitEvidenceRequest, + MerchantConnectorResponse, + MerchantConnectorId, + MandateResponse, + MandateRevokedResponse, + RetrievePaymentLinkRequest, + MandateId, + DisputeListConstraints, + RetrieveApiKeyResponse, + BusinessProfileResponse, + BusinessProfileUpdate, + BusinessProfileCreate, + RevokeApiKeyResponse, + ToggleKVResponse, + ToggleKVRequest, + MerchantAccountDeleteResponse, + MerchantAccountUpdate, + CardInfoResponse, + CreateApiKeyResponse, + CreateApiKeyRequest, + MerchantConnectorDeleteResponse, + MerchantConnectorUpdate, + MerchantConnectorCreate, + MerchantId, + CardsInfoRequest, + MerchantAccountResponse, + MerchantAccountListRequest, + MerchantAccountCreate, + PaymentsSessionRequest, + ApplepayMerchantVerificationRequest, + ApplepayMerchantResponse, + ApplepayVerifiedDomainsResponse, + UpdateApiKeyRequest +); + +#[cfg(feature = "stripe")] +impl_misc_api_event_type!( + StripeSetupIntentResponse, + StripeRefundResponse, + StripePaymentIntentListResponse, + StripePaymentIntentResponse, + CustomerDeleteResponse, + CustomerPaymentMethodListResponse, + CreateCustomerResponse +); diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs new file mode 100644 index 000000000000..29f565042181 --- /dev/null +++ b/crates/api_models/src/events/customer.rs @@ -0,0 +1,35 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::customers::{CustomerDeleteResponse, CustomerId, CustomerRequest, CustomerResponse}; + +impl ApiEventMetric for CustomerDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs new file mode 100644 index 000000000000..2f3336fc2777 --- /dev/null +++ b/crates/api_models/src/events/payment.rs @@ -0,0 +1,151 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::{ + payment_methods::{ + CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, + PaymentMethodResponse, PaymentMethodUpdate, + }, + payments::{ + PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, + PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, + PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + }, +}; +impl ApiEventMetric for PaymentsRetrieveRequest { + fn get_api_event_type(&self) -> Option { + match self.resource_id { + PaymentIdType::PaymentIntentId(ref id) => Some(ApiEventsType::Payment { + payment_id: id.clone(), + }), + _ => None, + } + } +} + +impl ApiEventMetric for PaymentsStartRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsCaptureRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.to_owned(), + }) + } +} + +impl ApiEventMetric for PaymentsCancelRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsApproveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsRejectRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsRequest { + fn get_api_event_type(&self) -> Option { + match self.payment_id { + Some(PaymentIdType::PaymentIntentId(ref id)) => Some(ApiEventsType::Payment { + payment_id: id.clone(), + }), + _ => None, + } + } +} + +impl ApiEventMetric for PaymentsResponse { + fn get_api_event_type(&self) -> Option { + self.payment_id + .clone() + .map(|payment_id| ApiEventsType::Payment { payment_id }) + } +} + +impl ApiEventMetric for PaymentMethodResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: Some(self.payment_method), + payment_method_type: self.payment_method_type, + }) + } +} + +impl ApiEventMetric for PaymentMethodUpdate {} + +impl ApiEventMetric for PaymentMethodDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: None, + payment_method_type: None, + }) + } +} + +impl ApiEventMetric for CustomerPaymentMethodsListResponse {} + +impl ApiEventMetric for PaymentMethodListRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethodList { + payment_id: self + .client_secret + .as_ref() + .and_then(|cs| cs.rsplit_once("_secret_")) + .map(|(pid, _)| pid.to_string()), + }) + } +} + +impl ApiEventMetric for PaymentListFilterConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListFilters { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListResponseV2 { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RedirectionResponse {} diff --git a/crates/api_models/src/events/payouts.rs b/crates/api_models/src/events/payouts.rs new file mode 100644 index 000000000000..303709acc476 --- /dev/null +++ b/crates/api_models/src/events/payouts.rs @@ -0,0 +1,29 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::payouts::{ + PayoutActionRequest, PayoutCreateRequest, PayoutCreateResponse, PayoutRetrieveRequest, +}; + +impl ApiEventMetric for PayoutRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutCreateResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutActionRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} diff --git a/crates/api_models/src/events/refund.rs b/crates/api_models/src/events/refund.rs new file mode 100644 index 000000000000..424a3191db66 --- /dev/null +++ b/crates/api_models/src/events/refund.rs @@ -0,0 +1,63 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::refunds::{ + RefundListMetaData, RefundListRequest, RefundListResponse, RefundRequest, RefundResponse, + RefundUpdateRequest, RefundsRetrieveRequest, +}; + +impl ApiEventMetric for RefundRequest { + fn get_api_event_type(&self) -> Option { + let payment_id = self.payment_id.clone(); + self.refund_id + .clone() + .map(|refund_id| ApiEventsType::Refund { + payment_id: Some(payment_id), + refund_id, + }) + } +} + +impl ApiEventMetric for RefundResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: Some(self.payment_id.clone()), + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundsRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: None, + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundUpdateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: None, + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundListRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RefundListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RefundListMetaData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs new file mode 100644 index 000000000000..5eca01acc6fb --- /dev/null +++ b/crates/api_models/src/events/routing.rs @@ -0,0 +1,58 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::routing::{ + LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, RoutingAlgorithmId, + RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, +}; +#[cfg(feature = "business_profile_routing")] +use crate::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; + +impl ApiEventMetric for RoutingKind { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for MerchantRoutingAlgorithm { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingAlgorithmId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingDictionaryRecord { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for LinkedRoutingConfigRetrieveResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +#[cfg(feature = "business_profile_routing")] +impl ApiEventMetric for RoutingRetrieveQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingConfigRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +#[cfg(feature = "business_profile_routing")] +impl ApiEventMetric for RoutingRetrieveLinkQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index ec272514e38a..9fff344b9ff7 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -9,6 +9,7 @@ pub mod enums; pub mod ephemeral_key; #[cfg(feature = "errors")] pub mod errors; +pub mod events; pub mod files; pub mod mandates; pub mod organization; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index dcbdb56bf7b7..289f652981eb 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -12,7 +12,9 @@ use utoipa::ToSchema; #[cfg(feature = "payouts")] use crate::payouts; use crate::{ - admin, enums as api_enums, + admin, + customers::CustomerId, + enums as api_enums, payments::{self, BankCodeResponse}, }; @@ -476,6 +478,8 @@ pub struct RequestPaymentMethodTypes { #[derive(Debug, Clone, serde::Serialize, Default, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodListRequest { + #[serde(skip_deserializing)] + pub customer_id: Option, /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893ein2d")] pub client_secret: Option, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f9cb21dae5f2..c1880d58ad19 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2232,7 +2232,9 @@ pub struct PaymentListFilters { pub authentication_type: Vec, } -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] +#[derive( + Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, ToSchema, +)] pub struct TimeRange { /// The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed #[serde(with = "common_utils::custom_serde::iso8601")] diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 7b4eae4238ac..6fe8be8b5291 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use utoipa::ToSchema; +use super::payments::TimeRange; use crate::{admin, enums}; #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] @@ -75,6 +76,8 @@ pub struct RefundsRetrieveRequest { #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct RefundUpdateRequest { + #[serde(skip)] + pub refund_id: String, /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive #[schema(max_length = 255, example = "Customer returned the product")] pub reason: Option, @@ -152,16 +155,6 @@ pub struct RefundListRequest { pub refund_status: Option>, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, ToSchema)] -pub struct TimeRange { - /// The start time to filter refunds list or to get list of filters. To get list of filters start time is needed to be passed - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - /// The end time to filter refunds list or to get list of filters. If not passed the default time is now - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] pub struct RefundListResponse { /// The number of refunds included in the list diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 95d4c5e10ece..425ca364191d 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -592,3 +592,8 @@ pub enum RoutingKind { Config(RoutingDictionary), RoutingAlgorithm(Vec), } + +#[repr(transparent)] +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(transparent)] +pub struct RoutingAlgorithmId(pub String); diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 10b4fb509e88..e9f2dffcc050 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -19,7 +19,6 @@ time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates -common_utils = { version = "0.1.0", path = "../common_utils" } router_derive = { version = "0.1.0", path = "../router_derive" } [dev-dependencies] diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index c1fd91a351c7..62bd747da1b0 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -42,6 +42,7 @@ phonenumber = "0.3.3" # First party crates 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"], optional = true } +common_enums = { version = "0.1.0", path = "../common_enums" } [target.'cfg(not(target_os = "windows"))'.dependencies] signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs new file mode 100644 index 000000000000..1d487364031d --- /dev/null +++ b/crates/common_utils/src/events.rs @@ -0,0 +1,91 @@ +use common_enums::{PaymentMethod, PaymentMethodType}; +use serde::Serialize; + +pub trait ApiEventMetric { + fn get_api_event_type(&self) -> Option { + None + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(tag = "flow_type")] +pub enum ApiEventsType { + Payout, + Payment { + payment_id: String, + }, + Refund { + payment_id: Option, + refund_id: String, + }, + PaymentMethod { + payment_method_id: String, + payment_method: Option, + payment_method_type: Option, + }, + Customer { + customer_id: String, + }, + User { + //specified merchant_id will overridden on global defined + merchant_id: String, + user_id: String, + }, + PaymentMethodList { + payment_id: Option, + }, + Webhooks { + connector: String, + payment_id: Option, + }, + Routing, + ResourceListAPI, + PaymentRedirectionResponse, + // TODO: This has to be removed once the corresponding apiEventTypes are created + Miscellaneous, +} + +impl ApiEventMetric for serde_json::Value {} +impl ApiEventMetric for () {} + +impl ApiEventMetric for Result { + fn get_api_event_type(&self) -> Option { + match self { + Ok(q) => q.get_api_event_type(), + Err(_) => None, + } + } +} + +// TODO: Ideally all these types should be replaced by newtype responses +impl ApiEventMetric for Vec { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} + +#[macro_export] +macro_rules! impl_misc_api_event_type { + ($($type:ty),+) => { + $( + impl ApiEventMetric for $type { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } + } + )+ + }; +} + +impl_misc_api_event_type!( + String, + (&String, &String), + (Option, Option, String), + bool +); + +impl ApiEventMetric for &T { + fn get_api_event_type(&self) -> Option { + T::get_api_event_type(self) + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 724c3bca0a27..62428dccfb6a 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -6,6 +6,8 @@ pub mod consts; pub mod crypto; pub mod custom_serde; pub mod errors; +#[allow(missing_docs)] // Todo: add docs +pub mod events; pub mod ext_traits; pub mod fp_utils; pub mod pii; diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index 96bd6e497c33..77b9c647e43b 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -14,3 +14,9 @@ pub struct EphemeralKey { pub expires: i64, pub secret: String, } + +impl common_utils::events::ApiEventMetric for EphemeralKey { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 73ff34030f81..62aec3fb27d8 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -227,3 +227,12 @@ pub struct RefundCoreWorkflow { pub merchant_id: String, pub payment_id: String, } + +impl common_utils::events::ApiEventMetric for Refund { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Refund { + payment_id: Some(self.payment_id.clone()), + refund_id: self.refund_id.clone(), + }) + } +} diff --git a/crates/router/src/compatibility/stripe/refunds.rs b/crates/router/src/compatibility/stripe/refunds.rs index dc147443828c..ad4accf6ca74 100644 --- a/crates/router/src/compatibility/stripe/refunds.rs +++ b/crates/router/src/compatibility/stripe/refunds.rs @@ -149,8 +149,8 @@ pub async fn refund_update( path: web::Path, form_payload: web::Form, ) -> HttpResponse { - let refund_id = path.into_inner(); - let payload = form_payload.into_inner(); + let mut payload = form_payload.into_inner(); + payload.refund_id = path.into_inner(); let create_refund_update_req: refund_types::RefundUpdateRequest = payload.into(); let flow = Flow::RefundsUpdate; @@ -169,9 +169,7 @@ pub async fn refund_update( state.into_inner(), &req, create_refund_update_req, - |state, auth, req| { - refunds::refund_update_core(state, auth.merchant_account, &refund_id, req) - }, + |state, auth, req| refunds::refund_update_core(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/compatibility/stripe/refunds/types.rs b/crates/router/src/compatibility/stripe/refunds/types.rs index e1486186491a..8d65a09187d3 100644 --- a/crates/router/src/compatibility/stripe/refunds/types.rs +++ b/crates/router/src/compatibility/stripe/refunds/types.rs @@ -17,6 +17,8 @@ pub struct StripeCreateRefundRequest { #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct StripeUpdateRefundRequest { + #[serde(skip)] + pub refund_id: String, pub metadata: Option, } @@ -58,6 +60,7 @@ impl From for refunds::RefundRequest { impl From for refunds::RefundUpdateRequest { fn from(req: StripeUpdateRefundRequest) -> Self { Self { + refund_id: req.refund_id, metadata: req.metadata, reason: None, } diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index 75cb07de02ba..1ab156d32ad4 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -7,6 +7,7 @@ use serde::Serialize; use crate::{ core::{api_locking, errors}, + events::api_logs::ApiEventMetric, routes::{app::AppStateInfo, metrics}, services::{self, api, authentication as auth, logger}, }; @@ -25,12 +26,12 @@ where F: Fn(A, U, T) -> Fut, Fut: Future, E2>>, E2: ErrorSwitch + std::error::Error + Send + Sync + 'static, - Q: Serialize + std::fmt::Debug + 'a, + Q: Serialize + std::fmt::Debug + 'a + ApiEventMetric, S: TryFrom + Serialize, E: Serialize + error_stack::Context + actix_web::ResponseError + Clone, error_stack::Report: services::EmbedError, errors::ApiErrorResponse: ErrorSwitch, - T: std::fmt::Debug + Serialize, + T: std::fmt::Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, { let request_method = request.method().as_str(); diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 7bda894826a1..c1ddc43cd65d 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -294,10 +294,10 @@ pub async fn retrieve_api_key( #[instrument(skip_all)] pub async fn update_api_key( state: AppState, - merchant_id: &str, - key_id: &str, api_key: api::UpdateApiKeyRequest, ) -> RouterResponse { + let merchant_id = api_key.merchant_id.clone(); + let key_id = api_key.key_id.clone(); let store = state.store.as_ref(); let api_key = store @@ -313,7 +313,7 @@ pub async fn update_api_key( { let expiry_reminder_days = state.conf.api_keys.expiry_reminder_days.clone(); - let task_id = generate_task_id_for_api_key_expiry_workflow(key_id); + let task_id = generate_task_id_for_api_key_expiry_workflow(&key_id); // In order to determine how to update the existing process in the process_tracker table, // we need access to the current entry in the table. let existing_process_tracker_task = store @@ -339,7 +339,7 @@ pub async fn update_api_key( // If an expiry is set to 'never' else { // Process exist in process, revoke it - revoke_api_key_expiry_task(store, key_id) + revoke_api_key_expiry_task(store, &key_id) .await .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index fcda3c8daf03..a42e46ca62d5 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -476,14 +476,13 @@ pub async fn sync_refund_with_gateway( pub async fn refund_update_core( state: AppState, merchant_account: domain::MerchantAccount, - refund_id: &str, req: refunds::RefundUpdateRequest, ) -> RouterResponse { let db = state.store.as_ref(); let refund = db .find_refund_by_merchant_id_refund_id( &merchant_account.merchant_id, - refund_id, + &req.refund_id, merchant_account.storage_scheme, ) .await @@ -501,7 +500,9 @@ pub async fn refund_update_core( ) .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("Unable to update refund with refund_id: {refund_id}"))?; + .attach_printable_lazy(|| { + format!("Unable to update refund with refund_id: {}", req.refund_id) + })?; Ok(services::ApplicationResponse::Json(response.foreign_into())) } @@ -698,7 +699,7 @@ pub async fn refund_list( pub async fn refund_filter_list( state: AppState, merchant_account: domain::MerchantAccount, - req: api_models::refunds::TimeRange, + req: api_models::payments::TimeRange, ) -> RouterResponse { let db = state.store; let filter_list = db diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 8033cc792b54..723611ed5009 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1,7 +1,7 @@ pub mod helpers; pub mod transformers; -use api_models::routing as routing_types; +use api_models::routing::{self as routing_types, RoutingAlgorithmId}; #[cfg(feature = "business_profile_routing")] use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; #[cfg(not(feature = "business_profile_routing"))] @@ -319,14 +319,14 @@ pub async fn link_routing_config( pub async fn retrieve_routing_config( state: AppState, merchant_account: domain::MerchantAccount, - algorithm_id: String, + algorithm_id: RoutingAlgorithmId, ) -> RouterResponse { let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { let routing_algorithm = db .find_routing_algorithm_by_algorithm_id_merchant_id( - &algorithm_id, + &algorithm_id.0, &merchant_account.merchant_id, ) .await @@ -356,13 +356,13 @@ pub async fn retrieve_routing_config( let record = merchant_dictionary .records .into_iter() - .find(|rec| rec.id == algorithm_id) + .find(|rec| rec.id == algorithm_id.0) .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) .into_report() .attach_printable("Algorithm with the given ID not found in the merchant dictionary")?; let algorithm_config = db - .find_config_by_key(&algorithm_id) + .find_config_by_key(&algorithm_id.0) .await .change_context(errors::ApiErrorResponse::ResourceIdNotFound) .attach_printable("Routing config not found in DB")?; diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index fa700b4cd663..e643e0455b8b 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -1,5 +1,4 @@ pub mod utils; -use actix_web::web; use api_models::verifications::{self, ApplepayMerchantResponse}; use common_utils::{errors::CustomResult, ext_traits::Encode}; use error_stack::ResultExt; @@ -18,7 +17,7 @@ const APPLEPAY_INTERNAL_MERCHANT_NAME: &str = "Applepay_merchant"; pub async fn verify_merchant_creds_for_applepay( state: AppState, _req: &actix_web::HttpRequest, - body: web::Json, + body: verifications::ApplepayMerchantVerificationRequest, kms_config: &kms::KmsConfig, merchant_id: String, ) -> CustomResult< diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index a6133edad673..c9b9f8ac55f5 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -78,7 +78,7 @@ pub trait RefundInterface { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult; @@ -232,7 +232,7 @@ mod storage { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -707,7 +707,7 @@ mod storage { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -979,7 +979,7 @@ impl RefundInterface for MockDb { async fn filter_refund_by_meta_constraints( &self, _merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let refunds = self.refunds.lock().await; diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 5a66ba3e0bf9..1a47568e7ad8 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -1,10 +1,25 @@ use actix_web::HttpRequest; +pub use common_utils::events::{ApiEventMetric, ApiEventsType}; +use common_utils::impl_misc_api_event_type; use router_env::{tracing_actix_web::RequestId, types::FlowMetric}; use serde::Serialize; use time::OffsetDateTime; use super::{EventType, RawEvent}; -use crate::services::authentication::AuthenticationType; +#[cfg(feature = "dummy_connector")] +use crate::routes::dummy_connector::types::{ + DummyConnectorPaymentCompleteRequest, DummyConnectorPaymentConfirmRequest, + DummyConnectorPaymentRequest, DummyConnectorPaymentResponse, + DummyConnectorPaymentRetrieveRequest, DummyConnectorRefundRequest, + DummyConnectorRefundResponse, DummyConnectorRefundRetrieveRequest, +}; +use crate::{ + core::payments::PaymentsRedirectResponseData, + services::{authentication::AuthenticationType, ApplicationResponse, PaymentLinkFormData}, + types::api::{ + AttachEvidenceRequest, Config, ConfigUpdate, CreateFileRequest, DisputeId, FileId, + }, +}; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct ApiEvent { @@ -20,6 +35,8 @@ pub struct ApiEvent { ip_addr: Option, url_path: String, response: Option, + #[serde(flatten)] + event_type: ApiEventsType, } impl ApiEvent { @@ -32,6 +49,7 @@ impl ApiEvent { request: serde_json::Value, response: Option, auth_type: AuthenticationType, + event_type: ApiEventsType, http_req: &HttpRequest, ) -> Self { Self { @@ -52,6 +70,7 @@ impl ApiEvent { .get("user-agent") .and_then(|user_agent_value| user_agent_value.to_str().ok().map(ToOwned::to_owned)), url_path: http_req.path().to_string(), + event_type, } } } @@ -67,3 +86,35 @@ impl TryFrom for RawEvent { }) } } + +impl ApiEventMetric for ApplicationResponse { + fn get_api_event_type(&self) -> Option { + match self { + Self::Json(r) => r.get_api_event_type(), + Self::JsonWithHeaders((r, _)) => r.get_api_event_type(), + _ => None, + } + } +} +impl_misc_api_event_type!( + Config, + CreateFileRequest, + FileId, + AttachEvidenceRequest, + DisputeId, + PaymentLinkFormData, + PaymentsRedirectResponseData, + ConfigUpdate +); + +#[cfg(feature = "dummy_connector")] +impl_misc_api_event_type!( + DummyConnectorPaymentCompleteRequest, + DummyConnectorPaymentRequest, + DummyConnectorPaymentResponse, + DummyConnectorPaymentRetrieveRequest, + DummyConnectorPaymentConfirmRequest, + DummyConnectorRefundRetrieveRequest, + DummyConnectorRefundResponse, + DummyConnectorRefundRequest +); diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index a5bce200889b..dbcd8cbe4ce2 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -305,7 +305,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::RequiredFieldInfo, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, - api_models::refunds::TimeRange, + api_models::payments::TimeRange, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, api_models::mandates::MandateCardDetails, diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a93556202aab..9153e9e747f6 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -388,15 +388,15 @@ pub async fn merchant_account_toggle_kv( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::ConfigKeyUpdate; - let payload = json_payload.into_inner(); - let merchant_id = path.into_inner(); + let mut payload = json_payload.into_inner(); + payload.merchant_id = path.into_inner(); api::server_wrap( flow, state, &req, - (merchant_id, payload), - |state, _, (merchant_id, payload)| kv_for_merchant(state, merchant_id, payload.kv_enabled), + payload, + |state, _, payload| kv_for_merchant(state, payload.merchant_id, payload.kv_enabled), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, ) diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 6057b4c5db24..c2e289cd0f7e 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -124,16 +124,16 @@ pub async fn api_key_update( ) -> impl Responder { let flow = Flow::ApiKeyUpdate; let (merchant_id, key_id) = path.into_inner(); - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); + payload.key_id = key_id; + payload.merchant_id = merchant_id; api::server_wrap( flow, state, &req, - (&merchant_id, &key_id, payload), - |state, _, (merchant_id, key_id, payload)| { - api_keys::update_api_key(state, merchant_id, key_id, payload) - }, + payload, + |state, _, payload| api_keys::update_api_key(state, payload), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, ) diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs index 52a7f7f77c9a..7d2aad7e3482 100644 --- a/crates/router/src/routes/dummy_connector.rs +++ b/crates/router/src/routes/dummy_connector.rs @@ -10,7 +10,7 @@ use crate::{ mod consts; mod core; mod errors; -mod types; +pub mod types; mod utils; #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 4c4121b5d532..c20f3fbf975d 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -161,13 +161,14 @@ pub async fn refunds_update( path: web::Path, ) -> HttpResponse { let flow = Flow::RefundsUpdate; - let refund_id = path.into_inner(); + let mut refund_update_req = json_payload.into_inner(); + refund_update_req.refund_id = path.into_inner(); api::server_wrap( flow, state, &req, - json_payload.into_inner(), - |state, auth, req| refund_update_core(state, auth.merchant_account, &refund_id, req), + refund_update_req, + |state, auth, req| refund_update_core(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, ) @@ -225,7 +226,7 @@ pub async fn refunds_list( pub async fn refunds_filter_list( state: web::Data, req: HttpRequest, - payload: web::Json, + payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsList; api::server_wrap( diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 1d5ccdf502fc..9252c360a9ce 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -47,7 +47,7 @@ pub async fn routing_create_config( pub async fn routing_link_config( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, ) -> impl Responder { let flow = Flow::RoutingLinkConfig; Box::pin(oss_api::server_wrap( @@ -61,7 +61,7 @@ pub async fn routing_link_config( auth.merchant_account, #[cfg(not(feature = "business_profile_routing"))] auth.key_store, - algorithm_id, + algorithm_id.0, ) }, #[cfg(not(feature = "release"))] @@ -78,7 +78,7 @@ pub async fn routing_link_config( pub async fn routing_retrieve_config( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, ) -> impl Responder { let algorithm_id = path.into_inner(); let flow = Flow::RoutingRetrieveConfig; diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index a0861f2b14d7..2ad061848c92 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -22,7 +22,7 @@ pub async fn apple_pay_merchant_registration( flow, state, &req, - json_payload, + json_payload.into_inner(), |state, _, body| { verification::verify_merchant_creds_for_applepay( state.clone(), diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 362644906971..bb0e70b4b27b 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -34,7 +34,7 @@ use crate::{ errors::{self, CustomResult}, payments, }, - events::api_logs::ApiEvent, + events::api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, logger, routes::{ app::AppStateInfo, @@ -769,8 +769,8 @@ where F: Fn(A, U, T) -> Fut, 'b: 'a, Fut: Future, E>>, - Q: Serialize + Debug + 'a, - T: Debug + Serialize, + Q: Serialize + Debug + 'a + ApiEventMetric, + T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + error_stack::Context, OErr: ResponseError + error_stack::Context, @@ -791,6 +791,8 @@ where .attach_printable("Failed to serialize json request") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?; + let mut event_type = payload.get_api_event_type(); + // Currently auth failures are not recorded as API events let (auth_out, auth_type) = api_auth .authenticate_and_fetch(request.headers(), &request_state) @@ -838,6 +840,7 @@ where .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, ); } + event_type = res.get_api_event_type().or(event_type); metrics::request::track_response_status_code(res) } @@ -852,6 +855,7 @@ where serialized_request, serialized_response, auth_type, + event_type.unwrap_or(ApiEventsType::Miscellaneous), request, ); match api_event.clone().try_into() { @@ -884,8 +888,8 @@ pub async fn server_wrap<'a, A, T, U, Q, F, Fut, E>( where F: Fn(A, U, T) -> Fut, Fut: Future, E>>, - Q: Serialize + Debug + 'a, - T: Debug + Serialize, + Q: Serialize + Debug + ApiEventMetric + 'a, + T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, ApplicationResponse: Debug, E: ErrorSwitch + error_stack::Context, diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 2050b4149ef8..32430c0918a2 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -10,6 +10,12 @@ newtype!( derives = (Debug, Clone, Serialize) ); +impl common_utils::events::ApiEventMetric for CustomerResponse { + fn get_api_event_type(&self) -> Option { + self.0.get_api_event_type() + } +} + pub(crate) trait CustomerRequestExt: Sized { fn validate(self) -> RouterResult; } diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index bdfa8dc5b5ff..4d5667700122 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -27,7 +27,7 @@ pub trait RefundDbExt: Sized { async fn filter_by_meta_constraints( conn: &PgPooledConn, merchant_id: &str, - refund_list_details: &api_models::refunds::TimeRange, + refund_list_details: &api_models::payments::TimeRange, ) -> CustomResult; async fn get_refunds_count( @@ -114,7 +114,7 @@ impl RefundDbExt for Refund { async fn filter_by_meta_constraints( conn: &PgPooledConn, merchant_id: &str, - refund_list_details: &api_models::refunds::TimeRange, + refund_list_details: &api_models::payments::TimeRange, ) -> CustomResult { let start_time = refund_list_details.start_time; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 822b1aacee96..5af67e499275 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11218,12 +11218,12 @@ "start_time": { "type": "string", "format": "date-time", - "description": "The start time to filter refunds list or to get list of filters. To get list of filters start time is needed to be passed" + "description": "The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed" }, "end_time": { "type": "string", "format": "date-time", - "description": "The end time to filter refunds list or to get list of filters. If not passed the default time is now", + "description": "The end time to filter payments list or to get list of filters. If not passed the default time is now", "nullable": true } } From e93f76b9168c4826414aea0690fcf0adc147f589 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Thu, 9 Nov 2023 01:14:42 +0530 Subject: [PATCH 48/57] ci(rustman): Fix Rustman custom headers (#2813) --- crates/test_utils/src/newman_runner.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/crates/test_utils/src/newman_runner.rs b/crates/test_utils/src/newman_runner.rs index af7fb5592813..a6e0268e2c29 100644 --- a/crates/test_utils/src/newman_runner.rs +++ b/crates/test_utils/src/newman_runner.rs @@ -1,10 +1,4 @@ -use std::{ - env, - fs::OpenOptions, - io::{self, Write}, - path::Path, - process::Command, -}; +use std::{env, io::Write, path::Path, process::Command}; use clap::{arg, command, Parser}; use masking::PeekInterface; @@ -52,22 +46,22 @@ fn get_path(name: impl AsRef) -> String { // This function currently allows you to add only custom headers. // In future, as we scale, this can be modified based on the need -fn insert_content(dir: T, content_to_insert: U) -> io::Result<()> +fn insert_content(dir: T, content_to_insert: U) -> std::io::Result<()> where - T: AsRef + std::fmt::Debug, - U: AsRef + std::fmt::Debug, + T: AsRef, + U: AsRef, { let file_name = "event.prerequest.js"; let file_path = dir.as_ref().join(file_name); // Open the file in write mode or create it if it doesn't exist - let mut file = OpenOptions::new() + let mut file = std::fs::OpenOptions::new() .write(true) .append(true) .create(true) .open(file_path)?; - write!(file, "\n{:#?}", content_to_insert)?; + write!(file, "{}", content_to_insert.as_ref())?; Ok(()) } From aab8f6035c16ca19009f8f1e0db688c17bc0b2b6 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:57:13 +0530 Subject: [PATCH 49/57] fix(analytics): added hs latency to api event for paymentconfirm call (#2787) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> --- crates/router/src/core/payments/transformers.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 2fcd792eca83..c62529826387 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -346,7 +346,7 @@ pub fn payments_to_payments_response( connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, connector_http_status_code: Option, external_latency: Option, - is_latency_header_enabled: Option, + _is_latency_header_enabled: Option, ) -> RouterResponse where Op: Debug, @@ -451,13 +451,13 @@ where payment_confirm_source.to_string(), )) } - if Some(true) == is_latency_header_enabled { - headers.extend( - external_latency - .map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())]) - .unwrap_or_default(), - ); - } + + headers.extend( + external_latency + .map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())]) + .unwrap_or_default(), + ); + let output = Ok(match payment_request { Some(_request) => { if payments::is_start_pay(&operation) From 8b151898dc0d8eefe5ed2bbdafe59e8f58b4698c Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 9 Nov 2023 16:28:52 +0530 Subject: [PATCH 50/57] feat(router): added merchant custom name support for payment link (#2685) Co-authored-by: Sahkal Poddar Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 8 +++-- crates/diesel_models/src/payment_link.rs | 3 +- crates/diesel_models/src/schema.rs | 2 ++ crates/router/src/core/payment_link.rs | 32 +++++++++---------- .../payments/operations/payment_create.rs | 1 + .../down.sql | 2 ++ .../up.sql | 2 ++ openapi/openapi_spec.json | 5 +++ 8 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql create mode 100644 migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c1880d58ad19..196dd108333b 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3100,6 +3100,8 @@ pub struct PaymentLinkObject { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, pub merchant_custom_domain_name: Option, + /// Custom merchant name for payment link + pub custom_merchant_name: Option, } #[derive(Default, Debug, serde::Deserialize, Clone, ToSchema, serde::Serialize)] @@ -3143,11 +3145,11 @@ pub struct PaymentLinkDetails { pub pub_key: String, pub client_secret: String, pub payment_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub expiry: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub expiry: Option, pub merchant_logo: String, pub return_url: String, - pub merchant_name: crypto::OptionalEncryptableName, + pub merchant_name: String, pub order_details: Vec, pub max_items_visible_after_collapse: i8, } diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 4b182a8155a5..50cc5e89cee9 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -20,8 +20,8 @@ pub struct PaymentLink { pub last_modified_at: PrimitiveDateTime, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, + pub custom_merchant_name: Option, } - #[derive( Clone, Debug, @@ -47,4 +47,5 @@ pub struct PaymentLinkNew { pub last_modified_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, + pub custom_merchant_name: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 50531e432adc..9933bf90a59b 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -691,6 +691,8 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, fulfilment_time -> Nullable, + #[max_length = 64] + custom_merchant_name -> Nullable, } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2c51fa0c3cbb..0012efc86c9f 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,6 +1,6 @@ use api_models::admin as admin_types; -use common_utils::ext_traits::AsyncExt; use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -43,6 +43,11 @@ pub async fn intiate_payment_link_flow( .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_link_id = payment_intent + .payment_link_id + .get_required_value("payment_link_id") + .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; + helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ @@ -55,20 +60,10 @@ pub async fn intiate_payment_link_flow( "create payment link", )?; - let fulfillment_time = payment_intent - .payment_link_id - .as_ref() - .async_and_then(|pli| async move { - db.find_payment_link_by_payment_link_id(pli) - .await - .ok()? - .fulfilment_time - .ok_or(errors::ApiErrorResponse::PaymentNotFound) - .ok() - }) + let payment_link = db + .find_payment_link_by_payment_link_id(&payment_link_id) .await - .get_required_value("fulfillment_time") - .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; let payment_link_config = merchant_account .payment_link_config @@ -108,10 +103,15 @@ pub async fn intiate_payment_link_flow( amount: payment_intent.amount, currency, payment_id: payment_intent.payment_id, - merchant_name: merchant_account.merchant_name, + merchant_name: payment_link.custom_merchant_name.unwrap_or( + merchant_account + .merchant_name + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(), + ), order_details, return_url, - expiry: fulfillment_time, + expiry: payment_link.fulfilment_time, pub_key, client_secret, merchant_logo: payment_link_config diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 909f4d456530..adb3c415532d 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -813,6 +813,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + custom_merchant_name: payment_link_object.custom_merchant_name, }; let payment_link_db = db .insert_payment_link(payment_link_req) diff --git a/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql new file mode 100644 index 000000000000..84f009021df8 --- /dev/null +++ b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN custom_merchant_name; diff --git a/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql new file mode 100644 index 000000000000..c4fa756e57a0 --- /dev/null +++ b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN custom_merchant_name VARCHAR(64); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 5af67e499275..6e61f2eb614e 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7866,6 +7866,11 @@ "merchant_custom_domain_name": { "type": "string", "nullable": true + }, + "custom_merchant_name": { + "type": "string", + "description": "Custom merchant name for payment link", + "nullable": true } } }, From 5c9e235bd30dd3e03d086a83613edfcc62b2ead2 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:13:38 +0530 Subject: [PATCH 51/57] feat(router): add `gateway_status_map` CRUD APIs (#2809) --- crates/api_models/src/events.rs | 1 + crates/api_models/src/events/gsm.rs | 33 ++++++ crates/api_models/src/gsm.rs | 75 +++++++++++++ crates/api_models/src/lib.rs | 1 + crates/common_utils/src/events.rs | 1 + crates/diesel_models/src/gsm.rs | 11 +- crates/router/src/core.rs | 1 + crates/router/src/core/gsm.rs | 137 ++++++++++++++++++++++++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 3 +- crates/router/src/routes/app.rs | 16 ++- crates/router/src/routes/gsm.rs | 93 ++++++++++++++++ crates/router/src/routes/lock_utils.rs | 5 + crates/router/src/types.rs | 2 + crates/router/src/types/transformers.rs | 18 +++- crates/router_env/src/logger/types.rs | 8 ++ 16 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 crates/api_models/src/events/gsm.rs create mode 100644 crates/api_models/src/gsm.rs create mode 100644 crates/router/src/core/gsm.rs create mode 100644 crates/router/src/routes/gsm.rs diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 78f34b4b87fa..23e7c9dc706a 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,4 +1,5 @@ pub mod customer; +pub mod gsm; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; diff --git a/crates/api_models/src/events/gsm.rs b/crates/api_models/src/events/gsm.rs new file mode 100644 index 000000000000..d984ae1ff698 --- /dev/null +++ b/crates/api_models/src/events/gsm.rs @@ -0,0 +1,33 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::gsm; + +impl ApiEventMetric for gsm::GsmCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmUpdateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs new file mode 100644 index 000000000000..6bd8fd99dd93 --- /dev/null +++ b/crates/api_models/src/gsm.rs @@ -0,0 +1,75 @@ +use crate::enums; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmCreateRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: GsmDecision, + pub step_up_possible: bool, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmRetrieveRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive( + Default, + Clone, + Copy, + Debug, + strum::Display, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::EnumString, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GsmDecision { + Retry, + Requeue, + #[default] + DoDefault, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmUpdateRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: Option, + pub router_error: Option, + pub decision: Option, + pub step_up_possible: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmDeleteRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct GsmDeleteResponse { + pub gsm_rule_delete: bool, + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 9fff344b9ff7..5da916b14817 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -11,6 +11,7 @@ pub mod ephemeral_key; pub mod errors; pub mod events; pub mod files; +pub mod gsm; pub mod mandates; pub mod organization; pub mod payment_methods; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 1d487364031d..8c52f6c36d63 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -41,6 +41,7 @@ pub enum ApiEventsType { Routing, ResourceListAPI, PaymentRedirectionResponse, + Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, } diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index d5b3122c7806..2e824758aa5a 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -1,6 +1,9 @@ //! Gateway status mapping -use common_utils::custom_serde; +use common_utils::{ + custom_serde, + events::{ApiEventMetric, ApiEventsType}, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use time::PrimitiveDateTime; @@ -95,3 +98,9 @@ impl From for GatewayStatusMapperUpdateInternal { } } } + +impl ApiEventMetric for GatewayStatusMap { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index d87ff64003b4..817fafdae520 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -8,6 +8,7 @@ pub mod customers; pub mod disputes; pub mod errors; pub mod files; +pub mod gsm; pub mod mandate; pub mod metrics; pub mod payment_link; diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs new file mode 100644 index 000000000000..d25860674570 --- /dev/null +++ b/crates/router/src/core/gsm.rs @@ -0,0 +1,137 @@ +use api_models::gsm as gsm_api_types; +use diesel_models::gsm as storage; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use crate::{ + core::{ + errors, + errors::{RouterResponse, StorageErrorExt}, + }, + db::gsm::GsmInterface, + services, + types::{self, transformers::ForeignInto}, + AppState, +}; + +#[instrument(skip_all)] +pub async fn create_gsm_rule( + state: AppState, + gsm_rule: gsm_api_types::GsmCreateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + GsmInterface::add_gsm_rule(db, gsm_rule.foreign_into()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: "GSM with given key already exists in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn retrieve_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmRetrieveRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmRetrieveRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + GsmInterface::find_gsm_rule(db, connector.to_string(), flow, sub_flow, code, message) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn update_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmUpdateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmUpdateRequest { + connector, + flow, + sub_flow, + code, + message, + decision, + status, + router_error, + step_up_possible, + } = gsm_request; + GsmInterface::update_gsm_rule( + db, + connector.to_string(), + flow, + sub_flow, + code, + message, + storage::GatewayStatusMappingUpdate { + decision: decision.map(|d| d.to_string()), + status, + router_error: Some(router_error), + step_up_possible, + }, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while updating Gsm rule") + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn delete_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmDeleteRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmDeleteRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + match GsmInterface::delete_gsm_rule( + db, + connector.to_string(), + flow.to_owned(), + sub_flow.to_owned(), + code.to_owned(), + message.to_owned(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while Deleting Gsm rule") + { + Ok(is_deleted) => { + if is_deleted { + Ok(services::ApplicationResponse::Json( + gsm_api_types::GsmDeleteResponse { + gsm_rule_delete: true, + connector, + flow, + sub_flow, + code, + }, + )) + } else { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed while Deleting Gsm rule, got response as false") + } + } + Err(err) => Err(err), + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 21ebfc06137b..38efe8b75134 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -142,6 +142,7 @@ pub fn mk_app( .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::Gsm::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 38f95c4cdda8..47b9f23cf8cb 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -10,6 +10,7 @@ pub mod disputes; pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +pub mod gsm; pub mod health; pub mod lock_utils; pub mod mandates; @@ -36,7 +37,7 @@ pub use self::app::Routing; pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, + Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Refunds, Webhooks, }; #[cfg(feature = "stripe")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 268b2ed703bf..ec87fcdc3900 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,7 +19,7 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -666,6 +666,20 @@ impl BusinessProfile { } } +pub struct Gsm; + +#[cfg(feature = "olap")] +impl Gsm { + pub fn server(state: AppState) -> Scope { + web::scope("/gsm") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(create_gsm_rule))) + .service(web::resource("/get").route(web::post().to(get_gsm_rule))) + .service(web::resource("/update").route(web::post().to(update_gsm_rule))) + .service(web::resource("/delete").route(web::post().to(delete_gsm_rule))) + } +} + #[cfg(all(feature = "olap", feature = "kms"))] pub struct Verify; diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs new file mode 100644 index 000000000000..02d943792dba --- /dev/null +++ b/crates/router/src/routes/gsm.rs @@ -0,0 +1,93 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models::gsm as gsm_api_types; +use router_env::{instrument, tracing, Flow}; + +use super::app::AppState; +use crate::{ + core::{api_locking, gsm}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleCreate))] +pub async fn create_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleCreate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::create_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleRetrieve))] +pub async fn get_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let gsm_retrieve_req = json_payload.into_inner(); + let flow = Flow::GsmRuleRetrieve; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + gsm_retrieve_req, + |state, _, gsm_retrieve_req| gsm::retrieve_gsm_rule(state, gsm_retrieve_req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleUpdate))] +pub async fn update_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleUpdate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::update_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleDelete))] +pub async fn delete_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleDelete; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, _, payload| gsm::delete_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 14614268d79d..4e6fc1870f56 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Gsm, } impl From for ApiIdentifier { @@ -129,6 +130,10 @@ impl From for ApiIdentifier { Flow::Verification => Self::Verification, Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, + Flow::GsmRuleCreate + | Flow::GsmRuleRetrieve + | Flow::GsmRuleUpdate + | Flow::GsmRuleDelete => Self::Gsm, } } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8f08ce062560..f2e86a4bf335 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1193,3 +1193,5 @@ impl } } } + +pub type GsmResponse = storage::GatewayStatusMap; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 83ca0d014dc8..1cd016de18e6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1,6 +1,6 @@ // use actix_web::HttpMessage; use actix_web::http::header::HeaderMap; -use api_models::{enums as api_enums, payments, routing::ConnectorSelection}; +use api_models::{enums as api_enums, gsm as gsm_api_types, payments, routing::ConnectorSelection}; use common_utils::{ consts::X_HS_LATENCY, crypto::Encryptable, @@ -1031,3 +1031,19 @@ impl ForeignFrom } } } + +impl ForeignFrom for storage::GatewayStatusMappingNew { + fn foreign_from(value: gsm_api_types::GsmCreateRequest) -> Self { + Self { + connector: value.connector.to_string(), + flow: value.flow, + sub_flow: value.sub_flow, + code: value.code, + message: value.message, + decision: value.decision.to_string(), + status: value.status, + router_error: value.router_error, + step_up_possible: value.step_up_possible, + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9822432115b0..0c9751aee440 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -235,6 +235,14 @@ pub enum Flow { BusinessProfileList, /// Different verification flows Verification, + /// Gsm Rule Creation flow + GsmRuleCreate, + /// Gsm Rule Retrieve flow + GsmRuleRetrieve, + /// Gsm Rule Update flow + GsmRuleUpdate, + /// Gsm Rule Delete flow + GsmRuleDelete, } /// From 9239cf50adafc441f6a0973e88494ce220849386 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:32:00 +0000 Subject: [PATCH 52/57] chore(version): v1.75.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa00381bf62..412b42afc2eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.75.0 (2023-11-09) + +### Features + +- **events:** Add extracted fields based on req/res types ([#2795](https://github.com/juspay/hyperswitch/pull/2795)) ([`8985794`](https://github.com/juspay/hyperswitch/commit/89857941b09c5fbe0f3e7d5b4f908bb144ae162d)) +- **router:** + - Added merchant custom name support for payment link ([#2685](https://github.com/juspay/hyperswitch/pull/2685)) ([`8b15189`](https://github.com/juspay/hyperswitch/commit/8b151898dc0d8eefe5ed2bbdafe59e8f58b4698c)) + - Add `gateway_status_map` CRUD APIs ([#2809](https://github.com/juspay/hyperswitch/pull/2809)) ([`5c9e235`](https://github.com/juspay/hyperswitch/commit/5c9e235bd30dd3e03d086a83613edfcc62b2ead2)) + +### Bug Fixes + +- **analytics:** Added hs latency to api event for paymentconfirm call ([#2787](https://github.com/juspay/hyperswitch/pull/2787)) ([`aab8f60`](https://github.com/juspay/hyperswitch/commit/aab8f6035c16ca19009f8f1e0db688c17bc0b2b6)) +- [mollie] locale validation irrespective of auth type ([#2814](https://github.com/juspay/hyperswitch/pull/2814)) ([`25a73c2`](https://github.com/juspay/hyperswitch/commit/25a73c29a4c4715a54862dd6a28c875fd3752f63)) + +**Full Changelog:** [`v1.74.0...v1.75.0`](https://github.com/juspay/hyperswitch/compare/v1.74.0...v1.75.0) + +- - - + + ## 1.74.0 (2023-11-08) ### Features From e67e808d70d41c371fff168824e5a4dbb8b3a040 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Thu, 9 Nov 2023 21:27:08 +0530 Subject: [PATCH 53/57] docs(README): add bootstrap button for cloudformation deployment (#2827) Co-authored-by: venkatesh.devendran --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6e9baa07f7d..129a0512d4a0 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,16 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. - +   Click here if you have not bootstrapped your region before deploying + +   + 2. Sign-in to your AWS console. 3. Follow the instructions provided on the console to successfully deploy Hyperswitch -For an early access to the production-ready setup fill this Early Access Form +For an early access to the production-ready setup fill this Early Access Form

🔌 Fast Integration for Stripe Users

From 966369b6f2c205b59524c23ad3b21ebab547631f Mon Sep 17 00:00:00 2001 From: Abhishek Marrivagu <68317979+Abhicodes-crypto@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:39:12 +0530 Subject: [PATCH 54/57] refactor(core): remove connector response table and use payment_attempt instead (#2644) --- .../src/payments/payment_attempt.rs | 2 + .../diesel_models/src/connector_response.rs | 122 ------- crates/diesel_models/src/kv.rs | 9 - crates/diesel_models/src/lib.rs | 10 +- crates/diesel_models/src/payment_attempt.rs | 6 + crates/diesel_models/src/query.rs | 2 +- crates/diesel_models/src/schema.rs | 26 -- crates/drainer/src/lib.rs | 13 - crates/router/src/core/payments.rs | 6 +- crates/router/src/core/payments/helpers.rs | 54 --- .../payments/operations/payment_approve.rs | 11 - .../payments/operations/payment_cancel.rs | 11 +- .../payments/operations/payment_capture.rs | 44 +-- .../operations/payment_complete_authorize.rs | 11 - .../payments/operations/payment_confirm.rs | 107 ++---- .../payments/operations/payment_create.rs | 31 +- .../operations/payment_method_validate.rs | 18 +- .../payments/operations/payment_reject.rs | 11 +- .../payments/operations/payment_response.rs | 100 ++--- .../payments/operations/payment_session.rs | 15 - .../core/payments/operations/payment_start.rs | 15 - .../payments/operations/payment_status.rs | 18 +- .../payments/operations/payment_update.rs | 15 - .../router/src/core/payments/transformers.rs | 23 +- crates/router/src/db.rs | 2 - crates/router/src/db/connector_response.rs | 343 ------------------ crates/router/src/types/storage.rs | 11 +- .../src/types/storage/connector_response.rs | 41 --- crates/router/src/types/storage/kv.rs | 4 +- crates/storage_impl/src/connector_response.rs | 5 - crates/storage_impl/src/lib.rs | 1 - crates/storage_impl/src/mock_db.rs | 2 - .../src/payments/payment_attempt.rs | 9 +- .../down.sql | 34 ++ .../up.sql | 2 + 35 files changed, 155 insertions(+), 979 deletions(-) delete mode 100644 crates/diesel_models/src/connector_response.rs delete mode 100644 crates/router/src/db/connector_response.rs delete mode 100644 crates/router/src/types/storage/connector_response.rs delete mode 100644 crates/storage_impl/src/connector_response.rs create mode 100644 migrations/2023-11-08-144951_drop_connector_response_table/down.sql create mode 100644 migrations/2023-11-08-144951_drop_connector_response_table/up.sql diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 734de8fe4a55..cdd41ea9db2d 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -286,6 +286,8 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, amount_capturable: Option, updated_by: String, + authentication_data: Option, + encoded_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, diff --git a/crates/diesel_models/src/connector_response.rs b/crates/diesel_models/src/connector_response.rs deleted file mode 100644 index 863ce28ee0ae..000000000000 --- a/crates/diesel_models/src/connector_response.rs +++ /dev/null @@ -1,122 +0,0 @@ -use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; -use serde::{Deserialize, Serialize}; -use time::PrimitiveDateTime; - -use crate::schema::connector_response; - -#[derive(Clone, Debug, Deserialize, Serialize, Insertable, router_derive::DebugAsDisplay)] -#[diesel(table_name = connector_response)] -#[serde(deny_unknown_fields)] -pub struct ConnectorResponseNew { - pub payment_id: String, - pub merchant_id: String, - pub attempt_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub modified_at: PrimitiveDateTime, - pub connector_name: Option, - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub encoded_data: Option, - pub updated_by: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Identifiable, Queryable)] -#[diesel(table_name = connector_response)] -pub struct ConnectorResponse { - pub id: i32, - pub payment_id: String, - pub merchant_id: String, - pub attempt_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub modified_at: PrimitiveDateTime, - pub connector_name: Option, - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub encoded_data: Option, - pub updated_by: String, -} - -#[derive(Clone, Default, Debug, Deserialize, AsChangeset, Serialize)] -#[diesel(table_name = connector_response)] -pub struct ConnectorResponseUpdateInternal { - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub modified_at: Option, - pub encoded_data: Option, - pub connector_name: Option, - pub updated_by: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ConnectorResponseUpdate { - ResponseUpdate { - connector_transaction_id: Option, - authentication_data: Option, - encoded_data: Option, - connector_name: Option, - updated_by: String, - }, - ErrorUpdate { - connector_name: Option, - updated_by: String, - }, -} - -impl ConnectorResponseUpdate { - pub fn apply_changeset(self, source: ConnectorResponse) -> ConnectorResponse { - let connector_response_update: ConnectorResponseUpdateInternal = self.into(); - ConnectorResponse { - modified_at: connector_response_update - .modified_at - .unwrap_or_else(common_utils::date_time::now), - connector_name: connector_response_update - .connector_name - .or(source.connector_name), - connector_transaction_id: source - .connector_transaction_id - .or(connector_response_update.connector_transaction_id), - authentication_data: connector_response_update - .authentication_data - .or(source.authentication_data), - encoded_data: connector_response_update - .encoded_data - .or(source.encoded_data), - updated_by: connector_response_update.updated_by, - ..source - } - } -} - -impl From for ConnectorResponseUpdateInternal { - fn from(connector_response_update: ConnectorResponseUpdate) -> Self { - match connector_response_update { - ConnectorResponseUpdate::ResponseUpdate { - connector_transaction_id, - authentication_data, - encoded_data, - connector_name, - updated_by, - } => Self { - connector_transaction_id, - authentication_data, - encoded_data, - modified_at: Some(common_utils::date_time::now()), - connector_name, - updated_by, - }, - ConnectorResponseUpdate::ErrorUpdate { - connector_name, - updated_by, - } => Self { - connector_name, - modified_at: Some(common_utils::date_time::now()), - updated_by, - ..Self::default() - }, - } - } -} diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index 81fa7a88ee3b..f56ef8304186 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; use crate::{ address::{Address, AddressNew, AddressUpdateInternal}, - connector_response::{ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate}, errors, payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, @@ -51,7 +50,6 @@ pub enum Insertable { PaymentIntent(PaymentIntentNew), PaymentAttempt(PaymentAttemptNew), Refund(RefundNew), - ConnectorResponse(ConnectorResponseNew), Address(Box), ReverseLookUp(ReverseLookupNew), } @@ -62,16 +60,9 @@ pub enum Updateable { PaymentIntentUpdate(PaymentIntentUpdateMems), PaymentAttemptUpdate(PaymentAttemptUpdateMems), RefundUpdate(RefundUpdateMems), - ConnectorResponseUpdate(ConnectorResponseUpdateMems), AddressUpdate(Box), } -#[derive(Debug, Serialize, Deserialize)] -pub struct ConnectorResponseUpdateMems { - pub orig: ConnectorResponse, - pub update_data: ConnectorResponseUpdate, -} - #[derive(Debug, Serialize, Deserialize)] pub struct AddressUpdateMems { pub orig: Address, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 08d74fb8fd37..46a6965b3a7b 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -4,7 +4,7 @@ pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; + pub mod customers; pub mod dispute; pub mod encryption; @@ -44,10 +44,10 @@ use diesel_impl::{DieselArray, OptionalDieselArray}; pub type StorageResult = error_stack::Result; pub type PgPooledConn = async_bb8_diesel::Connection; pub use self::{ - address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*, - dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, payment_attempt::*, payment_intent::*, - payment_method::*, process_tracker::*, refund::*, reverse_lookup::*, + address::*, api_keys::*, cards_info::*, configs::*, customers::*, dispute::*, ephemeral_key::*, + events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, + merchant_connector_account::*, payment_attempt::*, payment_intent::*, payment_method::*, + process_tracker::*, refund::*, reverse_lookup::*, }; /// The types and implementations provided by this module are required for the schema generated by diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 058086106111..ce388fea10eb 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -203,6 +203,8 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, amount_capturable: Option, updated_by: String, + authentication_data: Option, + encoded_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -478,6 +480,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => Self { status: Some(status), connector, @@ -494,6 +498,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ac3eeba44359..f315327702ad 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -4,7 +4,7 @@ pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; + pub mod customers; pub mod dispute; pub mod events; diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9933bf90a59b..6c9cea035b3f 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -157,31 +157,6 @@ diesel::table! { } } -diesel::table! { - use diesel::sql_types::*; - use crate::enums::diesel_exports::*; - - connector_response (id) { - id -> Int4, - #[max_length = 64] - payment_id -> Varchar, - #[max_length = 64] - merchant_id -> Varchar, - #[max_length = 64] - attempt_id -> Varchar, - created_at -> Timestamp, - modified_at -> Timestamp, - #[max_length = 64] - connector_name -> Nullable, - #[max_length = 128] - connector_transaction_id -> Nullable, - authentication_data -> Nullable, - encoded_data -> Nullable, - #[max_length = 32] - updated_by -> Varchar, - } -} - diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -932,7 +907,6 @@ diesel::allow_tables_to_appear_in_same_query!( captures, cards_info, configs, - connector_response, customers, dispute, events, diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 19abe9ba3aad..7ccfd600d662 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -206,7 +206,6 @@ async fn drainer( let payment_attempt = "payment_attempt"; let refund = "refund"; let reverse_lookup = "reverse_lookup"; - let connector_response = "connector_response"; let address = "address"; match db_op { // TODO: Handle errors @@ -230,13 +229,6 @@ async fn drainer( kv::Insertable::Refund(a) => { macro_util::handle_resp!(a.insert(&conn).await, insert_op, refund) } - kv::Insertable::ConnectorResponse(a) => { - macro_util::handle_resp!( - a.insert(&conn).await, - insert_op, - connector_response - ) - } kv::Insertable::Address(addr) => { macro_util::handle_resp!(addr.insert(&conn).await, insert_op, address) } @@ -283,11 +275,6 @@ async fn drainer( refund ) } - kv::Updateable::ConnectorResponseUpdate(a) => macro_util::handle_resp!( - a.orig.update(&conn, a.update_data).await, - update_op, - connector_response - ), kv::Updateable::AddressUpdate(a) => macro_util::handle_resp!( a.orig.update(&conn, a.update_data).await, update_op, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 98ab158e7935..a114b20380bf 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1578,7 +1578,6 @@ where pub payment_intent: storage::PaymentIntent, pub payment_attempt: storage::PaymentAttempt, pub multiple_capture_data: Option, - pub connector_response: storage::ConnectorResponse, pub amount: api::Amount, pub mandate_id: Option, pub mandate_connector: Option, @@ -1671,10 +1670,7 @@ pub fn should_call_connector( !matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::Failed | storage_enums::IntentStatus::Succeeded - ) && payment_data - .connector_response - .authentication_data - .is_none() + ) && payment_data.payment_attempt.authentication_data.is_none() } "PaymentStatus" => { matches!( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fd9fd7361da3..4ee2fd4b94d3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2998,60 +2998,6 @@ impl AttemptType { } } } - - #[instrument(skip_all)] - pub async fn get_or_insert_connector_response( - &self, - payment_attempt: &PaymentAttempt, - db: &dyn StorageInterface, - storage_scheme: storage::enums::MerchantStorageScheme, - ) -> RouterResult { - match self { - Self::New => db - .insert_connector_response( - payments::PaymentCreate::make_connector_response(payment_attempt), - storage_scheme, - ) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { - payment_id: payment_attempt.payment_id.clone(), - }), - Self::SameOld => db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound), - } - } - - #[instrument(skip_all)] - pub async fn get_connector_response( - &self, - db: &dyn StorageInterface, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> RouterResult { - match self { - Self::New => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("Precondition failed, the attempt type should not be `New`"), - Self::SameOld => db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - payment_id, - merchant_id, - attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound), - } - } } #[inline(always)] diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 4cd1bae04dee..d5d0d2d01765 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -161,16 +161,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let redirect_response = request .feature_metadata .as_ref() @@ -224,7 +214,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 43fdc440e64d..f734afef7826 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -106,15 +106,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let currency = payment_attempt.currency.get_required_value("currency")?; let amount = payment_attempt.amount.into(); @@ -161,7 +152,7 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, + sessions_token: vec![], card_cvc: None, creds_identifier, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 1cfcbce5532f..6e794b1ba618 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -3,7 +3,6 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; -use diesel_models::connector_response::ConnectorResponse; use error_stack::ResultExt; use router_env::{instrument, tracing}; @@ -20,7 +19,7 @@ use crate::{ types::{ api::{self, PaymentIdTypeExt}, domain, - storage::{self, enums, payment_attempt::PaymentAttemptExt, ConnectorResponseExt}, + storage::{self, enums, payment_attempt::PaymentAttemptExt}, }, utils::OptionExt, }; @@ -89,9 +88,7 @@ impl helpers::validate_capture_method(capture_method)?; - let (multiple_capture_data, connector_response) = if capture_method - == enums::CaptureMethod::ManualMultiple - { + let multiple_capture_data = if capture_method == enums::CaptureMethod::ManualMultiple { let amount_to_capture = request .amount_to_capture .get_required_value("amount_to_capture")?; @@ -121,37 +118,13 @@ impl .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { payment_id: payment_id.clone(), })?; - let new_connector_response = db - .insert_connector_response( - ConnectorResponse::make_new_connector_response( - capture.payment_id.clone(), - capture.merchant_id.clone(), - capture.capture_id.clone(), - Some(capture.connector.clone()), - storage_scheme.to_string(), - ), - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { payment_id })?; - ( - Some(MultipleCaptureData::new_for_create( - previous_captures, - capture, - )), - new_connector_response, - ) + + Some(MultipleCaptureData::new_for_create( + previous_captures, + capture, + )) } else { - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - (None, connector_response) + None }; currency = payment_attempt.currency.get_required_value("currency")?; @@ -223,7 +196,6 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, sessions_token: vec![], card_cvc: None, creds_identifier, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 4e87b3869431..038d34ea290f 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -170,16 +170,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let redirect_response = request .feature_metadata .as_ref() @@ -219,7 +209,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 5de281a5e63c..21f7db3d0b41 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -157,77 +157,49 @@ impl }) .map(|x| x.transpose()); - let (mut payment_attempt, shipping_address, billing_address, connector_response) = - match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let attempt_type = helpers::AttemptType::SameOld; - - let attempt_id = payment_intent.active_attempt.get_id(); - let connector_response_fut = attempt_type.get_connector_response( + let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + let (payment_attempt, shipping_address, billing_address, _) = futures::try_join!( + payment_attempt_fut, + shipping_address_fut, + billing_address_fut, + config_update_fut + )?; + + (payment_attempt, shipping_address, billing_address) + } + _ => { + let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( + payment_attempt_fut, + shipping_address_fut, + billing_address_fut, + config_update_fut + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + // 3 + request, + payment_intent, + payment_attempt, db, - &payment_intent.payment_id, - &payment_intent.merchant_id, - attempt_id.as_str(), storage_scheme, - ); - - let (payment_attempt, shipping_address, billing_address, connector_response, _) = - futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - connector_response_fut, - config_update_fut - )?; - - ( - payment_attempt, - shipping_address, - billing_address, - connector_response, ) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - // 3 - request, - payment_intent, - payment_attempt, - db, - storage_scheme, - ) - .await?; - - let connector_response = attempt_type - .get_or_insert_connector_response(&payment_attempt, db, storage_scheme) - .await?; + .await?; - ( - payment_attempt, - shipping_address, - billing_address, - connector_response, - ) - } - }; + (payment_attempt, shipping_address, billing_address) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -354,7 +326,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index adb3c415532d..97bb84371306 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -62,7 +62,7 @@ impl let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (payment_intent, payment_attempt, connector_response); + let (payment_intent, payment_attempt); let money @ (amount, currency) = payments_create_request_validation(request)?; @@ -196,16 +196,6 @@ impl payment_id: payment_id.clone(), })?; - connector_response = db - .insert_connector_response( - Self::make_connector_response(&payment_attempt), - storage_scheme, - ) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { - payment_id: payment_id.clone(), - })?; - let mandate_id = request .mandate_id .as_ref() @@ -300,7 +290,6 @@ impl disputes: vec![], attempts: None, force_sync: None, - connector_response, sessions_token: vec![], card_cvc: request.card_cvc.clone(), creds_identifier, @@ -727,24 +716,6 @@ impl PaymentCreate { }) } - #[instrument(skip_all)] - pub fn make_connector_response( - payment_attempt: &PaymentAttempt, - ) -> storage::ConnectorResponseNew { - storage::ConnectorResponseNew { - payment_id: payment_attempt.payment_id.clone(), - merchant_id: payment_attempt.merchant_id.clone(), - attempt_id: payment_attempt.attempt_id.clone(), - created_at: payment_attempt.created_at, - modified_at: payment_attempt.modified_at, - connector_name: payment_attempt.connector.clone(), - connector_transaction_id: None, - authentication_data: None, - encoded_data: None, - updated_by: payment_attempt.updated_by.clone(), - } - } - #[instrument(skip_all)] pub async fn get_ephemeral_key( request: &api::PaymentsRequest, diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 33f6c23c8363..7e4fe0951b03 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -7,7 +7,7 @@ use error_stack::ResultExt; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; -use super::{BoxedOperation, Domain, GetTracker, PaymentCreate, UpdateTracker, ValidateRequest}; +use super::{BoxedOperation, Domain, GetTracker, UpdateTracker, ValidateRequest}; use crate::{ consts, core::{ @@ -89,7 +89,7 @@ impl let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (payment_intent, payment_attempt, connector_response); + let (payment_intent, payment_attempt); let payment_id = payment_id .get_payment_intent_id() @@ -135,19 +135,6 @@ impl } }?; - connector_response = match db - .insert_connector_response( - PaymentCreate::make_connector_response(&payment_attempt), - storage_scheme, - ) - .await - { - Ok(connector_resp) => Ok(connector_resp), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - let creds_identifier = request .merchant_connector_details .as_ref() @@ -180,7 +167,6 @@ impl mandate_connector: None, setup_mandate: request.mandate_data.clone().map(Into::into), token: request.payment_token.clone(), - connector_response, payment_method_data: request.payment_method_data.clone(), confirm: Some(true), address: types::PaymentAddress::default(), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 415ab3eccfe7..a6c2561aaeed 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -104,15 +104,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let currency = payment_attempt.currency.get_required_value("currency")?; let amount = payment_attempt.amount.into(); @@ -147,7 +138,7 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, + sessions_token: vec![], card_cvc: None, creds_identifier: None, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 60d3bc165a97..77c344949660 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -296,10 +296,7 @@ async fn payment_response_update_tracker( router_data: types::RouterData, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { - let (capture_update, mut payment_attempt_update, connector_response_update) = match router_data - .response - .clone() - { + let (capture_update, mut payment_attempt_update) = match router_data.response.clone() { Err(err) => { let (capture_update, attempt_update) = match payment_data.multiple_capture_data { Some(multiple_capture_data) => { @@ -356,14 +353,7 @@ async fn payment_response_update_tracker( ) } }; - ( - capture_update, - attempt_update, - Some(storage::ConnectorResponseUpdate::ErrorUpdate { - connector_name: Some(router_data.connector.clone()), - updated_by: storage_scheme.to_string(), - }), - ) + (capture_update, attempt_update) } Ok(payments_response) => match payments_response { types::PaymentsResponseData::PreProcessingResponse { @@ -394,7 +384,7 @@ async fn payment_response_update_tracker( updated_by: storage_scheme.to_string(), }; - (None, Some(payment_attempt_update), None) + (None, Some(payment_attempt_update)) } types::PaymentsResponseData::TransactionResponse { resource_id, @@ -409,8 +399,7 @@ async fn payment_response_update_tracker( | types::ResponseId::EncodedData(id) => Some(id), }; - let encoded_data = payment_data.connector_response.encoded_data.clone(); - let connector_name = router_data.connector.clone(); + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); let authentication_data = redirection_data .map(|data| utils::Encode::::encode_to_value(&data)) @@ -478,23 +467,13 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + authentication_data, + encoded_data, }), ), }; - let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate { - connector_transaction_id, - authentication_data, - encoded_data, - connector_name: Some(connector_name), - updated_by: storage_scheme.to_string(), - }; - - ( - capture_updates, - payment_attempt_update, - Some(connector_response_update), - ) + (capture_updates, payment_attempt_update) } types::PaymentsResponseData::TransactionUnresolvedResponse { resource_id, @@ -519,14 +498,13 @@ async fn payment_response_update_tracker( connector_response_reference_id, updated_by: storage_scheme.to_string(), }), - None, ) } - types::PaymentsResponseData::SessionResponse { .. } => (None, None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None, None), + types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -535,13 +513,9 @@ async fn payment_response_update_tracker( &multiple_capture_data, capture_sync_response_list, )?; - ( - Some((multiple_capture_data, capture_update_list)), - None, - None, - ) + (Some((multiple_capture_data, capture_update_list)), None) } - None => (None, None, None), + None => (None, None), }, }, }; @@ -571,40 +545,18 @@ async fn payment_response_update_tracker( // Stage 1 let payment_attempt = payment_data.payment_attempt.clone(); - let connector_response = payment_data.connector_response.clone(); - - let payment_attempt_fut = Box::pin(async move { - Ok::<_, error_stack::Report>(match payment_attempt_update { - Some(payment_attempt_update) => db - .update_payment_attempt_with_attempt_id( - payment_attempt, - payment_attempt_update, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_attempt, - }) - }); - - let connector_response_fut = Box::pin(async move { - Ok::<_, error_stack::Report>(match connector_response_update { - Some(connector_response_update) => db - .update_connector_response( - connector_response, - connector_response_update, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => connector_response, - }) - }); - - let (payment_attempt, connector_response) = - futures::try_join!(payment_attempt_fut, connector_response_fut)?; - payment_data.payment_attempt = payment_attempt; - payment_data.connector_response = connector_response; + + payment_data.payment_attempt = match payment_attempt_update { + Some(payment_attempt_update) => db + .update_payment_attempt_with_attempt_id( + payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => payment_attempt, + }; let amount_captured = get_total_amount_captured( router_data.request, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 354c62648bb3..52677ab3cc8d 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -126,20 +126,6 @@ impl payment_intent.shipping_address_id = shipping_address.clone().map(|x| x.address_id); payment_intent.billing_address_id = billing_address.clone().map(|x| x.address_id); - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - let customer_details = payments::CustomerDetails { customer_id: payment_intent.customer_id.clone(), name: None, @@ -190,7 +176,6 @@ impl disputes: vec![], attempts: None, sessions_token: vec![], - connector_response, card_cvc: None, creds_identifier, pm_token: None, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index e9fa301bf07b..5578f6b3dc15 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -126,20 +126,6 @@ impl ..CustomerDetails::default() }; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - Ok(( Box::new(self), PaymentData { @@ -150,7 +136,6 @@ impl email: None, mandate_id: None, mandate_connector: None, - connector_response, setup_mandate: None, token: payment_attempt.payment_token.clone(), address: PaymentAddress { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 96aac6c9d79b..83e7131b2675 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -226,7 +226,7 @@ async fn get_tracker_for_sync< PaymentData, Option, )> { - let (payment_intent, payment_attempt, currency, amount); + let (payment_intent, mut payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -250,18 +250,7 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - let mut connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentNotFound) - .attach_printable("Database error when finding connector response")?; - - connector_response.encoded_data = request.param.clone(); + payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -349,7 +338,7 @@ async fn get_tracker_for_sync< format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {payment_id_str}", &merchant_account.merchant_id) }); - let contains_encoded_data = connector_response.encoded_data.is_some(); + let contains_encoded_data = payment_attempt.encoded_data.is_some(); let creds_identifier = request .merchant_connector_details @@ -373,7 +362,6 @@ async fn get_tracker_for_sync< PaymentData { flow: PhantomData, payment_intent, - connector_response, currency, amount, email: None, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 622d09754396..0a49c830b732 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -219,20 +219,6 @@ impl )?; } - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - let mandate_id = request .mandate_id .as_ref() @@ -341,7 +327,6 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, sessions_token: vec![], card_cvc: request.card_cvc.clone(), creds_identifier, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index c62529826387..6c6b4ae9339f 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -460,14 +460,8 @@ where let output = Ok(match payment_request { Some(_request) => { - if payments::is_start_pay(&operation) - && payment_data - .connector_response - .authentication_data - .is_some() - { - let redirection_data = payment_data - .connector_response + if payments::is_start_pay(&operation) && payment_attempt.authentication_data.is_some() { + let redirection_data = payment_attempt .authentication_data .get_required_value("redirection_data")?; @@ -523,16 +517,15 @@ where display_to_timestamp: wait_screen_data.display_to_timestamp, } })) - .or(payment_data - .connector_response - .authentication_data - .map(|_| api_models::payments::NextActionData::RedirectToUrl { + .or(payment_attempt.authentication_data.as_ref().map(|_| { + api_models::payments::NextActionData::RedirectToUrl { redirect_to_url: helpers::create_startpay_url( server, &payment_attempt, &payment_intent, ), - })); + } + })); }; // next action check for third party sdk session (for ex: Apple pay through trustpay has third party sdk session response) @@ -1056,7 +1049,7 @@ impl TryFrom> for types::PaymentsSyncData } None => types::ResponseId::NoResponseId, }, - encoded_data: payment_data.connector_response.encoded_data, + encoded_data: payment_data.payment_attempt.encoded_data, capture_method: payment_data.payment_attempt.capture_method, connector_meta: payment_data.payment_attempt.connector_metadata, sync_type: match payment_data.multiple_capture_data { @@ -1356,7 +1349,7 @@ impl TryFrom> for types::CompleteAuthoriz browser_info, email: payment_data.email, payment_method_data: payment_data.payment_method_data, - connector_transaction_id: payment_data.connector_response.connector_transaction_id, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, redirect_response, connector_meta: payment_data.payment_attempt.connector_metadata, }) diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 3efef2c40f29..6fe34d8dd69b 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -5,7 +5,6 @@ pub mod cache; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; pub mod customers; pub mod dispute; pub mod ephemeral_key; @@ -52,7 +51,6 @@ pub trait StorageInterface: + api_keys::ApiKeyInterface + configs::ConfigInterface + capture::CaptureInterface - + connector_response::ConnectorResponseInterface + customers::CustomerInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface diff --git a/crates/router/src/db/connector_response.rs b/crates/router/src/db/connector_response.rs deleted file mode 100644 index 354231d136ec..000000000000 --- a/crates/router/src/db/connector_response.rs +++ /dev/null @@ -1,343 +0,0 @@ -use error_stack::{IntoReport, ResultExt}; -use router_env::{instrument, tracing}; - -use super::{MockDb, Store}; -use crate::{ - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums}, -}; - -#[async_trait::async_trait] -pub trait ConnectorResponseInterface { - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; - - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - payment_attempt: storage_type::ConnectorResponseUpdate, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; -} - -#[cfg(not(feature = "kv_store"))] -mod storage { - use error_stack::IntoReport; - use router_env::{instrument, tracing}; - - use super::Store; - use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums}, - }; - - #[async_trait::async_trait] - impl super::ConnectorResponseInterface for Store { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - connector_response - .insert(&conn) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage_type::ConnectorResponse::find_by_payment_id_merchant_id_attempt_id( - &conn, - payment_id, - merchant_id, - attempt_id, - ) - .await - .map_err(Into::into) - .into_report() - } - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - this.update(&conn, connector_response_update) - .await - .map_err(Into::into) - .into_report() - } - } -} - -#[cfg(feature = "kv_store")] -mod storage { - - use diesel_models::enums as storage_enums; - use error_stack::{IntoReport, ResultExt}; - use redis_interface::HsetnxReply; - use router_env::{instrument, tracing}; - use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; - - use super::Store; - use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums, kv}, - utils::db_utils, - }; - - #[async_trait::async_trait] - impl super::ConnectorResponseInterface for Store { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => connector_response - .insert(&conn) - .await - .map_err(Into::into) - .into_report(), - storage_enums::MerchantStorageScheme::RedisKv => { - let merchant_id = &connector_response.merchant_id; - let payment_id = &connector_response.payment_id; - let attempt_id = &connector_response.attempt_id; - - let key = format!("mid_{merchant_id}_pid_{payment_id}"); - let field = format!("connector_resp_{merchant_id}_{payment_id}_{attempt_id}"); - - let created_connector_resp = storage_type::ConnectorResponse { - id: Default::default(), - payment_id: connector_response.payment_id.clone(), - merchant_id: connector_response.merchant_id.clone(), - attempt_id: connector_response.attempt_id.clone(), - created_at: connector_response.created_at, - modified_at: connector_response.modified_at, - connector_name: connector_response.connector_name.clone(), - connector_transaction_id: connector_response - .connector_transaction_id - .clone(), - authentication_data: connector_response.authentication_data.clone(), - encoded_data: connector_response.encoded_data.clone(), - updated_by: storage_scheme.to_string(), - }; - - let redis_entry = kv::TypedSql { - op: kv::DBOperation::Insert { - insertable: kv::Insertable::ConnectorResponse( - connector_response.clone(), - ), - }, - }; - - match kv_wrapper::( - self, - KvOperation::HSetNx(&field, &created_connector_resp, redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hsetnx() - { - Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { - entity: "address", - key: Some(key), - }) - .into_report(), - Ok(HsetnxReply::KeySet) => Ok(created_connector_resp), - Err(er) => Err(er).change_context(errors::StorageError::KVError), - } - } - } - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - let database_call = || async { - storage_type::ConnectorResponse::find_by_payment_id_merchant_id_attempt_id( - &conn, - payment_id, - merchant_id, - attempt_id, - ) - .await - .map_err(Into::into) - .into_report() - }; - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => database_call().await, - storage_enums::MerchantStorageScheme::RedisKv => { - let key = format!("mid_{merchant_id}_pid_{payment_id}"); - let field = format!("connector_resp_{merchant_id}_{payment_id}_{attempt_id}"); - - db_utils::try_redis_get_else_try_database_get( - async { - kv_wrapper( - self, - KvOperation::::HGet(&field), - key, - ) - .await? - .try_into_hget() - }, - database_call, - ) - .await - } - } - } - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => this - .update(&conn, connector_response_update) - .await - .map_err(Into::into) - .into_report(), - storage_enums::MerchantStorageScheme::RedisKv => { - let key = format!("mid_{}_pid_{}", this.merchant_id, this.payment_id); - let updated_connector_response = connector_response_update - .clone() - .apply_changeset(this.clone()); - let redis_value = serde_json::to_string(&updated_connector_response) - .into_report() - .change_context(errors::StorageError::KVError)?; - let field = format!( - "connector_resp_{}_{}_{}", - &updated_connector_response.merchant_id, - &updated_connector_response.payment_id, - &updated_connector_response.attempt_id - ); - - let redis_entry = kv::TypedSql { - op: kv::DBOperation::Update { - updatable: kv::Updateable::ConnectorResponseUpdate( - kv::ConnectorResponseUpdateMems { - orig: this, - update_data: connector_response_update, - }, - ), - }, - }; - - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::( - (&field, redis_value), - redis_entry, - ), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - - Ok(updated_connector_response) - } - } - } - } -} - -#[async_trait::async_trait] -impl ConnectorResponseInterface for MockDb { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - new: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let mut connector_response = self.connector_response.lock().await; - let response = storage_type::ConnectorResponse { - id: connector_response - .len() - .try_into() - .into_report() - .change_context(errors::StorageError::MockDbError)?, - payment_id: new.payment_id, - merchant_id: new.merchant_id, - attempt_id: new.attempt_id, - created_at: new.created_at, - modified_at: new.modified_at, - connector_name: new.connector_name, - connector_transaction_id: new.connector_transaction_id, - authentication_data: new.authentication_data, - encoded_data: new.encoded_data, - updated_by: storage_scheme.to_string(), - }; - connector_response.push(response.clone()); - Ok(response) - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - _payment_id: &str, - _merchant_id: &str, - _attempt_id: &str, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - // [#172]: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - // safety: interface only used for testing - #[allow(clippy::unwrap_used)] - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let mut connector_response = self.connector_response.lock().await; - let response = connector_response - .iter_mut() - .find(|item| item.id == this.id) - .unwrap(); - *response = connector_response_update.apply_changeset(response.clone()); - Ok(response.clone()) - } -} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 1e7c34a420b1..c63ff5fb7f86 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -4,7 +4,6 @@ pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; pub mod customers; pub mod dispute; pub mod enums; @@ -41,11 +40,11 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, connector_response::*, - customers::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, - mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, - payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, - refund::*, reverse_lookup::*, routing_algorithm::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, + ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, + merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, + payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, + reverse_lookup::*, routing_algorithm::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/connector_response.rs b/crates/router/src/types/storage/connector_response.rs deleted file mode 100644 index c93c231e3d1c..000000000000 --- a/crates/router/src/types/storage/connector_response.rs +++ /dev/null @@ -1,41 +0,0 @@ -pub use diesel_models::{ - connector_response::{ - ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate, - ConnectorResponseUpdateInternal, - }, - enums::MerchantStorageScheme, -}; - -pub trait ConnectorResponseExt { - fn make_new_connector_response( - payment_id: String, - merchant_id: String, - attempt_id: String, - connector: Option, - storage_scheme: String, - ) -> ConnectorResponseNew; -} - -impl ConnectorResponseExt for ConnectorResponse { - fn make_new_connector_response( - payment_id: String, - merchant_id: String, - attempt_id: String, - connector: Option, - storage_scheme: String, - ) -> ConnectorResponseNew { - let now = common_utils::date_time::now(); - ConnectorResponseNew { - payment_id, - merchant_id, - attempt_id, - created_at: now, - modified_at: now, - connector_name: connector, - connector_transaction_id: None, - authentication_data: None, - encoded_data: None, - updated_by: storage_scheme, - } - } -} diff --git a/crates/router/src/types/storage/kv.rs b/crates/router/src/types/storage/kv.rs index 2afc73e6637d..6bb6c38e7b26 100644 --- a/crates/router/src/types/storage/kv.rs +++ b/crates/router/src/types/storage/kv.rs @@ -1,4 +1,4 @@ pub use diesel_models::kv::{ - AddressUpdateMems, ConnectorResponseUpdateMems, DBOperation, Insertable, - PaymentAttemptUpdateMems, PaymentIntentUpdateMems, RefundUpdateMems, TypedSql, Updateable, + AddressUpdateMems, DBOperation, Insertable, PaymentAttemptUpdateMems, PaymentIntentUpdateMems, + RefundUpdateMems, TypedSql, Updateable, }; diff --git a/crates/storage_impl/src/connector_response.rs b/crates/storage_impl/src/connector_response.rs deleted file mode 100644 index 7d4ff6df94d9..000000000000 --- a/crates/storage_impl/src/connector_response.rs +++ /dev/null @@ -1,5 +0,0 @@ -use diesel_models::connector_response::ConnectorResponse; - -use crate::redis::kv_store::KvStorePartition; - -impl KvStorePartition for ConnectorResponse {} diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index cef4a8981a43..00d8703940c7 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -8,7 +8,6 @@ use redis::{kv_store::RedisConnInterface, RedisStore}; mod address; pub mod config; pub mod connection; -mod connector_response; pub mod database; pub mod errors; mod lookup; diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 76bdb1160ccc..33f3f7a77f27 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -27,7 +27,6 @@ pub struct MockDb { pub customers: Arc>>, pub refunds: Arc>>, pub processes: Arc>>, - pub connector_response: Arc>>, pub redis: Arc, pub api_keys: Arc>>, pub ephemeral_keys: Arc>>, @@ -57,7 +56,6 @@ impl MockDb { customers: Default::default(), refunds: Default::default(), processes: Default::default(), - connector_response: Default::default(), redis: Arc::new( RedisStore::new(redis) .await diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index e3047221b6f5..21002917df83 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1062,7 +1062,6 @@ impl DataModelExt for PaymentAttemptNew { connector_response_reference_id: self.connector_response_reference_id, multiple_capture_count: self.multiple_capture_count, amount_capturable: self.amount_capturable, - updated_by: self.updated_by, authentication_data: self.authentication_data, encoded_data: self.encoded_data, @@ -1244,6 +1243,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1259,6 +1260,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, }, Self::UnresolvedResponseUpdate { status, @@ -1481,6 +1484,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => Self::ResponseUpdate { status, connector, @@ -1496,6 +1501,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, diff --git a/migrations/2023-11-08-144951_drop_connector_response_table/down.sql b/migrations/2023-11-08-144951_drop_connector_response_table/down.sql new file mode 100644 index 000000000000..ff9ca4e4f9c7 --- /dev/null +++ b/migrations/2023-11-08-144951_drop_connector_response_table/down.sql @@ -0,0 +1,34 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE connector_response ( + id SERIAL PRIMARY KEY, + payment_id VARCHAR(255) NOT NULL, + merchant_id VARCHAR(255) NOT NULL, + txn_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + connector_name VARCHAR(32) NOT NULL, + connector_transaction_id VARCHAR(255), + authentication_data JSON, + encoded_data TEXT +); + +CREATE UNIQUE INDEX connector_response_id_index ON connector_response (payment_id, merchant_id, txn_id); + +ALTER TABLE connector_response ALTER COLUMN connector_name DROP NOT NULL; +ALTER TABLE connector_response RENAME COLUMN txn_id TO attempt_id; +ALTER TABLE connector_response + ALTER COLUMN payment_id TYPE VARCHAR(64), + ALTER COLUMN merchant_id TYPE VARCHAR(64), + ALTER COLUMN attempt_id TYPE VARCHAR(64), + ALTER COLUMN connector_name TYPE VARCHAR(64), + ALTER COLUMN connector_transaction_id TYPE VARCHAR(128); + + + +ALTER TABLE connector_response +ALTER COLUMN modified_at DROP DEFAULT; + +ALTER TABLE connector_response +ALTER COLUMN created_at DROP DEFAULT; + +ALTER TABLE connector_response ADD column updated_by VARCHAR(32) NOT NULL DEFAULT 'postgres_only'; diff --git a/migrations/2023-11-08-144951_drop_connector_response_table/up.sql b/migrations/2023-11-08-144951_drop_connector_response_table/up.sql new file mode 100644 index 000000000000..0059a6b38dc1 --- /dev/null +++ b/migrations/2023-11-08-144951_drop_connector_response_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +DROP TABLE connector_response; --NOT to run in deployment envs \ No newline at end of file From 20c4226a36e4650a3ba8811b758ac5f7969bcfb3 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:47:32 +0530 Subject: [PATCH 55/57] feat(user): setup user tables (#2803) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sahkal Poddar Co-authored-by: Sahkal Poddar Co-authored-by: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Co-authored-by: Venkatesh Co-authored-by: venkatesh.devendran Co-authored-by: Abhishek Marrivagu <68317979+Abhicodes-crypto@users.noreply.github.com> --- crates/diesel_models/src/enums.rs | 22 ++ crates/diesel_models/src/lib.rs | 2 + crates/diesel_models/src/query.rs | 2 + crates/diesel_models/src/query/user.rs | 62 ++++ crates/diesel_models/src/query/user_role.rs | 58 ++++ crates/diesel_models/src/schema.rs | 47 ++++ crates/diesel_models/src/user.rs | 76 +++++ crates/diesel_models/src/user_role.rs | 79 ++++++ crates/router/src/db.rs | 4 + crates/router/src/db/user.rs | 265 ++++++++++++++++++ crates/router/src/db/user_role.rs | 255 +++++++++++++++++ crates/router/src/types/storage.rs | 4 +- crates/router/src/types/storage/user.rs | 1 + crates/router/src/types/storage/user_role.rs | 1 + crates/storage_impl/src/mock_db.rs | 4 + .../down.sql | 2 + .../up.sql | 14 + .../down.sql | 4 + .../up.sql | 18 ++ 19 files changed, 919 insertions(+), 1 deletion(-) create mode 100644 crates/diesel_models/src/query/user.rs create mode 100644 crates/diesel_models/src/query/user_role.rs create mode 100644 crates/diesel_models/src/user.rs create mode 100644 crates/diesel_models/src/user_role.rs create mode 100644 crates/router/src/db/user.rs create mode 100644 crates/router/src/db/user_role.rs create mode 100644 crates/router/src/types/storage/user.rs create mode 100644 crates/router/src/types/storage/user_role.rs create mode 100644 migrations/2023-11-06-110233_create_user_table/down.sql create mode 100644 migrations/2023-11-06-110233_create_user_table/up.sql create mode 100644 migrations/2023-11-06-113726_create_user_roles_table/down.sql create mode 100644 migrations/2023-11-06-113726_create_user_roles_table/up.sql diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 0e06a324f038..ec021f0f51a5 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -401,3 +401,25 @@ pub enum FraudCheckLastStep { TransactionOrRecordRefund, Fulfillment, } + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum UserStatus { + Active, + #[default] + InvitationSent, +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 46a6965b3a7b..781099662a50 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -38,6 +38,8 @@ pub mod reverse_lookup; pub mod routing_algorithm; #[allow(unused_qualifications)] pub mod schema; +pub mod user; +pub mod user_role; use diesel_impl::{DieselArray, OptionalDieselArray}; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index f315327702ad..cf5a993c2686 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -28,3 +28,5 @@ pub mod process_tracker; pub mod refund; pub mod reverse_lookup; pub mod routing_algorithm; +pub mod user; +pub mod user_role; diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs new file mode 100644 index 000000000000..5761d8af814d --- /dev/null +++ b/crates/diesel_models/src/query/user.rs @@ -0,0 +1,62 @@ +use diesel::{associations::HasTable, ExpressionMethods}; +use error_stack::report; +use router_env::tracing::{self, instrument}; + +use crate::{ + errors::{self}, + query::generics, + schema::users::dsl, + user::*, + PgPooledConn, StorageResult, +}; + +impl UserNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl User { + pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::email.eq(user_email.to_owned()), + ) + .await + } + + pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + ) + .await + } + + pub async fn update_by_user_id( + conn: &PgPooledConn, + user_id: &str, + user: UserUpdate, + ) -> StorageResult { + generics::generic_update_with_results::<::Table, _, _, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + UserUpdateInternal::from(user), + ) + .await? + .first() + .cloned() + .ok_or_else(|| { + report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") + }) + } + + pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs new file mode 100644 index 000000000000..d2f9564a5309 --- /dev/null +++ b/crates/diesel_models/src/query/user_role.rs @@ -0,0 +1,58 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{query::generics, schema::user_roles::dsl, user_role::*, PgPooledConn, StorageResult}; + +impl UserRoleNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl UserRole { + pub async fn find_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id.eq(user_id), + ) + .await + } + + pub async fn update_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + update: UserRoleUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + UserRoleUpdateInternal::from(update), + ) + .await + } + + pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { + generics::generic_delete::<::Table, _>(conn, dsl::user_id.eq(user_id)) + .await + } + + pub async fn list_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::user_id.eq(user_id), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6c9cea035b3f..72d5217038c1 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -900,6 +900,51 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + user_roles (id) { + id -> Int4, + #[max_length = 64] + user_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + role_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + status -> Varchar, + #[max_length = 64] + created_by -> Varchar, + #[max_length = 64] + last_modified_by -> Varchar, + created_at -> Timestamp, + last_modified_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + users (id) { + id -> Int4, + #[max_length = 64] + user_id -> Varchar, + #[max_length = 255] + email -> Varchar, + #[max_length = 255] + name -> Varchar, + #[max_length = 255] + password -> Varchar, + is_verified -> Bool, + created_at -> Timestamp, + last_modified_at -> Timestamp, + } +} + diesel::allow_tables_to_appear_in_same_query!( address, api_keys, @@ -929,4 +974,6 @@ diesel::allow_tables_to_appear_in_same_query!( refund, reverse_lookup, routing_algorithm, + user_roles, + users, ); diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs new file mode 100644 index 000000000000..6a2e864b291c --- /dev/null +++ b/crates/diesel_models/src/user.rs @@ -0,0 +1,76 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use masking::Secret; +use time::PrimitiveDateTime; + +use crate::schema::users; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = users)] +pub struct User { + pub id: i32, + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub password: Secret, + pub is_verified: bool, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Default, Insertable, router_derive::DebugAsDisplay, +)] +#[diesel(table_name = users)] +pub struct UserNew { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub password: Secret, + pub is_verified: bool, + pub created_at: Option, + pub last_modified_at: Option, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = users)] +pub struct UserUpdateInternal { + name: Option, + password: Option>, + is_verified: Option, + last_modified_at: PrimitiveDateTime, +} + +#[derive(Debug)] +pub enum UserUpdate { + VerifyUser, + AccountUpdate { + name: Option, + password: Option>, + is_verified: Option, + }, +} + +impl From for UserUpdateInternal { + fn from(user_update: UserUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match user_update { + UserUpdate::VerifyUser => Self { + name: None, + password: None, + is_verified: Some(true), + last_modified_at, + }, + UserUpdate::AccountUpdate { + name, + password, + is_verified, + } => Self { + name, + password, + is_verified, + last_modified_at, + }, + } + } +} diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs new file mode 100644 index 000000000000..467584ac59db --- /dev/null +++ b/crates/diesel_models/src/user_role.rs @@ -0,0 +1,79 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::user_roles}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = user_roles)] +pub struct UserRole { + pub id: i32, + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, + pub status: enums::UserStatus, + pub created_by: String, + pub last_modified_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] +#[diesel(table_name = user_roles)] +pub struct UserRoleNew { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, + pub status: enums::UserStatus, + pub created_by: String, + pub last_modified_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = user_roles)] +pub struct UserRoleUpdateInternal { + role_id: Option, + status: Option, + last_modified_by: Option, + last_modified_at: PrimitiveDateTime, +} + +pub enum UserRoleUpdate { + UpdateStatus { + status: enums::UserStatus, + modified_by: String, + }, + UpdateRole { + role_id: String, + modified_by: String, + }, +} + +impl From for UserRoleUpdateInternal { + fn from(value: UserRoleUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match value { + UserRoleUpdate::UpdateRole { + role_id, + modified_by, + } => Self { + role_id: Some(role_id), + last_modified_by: Some(modified_by), + status: None, + last_modified_at, + }, + UserRoleUpdate::UpdateStatus { + status, + modified_by, + } => Self { + status: Some(status), + last_modified_at, + last_modified_by: Some(modified_by), + role_id: None, + }, + } + } +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 6fe34d8dd69b..9687f7f97c92 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -25,6 +25,8 @@ pub mod payouts; pub mod refund; pub mod reverse_lookup; pub mod routing_algorithm; +pub mod user; +pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, @@ -80,6 +82,8 @@ pub trait StorageInterface: + organization::OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + + user::UserInterface + + user_role::UserRoleInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs new file mode 100644 index 000000000000..6bb1d9e50b6a --- /dev/null +++ b/crates/router/src/db/user.rs @@ -0,0 +1,265 @@ +use diesel_models::user as storage; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait UserInterface { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult; + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult; + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult; + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult; + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl UserInterface for Store { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + user_data + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_by_user_email(&conn, user_email) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_by_user_id(&conn, user_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::update_by_user_id(&conn, user_id, user) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::delete_by_user_id(&conn, user_id) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl UserInterface for MockDb { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + let mut users = self.users.lock().await; + if users + .iter() + .any(|user| user.email == user_data.email || user.user_id == user_data.user_id) + { + Err(errors::StorageError::DuplicateValue { + entity: "email or user_id", + key: None, + })? + } + let time_now = common_utils::date_time::now(); + let user = storage::User { + id: users + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: user_data.user_id, + email: user_data.email, + name: user_data.name, + password: user_data.password, + is_verified: user_data.is_verified, + created_at: user_data.created_at.unwrap_or(time_now), + last_modified_at: user_data.created_at.unwrap_or(time_now), + }; + users.push(user.clone()); + Ok(user) + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + let users = self.users.lock().await; + let user_email_pii: common_utils::pii::Email = user_email + .to_string() + .try_into() + .map_err(|_| errors::StorageError::MockDbError)?; + users + .iter() + .find(|user| user.email == user_email_pii) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for email = {user_email}" + )) + .into(), + ) + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + let users = self.users.lock().await; + users + .iter() + .find(|user| user.user_id == user_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + update_user: storage::UserUpdate, + ) -> CustomResult { + let mut users = self.users.lock().await; + users + .iter_mut() + .find(|user| user.user_id == user_id) + .map(|user| { + *user = match &update_user { + storage::UserUpdate::VerifyUser => storage::User { + is_verified: true, + ..user.to_owned() + }, + storage::UserUpdate::AccountUpdate { + name, + password, + is_verified, + } => storage::User { + name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), + password: password.clone().unwrap_or(user.password.clone()), + is_verified: is_verified.unwrap_or(user.is_verified), + ..user.to_owned() + }, + }; + user.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let mut users = self.users.lock().await; + let user_index = users + .iter() + .position(|user| user.user_id == user_id) + .ok_or(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )))?; + users.remove(user_index); + Ok(true) + } +} +#[cfg(feature = "kafka_events")] +#[async_trait::async_trait] +impl UserInterface for super::KafkaStore { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + self.diesel_store.insert_user(user_data).await + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_email(user_email).await + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_id(user_id).await + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_user_id(user_id, user) + .await + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.delete_user_by_user_id(user_id).await + } +} diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs new file mode 100644 index 000000000000..37e38e8afca7 --- /dev/null +++ b/crates/router/src/db/user_role.rs @@ -0,0 +1,255 @@ +use diesel_models::user_role as storage; +use error_stack::{IntoReport, ResultExt}; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait UserRoleInterface { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult; + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult; + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult; + async fn delete_user_role(&self, user_id: &str) -> CustomResult; + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl UserRoleInterface for Store { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + user_role + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::find_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::update_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + update, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::delete_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::list_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl UserRoleInterface for MockDb { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + if user_roles + .iter() + .any(|user_role_inner| user_role_inner.user_id == user_role.user_id) + { + Err(errors::StorageError::DuplicateValue { + entity: "user_id", + key: None, + })? + } + let user_role = storage::UserRole { + id: user_roles + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: user_role.user_id, + merchant_id: user_role.merchant_id, + role_id: user_role.role_id, + status: user_role.status, + created_by: user_role.created_by, + created_at: user_role.created_at, + last_modified_at: user_role.last_modified_at, + last_modified_by: user_role.last_modified_by, + org_id: user_role.org_id, + }; + user_roles.push(user_role.clone()); + Ok(user_role) + } + + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let user_roles = self.user_roles.lock().await; + user_roles + .iter() + .find(|user_role| user_role.user_id == user_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + user_roles + .iter_mut() + .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id) + .map(|user_role| { + *user_role = match &update { + storage::UserRoleUpdate::UpdateRole { + role_id, + modified_by, + } => storage::UserRole { + role_id: role_id.to_string(), + last_modified_by: modified_by.to_string(), + ..user_role.to_owned() + }, + storage::UserRoleUpdate::UpdateStatus { + status, + modified_by, + } => storage::UserRole { + status: status.to_owned(), + last_modified_by: modified_by.to_owned(), + ..user_role.to_owned() + }, + }; + user_role.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } + + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + let user_role_index = user_roles + .iter() + .position(|user_role| user_role.user_id == user_id) + .ok_or(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )))?; + user_roles.remove(user_role_index); + Ok(true) + } + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + let user_roles = self.user_roles.lock().await; + + Ok(user_roles + .iter() + .cloned() + .filter_map(|ele| { + if ele.user_id == user_id { + return Some(ele); + } + None + }) + .collect()) + } +} + +#[cfg(feature = "kafka_events")] +#[async_trait::async_trait] +impl UserRoleInterface for super::KafkaStore { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + self.diesel_store.insert_user_role(user_role).await + } + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) + .await + } + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_role_by_user_id(user_id).await + } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + self.diesel_store.delete_user_role(user_id).await + } + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_user_id(user_id).await + } +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index c63ff5fb7f86..e3e19323357b 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -32,6 +32,8 @@ pub mod payout_attempt; pub mod payouts; mod query; pub mod refund; +pub mod user; +pub mod user_role; pub use data_models::payments::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, @@ -44,7 +46,7 @@ pub use self::{ ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, + reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/user.rs b/crates/router/src/types/storage/user.rs new file mode 100644 index 000000000000..17dc9d365243 --- /dev/null +++ b/crates/router/src/types/storage/user.rs @@ -0,0 +1 @@ +pub use diesel_models::user::*; diff --git a/crates/router/src/types/storage/user_role.rs b/crates/router/src/types/storage/user_role.rs new file mode 100644 index 000000000000..780b9b2971db --- /dev/null +++ b/crates/router/src/types/storage/user_role.rs @@ -0,0 +1 @@ +pub use diesel_models::user_role::*; diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 33f3f7a77f27..4cdf8e2456bb 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -41,6 +41,8 @@ pub struct MockDb { pub reverse_lookups: Arc>>, pub payment_link: Arc>>, pub organizations: Arc>>, + pub users: Arc>>, + pub user_roles: Arc>>, } impl MockDb { @@ -74,6 +76,8 @@ impl MockDb { reverse_lookups: Default::default(), payment_link: Default::default(), organizations: Default::default(), + users: Default::default(), + user_roles: Default::default(), }) } } diff --git a/migrations/2023-11-06-110233_create_user_table/down.sql b/migrations/2023-11-06-110233_create_user_table/down.sql new file mode 100644 index 000000000000..0172a87499bb --- /dev/null +++ b/migrations/2023-11-06-110233_create_user_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users; \ No newline at end of file diff --git a/migrations/2023-11-06-110233_create_user_table/up.sql b/migrations/2023-11-06-110233_create_user_table/up.sql new file mode 100644 index 000000000000..410436c461ce --- /dev/null +++ b/migrations/2023-11-06-110233_create_user_table/up.sql @@ -0,0 +1,14 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + is_verified bool NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS user_id_index ON users (user_id); +CREATE UNIQUE INDEX IF NOT EXISTS user_email_index ON users (email); \ No newline at end of file diff --git a/migrations/2023-11-06-113726_create_user_roles_table/down.sql b/migrations/2023-11-06-113726_create_user_roles_table/down.sql new file mode 100644 index 000000000000..5e6350de9e70 --- /dev/null +++ b/migrations/2023-11-06-113726_create_user_roles_table/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +-- Drop the table +DROP TABLE IF EXISTS user_roles; \ No newline at end of file diff --git a/migrations/2023-11-06-113726_create_user_roles_table/up.sql b/migrations/2023-11-06-113726_create_user_roles_table/up.sql new file mode 100644 index 000000000000..768306721626 --- /dev/null +++ b/migrations/2023-11-06-113726_create_user_roles_table/up.sql @@ -0,0 +1,18 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS user_roles ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + role_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + status VARCHAR(64) NOT NULL, + created_by VARCHAR(64) NOT NULL, + last_modified_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_at TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT user_merchant_unique UNIQUE (user_id, merchant_id) +); + + +CREATE INDEX IF NOT EXISTS user_id_roles_index ON user_roles (user_id); +CREATE INDEX IF NOT EXISTS user_mid_roles_index ON user_roles (merchant_id); \ No newline at end of file From 2a4f5d13717a78dc2e2e4fc9a492a45b92151dbe Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Fri, 10 Nov 2023 14:39:32 +0530 Subject: [PATCH 56/57] feat(router): added Payment link new design (#2731) Co-authored-by: Sahkal Poddar Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif --- crates/api_models/src/admin.rs | 5 +- crates/api_models/src/payments.rs | 3 +- crates/common_enums/Cargo.toml | 4 +- crates/common_utils/src/consts.rs | 12 + crates/router/src/core/payment_link.rs | 98 +- .../src/core/payment_link/payment_link.html | 1274 +++++++++-------- openapi/openapi_spec.json | 8 +- 7 files changed, 737 insertions(+), 667 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index e844d1900a1a..979214a071a9 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -463,9 +463,8 @@ pub struct PaymentLinkConfig { #[serde(deny_unknown_fields)] pub struct PaymentLinkColorSchema { - pub primary_color: Option, - pub primary_accent_color: Option, - pub secondary_color: Option, + pub background_primary_color: Option, + pub sdk_theme: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 196dd108333b..22579ed6d6ea 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3150,6 +3150,7 @@ pub struct PaymentLinkDetails { pub merchant_logo: String, pub return_url: String, pub merchant_name: String, - pub order_details: Vec, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, + pub sdk_theme: Option, } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index e9f2dffcc050..db37d27ab0f1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -12,9 +12,9 @@ dummy_connector = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = [ "derive" ] } +serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 2f517295ae48..7bc248bf8d1b 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -29,3 +29,15 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; + +/// SDK Default Theme const +pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; + +/// Default Payment Link Background color +pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; + +/// Default product Img Link +pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; + +/// Default Merchant Logo Link +pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 0012efc86c9f..2ea6a4d7f219 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,6 +1,12 @@ use api_models::admin as admin_types; +use common_utils::{ + consts::{ + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + }, + ext_traits::ValueExt, +}; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -76,12 +82,7 @@ pub async fn intiate_payment_link_flow( }) .transpose()?; - let order_details = payment_intent - .order_details - .get_required_value("order_details") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "order_details", - })?; + let order_details = validate_order_details(payment_intent.order_details)?; let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { payment_create_return_url @@ -99,6 +100,9 @@ pub async fn intiate_payment_link_flow( payment_intent.client_secret, )?; + let (default_sdk_theme, default_background_color) = + (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + let payment_details = api_models::payments::PaymentLinkDetails { amount: payment_intent.amount, currency, @@ -116,13 +120,25 @@ pub async fn intiate_payment_link_flow( client_secret, merchant_logo: payment_link_config .clone() - .map(|pl_metadata| pl_metadata.merchant_logo.unwrap_or_default()) + .map(|pl_config| { + pl_config + .merchant_logo + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) + }) .unwrap_or_default(), max_items_visible_after_collapse: 3, + sdk_theme: payment_link_config.clone().and_then(|pl_config| { + pl_config + .color_scheme + .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) + }), }; let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css(payment_link_config.clone()); + let css_script = get_color_scheme_css( + payment_link_config.clone(), + default_background_color.to_string(), + ); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), @@ -149,38 +165,21 @@ fn get_js_script( fn get_color_scheme_css( payment_link_config: Option, + default_primary_color: String, ) -> String { - let (default_primary_color, default_accent_color, default_secondary_color) = ( - "#C6C7C8".to_string(), - "#6A8EF5".to_string(), - "#0C48F6".to_string(), - ); - - let (primary_color, primary_accent_color, secondary_color) = payment_link_config + let background_primary_color = payment_link_config .and_then(|pl_config| { pl_config.color_scheme.map(|color| { - ( - color.primary_color.unwrap_or(default_primary_color.clone()), - color - .primary_accent_color - .unwrap_or(default_accent_color.clone()), - color - .secondary_color - .unwrap_or(default_secondary_color.clone()), - ) + color + .background_primary_color + .unwrap_or(default_primary_color.clone()) }) }) - .unwrap_or(( - default_primary_color, - default_accent_color, - default_secondary_color, - )); + .unwrap_or(default_primary_color); format!( ":root {{ - --primary-color: {primary_color}; - --primary-accent-color: {primary_accent_color}; - --secondary-color: {secondary_color}; + --primary-color: {background_primary_color}; }}" ) } @@ -203,3 +202,36 @@ fn validate_sdk_requirements( })?; Ok((pub_key, currency, client_secret)) } + +fn validate_order_details( + order_details: Option>>, +) -> Result< + Option>, + error_stack::Report, +> { + let order_details = order_details + .map(|order_details| { + order_details + .iter() + .map(|data| { + data.to_owned() + .parse_value("OrderDetailsWithAmount") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "OrderDetailsWithAmount", + }) + .attach_printable("Unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + }) + .transpose()?; + + let updated_order_details = order_details.map(|mut order_details| { + for order in order_details.iter_mut() { + if order.product_img_link.is_none() { + order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + } + } + order_details + }); + Ok(updated_order_details) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 462a11d2567e..67410cac8418 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -112,8 +112,8 @@ } #hyper-checkout-merchant-image > img { - height: 48px; - width: 48px; + height: 40px; + width: 40px; } #hyper-checkout-cart-image { @@ -175,8 +175,8 @@ } .hyper-checkout-cart-product-image { - height: 72px; - width: 72px; + height: 56px; + width: 56px; } .hyper-checkout-card-item-name { @@ -234,13 +234,21 @@ background-color: var(--primary-color); box-shadow: 0px 1px 10px #f2f2f2; display: flex; + flex-flow: column; align-items: center; justify-content: center; } #payment-form-wrap { - min-width: 584px; - padding: 50px; + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; + } + + .powered-by-hyper { + margin-top: 20px; } #hyper-checkout-sdk-header { @@ -295,28 +303,13 @@ margin-top: 10px; } - .checkoutButton { - height: 48px; - border-radius: 25px; - width: 100%; - border: transparent; - background: var(--secondary-color); - color: #ffffff; - font-weight: 600; - cursor: pointer; - } - .page-spinner, .page-spinner::before, - .page-spinner::after, - .spinner, - .spinner:before, - .spinner:after { + .page-spinner::after { border-radius: 50%; } - .page-spinner, - .spinner { + .page-spinner { color: #ffffff; font-size: 22px; text-indent: -99999px; @@ -331,9 +324,7 @@ } .page-spinner::before, - .page-spinner::after, - .spinner:before, - .spinner:after { + .page-spinner::after { position: absolute; content: ""; } @@ -405,19 +396,6 @@ } } - .spinner:before { - width: 10.4px; - height: 20.4px; - background: var(--primary-color); - border-radius: 20.4px 0 0 20.4px; - top: -0.2px; - left: -0.2px; - -webkit-transform-origin: 10.4px 10.2px; - transform-origin: 10.4px 10.2px; - -webkit-animation: loading 2s infinite ease 1.5s; - animation: loading 2s infinite ease 1.5s; - } - #payment-message { font-size: 12px; font-weight: 500; @@ -426,19 +404,6 @@ font-family: "Montserrat"; } - .spinner:after { - width: 10.4px; - height: 10.2px; - background: var(--primary-color); - border-radius: 0 10.2px 10.2px 0; - top: -0.1px; - left: 10.2px; - -webkit-transform-origin: 0px 10.2px; - transform-origin: 0px 10.2px; - -webkit-animation: loading 2s infinite ease; - animation: loading 2s infinite ease; - } - #payment-form { max-width: 560px; width: 100%; @@ -447,11 +412,6 @@ } @media only screen and (max-width: 1200px) { - .checkoutButton { - width: 95%; - background-color: var(--primary-color); - } - .hyper-checkout { flex-flow: column; margin: 0; @@ -627,16 +587,16 @@
@@ -700,7 +660,7 @@
-
+
-
+ +
+ + + + + + + + + + + + + + + + +
- - + function showSDK(e) { + if (window.state.isMobileView) { + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } + setPageLoading(true); + checkStatus() + .then((res) => { + if (res.showSdk) { + renderPaymentDetails(); + renderCart(); + renderSDKHeader(); + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + } else { + show("#hyper-checkout-status"); + show("#hyper-footer"); + } + }) + .catch((err) => {}) + .finally(() => { + setPageLoading(false); + }); + } + + window.addEventListener("resize", (event) => { + const currentHeight = window.innerHeight; + const currentWidth = window.innerWidth; + if (currentWidth <= 1200 && window.state.prevWidth > 1200) { + hide("#hyper-checkout-cart"); + } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { + show("#hyper-checkout-cart"); + } + + window.state.prevHeight = currentHeight; + window.state.prevWidth = currentWidth; + window.state.isMobileView = currentWidth <= 1200; + }); + + diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 6e61f2eb614e..23f8f1b3628b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7809,15 +7809,11 @@ "PaymentLinkColorSchema": { "type": "object", "properties": { - "primary_color": { + "background_primary_color": { "type": "string", "nullable": true }, - "primary_accent_color": { - "type": "string", - "nullable": true - }, - "secondary_color": { + "sdk_theme": { "type": "string", "nullable": true } From b5ea8db2d2b7e7544931704a7191b42d3a8299be Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:38:30 +0530 Subject: [PATCH 57/57] refactor(connector): [Zen] change error message from NotSupported to NotImplemented (#2831) --- .../router/src/connector/zen/transformers.rs | 91 +++++++------------ 1 file changed, 32 insertions(+), 59 deletions(-) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index d13c9b6421f4..6b0d46dec8d1 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -290,10 +290,9 @@ impl | api_models::payments::VoucherData::FamilyMart { .. } | api_models::payments::VoucherData::Seicomart { .. } | api_models::payments::VoucherData::PayEasy { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -342,12 +341,8 @@ impl api_models::payments::BankTransferData::Pse { .. } => { ZenPaymentChannels::PclBoacompraPse } - api_models::payments::BankTransferData::SepaBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::SepaBankTransfer { .. } + | api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::PermataBankTransfer { .. } | api_models::payments::BankTransferData::BcaBankTransfer { .. } @@ -356,10 +351,9 @@ impl | api_models::payments::BankTransferData::CimbVaBankTransfer { .. } | api_models::payments::BankTransferData::DanamonVaBankTransfer { .. } | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -489,12 +483,8 @@ impl api_models::payments::WalletData::WeChatPayRedirect(_) | api_models::payments::WalletData::PaypalRedirect(_) | api_models::payments::WalletData::ApplePay(_) - | api_models::payments::WalletData::GooglePay(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) | api_models::payments::WalletData::MomoRedirect(_) @@ -514,10 +504,9 @@ impl | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) | api_models::payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; let terminal_uuid = session_data @@ -719,10 +708,9 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } } } @@ -736,13 +724,8 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::Sofort { .. } | api_models::payments::BankRedirectData::BancontactCard { .. } | api_models::payments::BankRedirectData::Blik { .. } - | api_models::payments::BankRedirectData::Trustly { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ) - .into()) - } - api_models::payments::BankRedirectData::Eps { .. } + | api_models::payments::BankRedirectData::Trustly { .. } + | api_models::payments::BankRedirectData::Eps { .. } | api_models::payments::BankRedirectData::Giropay { .. } | api_models::payments::BankRedirectData::Przelewy24 { .. } | api_models::payments::BankRedirectData::Bizum {} @@ -754,10 +737,9 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::OpenBankingUk { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -776,10 +758,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for ZenPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -794,10 +775,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for ZenPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -811,10 +791,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -825,19 +804,13 @@ impl TryFrom<&api_models::payments::GiftCardData> for ZenPaymentsRequest { type Error = error_stack::Report; fn try_from(value: &api_models::payments::GiftCardData) -> Result { match value { - api_models::payments::GiftCardData::PaySafeCard {} => { + api_models::payments::GiftCardData::PaySafeCard {} + | api_models::payments::GiftCardData::Givex(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) .into()) } - api_models::payments::GiftCardData::Givex(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } - .into()) - } } } }