diff --git a/.gitignore b/.gitignore index 413afe45..613469d1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ routing-config/venv routing-config/output.sql dump.rdb -.vscode \ No newline at end of file +.vscode +**/node_modules +cypress/screenshots +cypress/videos +cypress/fixtures +# Added by code-review-graph +.code-review-graph/ diff --git a/.mintlify-dev.log b/.mintlify-dev.log new file mode 100644 index 00000000..8f8cc9f1 --- /dev/null +++ b/.mintlify-dev.log @@ -0,0 +1,26 @@ +⠋ preparing local preview... +⠙ preparing local preview... +warning - Error validating OpenAPI file /mint.json: Error: Failed to validate OpenAPI schema:Unknown path: Can’t find supported Swagger/OpenAPI version in specification, version must be a string. +⠙ preparing local preview... +⠹ preparing local preview... +⠸ preparing local preview... +⠼ preparing local preview... +update available - run `mint update` to get the latest version + +✓ preview ready + +  local   → http://localhost:3000 +  network → http://192.168.1.4:3000 + +press ctrl+c to exit the preview + + +update available - run `mint update` to get the latest version + +✓ preview ready + +  local   → http://localhost:3000 +  network → http://192.168.1.4:3000 + +press ctrl+c to exit the preview + diff --git a/.typos.toml b/.typos.toml index 7c107218..3f79ac71 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,2 +1,84 @@ [default] check-filename = true + +# Ignore 2-3 letter all-caps identifiers (likely codes/abbreviations) +# and 2-3 letter lowercase identifiers (likely variable names) +extend-ignore-identifiers-re = [ + "^[A-Z]{2,3}$", + "^[a-z]{2,3}$", +] + +# Module/type aliases used throughout the codebase +[default.extend-identifiers] +HasTable = "HasTable" +ETO = "ETO" +ETCC = "ETCC" +ETM = "ETM" +ETD = "ETD" +ETCa = "ETCa" +ETTD = "ETTD" +ETOd = "ETOd" +ETMo = "ETMo" +TE = "TE" +ser = "ser" + +# ISO 3166-1 alpha-3 country codes +CAF = "CAF" +NAM = "NAM" +SOM = "SOM" +THA = "THA" +FO = "FO" + +# ISO 4217 currency codes +ZAR = "ZAR" + +# Common abbreviations in code (e.g., ect = extendedCardType) +ect = "ect" + +# CamelCase identifiers (Mis = Mismatch, etc.) +Mis = "Mis" + +# Time zones (IST = Indian Standard Time) +IST = "IST" +Ist = "Ist" +ist = "ist" + +# Order Type abbreviation +OT = "OT" + +# Value Added Services (banking term) +VAS = "VAS" +Vas = "Vas" + +# JWT library API term +encrypter = "encrypter" + +[default.extend-words] +# Project-specific terms that are valid +incase = "incase" +# Database column name (would require migration to fix) +penality = "penality" +# HashiCorp (company name) +hashi = "hashi" +# CamelCase function name (Mismatch) +Mis = "Mis" +# Time zones (Indian Standard Time) +IST = "IST" +Ist = "Ist" +ist = "ist" +# Order Type abbreviation +OT = "OT" +# Value Added Services (banking term) +VAS = "VAS" +Vas = "Vas" +# JWT library API term +encrypter = "encrypter" + +[files] +extend-exclude = [ + # Exclude non-source files + "*.groovy", + "**/Untitled*", + # Exclude build artifacts + "website/dist/**", +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e48dd682 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,159 @@ +# Decision Engine Agent Guide + +## Purpose + +Use this file as the first-stop operating guide for work in `decision-engine`. + +- Use docs for orientation, then verify behavior against code, config, and CI before making claims. +- Prefer the smallest relevant surface first: route -> service -> config -> storage -> deployment. +- When docs and code disagree, trust code, config, and CI over prose. + +## Source of Truth + +Use this precedence order when answering questions or planning changes: + +1. Runtime code and entrypoints in `src/` +2. Config files and deployment manifests in `config/`, `docker-compose.yaml`, and `helm-charts/` +3. `justfile`, `Makefile`, `.github/workflows/`, and `scripts/ci-checks.sh` +4. Setup and API docs in `docs/` +5. `README.md` for high-level product context only + +Do not treat README claims or marketing language as definitive implementation truth. + +## Repo Map + +- `src/bin/open_router.rs`: process entrypoint; loads config, fetches secrets, starts app and metrics servers +- `src/app.rs`: main HTTP server wiring, route registration, middleware, graceful shutdown, TLS handling +- `src/routes/`: REST handlers and endpoint behavior +- `src/config.rs`: config structs, config loading, env override behavior, validation hooks +- `src/tenant.rs`: tenant-aware app state wiring +- `src/decider/`: routing decision logic +- `src/euclid/`: routing rules and evaluation engine +- `src/feedback/`: score updates and routing feedback flows +- `docs/local-setup.md`: canonical local and on-prem style startup guide +- `docs/configuration.md`: config-oriented documentation and deployment pointers +- `docs/api-reference1.md`: human-readable API examples +- `docs/openapi.json`: machine-readable API contract +- `docker-compose.yaml`: local topology, compose profiles, supporting services +- `helm-charts/`: Kubernetes and on-prem deployment assets +- `justfile`: canonical engineering commands +- `Makefile`: convenience wrappers around common compose flows +- `.github/workflows/`: CI expectations and release automation +- `scripts/ci-checks.sh`: DB feature compile matrix enforced by CI +- `website/`: dashboard/frontend assets +- `cypress/`: frontend/end-to-end test area + +## Which Docs to Open First + +- Local startup or environment bring-up: `docs/local-setup.md` -> `docker-compose.yaml` -> `justfile` +- Runtime/config questions: `docs/configuration.md` -> `config/*.toml` -> `src/config.rs` +- API behavior: `docs/openapi.json` -> `docs/api-reference1.md` -> `src/routes/*` +- Server startup and wiring: `src/bin/open_router.rs` -> `src/app.rs` +- Deployment or on-prem: `helm-charts/README.md` -> `helm-charts/templates/*` -> `docker-compose.yaml` +- CI or verification expectations: `justfile` -> `.github/workflows/*` -> `scripts/ci-checks.sh` +- Routing logic: `src/decider/` -> `src/euclid/` -> `src/routes/decide_gateway.rs` -> `src/routes/rule_configuration.rs` -> `src/routes/update_gateway_score.rs` +- Tenant behavior: `src/tenant.rs` -> `src/custom_extractors.rs` -> `src/app.rs` + +## Canonical Startup Paths + +Use `docs/local-setup.md` as the canonical startup guide. Default to PostgreSQL unless the task is explicitly MySQL-specific. + +Preferred quick-start paths: + +- PostgreSQL with GHCR images: + `docker compose --profile postgres-ghcr up -d` +- PostgreSQL with local build: + `docker compose --profile postgres-local up -d --build` +- Convenience wrappers: + `make init-pg-ghcr` + `make init-pg-local` + +Source-run commands: + +- PostgreSQL build: + `cargo build --release --no-default-features --features middleware,kms-aws,postgres` +- PostgreSQL run: + `RUSTFLAGS="-Awarnings" cargo run --no-default-features --features postgres` +- MySQL build: + `cargo build --release --features release` +- MySQL run: + `RUSTFLAGS="-Awarnings" cargo run --features release` +- PostgreSQL migration: + `just migrate-pg` + +Verification: + +- Health: + `curl http://localhost:8080/health` + +Choose the startup path based on task intent: + +- local debugging: compose or source run +- deployment review: Helm and compose manifests +- API behavior inspection: route code plus health/startup verification + +## Coding Expectations + +- Rust edition is 2021; MSRV in `Cargo.toml` is `1.85.0` +- `unsafe` is forbidden +- Treat clippy warnings around `unwrap`, `expect`, `panic`, `todo`, and `unimplemented` as meaningful design constraints +- Verify both DB tracks when changing shared core logic; do not assume only one backend is affected +- The default feature path is MySQL; PostgreSQL uses `--no-default-features --features postgres` +- Touch the smallest relevant surface first instead of broad refactors +- When changing externally visible behavior, update the relevant docs in `docs/` +- Confirm implementation facts in `src/`, config, and deployment manifests instead of relying on README wording + +## Verification Expectations + +Before claiming a change is done, choose the smallest sufficient set of checks and state what was run. + +Recommended command ladder: + +- format check: + `cargo +nightly fmt --all --check` +- lint: + `just clippy` +- broad compile coverage: + `just check` +- unit tests: + `cargo test` +- PostgreSQL migration path when relevant: + `just migrate-pg` +- startup/config smoke check when relevant: + `curl http://localhost:8080/health` +- CI-sensitive Rust changes: + inspect `scripts/ci-checks.sh` + +Rules: + +- If a change touches shared Rust logic in `src/`, validate both MySQL and PostgreSQL compile paths because CI checks both. +- If a task is docs-only, config-reading only, or frontend-only, say which checks were intentionally skipped. +- If deployment behavior is in question, inspect manifests and templates directly instead of assuming docs are current. + +## Task Playbooks + +- Bug fix: + reproduce -> identify route/config/storage surface -> patch -> run targeted checks -> run broader compile checks if core logic changed +- API change: + inspect route handler -> inspect request/response types -> inspect `docs/openapi.json` and `docs/api-reference1.md` -> update docs if behavior changed +- Deployment issue: + inspect `docs/local-setup.md` -> inspect `docker-compose.yaml` or `helm-charts/templates/*` -> inspect config loading in `src/config.rs` +- Prod or on-prem readiness question: + verify against Helm, compose, config defaults, secrets handling, probes, CI, and runtime code +- Routing logic question: + inspect `src/routes/decide_gateway.rs`, `src/decider/`, `src/euclid/`, and feedback/update-score flows +- Tenant question: + inspect `src/tenant.rs`, `src/custom_extractors.rs`, route handlers, and config-backed tenant wiring + +## Optional Local Tooling + +- When available, prefer `code-review-graph` for code navigation, dependency tracing, and change-impact analysis. +- If unavailable, fall back to normal repository inspection. +- Do not make task success depend on optional local AI tooling. + +## Important Caveats + +- Default config files are development-oriented and should not be treated as production-ready defaults. +- Deployment docs are useful, but readiness claims must be verified against chart templates, compose manifests, runtime config, and code. +- Use actual route wiring and config structs as the authority for behavior. +- Some docs overlap or duplicate each other; prefer `docs/local-setup.md`, `docs/configuration.md`, `docs/openapi.json`, and the route code as canonical references. diff --git a/Cargo.lock b/Cargo.lock index d7f0734f..3f660f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,6 +999,12 @@ dependencies = [ "half", ] +[[package]] +name = "cityhash-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d" + [[package]] name = "clang-sys" version = "1.8.1" @@ -1035,6 +1041,45 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clickhouse" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3093f817c4f81c8bd174ed8dd30eac785821a8a7eef27a7dcb7f8cd0d0f6548" +dependencies = [ + "bstr", + "bytes", + "cityhash-rs", + "clickhouse-derive", + "futures", + "futures-channel", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "lz4_flex", + "replace_with", + "sealed", + "serde", + "static_assertions", + "thiserror 1.0.69", + "time", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "clickhouse-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70f3e2893f7d3e017eeacdc9a708fbc29a10488e3ebca21f9df6a5d2b616dbb" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.100", +] + [[package]] name = "cmake" version = "0.1.54" @@ -1256,6 +1301,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc16" version = "0.4.0" @@ -1646,7 +1706,7 @@ checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" dependencies = [ "darling 0.20.10", "either", - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.100", @@ -2089,6 +2149,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2665,6 +2731,25 @@ dependencies = [ "serde", ] +[[package]] +name = "kafka" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054ba4edcb4dcda4209e138c7e88caf26d4a325b3db76fbdb6ca5eecc23e426" +dependencies = [ + "byteorder", + "crc", + "flate2", + "fnv", + "openssl", + "openssl-sys", + "ref_slice", + "snap", + "thiserror 1.0.69", + "tracing", + "twox-hash", +] + [[package]] name = "keyed_priority_queue" version = "0.4.2" @@ -2787,6 +2872,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + [[package]] name = "masking" version = "0.1.0" @@ -2955,7 +3046,7 @@ dependencies = [ "subprocess", "thiserror 1.0.69", "uuid", - "zstd", + "zstd 0.13.3", ] [[package]] @@ -3126,6 +3217,7 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", + "clickhouse", "config", "console-subscriber", "cpu-time", @@ -3138,10 +3230,12 @@ dependencies = [ "futures", "gethostname", "hex", + "http-body-util", "hyper 1.6.0", "jemalloc-ctl", "jemallocator", "josekit", + "kafka", "lazy_static", "masking 0.1.0 (git+https://github.com/juspay/hyperswitch?tag=v1.111.1)", "mysqlclient-sys", @@ -3151,6 +3245,7 @@ dependencies = [ "rand 0.8.5", "rand_distr", "redis_interface", + "regex", "reqwest 0.12.15", "ring", "rustc-hash 2.1.1", @@ -3171,6 +3266,7 @@ dependencies = [ "tracing-subscriber", "uuid", "vaultrs", + "zstd 0.12.4", ] [[package]] @@ -3862,6 +3958,12 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "ref_slice" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ed1d73fb92eba9b841ba2aef69533a060ccc0d3ec71c90aeda5996d4afb7a9" + [[package]] name = "regex" version = "1.11.1" @@ -3933,6 +4035,12 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + [[package]] name = "reqwest" version = "0.11.27" @@ -4409,6 +4517,18 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sealed" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4474,6 +4594,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_json" version = "1.0.140" @@ -4613,6 +4744,12 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + [[package]] name = "socket2" version = "0.5.8" @@ -4673,7 +4810,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -6129,13 +6266,32 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe 6.0.6", +] + [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe", + "zstd-safe 7.2.4", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 77bf6526..fd0cb962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ rust-version = "1.85.0" [features] default = ["middleware","mysql"] -release = ["middleware", "kms-aws","mysql"] +release = ["middleware", "kms-aws","mysql", "redis_compression"] kms-aws = ["dep:aws-config", "dep:aws-sdk-kms"] kms-hashicorp-vault = ["dep:vaultrs"] limit = [] @@ -19,6 +19,7 @@ external_key_manager = [] external_key_manager_mtls = ["external_key_manager", "reqwest/rustls-tls"] postgres = [] mysql = [] +redis_compression = ["dep:zstd"] [dependencies] aws-config = { version = "1.5.5", optional = true } @@ -36,6 +37,7 @@ jemallocator = "0.5" jemalloc-ctl = "0.5" prometheus = "0.13" lazy_static = "1.4" +zstd = { version = "0.12", optional = true } async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } redis_interface = { git = "https://github.com/juspay/hyperswitch.git", tag = "2024.09.30.0" } @@ -73,6 +75,9 @@ hex = "0.4.3" time = { version = "0.3.36", features = ["serde"] } uuid = { version = "1.10.0", features = ["v4", "v7", "fast-rng"] } reqwest = { version = "0.12.7", features = ["json", "__rustls"] } +http-body-util = "0.1.3" +kafka = "0.10.0" +clickhouse = { version = "0.12.2", features = ["time", "uuid"] } nanoid = "0.4.0" mysqlclient-sys = { version = "0.4.2", features = ["buildtime_bindgen"] } @@ -93,6 +98,7 @@ rand = "0.8.5" tower-http = { version = "0.6.2", features = ["trace"] } bytes = "1.10.1" strum = { version = "0.26.2", features = ["derive"] } +regex = "1.10" # ------------------------------------- [dev-dependencies] diff --git a/Dockerfile.docs b/Dockerfile.docs new file mode 100644 index 00000000..afaa64be --- /dev/null +++ b/Dockerfile.docs @@ -0,0 +1,12 @@ +FROM node:20-alpine + +ARG MINTLIFY_VERSION=4.2.504 +RUN npm install -g "mintlify@${MINTLIFY_VERSION}" + +WORKDIR /docs + +COPY docs/ . + +EXPOSE 3000 + +CMD ["mintlify", "dev", "--port", "3000", "--no-browser"] diff --git a/Makefile b/Makefile index 43d893c9..57dddd0e 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,58 @@ COMMIT_HASH := $(shell git rev-parse --short HEAD) TAG := ghcr.io/juspay/decision-engine:sha_$(COMMIT_HASH) +DECISION_ENGINE_TAG ?= v1.4 +GROOVY_RUNNER_TAG ?= v1.4 + docker-build: - docker build --platform=linux/amd64 -t $(TAG) . + docker build --platform=linux/amd64 -t $(TAG) . docker-run: - docker run --platform=linux/amd64 -v `pwd`/config/docker-configuration.toml:/local/config/development.toml -p 8080:8080 -d $(TAG) + docker run --platform=linux/amd64 -v `pwd`/config/docker-configuration.toml:/local/config/development.toml -p 8080:8080 -d $(TAG) docker-it-run: - docker run --platform=linux/amd64 -v `pwd`/config/docker-configuration.toml:/local/config/development.toml -it $(TAG) /bin/bash + docker run --platform=linux/amd64 -v `pwd`/config/docker-configuration.toml:/local/config/development.toml -it $(TAG) /bin/bash + +init-mysql-ghcr: + DECISION_ENGINE_TAG=$(DECISION_ENGINE_TAG) GROOVY_RUNNER_TAG=$(GROOVY_RUNNER_TAG) docker compose --profile mysql-ghcr up -d + +init-pg-ghcr: + DECISION_ENGINE_TAG=$(DECISION_ENGINE_TAG) GROOVY_RUNNER_TAG=$(GROOVY_RUNNER_TAG) docker compose --profile postgres-ghcr up -d + +init-mysql-local: + docker compose --profile mysql-local up -d --build + +init-pg-local: + docker compose --profile postgres-local up -d --build + +run-mysql-ghcr: + DECISION_ENGINE_TAG=$(DECISION_ENGINE_TAG) GROOVY_RUNNER_TAG=$(GROOVY_RUNNER_TAG) docker compose --profile mysql-ghcr up -d open-router-mysql-ghcr -init: - docker-compose run --rm db-migrator && docker-compose up open-router +run-pg-ghcr: + DECISION_ENGINE_TAG=$(DECISION_ENGINE_TAG) GROOVY_RUNNER_TAG=$(GROOVY_RUNNER_TAG) docker compose --profile postgres-ghcr up -d open-router-pg-ghcr -init-pg: - docker-compose run --rm db-migrator-postgres && docker-compose up open-router-pg - -run: - docker-compose up open-router +run-mysql-local: + docker compose --profile mysql-local up -d --build open-router-mysql-local -init-local: - docker-compose run --rm db-migrator && docker-compose up --build open-router-local +run-pg-local: + docker compose --profile postgres-local up -d --build open-router-pg-local -init-local-pg: - docker-compose run --rm db-migrator-postgres && docker-compose up --build open-router-local-pg +init-pg-monitor: + DECISION_ENGINE_TAG=$(DECISION_ENGINE_TAG) GROOVY_RUNNER_TAG=$(GROOVY_RUNNER_TAG) docker compose --profile postgres-ghcr --profile monitoring up -d -run-local: - docker-compose up open-router-local +init-local-pg-monitor: + docker compose --profile postgres-local --profile monitoring up -d --build update-config: - docker-compose run --rm routing-config + docker compose --profile mysql-ghcr run --rm routing-config stop: - docker-compose down \ No newline at end of file + docker compose down + +# Backward-compatible aliases +init: init-mysql-ghcr +init-pg: init-pg-ghcr +run: run-mysql-ghcr +init-local: init-mysql-local +init-local-pg: init-pg-local +run-local: run-mysql-local diff --git a/README.md b/README.md index 8df9c155..cae69670 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,123 @@ -# Decision Engine -## Overview +# Decision Engine -The Decision Engine system helps in choosing the most optimal payment gateway in real-time for every transaction based on pre-defined rules, success rate, latency and other business requirements. It is a fully modular service that can work with any orchestrator and any PCI-compliant vaults. +
-## Vision + + + Juspay Decision Engine + -Build a reliable, open source payments software for the world \- which is interoperable, collaborative and community-driven. +
+
-## Features +Rust 1.85+ +AGPL v3 +PostgreSQL and MySQL +React dashboard +Mintlify docs -The Decision Engine comes with the following features out-of-the box for your payment routing needs. -✅ Eligibility Check – Ensures only eligible gateways are used, reducing payment failures and improving transaction success. +
+
-📌 Rule-Based Ordering – Routes transactions based on predefined merchant rules, ensuring predictable and obligation-driven payment processing. +Merchant-facing routing control plane for deciding gateways, updating score models, exposing connector analytics, and tracing payment-level audit flows from one product. -🔄 Dynamic Gateway Ordering – Uses real-time success rates and ML-driven optimization to route transactions to the best-performing gateway. +
+
-⚠️ Downtime Detection – Monitors gateway health, dynamically reordering or pausing routing to prevent transaction failures during downtime. +Quick Start +· +Dashboard +· +Analytics +· +Payment Audit +· +API Reference -To learn more, refer to this blog: [https://juspay.io/blog/juspay-orchestrator-and-merchant-controlled-routing-engine](https://juspay.io/blog/juspay-orchestrator-and-merchant-controlled-routing-engine) +
+
-## Architecture +![Decision Engine Analytics](docs/readme/analytics-overview.gif) -![](https://cdn.sanity.io/images/9sed75bn/production/fd872ae5b086e7a60011ad9d4d5c7988e1084d03-1999x1167.png) +## Table of Contents -### How it can fit into your existing architecture +- [What You Can Do](#what-you-can-do) +- [Quickstart](#quickstart) +- [Dashboard Surfaces](#dashboard-surfaces) +- [Docs Map](#docs-map) +- [Development](#development) +- [License](#license) -image +## What You Can Do -## Try it out - - Check the [SETUP.md](/docs/setup-guide-mysql.md) for detailed steps to try out the application. +- **Route with live score context** + Use `POST /decide-gateway` and `POST /update-gateway-score` to choose connectors and continuously feed transaction outcomes back into the scoring model. +- **Mix static and dynamic routing** + Combine `priority`, `single`, `volume_split`, and `advanced` rule-based routing under `/routing/*`, while keeping `/rule/*` for runtime routing configuration. +- **Operate from a real dashboard** + The React dashboard ships with routing views, analytics, connector score trends, and payment audit timelines instead of forcing operators into Redis, SQL, or raw logs. +- **Explain why a payment routed where it did** + Payment Audit links decision responses, rule hits, score updates, and connector score context so teams can answer why a connector was selected at that point in time. +## Quickstart +```bash +git clone https://github.com/juspay/decision-engine.git +cd decision-engine +docker compose --profile postgres-ghcr up -d +curl http://localhost:8080/health +``` -## API Reference : +Expected response: -Check the [API_REFERENCE.md](/docs/api-reference1.md) for more details +```json +{"message":"Health is good"} +``` +For the full local setup matrix, source runs, dashboard profiles, and Helm flows, use [docs/local-setup.md](docs/local-setup.md). -## Support, Feature Requests, Bugs +## Dashboard Surfaces -For any support, join the conversation in [Slack](https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2jqxmpsbm-WXUENx022HjNEy~Ark7Orw) - -For new product features, enhancements, roadmap discussions, or to share queries and ideas, visit our [GitHub Discussions](https://github.com/juspay/decision-engine/discussions) +| Surface | Why you open it | Primary doc | +| --- | --- | --- | +| `Analytics` | live connector metrics, score snapshots, rule hits, and routing summaries | [docs/analytics.mdx](docs/analytics.mdx) | +| `Payment Audit` | payment-by-payment request, response, score context, and timeline inspection | [docs/payment-audit.mdx](docs/payment-audit.mdx) | +| `Routing Hub` | configure rule-based routing, SR-based routing, volume splits, and debit routing | [docs/dashboard.mdx](docs/dashboard.mdx) | +| `API Reference` | OpenAPI-backed endpoint pages and curl examples | [docs/api-reference.md](docs/api-reference.md) | -For reporting a bug, please read the issue guidelines and search for [existing and closed issues]. If your problem or idea is not addressed yet, please [open a new issue]. +## Docs Map -[existing and closed issues]: https://github.com/juspay/decision-engine/issues -[open a new issue]: https://github.com/juspay/decision-engine/issues/new/choose - +- [Introduction](docs/introduction.mdx) +- [Local Setup](docs/local-setup.md) +- [Configuration](docs/configuration.md) +- [Dashboard](docs/dashboard.mdx) +- [Analytics](docs/analytics.mdx) +- [Payment Audit](docs/payment-audit.mdx) +- [API Reference](docs/api-reference.md) +- [API Examples](docs/api-reference1.md) +- [OpenAPI Source](docs/openapi.json) +- [PostgreSQL Setup](docs/setup-guide-postgres.md) +- [MySQL Setup](docs/setup-guide-mysql.md) -## Contributing +## Development -We welcome contributions from everyone\! Here's how you can help: +```bash +# lint +just clippy -See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. +# compile matrix +just check -## Versioning +# tests +cargo test -Check the [CHANGELOG.md](CHANGELOG.md) file for details. +# postgres migrations +just migrate-pg +``` -## Copyright and License +The repo also ships a one-command local startup path in [oneclick.sh](oneclick.sh), which brings up the API, dashboard, and docs preview together. -This product is licensed under the [AGPL V3](LICENSE) License. +## License + +Licensed under [GNU AGPL v3.0](LICENSE). diff --git a/analytics/README.md b/analytics/README.md new file mode 100644 index 00000000..966e12c9 --- /dev/null +++ b/analytics/README.md @@ -0,0 +1,303 @@ +# Decision Engine Analytics + +This directory contains the analytics infrastructure for the Decision Engine project, implementing real-time event tracking and analytics using ClickHouse and Kafka. + +## Architecture + +The analytics system follows the Hyperswitch analytics pattern with the following components: + +``` +┌─────────────────────┐ +│ Decision Engine │ +│ (Main Service) │ +└──────────┬──────────┘ + │ Events + ▼ +┌─────────────────────┐ +│ Kafka │ +│ (Event Stream) │ +└──────────┬──────────┘ + │ Real-time + ▼ +┌─────────────────────┐ +│ ClickHouse │ +│ ┌───────────────┐ │ +│ │ Kafka Engine │ │ +│ │ Tables │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────┐ │ +│ │ Materialized │ │ +│ │ Views │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────┐ │ +│ │ Storage │ │ +│ │ Tables │ │ +│ └───────────────┘ │ +└─────────────────────┘ +``` + +## Components + +### 1. Event Tracking +- **Endpoints Tracked**: `/routing/evaluate` and `/decide-gateway` +- **Event Data**: Request/response payloads, processing time, gateway selection, errors +- **Middleware**: Automatic event capture for tracked endpoints + +### 2. Data Storage +- **Kafka Topics**: `decision-engine-routing-events` +- **ClickHouse Tables**: + - `routing_events_queue` (Kafka engine for ingestion) + - `routing_events` (CollapsingMergeTree for storage) + - `routing_events_hourly` (Hourly aggregations) + - `routing_events_daily` (Daily aggregations) + +### 3. Real-time Processing +- **Kafka Producer**: Batched event publishing +- **Materialized Views**: Real-time data transformation +- **Aggregations**: Automatic hourly and daily rollups + +## Quick Start + +### 1. Start Analytics Infrastructure + +```bash +# Start with analytics profile +docker-compose --profile analytics up -d + +# This will start: +# - Zookeeper +# - Kafka +# - ClickHouse +# - Analytics migrator (runs schema setup) +``` + +### 2. Enable Analytics in Configuration + +Update your `config/development.toml`: + +```toml +[analytics] +enabled = true + +[analytics.kafka] +brokers = ["kafka:29092"] +topic_prefix = "decision-engine" +batch_size = 100 +batch_timeout_ms = 1000 + +[analytics.clickhouse] +host = "http://clickhouse:8123" +username = "analytics_user" +password = "analytics_pass" +database = "decision_engine_analytics" +``` + +### 3. Start Decision Engine + +```bash +# Start the main application +docker-compose up open-router-local +``` + +## Database Schema + +### Routing Events Table + +```sql +CREATE TABLE routing_events ( + event_id String, + merchant_id LowCardinality(String), + request_id String, + endpoint LowCardinality(String), + method LowCardinality(String), + request_payload String, + response_payload String, + status_code UInt16, + processing_time_ms UInt32, + gateway_selected LowCardinality(Nullable(String)), + routing_algorithm_id Nullable(String), + error_message Nullable(String), + user_agent Nullable(String), + ip_address Nullable(String), + created_at DateTime DEFAULT now(), + inserted_at DateTime DEFAULT now(), + sign_flag Int8 +) ENGINE = CollapsingMergeTree(sign_flag) +PARTITION BY toStartOfDay(created_at) +ORDER BY (created_at, merchant_id, request_id, event_id); +``` + +### Aggregated Views + +- **Hourly Aggregations**: Request counts, success/failure rates, performance metrics +- **Daily Aggregations**: Daily summaries with unique request tracking + +## Querying Analytics Data + +### Basic Queries + +```sql +-- Total requests by endpoint +SELECT + endpoint, + sum(sign_flag) as total_requests +FROM routing_events +WHERE created_at >= now() - INTERVAL 1 DAY +GROUP BY endpoint; + +-- Success rate by gateway +SELECT + gateway_selected, + sum(if(status_code < 400, sign_flag, 0)) as successful_requests, + sum(sign_flag) as total_requests, + (successful_requests / total_requests) * 100 as success_rate +FROM routing_events +WHERE created_at >= now() - INTERVAL 1 DAY + AND gateway_selected IS NOT NULL +GROUP BY gateway_selected; + +-- Performance metrics +SELECT + endpoint, + avg(processing_time_ms) as avg_processing_time, + quantile(0.95)(processing_time_ms) as p95_processing_time, + quantile(0.99)(processing_time_ms) as p99_processing_time +FROM routing_events +WHERE created_at >= now() - INTERVAL 1 DAY +GROUP BY endpoint; +``` + +### Using Aggregated Tables + +```sql +-- Hourly trends +SELECT + hour, + endpoint, + total_requests, + successful_requests, + (successful_requests / total_requests) * 100 as success_rate, + avg_processing_time_ms +FROM routing_events_hourly +WHERE hour >= now() - INTERVAL 24 HOUR +ORDER BY hour DESC; +``` + +## Configuration Options + +### Analytics Configuration + +```toml +[analytics] +enabled = true # Enable/disable analytics + +[analytics.kafka] +brokers = ["kafka:29092"] # Kafka broker addresses +topic_prefix = "decision-engine" # Topic prefix for events +batch_size = 100 # Events per batch +batch_timeout_ms = 1000 # Batch timeout in milliseconds + +[analytics.clickhouse] +host = "http://clickhouse:8123" # ClickHouse HTTP endpoint +username = "analytics_user" # ClickHouse username +password = "analytics_pass" # ClickHouse password +database = "decision_engine_analytics" # Database name +``` + +## Monitoring and Maintenance + +### Health Checks + +The analytics client provides health check functionality: + +```rust +// Check analytics connectivity +analytics_client.health_check().await?; +``` + +### Data Retention + +- **Raw Events**: 18 months (configurable via TTL) +- **Hourly Aggregations**: 12 months +- **Daily Aggregations**: 24 months + +### Performance Considerations + +1. **Batch Processing**: Events are batched for efficient Kafka publishing +2. **Async Processing**: Analytics don't block request processing +3. **Fallback Handling**: Graceful degradation when analytics are unavailable +4. **Partitioning**: Data partitioned by day for efficient queries + +## Troubleshooting + +### Common Issues + +1. **Kafka Connection Failed** + - Check Kafka broker connectivity + - Verify topic creation + - Check network configuration + +2. **ClickHouse Connection Failed** + - Verify ClickHouse is running + - Check credentials and database existence + - Verify network connectivity + +3. **Missing Events** + - Check analytics middleware is enabled + - Verify endpoint tracking configuration + - Check Kafka topic consumption + +### Debugging + +Enable debug logging for analytics: + +```toml +[log.console] +level = "DEBUG" +``` + +Check analytics client status: + +```bash +# Check if analytics is enabled +curl http://localhost:8080/health + +# Check Kafka topics +docker exec -it open-router-kafka kafka-topics --bootstrap-server localhost:9092 --list + +# Check ClickHouse tables +docker exec -it open-router-clickhouse clickhouse-client --query "SHOW TABLES FROM decision_engine_analytics" +``` + +## Development + +### Adding New Event Types + +1. Extend `RoutingEventData` struct in `src/analytics/types.rs` +2. Update ClickHouse schema in `analytics/migrations/` +3. Modify event extraction logic in middleware +4. Update aggregation queries if needed + +### Testing + +```bash +# Run analytics tests +cargo test analytics + +# Test with sample events +curl -X POST http://localhost:8080/routing/evaluate \ + -H "Content-Type: application/json" \ + -H "x-merchant-id: test-merchant" \ + -d '{"test": "data"}' +``` + +## Security Considerations + +1. **Data Privacy**: Ensure sensitive data is properly masked +2. **Access Control**: Restrict ClickHouse access to authorized users +3. **Network Security**: Use proper network isolation +4. **Data Retention**: Implement appropriate data retention policies diff --git a/analytics/migrations/001_routing_events.sql b/analytics/migrations/001_routing_events.sql new file mode 100644 index 00000000..27faa863 --- /dev/null +++ b/analytics/migrations/001_routing_events.sql @@ -0,0 +1,108 @@ +-- Merged SQL Migration File for Decision Engine Analytics +-- This file combines all migration steps into a single comprehensive migration +-- Created by merging: 001_routing_events.sql, 002_fix_datetime_parsing.sql, 002_fix_datetime_parsing_alternative.sql, 003_unix_timestamp_fix.sql + +-- ============================================================================= +-- STEP 1: Database Setup +-- ============================================================================= + +-- Create database if not exists +CREATE DATABASE IF NOT EXISTS decision_engine_analytics; + +-- Use the analytics database +USE decision_engine_analytics; + +-- ============================================================================= +-- STEP 2: Create Storage Tables (Final Schema) +-- ============================================================================= + +-- Create storage table with CollapsingMergeTree for routing events +CREATE TABLE IF NOT EXISTS routing_events ( + `event_id` String, + `merchant_id` LowCardinality(String), + `request_id` String, + `endpoint` LowCardinality(String), + `method` LowCardinality(String), + `request_payload` String, + `response_payload` String, + `status_code` UInt16, + `processing_time_ms` UInt32, + `gateway_selected` LowCardinality(Nullable(String)), + `routing_algorithm_id` Nullable(String), + `error_message` Nullable(String), + `user_agent` Nullable(String), + `ip_address` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX endpointIndex endpoint TYPE bloom_filter GRANULARITY 1, + INDEX gatewayIndex gateway_selected TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1, + INDEX merchantIndex merchant_id TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree(sign_flag) +PARTITION BY toStartOfDay(created_at) +ORDER BY (created_at, merchant_id, request_id, event_id) +TTL created_at + toIntervalMonth(18) +SETTINGS index_granularity = 8192; + +-- ============================================================================= +-- STEP 3: Create Kafka Integration (Final Version with Unix Timestamp Support) +-- ============================================================================= + +-- Drop existing views and tables if they exist (for clean migration) +DROP VIEW IF EXISTS routing_events_mv; +DROP TABLE IF EXISTS routing_events_queue; + +-- Create Kafka engine table with Unix timestamp handling (final version) +CREATE TABLE IF NOT EXISTS routing_events_queue ( + `event_id` String, + `merchant_id` String, + `request_id` String, + `endpoint` LowCardinality(String), + `method` LowCardinality(String), + `request_payload` String, + `response_payload` String, + `status_code` UInt16, + `processing_time_ms` UInt32, + `gateway_selected` LowCardinality(Nullable(String)), + `routing_algorithm_id` Nullable(String), + `error_message` Nullable(String), + `user_agent` Nullable(String), + `ip_address` Nullable(String), + `created_at` Int64, -- Unix timestamp (final fix) + `sign_flag` Int8 +) ENGINE = Kafka +SETTINGS + kafka_broker_list = 'open-router-kafka:29092', + kafka_topic_list = 'decision-engine-routing-events', + kafka_group_name = 'decision-engine-analytics', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +-- Create materialized view with Unix timestamp conversion (final version) +CREATE MATERIALIZED VIEW IF NOT EXISTS routing_events_mv TO routing_events AS +SELECT + event_id, + merchant_id, + request_id, + endpoint, + method, + request_payload, + response_payload, + status_code, + processing_time_ms, + gateway_selected, + routing_algorithm_id, + error_message, + user_agent, + ip_address, + -- Convert Unix timestamp to DateTime (final fix) + CASE + WHEN created_at > 0 + THEN toDateTime(created_at) + ELSE now() + END AS created_at, + now() AS inserted_at, + sign_flag +FROM routing_events_queue +WHERE length(_error) = 0; diff --git a/analytics/run_migrations.sh b/analytics/run_migrations.sh new file mode 100755 index 00000000..fa3a4e02 --- /dev/null +++ b/analytics/run_migrations.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +echo 'Waiting for ClickHouse to be ready...' +sleep 10 + +echo 'Running analytics migrations...' +for file in /analytics/migrations/*.sql; do + if [ -f "$file" ]; then + echo "Executing: $file" + clickhouse-client --host clickhouse --port 9000 --user analytics_user --password analytics_pass --multiquery --query "$(cat $file)" + if [ $? -eq 0 ]; then + echo "Successfully executed: $file" + else + echo "Failed to execute: $file" + exit 1 + fi + fi +done + +echo 'Analytics migrations completed!' diff --git a/config.example.toml b/config.example.toml index a5733002..ed691db9 100644 --- a/config.example.toml +++ b/config.example.toml @@ -11,11 +11,14 @@ port = 3001 # The port where the server should be hosted on request_count = 1 # The requests per duration duration = 60 # duration to rate limit the delete api (in sec) - [cache] tti = 7200 # Idle time after a get/insert of a cache entry to free the cache (in secs) max_capacity = 5000 # Max capacity of a single table cache +[cache_config] +service_config_redis_prefix = "DE_service_config_" +service_config_ttl = 300 # 5 minutes + [database] username = "sam" # username for the database password = "damn" # password of the database @@ -63,4 +66,3 @@ private_key = "key.pem" # path to the private key file (`pem` format) client_idle_timeout = 90 # timeout for idle sockets being kept-alive pool_max_idle_per_host = 10 # maximum idle connection per host allowed in the pool. identity = "" # identity to be used for client certificate authentication in mtls. - diff --git a/config/development.toml b/config/development.toml index 56b26693..bf996e26 100644 --- a/config/development.toml +++ b/config/development.toml @@ -9,7 +9,7 @@ port = 8080 [metrics] host = "127.0.0.1" -port = 9090 +port = 9094 [limit] request_count = 1 @@ -49,164 +49,689 @@ max_feed_count = 200 tti = 7200 # i.e. 2 hours max_capacity = 5000 +[cache_config] +service_config_redis_prefix = "DE_service_config_" +service_config_ttl = 300 # 5 minutes + +[compression_filepath] +zstd_compression_filepath = "/tmp/extra-paths/redis-zstd-dictionaries/sbx" + +[analytics] +enabled = false + +[analytics.kafka] +brokers = ["localhost:9092"] +topic_prefix = "decision-engine" +batch_size = 100 +batch_timeout_ms = 1000 +max_consecutive_failures = 5 + +[analytics.clickhouse] +host = "http://clickhouse:8123" +username = "analytics_user" +password = "analytics_pass" +database = "decision_engine_analytics" + +[secrets] +open_router_private_key = "" + [tenant_secrets] public = { schema = "public" } [routing_config.keys] +amount = { type = "integer", min = 0 } +authentication_type = { type = "enum", values = "three_ds, no_three_ds" } +bank_debit = { type = "enum", values = "ach, sepa, bacs, becs" } +bank_redirect = { type = "enum", values = "giropay, ideal, sofort, eft, eps, bancontact_card, blik, local_bank_redirect, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, przelewy24, trustly, bizum, interac, open_banking_uk, open_banking_pis" } +bank_transfer = { type = "enum", values = "ach, sepa, sepa_bank_transfer, bacs, multibanco, pix, pse, permata_bank_transfer, bca_bank_transfer, bni_va, bri_va, cimb_va, danamon_va, mandiri_va, local_bank_transfer, instant_bank_transfer" } billing_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } -issuer_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } business_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } business_label = { type = "str_value" } -metadata = { type = "udf" } -pay_later = { type = "enum", values = "affirm, alma, afterpay_clearpay, klarna, pay_bright, atome, walley" } -gift_card = { type = "enum", values = "givex, pay_safe_card" } -upi = { type = "enum", values = "upi_collect, upi_intent" } -wallet = { type = "enum", values = "amazon_pay, apple_pay, google_pay, paypal, ali_pay, ali_pay_hk, dana, mb_way, mobile_pay, samsung_pay, twint, vipps, touch_n_go, swish, we_chat_pay, go_pay, gcash, momo, kakao_pay, cashapp, mifinity, paze" } -voucher = { type = "enum", values = "boleto, efecty, pago_efectivo, red_compra, red_pagos, indomaret, alfamart, oxxo, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy" } -bank_transfer = { type = "enum", values = "ach, sepa, sepa_bank_transfer, bacs, multibanco, pix, pse, permata_bank_transfer, bca_bank_transfer, bni_va, bri_va, cimb_va, danamon_va, mandiri_va, local_bank_transfer, instant_bank_transfer" } -bank_redirect = { type = "enum", values = "giropay, ideal, sofort, eft, eps, bancontact_card, blik, local_bank_redirect, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, przelewy24, trustly, bizum, interac, open_banking_uk, open_banking_pis" } -customer_device_display_size = { type = "enum", values = "size320x568, size375x667, size390x844, size414x896, size428x926, size768x1024, size834x1112, size834x1194, size1024x1366, size1280x720, size1366x768, size1440x900, size1920x1080, size2560x1440, size3840x2160, size500x600, size600x400, size360x640, size412x915, size800x1280" } -customer_device_type = { type = "enum", values = "mobile, tablet, desktop, gaming_console" } -customer_device_platform = { type = "enum", values = "web, android, ios" } -bank_debit = { type = "enum", values = "ach, sepa, bacs, becs" } -crypto = { type = "enum", values = "crypto_currency" } -reward = { type = "enum", values = "evoucher, classic_reward" } +capture_method = { type = "enum", values = "automatic, manual, manual_multiple, scheduled, sequential_automatic" } +card = { type = "enum", values = "debit, credit" } +card_bin = { type = "str_value", exact_length = 6, regex = "^[0-9]{6}$" } +card_discovery = { type = "enum", values = "manual, saved_card, click_to_pay" } +card_network = { type = "enum", values = "visa, VISA, Visa, visaCard, mastercard, MASTERCARD, MasterCard, masterCard, mastercardCard, master_card, Master_card, Master_Card, Master Card, Mastercard, american_express, AMERICANEXPRESS, AmericanExpress, americanExpress, americanExpressCard, amex, AMEX, Amex, jcb, JCB, Jcb, diners_club, DINERSCLUB, DinersClub, dinersClub, dinersClubCard, discover, DISCOVER, Discover, discoverCard, cartes_bancaires, CARTESBANCAIRES, CartesBancaires, cartesBancaires, union_pay, UNIONPAY, UnionPay, unionPay, interac, INTERAC, Interac, rupay, RUPAY, RuPay, ruPay, maestro, MAESTRO, Maestro, star, STAR, Star, pulse, PULSE, Pulse, accel, ACCEL, Accel, nyce, NYCE, Nyce" } card_redirect = { type = "enum", values = "knet, benefit, momo_atm, card_redirect" } -real_time_payment = { type = "enum", values = "fps, duit_now, prompt_pay, viet_qr" } -open_banking = { type = "enum", values = "open_banking_pis" } -mobile_payment = { type = "enum", values = "direct_carrier_billing" } -payment_method = { type = "enum", values = "card, card_redirect, pay_later, wallet, bank_redirect, bank_transfer, crypto, bank_debit, reward, real_time_payment, upi, voucher, gift_card, open_banking, mobile_payment" } card_type = { type = "enum", values = "debit, credit" } -payment_card_bin = { type = "udf" } -issuer_name = { type = "str_value" } -payment_card_type = { type = "enum", values = "CREDIT, DEBIT" } +crypto = { type = "enum", values = "crypto_currency" } +currency = { type = "enum", values = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SLL, SOS, SRD, SSP, STD, STN, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +extended_card_bin = { type = "str_value", exact_length = 8, regex = "^[0-9]{8}$" } +gift_card = { type = "enum", values = "givex, pay_safe_card" } +issuer_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } +issuer_name = { type = "str_value", min_length = 1 } mandate_acceptance_type = { type = "enum", values = "online, offline" } mandate_type = { type = "enum", values = "single_use, multi_use" } -card_network = { type = "enum", values = "visa, VISA, Visa, visaCard, mastercard, MASTERCARD, MasterCard, masterCard, mastercardCard, master_card, Master_card, Master_Card, Master Card, Mastercard, american_express, AMERICANEXPRESS, AmericanExpress, americanExpress, americanExpressCard, amex, AMEX, Amex, jcb, JCB, Jcb, diners_club, DINERSCLUB, DinersClub, dinersClub, dinersClubCard, discover, DISCOVER, Discover, discoverCard, cartes_bancaires, CARTESBANCAIRES, CartesBancaires, cartesBancaires, union_pay, UNIONPAY, UnionPay, unionPay, interac, INTERAC, Interac, rupay, RUPAY, RuPay, ruPay, maestro, MAESTRO, Maestro, star, STAR, Star, pulse, PULSE, Pulse, accel, ACCEL, Accel, nyce, NYCE, Nyce" } -payment_type = { type = "enum", values = "normal, new_mandate, setup_mandate, recurring_mandate, non_mandate" } +metadata = { type = "udf" } +mobile_payment = { type = "enum", values = "direct_carrier_billing" } +network_token = { type = "enum", values = "network_token" } +open_banking = { type = "enum", values = "open_banking_pis" } +pay_later = { type = "enum", values = "affirm, alma, afterpay_clearpay, klarna, pay_bright, atome, walley" } +payment_method = { type = "enum", values = "card, card_redirect, pay_later, wallet, bank_redirect, bank_transfer, crypto, bank_debit, reward, real_time_payment, upi, voucher, gift_card, open_banking, mobile_payment" } payment_method_type = { type = "enum", values = "ach, affirm, afterpay_clearpay, alfamart, ali_pay, ali_pay_hk, alma, amazon_pay, apple_pay, atome, bacs, bancontact_card, becs, benefit, bizum, blik, boleto, bca_bank_transfer, bni_va, bri_va, card, card_redirect, cimb_va, classic_reward, credit, crypto_currency, cashapp, dana, danamon_va, debit, duit_now, efecty, eft, eps, fps, evoucher, giropay, givex, google_pay, go_pay, gcash, ideal, interac, indomaret, klarna, kakao_pay, local_bank_redirect, mandiri_va, knet, mb_way, mobile_pay, momo, momo_atm, multibanco, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, oxxo, pago_efectivo, permata_bank_transfer, open_banking_uk, pay_bright, paypal, paze, pix, pay_safe_card, przelewy24, prompt_pay, pse, red_compra, red_pagos, samsung_pay, sepa, sepa_bank_transfer, sofort, swish, touch_n_go, trustly, twint, upi_collect, upi_intent, vipps, viet_qr, venmo, walley, we_chat_pay, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy, local_bank_transfer, mifinity, open_banking_pis, direct_carrier_billing, instant_bank_transfer" } -authentication_type = { type = "enum", values = "three_ds, no_three_ds" } -capture_methods = { type = "enum", values = "automatic, manual, manual_multiple, scheduled, sequential_automatic" } +payment_type = { type = "enum", values = "normal, new_mandate, setup_mandate, recurring_mandate, non_mandate" } +real_time_payment = { type = "enum", values = "fps, duit_now, prompt_pay, viet_qr" } +reward = { type = "enum", values = "evoucher, classic_reward" } setup_future_usage = { type = "enum", values = "on_session, off_session" } -payment_card_network = { type = "enum", values = "visa, mastercard, american_express, jcb, diners_club, discover, cartes_bancaires, union_pay, interac, rupay, maestro" } -amount = { type = "integer" } -login_date = { type = "str_value" } -currency = { type = "enum", values = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SLL, SOS, SRD, SSP, STD, STN, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } -payment_card_issuer_country = { type = "enum", values = "AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, AZ, BS, BH, BD, BB, BY, BE, BZ, BJ, BM, BT, BO, BQ, BA, BW, BV, BR, IO, BN, BG, BF, BI, KH, CM, CA, CV, KY, CF, TD, CL, CN, CX, CC, CO, KM, CG, CD, CK, CR, CI, HR, CU, CW, CY, CZ, DK, DJ, DM, DO, EC, EG, SV, GQ, ER, EE, ET, FK, FO, FJ, FI, FR, GF, PF, TF, GA, GM, GE, DE, GH, GI, GR, GL, GD, GP, GU, GT, GG, GN, GW, GY, HT, HM, VA, HN, HK, HU, IS, IN, ID, IR, IQ, IE, IM, IL, IT, JM, JP, JE, JO, KZ, KE, KI, KP, KR, KW, KG, LA, LV, LB, LS, LR, LY, LI, LT, LU, MO, MK, MG, MW, MY, MV, ML, MT, MH, MQ, MR, MU, YT, MX, FM, MD, MC, MN, ME, MS, MA, MZ, MM, NA, NR, NP, NL, NC, NZ, NI, NE, NG, NU, NF, MP, NO, OM, PK, PW, PS, PA, PG, PY, PE, PH, PN, PL, PT, PR, QA, RE, RO, RU, RW, BL, SH, KN, LC, MF, PM, VC, WS, SM, ST, SA, SN, RS, SC, SL, SG, SX, SK, SI, SB, SO, ZA, GS, SS, ES, LK, SD, SR, SJ, SZ, SE, CH, SY, TW, TJ, TZ, TH, TL, TG, TK, TO, TT, TN, TR, TM, TC, TV, UG, UA, AE, GB, UM, UY, UZ, VU, VE, VN, VG, VI, WF, EH, YE, ZM, ZW, US" } -card_bin = { type = "str_value" } -capture_method = { type = "enum", values = "automatic, manual" } -new_customer = { type = "udf" } -udf1 = { type = "str_value" } -order_udf1 = { type = "global_ref" } -payment_payment_method = { type = "enum", values = "NB_HDFC, NB_ICICI, NB_SBI" } -payment_payment_source = { type = "enum", values = "net.one97.paytm, @paytm" } -txn_is_emi = { type = "enum", values = "true, false" } - -[routing_config.default] -output = ["stripe", "adyen"] - -[[routing_config.constraint_graph.nodes]] -preds = [] -succs = [0] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "payment_method" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "card" - -[[routing_config.constraint_graph.nodes]] -preds = [] -succs = [1] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "payment_method" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "bank_debit" - -[[routing_config.constraint_graph.nodes]] -preds = [0] -succs = [] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "output" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "stripe" - -[[routing_config.constraint_graph.nodes]] -preds = [1] -succs = [] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "output" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "adyen" - -[[routing_config.constraint_graph.edges]] -strength = "strong" -relation = "positive" -pred = 0 -succ = 2 - -[[routing_config.constraint_graph.edges]] -strength = "strong" -relation = "positive" -pred = 1 -succ = 3 +transaction_initiator = { type = "enum", values = "customer, merchant" } +upi = { type = "enum", values = "upi_collect, upi_intent" } +voucher = { type = "enum", values = "boleto, efecty, pago_efectivo, red_compra, red_pagos, indomaret, alfamart, oxxo, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy" } +wallet = { type = "enum", values = "amazon_pay, apple_pay, google_pay, paypal, ali_pay, ali_pay_hk, dana, mb_way, mobile_pay, samsung_pay, twint, vipps, touch_n_go, swish, we_chat_pay, go_pay, gcash, momo, kakao_pay, cashapp, mifinity, paze" } [debit_routing_config] -fraud_check_fee = 0.01 +fraud_check_fee = 1.0 [debit_routing_config.network_fee] -visa = { percentage = 0.1375, fixed_amount = 0.020 } -mastercard = { percentage = 0.15, fixed_amount = 0.040 } -accel = { percentage = 0.0, fixed_amount = 0.040 } -nyce = { percentage = 0.10, fixed_amount = 0.015 } -pulse = { percentage = 0.10, fixed_amount = 0.03 } -star = { percentage = 0.10, fixed_amount = 0.015 } +visa = { percentage = 0.1375, fixed_amount = 2.0 } +mastercard = { percentage = 0.15, fixed_amount = 4.0 } +accel = { percentage = 0.0, fixed_amount = 4.0 } +nyce = { percentage = 0.10, fixed_amount = 1.5 } +pulse = { percentage = 0.10, fixed_amount = 3.0 } +star = { percentage = 0.10, fixed_amount = 1.5 } [debit_routing_config.interchange_fee] regulated = { percentage = 0.05, fixed_amount = 0.21 } [debit_routing_config.interchange_fee.non_regulated] -merchant_category_code_0001.visa = { percentage = 1.65, fixed_amount = 0.15 } -merchant_category_code_0001.mastercard = { percentage = 1.65, fixed_amount = 0.15 } -merchant_category_code_0001.accel = { percentage = 1.55, fixed_amount = 0.04 } -merchant_category_code_0001.nyce = { percentage = 1.30, fixed_amount = 0.213125 } -merchant_category_code_0001.pulse = { percentage = 1.60, fixed_amount = 0.15 } -merchant_category_code_0001.star = { percentage = 1.63, fixed_amount = 0.15 } +merchant_category_code_0001.visa = { percentage = 1.65, fixed_amount = 15.0 } +merchant_category_code_0001.mastercard = { percentage = 1.65, fixed_amount = 15.0 } +merchant_category_code_0001.accel = { percentage = 1.55, fixed_amount = 4.0 } +merchant_category_code_0001.nyce = { percentage = 1.30, fixed_amount = 21.3125 } +merchant_category_code_0001.pulse = { percentage = 1.60, fixed_amount = 15.0 } +merchant_category_code_0001.star = { percentage = 1.63, fixed_amount = 15.0 } + +[pm_filters.default] +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", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +paypal = { currency = "AUD,BRL,CAD,CHF,CNY,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,MYR,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ", currency = "GBP,AUD,NZD,CAD,USD" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } + +[pm_filters.stripe] +google_pay = { country = "AU, AT, BE, BR, BG, CA, HR, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, ID, IE, IT, JP, LV, KE, LT, LU, MY, MX, NL, NZ, NO, PL, PT, RO, SG, SK, ZA, ES, SE, CH, TH, AE, GB, US, GI, LI, MT, CY, PH, IS, AR, CL, KR, IL"} +apple_pay = { country = "AU, AT, BE, BR, BG, CA, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, HK, IE, IT, JP, LV, LI, LT, LU, MT, MY, MX, NL, NZ, NO, PL, PT, RO, SK, SG, SI, ZA, ES, SE, CH, GB, AE, US" } +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" } +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +multibanco = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GI,GR,HU,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB", currency = "EUR" } +ach = { country = "US", currency = "USD" } +revolut_pay = { currency = "EUR,GBP" } +sepa = {country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GI,GR,HU,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,IS,LI", currency="EUR"} +bacs = { country = "GB", currency = "GBP" } +becs = { country = "AU", currency = "AUD" } +sofort = {country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE", currency = "EUR" } +blik = {country="PL", currency = "PLN"} +bancontact_card = { country = "BE", currency = "EUR" } +przelewy24 = { country = "PL", currency = "EUR,PLN" } +online_banking_fpx = { country = "MY", currency = "MYR" } +amazon_pay = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "USD,AUD,GBP,DKK,EUR,HKD,JPY,NZD,NOK,ZAR,SEK,CHF" } +we_chat_pay = { country = "CN", currency = "CNY,AUD,CAD,EUR,GBP,HKD,JPY,SGD,USD,DKK,NOK,SEK,CHF" } +ali_pay = {country = "CN", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,MYR,NZD,SGD,USD"} + +[pm_filters.volt] +open_banking = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "GBP,EUR,DKK,NOK,PLN,SEK" } +open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "GBP" } + +[pm_filters.razorpay] +upi_collect = { country = "IN", currency = "INR" } + +[pm_filters.phonepe] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } + +[pm_filters.paytm] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } + +[pm_filters.hyperpg] +credit = { currency = "INR,USD,GBP,EUR" } +debit = { currency = "INR,USD,GBP,EUR" } + +[pm_filters.plaid] +open_banking_pis = { currency = "EUR,GBP" } + +[pm_filters.adyen] +google_pay = { country = "AT, AU, BE, BG, CA, HR, CZ, EE, FI, FR, DE, GR, HK, DK, HU, IE, IT, LV, LT, LU, NL, NO, PL, PT, RO, SK, ES, SE, CH, GB, US, NZ, SG", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, GG, HU, IE, IM, IT, LV, LI, LT, LU, MT, NL, NO, PL, PT, RO, SK, SI, SE, ES, CH, GB, US, PR, CA, AU, HK, NZ, SG", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, CHF, UAH, ARS, BRL, CLP, COP, CRC, DOP, GTQ, HNL, MXN, PAB, USD, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, ILS, QAR, SAR, AED, CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +mb_way = { country = "PT", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +pay_bright = { country = "CA", currency = "CAD" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { not_available_flows = { capture_method = "manual" }, country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR" } +ideal = { not_available_flows = { capture_method = "manual" }, country = "NL", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +bancontact_card = { country = "BE", currency = "EUR" } +ach = { country = "US", currency = "USD" } +bacs = { country = "GB", currency = "GBP" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +ali_pay_hk = { country = "HK", currency = "HKD" } +bizum = { country = "ES", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +momo = { country = "VN", currency = "VND" } +gcash = { country = "PH", currency = "PHP" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_thailand = { country = "TH", currency = "THB" } +touch_n_go = { country = "MY", currency = "MYR" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +swish = { country = "SE", currency = "SEK" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } +pix = { country = "BR", currency = "BRL" } +boleto = { country = "BR", currency = "BRL" } + +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + +[pm_filters.airwallex] +credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "AL, DZ, AS, AO, AG, AR, AU, AZ, BH, BR, BG, CA, CL, CO, CZ, DK, DO, EG, HK, HU, ID, IL, JP, JO, KZ, KE, KW, LB, MY, MX, OM, PK, PA, PE, PH, PL, QA, RO, SA, SG, ZA, LK, SE, TW, TH, TR, UA, AE, UY, VN, AT, BE, HR, EE, FI, FR, DE, GR, IE, IT, LV, LT, LU, NL, PL, PT, SK, ES, SE, RO, BG", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +paypal = { currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,JPY,MYR,MXN,NOK,NZD,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } +klarna = { currency = "EUR, DKK, NOK, PLN, SEK, CHF, GBP, USD, CZK" } +trustly = {currency="DKK, EUR, GBP, NOK, PLN, SEK" } +blik = { country="PL" , currency = "PLN" } +ideal = { country="NL" , currency = "EUR" } +atome = { country = "SG, MY" , currency = "SGD, MYR" } +skrill = { country="AL, DZ, AD, AR, AM, AW, AU, AT, AZ, BS, BD, BE, BJ, BO, BA, BW, BR, BN, BG, KH, CM, CA, CL, CN, CX, CO, CR , HR, CW, CY, CZ, DK, DM, DO, EC, EG, EE , FK, FI, GE, DE, GH, GI, GR, GP, GU, GT, GG, HK, HU, IS, IN, ID , IQ, IE, IM, IL, IT, JE , KZ, KE , KR, KW, KG, LV , LS, LI, LT, LU , MK, MG, MY, MV, MT, MU, YT, MX, MD, MC, MN, ME, MA, NA, NP, NZ, NI, NE, NO, PK , PA, PY, PE, PH, PL, PT, PR, QA, RO , SM , SA, SN , SG, SX, SK, SI, ZA, SS, ES, LK, SE, CH, TW, TZ, TH, TN, AE, GB, UM, UY, VN, VG, VI, US" , currency = "EUR, GBP, USD" } +indonesian_bank_transfer = { country="ID" , currency = "IDR" } + +[pm_filters.elavon] +credit = { country = "US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.xendit] +credit = { country = "ID,PH", currency = "IDR,PHP,USD,SGD,MYR" } +debit = { country = "ID,PH", currency = "IDR,PHP,USD,SGD,MYR" } +qris = {currency = "IDR" } + +[pm_filters.tsys] +credit = { country = "NA", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SOS, SRD, SSP, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL, BYN, KPW, STN, MRU, VES" } +debit = { country = "NA", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SOS, SRD, SSP, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL, BYN, KPW, STN, MRU, VES" } + +[pm_filters.billwerk] +credit = { country = "DE, DK, FR, SE", currency = "DKK, NOK" } +debit = { country = "DE, DK, FR, SE", currency = "DKK, NOK" } + +[pm_filters.fiservemea] +credit = { country = "DE, FR, IT, NL, PL, ES, ZA, GB, AE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "DE, FR, IT, NL, PL, ES, ZA, GB, AE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.getnet] +credit = { country = "AR, BR, CL, MX, UY, ES, PT, DE, IT, FR, NL, BE, AT, PL, CH, GB, IE, LU, DK, SE, NO, FI, IN, AE", currency = "ARS, BRL, CLP, MXN, UYU, EUR, PLN, CHF, GBP, DKK, SEK, NOK, INR, AED" } + +[pm_filters.hipay] +credit = { country = "GB, CH, SE, DK, NO, PL, CZ, US, CA, JP, HK, AU, ZA", currency = "EUR, GBP, CHF, SEK, DKK, NOK, PLN, CZK, USD, CAD, JPY, HKD, AUD, ZAR" } +debit = { country = "GB, CH, SE, DK, NO, PL, CZ, US, CA, JP, HK, AU, ZA", currency = "EUR, GBP, CHF, SEK, DKK, NOK, PLN, CZK, USD, CAD, JPY, HKD, AUD, ZAR" } + +[pm_filters.moneris] +credit = { country = "AE, AF, AL, AO, AR, AT, AU, AW, AZ, BA, BB, BD, BE, BG, BH, BI, BM, BN, BO, BR, BT, BY, BZ, CH, CL, CN, CO, CR, CU, CV, CY, CZ, DE, DJ, DK, DO, DZ, EE, EG, ES, FI, FJ, FR, GB, GE, GI, GM, GN, GR, GT, GY, HK, HN, HR, HT, HU, ID, IE, IL, IN, IS, IT, JM, JO, JP, KE, KM, KR, KW, KY, KZ, LA, LK, LR, LS, LV, LT, LU, MA, MD, MG, MK, MO, MR, MT, MU, MV, MW, MX, MY, MZ, NA, NG, NI, NL, NO, NP, NZ, OM, PE, PG, PK, PL, PT, PY, QA, RO, RS, RU, RW, SA, SB, SC, SE, SG, SH, SI, SK, SL, SR, SV, SZ, TH, TJ, TM, TN, TR, TT, TW, TZ, UG, US, UY, UZ, VN, VU, WS, ZA, ZM", currency = "AED, AFN, ALL, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BTN, BYN, BZD, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, EUR, FJD, GBP, GEL, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JOD, JPY, KES, KMF, KRW, KWD, KYD, KZT, LAK, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SRD, SVC, SZL, THB, TJS, TMT, TND, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VND, VUV, WST, XCD, XOF, XPF, ZAR, ZMW" } +debit = { country = "AE, AF, AL, AO, AR, AT, AU, AW, AZ, BA, BB, BD, BE, BG, BH, BI, BM, BN, BO, BR, BT, BY, BZ, CH, CL, CN, CO, CR, CU, CV, CY, CZ, DE, DJ, DK, DO, DZ, EE, EG, ES, FI, FJ, FR, GB, GE, GI, GM, GN, GR, GT, GY, HK, HN, HR, HT, HU, ID, IE, IL, IN, IS, IT, JM, JO, JP, KE, KM, KR, KW, KY, KZ, LA, LK, LR, LS, LV, LT, LU, MA, MD, MG, MK, MO, MR, MT, MU, MV, MW, MX, MY, MZ, NA, NG, NI, NL, NO, NP, NZ, OM, PE, PG, PK, PL, PT, PY, QA, RO, RS, RU, RW, SA, SB, SC, SE, SG, SH, SI, SK, SL, SR, SV, SZ, TH, TJ, TM, TN, TR, TT, TW, TZ, UG, US, UY, UZ, VN, VU, WS, ZA, ZM", currency = "AED, AFN, ALL, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BTN, BYN, BZD, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, EUR, FJD, GBP, GEL, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JOD, JPY, KES, KMF, KRW, KWD, KYD, KZT, LAK, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SRD, SVC, SZL, THB, TJS, TMT, TND, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VND, VUV, WST, XCD, XOF, XPF, ZAR, ZMW" } + +[pm_filters.opennode] +crypto_currency = { country = "US, CA, GB, AU, BR, MX, SG, PH, NZ, ZA, JP, AT, BE, HR, CY, EE, FI, FR, DE, GR, IE, IT, LV, LT, LU, MT, NL, PT, SK, SI, ES", currency = "USD, CAD, GBP, AUD, BRL, MXN, SGD, PHP, NZD, ZAR, JPY, EUR" } + +[pm_filters.bambora] +credit = { country = "US,CA", currency = "USD" } +debit = { country = "US,CA", currency = "USD" } + +[pm_filters.bankofamerica] +credit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "USD" } +debit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "USD" } +apple_pay = { country = "AU,AT,BH,BE,BR,BG,CA,CL,CN,CO,CR,HR,CY,CZ,DK,DO,EC,EE,SV,FI,FR,DE,GR,GT,HN,HK,HU,IS,IN,IE,IL,IT,JP,JO,KZ,KW,LV,LI,LT,LU,MY,MT,MX,MC,ME,NL,NZ,NO,OM,PA,PY,PE,PL,PT,QA,RO,SA,SG,SK,SI,ZA,KR,ES,SE,CH,TW,AE,GB,US,UY,VN,VA", currency = "USD" } +google_pay = { country = "AU,AT,BE,BR,CA,CL,CO,CR,CY,CZ,DK,DO,EC,EE,SV,FI,FR,DE,GR,GT,HN,HK,HU,IS,IN,IE,IL,IT,JP,JO,KZ,KW,LV,LI,LT,LU,MY,MT,MX,NL,NZ,NO,OM,PA,PY,PE,PL,PT,QA,RO,SA,SG,SK,SI,ZA,KR,ES,SE,CH,TW,AE,GB,US,UY,VN,VA", currency = "USD" } +samsung_pay = { country = "AU,BH,BR,CA,CN,DK,FI,FR,DE,HK,IN,IT,JP,KZ,KR,KW,MY,NZ,NO,OM,QA,SA,SG,ZA,ES,SE,CH,TW,AE,GB,US", currency = "USD" } + +[pm_filters.cybersource] +credit = { currency = "USD,GBP,EUR,PLN,SEK,XOF,CAD,KWD,QAR" } +debit = { currency = "USD,GBP,EUR,PLN,SEK,XOF,CAD,KWD,QAR" } +apple_pay = { currency = "ARS, CAD, CLP, COP, CNY, EUR, HKD, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, GBP, AED, USD, PLN, SEK" } +google_pay = { currency = "ARS, AUD, CAD, CLP, COP, EUR, HKD, INR, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, AED, GBP, USD, PLN, SEK" } +samsung_pay = { currency = "USD,GBP,EUR,SEK" } +paze = { currency = "USD,SEK" } + +[pm_filters.barclaycard] +credit = { currency = "USD,GBP,EUR,PLN,SEK" } +debit = { currency = "USD,GBP,EUR,PLN,SEK" } +google_pay = { currency = "ARS, AUD, CAD, CLP, COP, EUR, HKD, INR, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, AED, GBP, USD, PLN, SEK" } +apple_pay = { currency = "ARS, CAD, CLP, COP, CNY, EUR, HKD, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, GBP, AED, USD, PLN, SEK" } + + +[pm_filters.globepay] +ali_pay = { country = "GB",currency = "GBP,CNY" } +we_chat_pay = { country = "GB",currency = "GBP,CNY" } + + +[pm_filters.itaubank] +pix = { country = "BR", currency = "BRL" } + +[pm_filters.nexinets] +credit = { country = "DE",currency = "EUR" } +debit = { country = "DE",currency = "EUR" } +ideal = { country = "DE",currency = "EUR" } +giropay = { country = "DE",currency = "EUR" } +sofort = { country = "DE",currency = "EUR" } +eps = { country = "DE",currency = "EUR" } +apple_pay = { country = "DE",currency = "EUR" } +paypal = { country = "DE",currency = "EUR" } + + +[pm_filters.nuvei] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +klarna = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +afterpay_clearpay = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +giropay = { currency = "EUR" } +ideal = { currency = "EUR" } +sofort = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +eps = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +apple_pay = { country = "AU,AT,BY,BE,BR,BG,CA,CN,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HK,HU,IS,IE,IM,IL,IT,JP,JE,KZ,LV,LI,LT,LU,MO,MT,MC,NL,NZ,NO,PL,PT,RO,RU,SM,SA,SG,SK,SI,ES,SE,CH,TW,UA,AE,GB,US,VA",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +google_pay = { country = "AF,AX,AL,DZ,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,KH,CM,CA,CV,KY,CF,TD,CL,CN,TW,CX,CC,CO,KM,CG,CK,CR,CI,HR,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,HN,HK,HU,IS,IN,ID,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VI,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UY,UZ,VU,VA,VE,VN,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +paypal = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } + +[payout_method_filters.nuvei] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } + +[pm_filters.checkout] +debit = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,US,AU,HK,SG,SA,AE,BH,MX,AR,CL,CO,PE", currency = "AED,AFN,ALL,AMD,ANG,AOA,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +credit = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,US,AU,HK,SG,SA,AE,BH,MX,AR,CL,CO,PE", currency = "AED,AFN,ALL,AMD,ANG,AOA,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "AL,DZ, AS, AO, AG, AR, AU, AT, AZ, BH, BY, BE, BR, CA, BG, CL, CO, HR, DK, DO, EE, EG, 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, SA, SG, SK, ZA, ES, LK, SE, CH, TH, TW, TR, UA, AE, US, UY, VN", currency = "AED, ALL, AOA, AUD, AZN, BGN, BHD, BRL, CAD, CHF, CLP, COP, CZK, DKK, DOP, DZD, EGP, EUR, GBP, HKD, HUF, IDR, ILS, INR, JPY, KES, KWD, KZT, LKR, MXN, MYR, NOK, NZD, OMR, PAB, PEN, PHP, PKR, PLN, QAR, RON, SAR, SEK, SGD, THB, TRY, TWD, UAH, USD, UYU, VND, XCD, ZAR" } +apple_pay = { country = "AM,US, AT, AZ, BY, BE, BG, HR, CY, 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, VA, AU , HK, JP , MY , MN, NZ, SG, TW, VN, EG , MA, ZA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, UY, BH, IL, JO, KW, OM,QA, SA, AE, CA", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, BRL, COP, CRC, DOP, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD, USD" } + +[pm_filters.checkbook] +ach = { country = "US", currency = "USD" } + +[pm_filters.nexixpay] +credit = { country = "AT,BE,CY,EE,FI,FR,DE,GR,IE,IT,LV,LT,LU,MT,NL,PT,SK,SI,ES,BG,HR,DK,GB,NO,PL,CZ,RO,SE,CH,HU,AU,BR,US", currency = "ARS,AUD,BHD,CAD,CLP,CNY,COP,HRK,CZK,DKK,HKD,HUF,INR,JPY,KZT,JOD,KRW,KWD,MYR,MXN,NGN,NOK,PHP,QAR,RUB,SAR,SGD,VND,ZAR,SEK,CHF,THB,AED,EGP,GBP,USD,TWD,BYN,RSD,AZN,RON,TRY,AOA,BGN,EUR,UAH,PLN,BRL" } +debit = { country = "AT,BE,CY,EE,FI,FR,DE,GR,IE,IT,LV,LT,LU,MT,NL,PT,SK,SI,ES,BG,HR,DK,GB,NO,PL,CZ,RO,SE,CH,HU,AU,BR,US", currency = "ARS,AUD,BHD,CAD,CLP,CNY,COP,HRK,CZK,DKK,HKD,HUF,INR,JPY,KZT,JOD,KRW,KWD,MYR,MXN,NGN,NOK,PHP,QAR,RUB,SAR,SGD,VND,ZAR,SEK,CHF,THB,AED,EGP,GBP,USD,TWD,BYN,RSD,AZN,RON,TRY,AOA,BGN,EUR,UAH,PLN,BRL" } + +[pm_filters.square] +credit = { country = "AU,CA,FR,IE,JP,ES,GB,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +debit = { country = "AU,CA,FR,IE,JP,ES,GB,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } + +[pm_filters.iatapay] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } +ideal = { country = "NL", currency = "EUR" } +local_bank_redirect = { country = "AT,BE,EE,FI,FR,DE,IE,IT,LV,LT,LU,NL,PT,ES,GB,IN,HK,SG,TH,BR,MX,GH,VN,MY,PH,JO,AU,CO", currency = "EUR,GBP,INR,HKD,SGD,THB,BRL,MXN,GHS,VND,MYR,PHP,JOD,AUD,COP" } +duit_now = { country = "MY", currency = "MYR" } +fps = { country = "GB", currency = "GBP" } +prompt_pay = { country = "TH", currency = "THB" } +viet_qr = { country = "VN", currency = "VND" } + +[pm_filters.coinbase] +crypto_currency = { country = "ZA,US,BR,CA,TR,SG,VN,GB,DE,FR,ES,PT,IT,NL,AU" } + +[pm_filters.novalnet] +credit = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +debit = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +apple_pay = { country = "EG, MA, ZA, CN, HK, JP, MY, MN, SG, KR, VN, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, GE, DE, GR, GL, HU, IS, IE, IT, LV, LI, LT, LU, MT, MD, MC, ME, NL, NO, PL, PT, RO, SM, RS, SK, SI, ES, SE, CH, UA, GB, VA, CA, US, BH, IL, JO, KW, OM, QA, SA, AE, AR, BR, CL, CO, CR, SV, GT, MX, PY, PE, UY, BS, DO, AM, KZ, NZ", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, AZN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } +google_pay = { country = "AO, EG, KE, ZA, AR, BR, CL, CO, MX, PE, UY, AG, DO, AE, TR, SA, QA, OM, LB, KW, JO, IL, BH, KZ, VN, TH, SG, MY, JP, HK, LK, IN, US, CA, GB, UA, CH, SE, ES, SK, PT, RO, PL, NO, NL, LU, LT, LV, IE, IT, HU, GR, DE, FR, FI, EE, DK, CZ, HR, BG, BE, AT, AL", currency = "ALL, DZD, USD, XCD, ARS, AUD, EUR, AZN, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +paypal = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +sepa = {country = "FR, IT, GR, BE, BG, FI, EE, HR, IE, DE, DK, LT, LV, MT, LU, AT, NL, PT, RO, PL, SK, SE, ES, SI, HU, CZ, CY, GB, LI, NO, IS, MC, CH, YT, PM, SM", currency="EUR"} +sepa_guaranteed_debit = {country = "AT, CH, DE", currency="EUR"} + +[pm_filters.braintree] +credit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} + +[pm_filters.facilitapay] +pix = { country = "BR", currency = "BRL" } + +[pm_filters.finix] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } + +[pm_filters.helcim] +credit = { country = "US, CA", currency = "USD, CAD" } +debit = { country = "US, CA", currency = "USD, CAD" } + +[pm_filters.globalpay] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,SZ,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AFN,DZD,ARS,AMD,AWG,AUD,AZN,BSD,BHD,THB,PAB,BBD,BYN,BZD,BMD,BOB,BRL,BND,BGN,BIF,CVE,CAD,CLP,COP,KMF,CDF,NIO,CRC,CUP,CZK,GMD,DKK,MKD,DJF,DOP,VND,XCD,EGP,SVC,ETB,EUR,FKP,FJD,HUF,GHS,GIP,HTG,PYG,GNF,GYD,HKD,UAH,ISK,INR,IRR,IQD,JMD,JOD,KES,PGK,HRK,KWD,AOA,MMK,LAK,GEL,LBP,ALL,HNL,SLL,LRD,LYD,SZL,LSL,MGA,MWK,MYR,MUR,MXN,MDL,MAD,MZN,NGN,ERN,NAD,NPR,ANG,ILS,TWD,NZD,BTN,KPW,NOK,TOP,PKR,MOP,UYU,PHP,GBP,BWP,QAR,GTQ,ZAR,OMR,KHR,RON,MVR,IDR,RUB,RWF,SHP,SAR,RSD,SCR,SGD,PEN,SBD,KGS,SOS,TJS,SSP,LKR,SDG,SRD,SEK,CHF,SYP,BDT,WST,TZS,KZT,TTD,MNT,TND,TRY,TMT,AED,UGX,USD,UZS,VUV,KRW,YER,JPY,CNY,ZMW,ZWL,PLN,CLF,STD,CUC" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,SZ,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AFN,DZD,ARS,AMD,AWG,AUD,AZN,BSD,BHD,THB,PAB,BBD,BYN,BZD,BMD,BOB,BRL,BND,BGN,BIF,CVE,CAD,CLP,COP,KMF,CDF,NIO,CRC,CUP,CZK,GMD,DKK,MKD,DJF,DOP,VND,XCD,EGP,SVC,ETB,EUR,FKP,FJD,HUF,GHS,GIP,HTG,PYG,GNF,GYD,HKD,UAH,ISK,INR,IRR,IQD,JMD,JOD,KES,PGK,HRK,KWD,AOA,MMK,LAK,GEL,LBP,ALL,HNL,SLL,LRD,LYD,SZL,LSL,MGA,MWK,MYR,MUR,MXN,MDL,MAD,MZN,NGN,ERN,NAD,NPR,ANG,ILS,TWD,NZD,BTN,KPW,NOK,TOP,PKR,MOP,UYU,PHP,GBP,BWP,QAR,GTQ,ZAR,OMR,KHR,RON,MVR,IDR,RUB,RWF,SHP,SAR,RSD,SCR,SGD,PEN,SBD,KGS,SOS,TJS,SSP,LKR,SDG,SRD,SEK,CHF,SYP,BDT,WST,TZS,KZT,TTD,MNT,TND,TRY,TMT,AED,UGX,USD,UZS,VUV,KRW,YER,JPY,CNY,ZMW,ZWL,PLN,CLF,STD,CUC" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,IT,NL", currency = "EUR" } + +[pm_filters.jpmorgan] +debit = { country = "CA, GB, US, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, EUR, GBP, AUD, NZD, SGD, CAD, JPY, HKD, KRW, TWD, MXN, BRL, DKK, NOK, ZAR, SEK, CHF, CZK, PLN, TRY, AFN, ALL, DZD, AOA, ARS, AMD, AWG, AZN, BSD, BDT, BBD, BYN, BZD, BMD, BOB, BAM, BWP, BND, BGN, BIF, BTN, XOF, XAF, XPF, KHR, CVE, KYD, CLP, CNY, COP, KMF, CDF, CRC, HRK, DJF, DOP, XCD, EGP, ETB, FKP, FJD, GMD, GEL, GHS, GIP, GTQ, GYD, HTG, HNL, HUF, ISK, INR, IDR, ILS, JMD, KZT, KES, LAK, LBP, LSL, LRD, MOP, MKD, MGA, MWK, MYR, MVR, MRU, MUR, MDL, MNT, MAD, MZN, MMK, NAD, NPR, ANG, PGK, NIO, NGN, PKR, PAB, PYG, PEN, PHP, QAR, RON, RWF, SHP, WST, STN, SAR, RSD, SCR, SLL, SBD, SOS, LKR, SRD, SZL, TJS, TZS, THB, TOP, TTD, UGX, UAH, AED, UYU, UZS, VUV, VND, YER, ZMW" } +credit = { country = "CA, GB, US, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, EUR, GBP, AUD, NZD, SGD, CAD, JPY, HKD, KRW, TWD, MXN, BRL, DKK, NOK, ZAR, SEK, CHF, CZK, PLN, TRY, AFN, ALL, DZD, AOA, ARS, AMD, AWG, AZN, BSD, BDT, BBD, BYN, BZD, BMD, BOB, BAM, BWP, BND, BGN, BIF, BTN, XOF, XAF, XPF, KHR, CVE, KYD, CLP, CNY, COP, KMF, CDF, CRC, HRK, DJF, DOP, XCD, EGP, ETB, FKP, FJD, GMD, GEL, GHS, GIP, GTQ, GYD, HTG, HNL, HUF, ISK, INR, IDR, ILS, JMD, KZT, KES, LAK, LBP, LSL, LRD, MOP, MKD, MGA, MWK, MYR, MVR, MRU, MUR, MDL, MNT, MAD, MZN, MMK, NAD, NPR, ANG, PGK, NIO, NGN, PKR, PAB, PYG, PEN, PHP, QAR, RON, RWF, SHP, WST, STN, SAR, RSD, SCR, SLL, SBD, SOS, LKR, SRD, SZL, TJS, TZS, THB, TOP, TTD, UGX, UAH, AED, UYU, UZS, VUV, VND, YER, ZMW" } + +[pm_filters.bitpay] +crypto_currency = { country = "US, CA, GB, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, AUD, CAD, GBP, MXN, NZD, CHF, EUR"} + +[pm_filters.paybox] +debit = { country = "FR", currency = "CAD, AUD, EUR, USD" } +credit = { country = "FR", currency = "CAD, AUD, EUR, USD" } + +[pm_filters.payload] +debit = { currency = "USD,CAD" } +credit = { currency = "USD,CAD" } +ach = { currency = "USD,CAD" } + + +[pm_filters.digitalvirgo] +direct_carrier_billing = {country = "MA, CM, ZA, EG, SN, DZ, TN, ML, GN, GH, LY, GA, CG, MG, BW, SD, NG, ID, SG, AZ, TR, FR, ES, PL, GB, PT, DE, IT, BE, IE, SK, GR, NL, CH, BR, MX, AR, CL, AE, IQ, KW, BH, SA, QA, PS, JO, OM, RU" , currency = "MAD, XOF, XAF, ZAR, EGP, DZD, TND, GNF, GHS, LYD, XAF, CDF, MGA, BWP, SDG, NGN, IDR, SGD, RUB, AZN, TRY, EUR, PLN, GBP, CHF, BRL, MXN, ARS, CLP, AED, IQD, KWD, BHD, SAR, QAR, ILS, JOD, OMR" } + +[pm_filters.payu] +debit = { country = "AE, AF, AL, AM, CW, AO, AR, AU, AW, AZ, BA, BB, BG, BH, BI, BM, BN, BO, BR, BS, BW, BY, BZ, CA, CD, LI, CL, CN, CO, CR, CV, CZ, DJ, DK, DO, DZ, EG, ET, AD, FJ, FK, GG, GE, GH, GI, GM, GN, GT, GY, HK, HN, HR, HT, HU, ID, IL, IQ, IS, JM, JO, JP, KG, KH, KM, KR, KW, KY, KZ, LA, LB, LR, LS, MA, MD, MG, MK, MN, MO, MR, MV, MW, MX, MY, MZ, NA, NG, NI, BV, CK, OM, PA, PE, PG, PL, PY, QA, RO, RS, RW, SA, SB, SC, SE, SG, SH, SO, SR, SV, SZ, TH, TJ, TM, TN, TO, TR, TT, TW, TZ, UG, AS, UY, UZ, VE, VN, VU, WS, CM, AI, BJ, PF, YE, ZA, ZM, ZW", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, IQD, ISK, JMD, JOD, JPY, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LRD, LSL, MAD, MDL, MGA, MKD, MNT, MOP, MRU, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NZD, OMR, PAB, PEN, PGK, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SOS, SRD, SVC, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +credit = { country = "AE, AF, AL, AM, CW, AO, AR, AU, AW, AZ, BA, BB, BG, BH, BI, BM, BN, BO, BR, BS, BW, BY, BZ, CA, CD, LI, CL, CN, CO, CR, CV, CZ, DJ, DK, DO, DZ, EG, ET, AD, FJ, FK, GG, GE, GH, GI, GM, GN, GT, GY, HK, HN, HR, HT, HU, ID, IL, IQ, IS, JM, JO, JP, KG, KH, KM, KR, KW, KY, KZ, LA, LB, LR, LS, MA, MD, MG, MK, MN, MO, MR, MV, MW, MX, MY, MZ, NA, NG, NI, BV, CK, OM, PA, PE, PG, PL, PY, QA, RO, RS, RW, SA, SB, SC, SE, SG, SH, SO, SR, SV, SZ, TH, TJ, TM, TN, TO, TR, TT, TW, TZ, UG, AS, UY, UZ, VE, VN, VU, WS, CM, AI, BJ, PF, YE, ZA, ZM, ZW", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, IQD, ISK, JMD, JOD, JPY, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LRD, LSL, MAD, MDL, MGA, MKD, MNT, MOP, MRU, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NZD, OMR, PAB, PEN, PGK, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SOS, SRD, SVC, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +google_pay = { country = "AL, DZ, AS, AO, AR, AU, AZ, BH, BY, BR, BG, CA, CL, CO, HR, CZ, DK, DO, EG, HK, HU, ID, IN, IL, JP, JO, KZ, KW, LB, MY, MX, OM, PA, PE, PL, QA, RO, SA, SG, SE, TW, TH, TR, AE, UY, VN", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PAB, PEN, PLN, QAR, RON, SAR, SGD, ZAR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } + +[pm_filters.klarna] +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,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } + +[pm_filters.flexiti] +flexiti = { country = "CA", currency = "CAD" } + +[pm_filters.mifinity] +mifinity = { country = "BR,CN,SG,MY,DE,CH,DK,GB,ES,AD,GI,FI,FR,GR,HR,IT,JP,MX,AR,CO,CL,PE,VE,UY,PY,BO,EC,GT,HN,SV,NI,CR,PA,DO,CU,PR,NL,NO,PL,PT,SE,RU,TR,TW,HK,MO,AX,AL,DZ,AS,AO,AI,AG,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BE,BZ,BJ,BM,BT,BQ,BA,BW,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CX,CC,KM,CG,CK,CI,CW,CY,CZ,DJ,DM,EG,GQ,ER,EE,ET,FK,FO,FJ,GF,PF,TF,GA,GM,GE,GH,GL,GD,GP,GU,GG,GN,GW,GY,HT,HM,VA,IS,IN,ID,IE,IM,IL,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LI,LT,LU,MK,MG,MW,MV,ML,MT,MH,MQ,MR,MU,YT,FM,MD,MC,MN,ME,MS,MA,MZ,NA,NR,NP,NC,NZ,NE,NG,NU,NF,MP,OM,PK,PW,PS,PG,PH,PN,QA,RE,RO,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SX,SK,SI,SB,SO,ZA,GS,KR,LK,SR,SJ,SZ,TH,TL,TG,TK,TO,TT,TN,TM,TC,TV,UG,UA,AE,UZ,VU,VN,VG,VI,WF,EH,ZM", currency = "AUD,CAD,CHF,CNY,CZK,DKK,EUR,GBP,INR,JPY,NOK,NZD,PLN,RUB,SEK,ZAR,USD,EGP,UYU,UZS" } + +[pm_filters.zen] +credit = { not_available_flows = { capture_method = "manual" } } +debit = { not_available_flows = { capture_method = "manual" } } +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pse = { country = "CO", currency = "COP" } +pix = { country = "BR", currency = "BRL" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[pm_filters.zsl] +local_bank_transfer = { country = "CN", currency = "CNY" } + +[pm_filters.aci] +credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" } +debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" } +mb_way = { country = "EE,ES,PT", currency = "EUR" } +ali_pay = { country = "CN", currency = "CNY" } +eps = { country = "AT", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +sofort = { country = "AT,BE,CH,DE,ES,GB,IT,NL,PL", currency = "CHF,EUR,GBP,HUF,PLN"} +interac = { country = "CA", currency = "CAD,USD"} +przelewy24 = { country = "PL", currency = "CZK,EUR,GBP,PLN" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +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 = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.gigadat] +interac = { currency = "CAD"} + +[pm_filters.loonio] +interac = { currency = "CAD"} + +[pm_filters.dlocal] +oxxo = { currency = "MXN" } + +[pm_filters.mollie] +eps = { country = "AT", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +przelewy24 = { country = "PL", currency = "PLN,EUR" } +klarna = { country = "DE,AT,NL,BE,FR,GB,IT,ES,PT,SE,DK,FI,NO,CH,IR,CZ,PL,GR,SK", currency = "EUR,GBP,DKK,SEK,NOK,CHF,PLN,CZK" } + +[pm_filters.redsys] +credit = { currency = "AUD,BGN,CAD,CHF,COP,CZK,DKK,EUR,GBP,HRK,HUF,ILS,INR,JPY,MYR,NOK,NZD,PEN,PLN,RUB,SAR,SEK,SGD,THB,USD,ZAR", country="ES" } +debit = { currency = "AUD,BGN,CAD,CHF,COP,CZK,DKK,EUR,GBP,HRK,HUF,ILS,INR,JPY,MYR,NOK,NZD,PEN,PLN,RUB,SAR,SEK,SGD,THB,USD,ZAR", country="ES" } + +[pm_filters.stax] +credit = { country = "US", currency = "USD" } +debit = { country = "US", currency = "USD" } +ach = { country = "US", currency = "USD" } + +[pm_filters.prophetpay] +card_redirect = { country = "US", currency = "USD" } + +[pm_filters.multisafepay] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AUD,BRL,CAD,CHF,CLP,CNY,COP,CZK,DKK,EUR,GBP,HKD,HRK,HUF,ILS,INR,ISK,JPY,KRW,MXN,MYR,NOK,NZD,PEN,PLN,RON,RUB,SEK,SGD,TRY,TWD,USD,ZAR", not_available_flows = { capture_method = "manual" } } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AUD,BRL,CAD,CHF,CLP,CNY,COP,CZK,DKK,EUR,GBP,HKD,HRK,HUF,ILS,INR,ISK,JPY,KRW,MXN,MYR,NOK,NZD,PEN,PLN,RON,RUB,SEK,SGD,TRY,TWD,USD,ZAR", not_available_flows = { capture_method = "manual" } } +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", currency = "AED, AUD, BRL, CAD, CHF, CLP, COP, CZK, DKK, EUR, GBP, HKD, HRK, HUF, ILS, INR, JPY, MXN, MYR, NOK, NZD, PEN, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, TWD, UAH, USD, ZAR" } +paypal = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,HRK,HUF,JPY,MXN,MYR,NOK,NZD,PHP,PLN,RUB,SEK,SGD,THB,TRY,TWD,USD" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IT,NL,NO,PT,ES,SE,GB", currency = "DKK,EUR,GBP,NOK,SEK" } +trustly = { country = "AT,CZ,DK,EE,FI,DE,LV,LT,NL,NO,PL,PT,ES,SE,GB" , currency = "EUR,GBP,SEK"} +ali_pay = { currency = "EUR,USD" } +we_chat_pay = { currency = "EUR"} +eps = { country = "AT" , currency = "EUR" } +mb_way = { country = "PT" , currency = "EUR" } +sofort = { country = "AT,BE,FR,DE,IT,PL,ES,CH,GB" , currency = "EUR"} + +[pm_filters.cashtocode] +classic = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +evoucher = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.wellsfargo] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,US,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,US,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "US", currency = "USD" } +apple_pay = { country = "US", currency = "USD" } +ach = { country = "US", currency = "USD" } + +[pm_filters.trustpay] +credit = { not_available_flows = { capture_method = "manual" } } +debit = { not_available_flows = { capture_method = "manual" } } +instant_bank_transfer = { country = "CZ,SK,GB,AT,DE,IT", currency = "CZK, EUR, GBP" } +instant_bank_transfer_poland = { country = "PL", currency = "PLN" } +instant_bank_transfer_finland = { country = "FI", currency = "EUR" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,GB", currency = "EUR" } + +[pm_filters.tesouro] +credit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +apple_pay = { currency = "USD" } +google_pay = { currency = "USD" } + +[pm_filters.authorizedotnet] +credit = {currency = "CAD,USD"} +debit = {currency = "CAD,USD"} +google_pay = {currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD"} +apple_pay = {currency = "EUR,GBP,ISK,USD,AUD,CAD,BRL,CLP,COP,CRC,CZK,DKK,EGP,GEL,GHS,GTQ,HNL,HKD,HUF,ILS,INR,JPY,KZT,KRW,KWD,MAD,MXN,MYR,NOK,NZD,PEN,PLN,PYG,QAR,RON,SAR,SEK,SGD,THB,TWD,UAH,AED,VND,ZAR"} +paypal = {currency = "AUD,BRL,CAD,CHF,CNY,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,MYR,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD"} + +[pm_filters.dwolla] +ach = { country = "US", currency = "USD" } + +[pm_filters.worldpay] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +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", currency = "DZD, AOA, USD, XCD, ARS, AUD, AZN, EUR, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "EG, MA, ZA, AU, CN, HK, JP, MO, MY, MN, NZ, SG, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, CZ, DK, EE, FO, FI, FR, GE, DE, GR, GL, GG, HU, IE, IS, 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, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US, PR", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } + +[pm_filters.worldpayxml] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +google_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +apple_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } + +[pm_filters.worldpayvantiv] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +apple_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +google_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } + +[pm_filters.worldpaymodular] +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", currency = "DZD, AOA, USD, XCD, ARS, AUD, AZN, EUR, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "EG, MA, ZA, AU, CN, HK, JP, MO, MY, MN, NZ, SG, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, CZ, DK, EE, FO, FI, FR, GE, DE, GR, GL, GG, HU, IE, IS, 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, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US, PR", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } + +[pm_filters.calida] +bluecode = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IE,IT,LV,LT,LU,MT,NL,PL,PT,RO,SK,SI,ES,SE,IS,LI,NO", currency = "EUR" } + +[file_upload_config] +bucket_name = "" +region = "" + +[pm_filters.forte] +credit = { country = "US, CA", currency = "CAD,USD"} +debit = { country = "US, CA", currency = "CAD,USD"} + +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + +[pm_filters.fiuu] +duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +credit = { country = "CN,HK,ID,MY,PH,SG,TH,TW,VN", currency = "CNY,HKD,IDR,MYR,PHP,SGD,THB,TWD,VND" } +debit = { country = "CN,HK,ID,MY,PH,SG,TH,TW,VN", currency = "CNY,HKD,IDR,MYR,PHP,SGD,THB,TWD,VND" } + +[pm_filters.inespay] +sepa = { country = "ES", currency = "EUR"} + +[pm_filters.bluesnap] +credit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,ARS,AUD,AWG,BAM,BBD,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,FJD,GBP,GEL,GIP,GTQ,HKD,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MUR,MWK,MXN,MYR,NAD,NGN,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PLN,PKR,QAR,RON,RSD,RUB,SAR,SCR,SDG,SEK,SGD,THB,TND,TRY,TTD,TWD,TZS,UAH,USD,UYU,UZS,VND,XAF,XCD,XOF,ZAR"} +google_pay = { country = "AL, DZ, AS, AO, AG, AR, AU, AT, AZ, BH, BY, BE, BR, BG, 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", currency = "ALL, DZD, USD, XCD, ARS, AUD, EUR, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND"} +apple_pay = { country = "EG, MA, ZA, AU, HK, JP, MO, MY, MN, NZ, SG, KR, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, 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, SI, ES, SE, CH, UA, GB, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, UY, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MYR, NZD, SGD, KRW, TWD, VND, AMD, EUR, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, GBP, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, MXN, PAB, PEN, BSD, UYU, BHD, ILS, KWD, OMR, QAR, SAR, AED, CAD"} + +[pm_filters.fiserv] +credit = {country = "AU,NZ,CN,HK,IN,LK,KR,MY,SG,GB,BE,FR,DE,IT,ME,NL,PL,ES,ZA,AR,BR,CO,MX,PA,UY,US,CA", currency = "AFN,ALL,DZD,AOA,ARS,AMD,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BYN,BZD,BMD,BTN,BOB,VES,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XAF,CLP,CNY,COP,KMF,CDF,CRC,HRK,CUP,CZK,DKK,DJF,DOP,XCD,EGP,ERN,ETB,EUR,FKP,FJD,XPF,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KGS,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,ANG,NZD,NIO,NGN,VUV,KPW,NOK,OMR,PKR,PAB,PGK,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SHP,SVC,WST,STN,SAR,RSD,SCR,SLL,SGD,SBD,SOS,ZAR,KRW,SSP,LKR,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,UGX,UAH,AED,USD,UYU,UZS,VND,XOF,YER,ZMW,ZWL"} +debit = {country = "AU,NZ,CN,HK,IN,LK,KR,MY,SG,GB,BE,FR,DE,IT,ME,NL,PL,ES,ZA,AR,BR,CO,MX,PA,UY,US,CA", currency = "AFN,ALL,DZD,AOA,ARS,AMD,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BYN,BZD,BMD,BTN,BOB,VES,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XAF,CLP,CNY,COP,KMF,CDF,CRC,HRK,CUP,CZK,DKK,DJF,DOP,XCD,EGP,ERN,ETB,EUR,FKP,FJD,XPF,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KGS,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,ANG,NZD,NIO,NGN,VUV,KPW,NOK,OMR,PKR,PAB,PGK,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SHP,SVC,WST,STN,SAR,RSD,SCR,SLL,SGD,SBD,SOS,ZAR,KRW,SSP,LKR,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,UGX,UAH,AED,USD,UYU,UZS,VND,XOF,YER,ZMW,ZWL"} +paypal = { currency = "AUD,EUR,BRL,CAD,CNY,EUR,EUR,EUR,GBP,HKD,INR,EUR,JPY,MYR,EUR,NZD,PHP,PLN,SGD,USD", country = "AU, BE, BR, CA, CN, DE, ES, FR, GB, HK, IN, IT, JP, MY, NL, NZ, PH, PL, SG, US" } +google_pay = { country = "AU,AT,BE,BR,CA,CN,HK,MY,NZ,SG,US", currency = "AUD,EUR,EUR,BRL,CAD,CNY,HKD,MYR,NZD,SGD,USD" } +apple_pay = { country = "AU,NZ,CN,HK,JP,SG,MY,KR,TW,VN,GB,IE,FR,DE,IT,ES,PT,NL,BE,LU,AT,CH,SE,FI,DK,NO,PL,CZ,SK,HU,LT,LV,EE,GR,RO,BG,HR,SI,MT,CY,IS,LI,MC,SM,VA,US,CA,MX,BR,AR,CL,CO,PE,UY,CR,PA,DO,EC,SV,GT,HN,BS,PR,AE,SA,QA,KW,BH,OM,IL,JO,PS,EG,MA,ZA,GE,AM,AZ,MD,ME,MK,AL,BA,RS,UA", currency = "AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD" } + + +[pm_filters.amazonpay] +amazon_pay = { country = "US", currency = "USD" } + +[pm_filters.rapyd] +apple_pay = { country = "BR, CA, CL, CO, DO, SV, MX, PE, PT, US, AT, BE, BG, HR, CY, CZ, DO, DK, EE, FI, FR, GE, DE, GR, GL, HU, IS, IE, IL, IT, LV, LI, LT, LU, MT, MD, MC, ME, NL, NO, PL, RO, SM, SK, SI, ZA, ES, SE, CH, GB, VA, AU, HK, JP, MY, NZ, SG, KR, TW, VN", currency = "AMD, AUD, BGN, BRL, BYN, CAD, CHF, CLP, CNY, COP, CRC, CZK, DKK, DOP, EUR, GBP, GEL, GTQ, HUF, ISK, JPY, KRW, MDL, MXN, MYR, NOK, PAB, PEN, PLN, PYG, RON, RSD, SEK, SGD, TWD, UAH, USD, UYU, VND, ZAR" } +google_pay = { country = "BR, CA, CL, CO, DO, MX, PE, PT, US, AT, BE, BG, HR, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IL, IT, LV, LT, LU, NZ, NO, GB, PL, RO, RU, SK, ZA, ES, SE, CH, TR, AU, HK, IN, ID, JP, MY, PH, SG, TW, TH, VN", currency = "AUD, BGN, BRL, BYN, CAD, CHF, CLP, COP, CZK, DKK, DOP, EUR, GBP, HUF, IDR, JPY, KES, MXN, MYR, NOK, PAB, PEN, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, TWD, UAH, USD, UYU, VND, ZAR" } +credit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } +debit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } + +[pm_filters.bamboraapac] +credit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } +debit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } + +[pm_filters.gocardless] +ach = { country = "US", currency = "USD" } +becs = { country = "AU", currency = "AUD" } +sepa = { country = "AU,AT,BE,BG,CA,HR,CY,CZ,DK,FI,FR,DE,HU,IT,LU,MT,NL,NZ,NO,PL,PT,IE,RO,SK,SI,ZA,ES,SE,CH,GB", currency = "GBP,EUR,SEK,DKK,AUD,NZD,CAD" } + +[pm_filters.powertranz] +credit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "BBD,BMD,BSD,CRC,GTQ,HNL,JMD,KYD,TTD,USD" } +debit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "BBD,BMD,BSD,CRC,GTQ,HNL,JMD,KYD,TTD,USD" } + +[pm_filters.worldline] +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +credit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.shift4] +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE", currency = "CHF,EUR" } +credit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +boleto = { country = "BR", currency = "BRL" } +trustly = { currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +ali_pay = { country = "CN", currency = "CNY" } +we_chat_pay = { country = "CN", currency = "CNY" } +klarna = { currency = "EUR,GBP,CHF,SEK" } +blik = { country = "PL", currency = "PLN" } +crypto_currency = { currency = "USD,GBP,AED" } +paysera = { currency = "EUR" } +skrill = { currency = "USD" } + +[pm_filters.placetopay] +credit = { country = "BE,CH,CO,CR,EC,HN,MX,PA,PR,UY", currency = "CLP,COP,USD"} +debit = { country = "BE,CH,CO,CR,EC,HN,MX,PA,PR,UY", currency = "CLP,COP,USD"} + +[pm_filters.coingate] +crypto_currency = { country = "AL, AD, AT, BE, BA, BG, HR, CZ, DK, EE, FI, FR, DE, GR, HU, IS, IE, IT, LV, LT, LU, MT, MD, NL, NO, PL, PT, RO, RS, SK, SI, ES, SE, CH, UA, GB, AR, BR, CL, CO, CR, DO, SV, GD, MX, PE, LC, AU, NZ, CY, HK, IN, IL, JP, KR, QA, SA, SG, EG", currency = "EUR, USD, GBP" } + +[pm_filters.paystack] +eft = { country = "NG, ZA, GH, KE, CI", currency = "NGN, GHS, ZAR, KES, USD" } + +[pm_filters.santander] +pix = { country = "BR", currency = "BRL" } +boleto = { country = "BR", currency = "BRL" } + +[pm_filters.boku] +dana = { country = "ID", currency = "IDR" } +gcash = { country = "PH", currency = "PHP" } +go_pay = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +momo = { country = "VN", currency = "VND" } + +[pm_filters.nmi] +credit = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +apple_pay = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.paypal] +credit = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +debit = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +paypal = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +eps = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +giropay = { currency = "EUR" } +ideal = { currency = "EUR" } +sofort = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } + +[pm_filters.datatrans] +credit = { country = "AL,AD,AM,AT,AZ,BY,BE,BA,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GE,GR,HR,HU,IE,IS,IT,KZ,LI,LT,LU,LV,MC,MD,ME,MK,MT,NL,NO,PL,PT,RO,RU,SE,SI,SK,SM,TR,UA,VA", currency = "BHD,BIF,CHF,DJF,EUR,GBP,GNF,IQD,ISK,JPY,JOD,KMF,KRW,KWD,LYD,OMR,PYG,RWF,TND,UGX,USD,VND,VUV,XAF,XOF,XPF" } +debit = { country = "AL,AD,AM,AT,AZ,BY,BE,BA,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GE,GR,HR,HU,IE,IS,IT,KZ,LI,LT,LU,LV,MC,MD,ME,MK,MT,NL,NO,PL,PT,RO,RU,SE,SI,SK,SM,TR,UA,VA", currency = "BHD,BIF,CHF,DJF,EUR,GBP,GNF,IQD,ISK,JPY,JOD,KMF,KRW,KWD,LYD,OMR,PYG,RWF,TND,UGX,USD,VND,VUV,XAF,XOF,XPF" } + +[pm_filters.payme] +credit = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } +debit = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } +apple_pay = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } + +[pm_filters.paysafe] +apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,PM,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,NL,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "ARS,AUD,AZN,BHD,BOB,BAM,BRL,BGN,CAD,CLP,CNY,COP,CRC,HRK,CZK,DKK,DOP,XCD,EGP,ETB,EUR,FJD,GEL,GTQ,HTG,HNL,HKD,HUF,INR,IDR,JMD,JPY,JOD,KZT,KES,KRW,KWD,LBP,LYD,MWK,MUR,MXN,MDL,MAD,ILS,NZD,NGN,NOK,OMR,PKR,PAB,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SAR,RSD,SGD,ZAR,LKR,SEK,CHF,SYP,TWD,THB,TTD,TND,TRY,UAH,AED,UYU,USD,VND" } + +[pm_filters.payjustnow] +payjustnow = { country = "ZA", currency = "ZAR" } + +[pm_filters.payjustnowinstore] +payjustnow = { country = "ZA", currency = "ZAR" } diff --git a/config/docker-configuration.toml b/config/docker-configuration.toml index 71a2d164..f6444b3a 100644 --- a/config/docker-configuration.toml +++ b/config/docker-configuration.toml @@ -1,7 +1,7 @@ [log.console] enabled = true level = "DEBUG" -log_format = "default" +log_format = "json" [server] host = "0.0.0.0" @@ -9,7 +9,7 @@ port = 8080 [metrics] host = "0.0.0.0" -port = 9090 +port = 9094 [limit] request_count = 1 @@ -49,6 +49,29 @@ max_feed_count = 200 tti = 7200 # i.e. 2 hours max_capacity = 5000 +[cache_config] +service_config_redis_prefix = "DE_service_config_" +service_config_ttl = 300 # 5 minutes + +[analytics] +enabled = false + +[analytics.kafka] +brokers = ["kafka:29092"] +topic_prefix = "decision-engine" +batch_size = 100 +batch_timeout_ms = 1000 +max_consecutive_failures = 5 + +[analytics.clickhouse] +host = "http://clickhouse:8123" +username = "analytics_user" +password = "analytics_pass" +database = "decision_engine_analytics" + +[secrets] +open_router_private_key = "" + [tenant_secrets] public = { schema = "public" } @@ -65,125 +88,659 @@ pool_max_idle_per_host = 10 identity = "" [routing_config.keys] -payment_method = { type = "enum", values = "card, bank_debit, bank_transfer" } -amount = { type = "integer" } +amount = { type = "integer", min = 0 } +authentication_type = { type = "enum", values = "three_ds, no_three_ds" } +bank_debit = { type = "enum", values = "ach, sepa, bacs, becs" } +bank_redirect = { type = "enum", values = "giropay, ideal, sofort, eft, eps, bancontact_card, blik, local_bank_redirect, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, przelewy24, trustly, bizum, interac, open_banking_uk, open_banking_pis" } +bank_transfer = { type = "enum", values = "ach, sepa, sepa_bank_transfer, bacs, multibanco, pix, pse, permata_bank_transfer, bca_bank_transfer, bni_va, bri_va, cimb_va, danamon_va, mandiri_va, local_bank_transfer, instant_bank_transfer" } +billing_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } +business_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } +business_label = { type = "str_value" } +capture_method = { type = "enum", values = "automatic, manual, manual_multiple, scheduled, sequential_automatic" } +card = { type = "enum", values = "debit, credit" } +card_bin = { type = "str_value", exact_length = 6, regex = "^[0-9]{6}$" } +card_discovery = { type = "enum", values = "manual, saved_card, click_to_pay" } +card_network = { type = "enum", values = "visa, VISA, Visa, visaCard, mastercard, MASTERCARD, MasterCard, masterCard, mastercardCard, master_card, Master_card, Master_Card, Master Card, Mastercard, american_express, AMERICANEXPRESS, AmericanExpress, americanExpress, americanExpressCard, amex, AMEX, Amex, jcb, JCB, Jcb, diners_club, DINERSCLUB, DinersClub, dinersClub, dinersClubCard, discover, DISCOVER, Discover, discoverCard, cartes_bancaires, CARTESBANCAIRES, CartesBancaires, cartesBancaires, union_pay, UNIONPAY, UnionPay, unionPay, interac, INTERAC, Interac, rupay, RUPAY, RuPay, ruPay, maestro, MAESTRO, Maestro, star, STAR, Star, pulse, PULSE, Pulse, accel, ACCEL, Accel, nyce, NYCE, Nyce" } +card_redirect = { type = "enum", values = "knet, benefit, momo_atm, card_redirect" } +card_type = { type = "enum", values = "debit, credit" } +crypto = { type = "enum", values = "crypto_currency" } +currency = { type = "enum", values = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SLL, SOS, SRD, SSP, STD, STN, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +extended_card_bin = { type = "str_value", exact_length = 8, regex = "^[0-9]{8}$" } +gift_card = { type = "enum", values = "givex, pay_safe_card" } +issuer_country = { type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe" } +issuer_name = { type = "str_value", min_length = 1 } +mandate_acceptance_type = { type = "enum", values = "online, offline" } +mandate_type = { type = "enum", values = "single_use, multi_use" } metadata = { type = "udf" } -currency = { type = "enum", values = "USD, EUR, GBP, JPY, CAD, AUD" } -order_udf1 = { type = "global_ref" } -payment_methodType = { type = "enum", values = "CARD, UPI, NB" } -payment_cardBrand = { type = "enum", values = "VISA, MASTERCARD, AMEX, RUPAY, DINERS" } -payment_cardBin = { type = "global_ref" } -payment_cardType = { type = "enum", values = "CREDIT, DEBIT" } -payment_cardIssuerCountry = { type = "enum", values = "INDIA, US, UK, SINGAPORE" } -payment_paymentMethod = { type = "enum", values = "NB_HDFC, NB_ICICI, NB_SBI" } -payment_paymentSource = { type = "enum", values = "net.one97.paytm, @paytm" } -txn_isEmi = { type = "enum", values = "true, false" } - -[routing_config.default] -output = ["stripe", "adyen"] - -[[routing_config.constraint_graph.nodes]] -preds = [] -succs = [0] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "payment_method" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "card" - -[[routing_config.constraint_graph.nodes]] -preds = [] -succs = [1] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "payment_method" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "bank_debit" - -[[routing_config.constraint_graph.nodes]] -preds = [0] -succs = [] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "output" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "stripe" - -[[routing_config.constraint_graph.nodes]] -preds = [1] -succs = [] - -[routing_config.constraint_graph.nodes.kind] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data] -kind = "value" - -[routing_config.constraint_graph.nodes.kind.data.data] -key = "output" -comparison = "equal" - -[routing_config.constraint_graph.nodes.kind.data.data.value] -type = "enum_variant" -value = "adyen" - -[[routing_config.constraint_graph.edges]] -strength = "strong" -relation = "positive" -pred = 0 -succ = 2 - -[[routing_config.constraint_graph.edges]] -strength = "strong" -relation = "positive" -pred = 1 -succ = 3 +mobile_payment = { type = "enum", values = "direct_carrier_billing" } +network_token = { type = "enum", values = "network_token" } +open_banking = { type = "enum", values = "open_banking_pis" } +pay_later = { type = "enum", values = "affirm, alma, afterpay_clearpay, klarna, pay_bright, atome, walley" } +payment_method = { type = "enum", values = "card, card_redirect, pay_later, wallet, bank_redirect, bank_transfer, crypto, bank_debit, reward, real_time_payment, upi, voucher, gift_card, open_banking, mobile_payment" } +payment_method_type = { type = "enum", values = "ach, affirm, afterpay_clearpay, alfamart, ali_pay, ali_pay_hk, alma, amazon_pay, apple_pay, atome, bacs, bancontact_card, becs, benefit, bizum, blik, boleto, bca_bank_transfer, bni_va, bri_va, card, card_redirect, cimb_va, classic_reward, credit, crypto_currency, cashapp, dana, danamon_va, debit, duit_now, efecty, eft, eps, fps, evoucher, giropay, givex, google_pay, go_pay, gcash, ideal, interac, indomaret, klarna, kakao_pay, local_bank_redirect, mandiri_va, knet, mb_way, mobile_pay, momo, momo_atm, multibanco, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, oxxo, pago_efectivo, permata_bank_transfer, open_banking_uk, pay_bright, paypal, paze, pix, pay_safe_card, przelewy24, prompt_pay, pse, red_compra, red_pagos, samsung_pay, sepa, sepa_bank_transfer, sofort, swish, touch_n_go, trustly, twint, upi_collect, upi_intent, vipps, viet_qr, venmo, walley, we_chat_pay, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy, local_bank_transfer, mifinity, open_banking_pis, direct_carrier_billing, instant_bank_transfer" } +payment_type = { type = "enum", values = "normal, new_mandate, setup_mandate, recurring_mandate, non_mandate" } +real_time_payment = { type = "enum", values = "fps, duit_now, prompt_pay, viet_qr" } +reward = { type = "enum", values = "evoucher, classic_reward" } +setup_future_usage = { type = "enum", values = "on_session, off_session" } +transaction_initiator = { type = "enum", values = "customer, merchant" } +upi = { type = "enum", values = "upi_collect, upi_intent" } +voucher = { type = "enum", values = "boleto, efecty, pago_efectivo, red_compra, red_pagos, indomaret, alfamart, oxxo, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy" } +wallet = { type = "enum", values = "amazon_pay, apple_pay, google_pay, paypal, ali_pay, ali_pay_hk, dana, mb_way, mobile_pay, samsung_pay, twint, vipps, touch_n_go, swish, we_chat_pay, go_pay, gcash, momo, kakao_pay, cashapp, mifinity, paze" } [debit_routing_config] -fraud_check_fee = 0.01 +fraud_check_fee = 1.0 [debit_routing_config.network_fee] -visa = { percentage = 0.1375, fixed_amount = 0.020 } -mastercard = { percentage = 0.15, fixed_amount = 0.040 } -accel = { percentage = 0.0, fixed_amount = 0.040 } -nyce = { percentage = 0.10, fixed_amount = 0.015 } -pulse = { percentage = 0.10, fixed_amount = 0.03 } -star = { percentage = 0.10, fixed_amount = 0.015 } +visa = { percentage = 0.1375, fixed_amount = 2.0 } +mastercard = { percentage = 0.15, fixed_amount = 4.0 } +accel = { percentage = 0.0, fixed_amount = 4.0 } +nyce = { percentage = 0.10, fixed_amount = 1.5 } +pulse = { percentage = 0.10, fixed_amount = 3.0 } +star = { percentage = 0.10, fixed_amount = 1.5 } [debit_routing_config.interchange_fee] regulated = { percentage = 0.05, fixed_amount = 0.21 } [debit_routing_config.interchange_fee.non_regulated] -merchant_category_code_0001.visa = { percentage = 1.65, fixed_amount = 0.15 } -merchant_category_code_0001.mastercard = { percentage = 1.65, fixed_amount = 0.15 } -merchant_category_code_0001.accel = { percentage = 1.55, fixed_amount = 0.04 } -merchant_category_code_0001.nyce = { percentage = 1.30, fixed_amount = 0.213125 } -merchant_category_code_0001.pulse = { percentage = 1.60, fixed_amount = 0.15 } -merchant_category_code_0001.star = { percentage = 1.63, fixed_amount = 0.15 } +merchant_category_code_0001.visa = { percentage = 1.65, fixed_amount = 15.0 } +merchant_category_code_0001.mastercard = { percentage = 1.65, fixed_amount = 15.0 } +merchant_category_code_0001.accel = { percentage = 1.55, fixed_amount = 4.0 } +merchant_category_code_0001.nyce = { percentage = 1.30, fixed_amount = 21.3125 } +merchant_category_code_0001.pulse = { percentage = 1.60, fixed_amount = 15.0 } +merchant_category_code_0001.star = { percentage = 1.63, fixed_amount = 15.0 } + +[pm_filters.default] +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", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +paypal = { currency = "AUD,BRL,CAD,CHF,CNY,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,MYR,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ", currency = "GBP,AUD,NZD,CAD,USD" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } + +[pm_filters.stripe] +google_pay = { country = "AU, AT, BE, BR, BG, CA, HR, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, ID, IE, IT, JP, LV, KE, LT, LU, MY, MX, NL, NZ, NO, PL, PT, RO, SG, SK, ZA, ES, SE, CH, TH, AE, GB, US, GI, LI, MT, CY, PH, IS, AR, CL, KR, IL"} +apple_pay = { country = "AU, AT, BE, BR, BG, CA, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, HK, IE, IT, JP, LV, LI, LT, LU, MT, MY, MX, NL, NZ, NO, PL, PT, RO, SK, SG, SI, ZA, ES, SE, CH, GB, AE, US" } +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" } +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +multibanco = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GI,GR,HU,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB", currency = "EUR" } +ach = { country = "US", currency = "USD" } +revolut_pay = { currency = "EUR,GBP" } +sepa = {country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GI,GR,HU,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,IS,LI", currency="EUR"} +bacs = { country = "GB", currency = "GBP" } +becs = { country = "AU", currency = "AUD" } +sofort = {country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE", currency = "EUR" } +blik = {country="PL", currency = "PLN"} +bancontact_card = { country = "BE", currency = "EUR" } +przelewy24 = { country = "PL", currency = "EUR,PLN" } +online_banking_fpx = { country = "MY", currency = "MYR" } +amazon_pay = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "USD,AUD,GBP,DKK,EUR,HKD,JPY,NZD,NOK,ZAR,SEK,CHF" } +we_chat_pay = { country = "CN", currency = "CNY,AUD,CAD,EUR,GBP,HKD,JPY,SGD,USD,DKK,NOK,SEK,CHF" } +ali_pay = {country = "CN", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,MYR,NZD,SGD,USD"} + +[pm_filters.volt] +open_banking = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "GBP,EUR,DKK,NOK,PLN,SEK" } +open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "GBP" } + +[pm_filters.razorpay] +upi_collect = { country = "IN", currency = "INR" } + +[pm_filters.phonepe] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } + +[pm_filters.paytm] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } + +[pm_filters.hyperpg] +credit = { currency = "INR,USD,GBP,EUR" } +debit = { currency = "INR,USD,GBP,EUR" } + +[pm_filters.plaid] +open_banking_pis = { currency = "EUR,GBP" } + +[pm_filters.adyen] +google_pay = { country = "AT, AU, BE, BG, CA, HR, CZ, EE, FI, FR, DE, GR, HK, DK, HU, IE, IT, LV, LT, LU, NL, NO, PL, PT, RO, SK, ES, SE, CH, GB, US, NZ, SG", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, GG, HU, IE, IM, IT, LV, LI, LT, LU, MT, NL, NO, PL, PT, RO, SK, SI, SE, ES, CH, GB, US, PR, CA, AU, HK, NZ, SG", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, CHF, UAH, ARS, BRL, CLP, COP, CRC, DOP, GTQ, HNL, MXN, PAB, USD, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, ILS, QAR, SAR, AED, CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +mb_way = { country = "PT", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +pay_bright = { country = "CA", currency = "CAD" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { not_available_flows = { capture_method = "manual" }, country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR" } +ideal = { not_available_flows = { capture_method = "manual" }, country = "NL", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +bancontact_card = { country = "BE", currency = "EUR" } +ach = { country = "US", currency = "USD" } +bacs = { country = "GB", currency = "GBP" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +ali_pay_hk = { country = "HK", currency = "HKD" } +bizum = { country = "ES", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +momo = { country = "VN", currency = "VND" } +gcash = { country = "PH", currency = "PHP" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_thailand = { country = "TH", currency = "THB" } +touch_n_go = { country = "MY", currency = "MYR" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +swish = { country = "SE", currency = "SEK" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } +pix = { country = "BR", currency = "BRL" } +boleto = { country = "BR", currency = "BRL" } + +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + +[pm_filters.airwallex] +credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "AL, DZ, AS, AO, AG, AR, AU, AZ, BH, BR, BG, CA, CL, CO, CZ, DK, DO, EG, HK, HU, ID, IL, JP, JO, KZ, KE, KW, LB, MY, MX, OM, PK, PA, PE, PH, PL, QA, RO, SA, SG, ZA, LK, SE, TW, TH, TR, UA, AE, UY, VN, AT, BE, HR, EE, FI, FR, DE, GR, IE, IT, LV, LT, LU, NL, PL, PT, SK, ES, SE, RO, BG", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +paypal = { currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,JPY,MYR,MXN,NOK,NZD,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } +klarna = { currency = "EUR, DKK, NOK, PLN, SEK, CHF, GBP, USD, CZK" } +trustly = {currency="DKK, EUR, GBP, NOK, PLN, SEK" } +blik = { country="PL" , currency = "PLN" } +ideal = { country="NL" , currency = "EUR" } +atome = { country = "SG, MY" , currency = "SGD, MYR" } +skrill = { country="AL, DZ, AD, AR, AM, AW, AU, AT, AZ, BS, BD, BE, BJ, BO, BA, BW, BR, BN, BG, KH, CM, CA, CL, CN, CX, CO, CR , HR, CW, CY, CZ, DK, DM, DO, EC, EG, EE , FK, FI, GE, DE, GH, GI, GR, GP, GU, GT, GG, HK, HU, IS, IN, ID , IQ, IE, IM, IL, IT, JE , KZ, KE , KR, KW, KG, LV , LS, LI, LT, LU , MK, MG, MY, MV, MT, MU, YT, MX, MD, MC, MN, ME, MA, NA, NP, NZ, NI, NE, NO, PK , PA, PY, PE, PH, PL, PT, PR, QA, RO , SM , SA, SN , SG, SX, SK, SI, ZA, SS, ES, LK, SE, CH, TW, TZ, TH, TN, AE, GB, UM, UY, VN, VG, VI, US" , currency = "EUR, GBP, USD" } +indonesian_bank_transfer = { country="ID" , currency = "IDR" } + +[pm_filters.elavon] +credit = { country = "US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.xendit] +credit = { country = "ID,PH", currency = "IDR,PHP,USD,SGD,MYR" } +debit = { country = "ID,PH", currency = "IDR,PHP,USD,SGD,MYR" } +qris = {currency = "IDR" } + +[pm_filters.tsys] +credit = { country = "NA", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SOS, SRD, SSP, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL, BYN, KPW, STN, MRU, VES" } +debit = { country = "NA", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SOS, SRD, SSP, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL, BYN, KPW, STN, MRU, VES" } + +[pm_filters.billwerk] +credit = { country = "DE, DK, FR, SE", currency = "DKK, NOK" } +debit = { country = "DE, DK, FR, SE", currency = "DKK, NOK" } + +[pm_filters.fiservemea] +credit = { country = "DE, FR, IT, NL, PL, ES, ZA, GB, AE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "DE, FR, IT, NL, PL, ES, ZA, GB, AE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.getnet] +credit = { country = "AR, BR, CL, MX, UY, ES, PT, DE, IT, FR, NL, BE, AT, PL, CH, GB, IE, LU, DK, SE, NO, FI, IN, AE", currency = "ARS, BRL, CLP, MXN, UYU, EUR, PLN, CHF, GBP, DKK, SEK, NOK, INR, AED" } + +[pm_filters.hipay] +credit = { country = "GB, CH, SE, DK, NO, PL, CZ, US, CA, JP, HK, AU, ZA", currency = "EUR, GBP, CHF, SEK, DKK, NOK, PLN, CZK, USD, CAD, JPY, HKD, AUD, ZAR" } +debit = { country = "GB, CH, SE, DK, NO, PL, CZ, US, CA, JP, HK, AU, ZA", currency = "EUR, GBP, CHF, SEK, DKK, NOK, PLN, CZK, USD, CAD, JPY, HKD, AUD, ZAR" } + +[pm_filters.moneris] +credit = { country = "AE, AF, AL, AO, AR, AT, AU, AW, AZ, BA, BB, BD, BE, BG, BH, BI, BM, BN, BO, BR, BT, BY, BZ, CH, CL, CN, CO, CR, CU, CV, CY, CZ, DE, DJ, DK, DO, DZ, EE, EG, ES, FI, FJ, FR, GB, GE, GI, GM, GN, GR, GT, GY, HK, HN, HR, HT, HU, ID, IE, IL, IN, IS, IT, JM, JO, JP, KE, KM, KR, KW, KY, KZ, LA, LK, LR, LS, LV, LT, LU, MA, MD, MG, MK, MO, MR, MT, MU, MV, MW, MX, MY, MZ, NA, NG, NI, NL, NO, NP, NZ, OM, PE, PG, PK, PL, PT, PY, QA, RO, RS, RU, RW, SA, SB, SC, SE, SG, SH, SI, SK, SL, SR, SV, SZ, TH, TJ, TM, TN, TR, TT, TW, TZ, UG, US, UY, UZ, VN, VU, WS, ZA, ZM", currency = "AED, AFN, ALL, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BTN, BYN, BZD, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, EUR, FJD, GBP, GEL, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JOD, JPY, KES, KMF, KRW, KWD, KYD, KZT, LAK, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SRD, SVC, SZL, THB, TJS, TMT, TND, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VND, VUV, WST, XCD, XOF, XPF, ZAR, ZMW" } +debit = { country = "AE, AF, AL, AO, AR, AT, AU, AW, AZ, BA, BB, BD, BE, BG, BH, BI, BM, BN, BO, BR, BT, BY, BZ, CH, CL, CN, CO, CR, CU, CV, CY, CZ, DE, DJ, DK, DO, DZ, EE, EG, ES, FI, FJ, FR, GB, GE, GI, GM, GN, GR, GT, GY, HK, HN, HR, HT, HU, ID, IE, IL, IN, IS, IT, JM, JO, JP, KE, KM, KR, KW, KY, KZ, LA, LK, LR, LS, LV, LT, LU, MA, MD, MG, MK, MO, MR, MT, MU, MV, MW, MX, MY, MZ, NA, NG, NI, NL, NO, NP, NZ, OM, PE, PG, PK, PL, PT, PY, QA, RO, RS, RU, RW, SA, SB, SC, SE, SG, SH, SI, SK, SL, SR, SV, SZ, TH, TJ, TM, TN, TR, TT, TW, TZ, UG, US, UY, UZ, VN, VU, WS, ZA, ZM", currency = "AED, AFN, ALL, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BTN, BYN, BZD, CHF, CLP, CNY, COP, CRC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, EUR, FJD, GBP, GEL, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JOD, JPY, KES, KMF, KRW, KWD, KYD, KZT, LAK, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SRD, SVC, SZL, THB, TJS, TMT, TND, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VND, VUV, WST, XCD, XOF, XPF, ZAR, ZMW" } + +[pm_filters.opennode] +crypto_currency = { country = "US, CA, GB, AU, BR, MX, SG, PH, NZ, ZA, JP, AT, BE, HR, CY, EE, FI, FR, DE, GR, IE, IT, LV, LT, LU, MT, NL, PT, SK, SI, ES", currency = "USD, CAD, GBP, AUD, BRL, MXN, SGD, PHP, NZD, ZAR, JPY, EUR" } + +[pm_filters.bambora] +credit = { country = "US,CA", currency = "USD" } +debit = { country = "US,CA", currency = "USD" } + +[pm_filters.bankofamerica] +credit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "USD" } +debit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "USD" } +apple_pay = { country = "AU,AT,BH,BE,BR,BG,CA,CL,CN,CO,CR,HR,CY,CZ,DK,DO,EC,EE,SV,FI,FR,DE,GR,GT,HN,HK,HU,IS,IN,IE,IL,IT,JP,JO,KZ,KW,LV,LI,LT,LU,MY,MT,MX,MC,ME,NL,NZ,NO,OM,PA,PY,PE,PL,PT,QA,RO,SA,SG,SK,SI,ZA,KR,ES,SE,CH,TW,AE,GB,US,UY,VN,VA", currency = "USD" } +google_pay = { country = "AU,AT,BE,BR,CA,CL,CO,CR,CY,CZ,DK,DO,EC,EE,SV,FI,FR,DE,GR,GT,HN,HK,HU,IS,IN,IE,IL,IT,JP,JO,KZ,KW,LV,LI,LT,LU,MY,MT,MX,NL,NZ,NO,OM,PA,PY,PE,PL,PT,QA,RO,SA,SG,SK,SI,ZA,KR,ES,SE,CH,TW,AE,GB,US,UY,VN,VA", currency = "USD" } +samsung_pay = { country = "AU,BH,BR,CA,CN,DK,FI,FR,DE,HK,IN,IT,JP,KZ,KR,KW,MY,NZ,NO,OM,QA,SA,SG,ZA,ES,SE,CH,TW,AE,GB,US", currency = "USD" } + +[pm_filters.cybersource] +credit = { currency = "USD,GBP,EUR,PLN,SEK,XOF,CAD,KWD,QAR" } +debit = { currency = "USD,GBP,EUR,PLN,SEK,XOF,CAD,KWD,QAR" } +apple_pay = { currency = "ARS, CAD, CLP, COP, CNY, EUR, HKD, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, GBP, AED, USD, PLN, SEK" } +google_pay = { currency = "ARS, AUD, CAD, CLP, COP, EUR, HKD, INR, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, AED, GBP, USD, PLN, SEK" } +samsung_pay = { currency = "USD,GBP,EUR,SEK" } +paze = { currency = "USD,SEK" } + +[pm_filters.barclaycard] +credit = { currency = "USD,GBP,EUR,PLN,SEK" } +debit = { currency = "USD,GBP,EUR,PLN,SEK" } +google_pay = { currency = "ARS, AUD, CAD, CLP, COP, EUR, HKD, INR, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, AED, GBP, USD, PLN, SEK" } +apple_pay = { currency = "ARS, CAD, CLP, COP, CNY, EUR, HKD, KWD, MYR, MXN, NZD, PEN, QAR, SAR, SGD, ZAR, UAH, GBP, AED, USD, PLN, SEK" } + + +[pm_filters.globepay] +ali_pay = { country = "GB",currency = "GBP,CNY" } +we_chat_pay = { country = "GB",currency = "GBP,CNY" } + + +[pm_filters.itaubank] +pix = { country = "BR", currency = "BRL" } + +[pm_filters.nexinets] +credit = { country = "DE",currency = "EUR" } +debit = { country = "DE",currency = "EUR" } +ideal = { country = "DE",currency = "EUR" } +giropay = { country = "DE",currency = "EUR" } +sofort = { country = "DE",currency = "EUR" } +eps = { country = "DE",currency = "EUR" } +apple_pay = { country = "DE",currency = "EUR" } +paypal = { country = "DE",currency = "EUR" } + + +[pm_filters.nuvei] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +klarna = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +afterpay_clearpay = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +giropay = { currency = "EUR" } +ideal = { currency = "EUR" } +sofort = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +eps = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +apple_pay = { country = "AU,AT,BY,BE,BR,BG,CA,CN,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HK,HU,IS,IE,IM,IL,IT,JP,JE,KZ,LV,LI,LT,LU,MO,MT,MC,NL,NZ,NO,PL,PT,RO,RU,SM,SA,SG,SK,SI,ES,SE,CH,TW,UA,AE,GB,US,VA",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +google_pay = { country = "AF,AX,AL,DZ,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,KH,CM,CA,CV,KY,CF,TD,CL,CN,TW,CX,CC,CO,KM,CG,CK,CR,CI,HR,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,HN,HK,HU,IS,IN,ID,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VI,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UY,UZ,VU,VA,VE,VN,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +paypal = { country = "AU,CA,GB,IN,JP,NZ,PH,SG,TH,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } + +[payout_method_filters.nuvei] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US",currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BDT,BGN,BHD,BMD,BND,BRL,BYN,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,GBP,GEL,GHS,GTQ,HKD,HUF,IDR,INR,IQD,ISK,JOD,JPY,KES,KGS,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MMK,MNT,MUR,MWK,MXN,MYR,MZN,NAD,NGN,NOK,NZD,OMR,PEN,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,SAR,SEK,SGD,SOS,THB,TND,TOP,TRY,TTD,TWD,UAH,UGX,USD,UYU,UZS,VND,XAF,XOF,YER,ZAR" } + +[pm_filters.checkout] +debit = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,US,AU,HK,SG,SA,AE,BH,MX,AR,CL,CO,PE", currency = "AED,AFN,ALL,AMD,ANG,AOA,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +credit = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MT,NL,NO,PL,PT,RO,SK,SI,ES,SE,CH,GB,US,AU,HK,SG,SA,AE,BH,MX,AR,CL,CO,PE", currency = "AED,AFN,ALL,AMD,ANG,AOA,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "AL,DZ, AS, AO, AG, AR, AU, AT, AZ, BH, BY, BE, BR, CA, BG, CL, CO, HR, DK, DO, EE, EG, 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, SA, SG, SK, ZA, ES, LK, SE, CH, TH, TW, TR, UA, AE, US, UY, VN", currency = "AED, ALL, AOA, AUD, AZN, BGN, BHD, BRL, CAD, CHF, CLP, COP, CZK, DKK, DOP, DZD, EGP, EUR, GBP, HKD, HUF, IDR, ILS, INR, JPY, KES, KWD, KZT, LKR, MXN, MYR, NOK, NZD, OMR, PAB, PEN, PHP, PKR, PLN, QAR, RON, SAR, SEK, SGD, THB, TRY, TWD, UAH, USD, UYU, VND, XCD, ZAR" } +apple_pay = { country = "AM,US, AT, AZ, BY, BE, BG, HR, CY, 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, VA, AU , HK, JP , MY , MN, NZ, SG, TW, VN, EG , MA, ZA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, UY, BH, IL, JO, KW, OM,QA, SA, AE, CA", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, BRL, COP, CRC, DOP, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD, USD" } + +[pm_filters.checkbook] +ach = { country = "US", currency = "USD" } + +[pm_filters.nexixpay] +credit = { country = "AT,BE,CY,EE,FI,FR,DE,GR,IE,IT,LV,LT,LU,MT,NL,PT,SK,SI,ES,BG,HR,DK,GB,NO,PL,CZ,RO,SE,CH,HU,AU,BR,US", currency = "ARS,AUD,BHD,CAD,CLP,CNY,COP,HRK,CZK,DKK,HKD,HUF,INR,JPY,KZT,JOD,KRW,KWD,MYR,MXN,NGN,NOK,PHP,QAR,RUB,SAR,SGD,VND,ZAR,SEK,CHF,THB,AED,EGP,GBP,USD,TWD,BYN,RSD,AZN,RON,TRY,AOA,BGN,EUR,UAH,PLN,BRL" } +debit = { country = "AT,BE,CY,EE,FI,FR,DE,GR,IE,IT,LV,LT,LU,MT,NL,PT,SK,SI,ES,BG,HR,DK,GB,NO,PL,CZ,RO,SE,CH,HU,AU,BR,US", currency = "ARS,AUD,BHD,CAD,CLP,CNY,COP,HRK,CZK,DKK,HKD,HUF,INR,JPY,KZT,JOD,KRW,KWD,MYR,MXN,NGN,NOK,PHP,QAR,RUB,SAR,SGD,VND,ZAR,SEK,CHF,THB,AED,EGP,GBP,USD,TWD,BYN,RSD,AZN,RON,TRY,AOA,BGN,EUR,UAH,PLN,BRL" } + +[pm_filters.square] +credit = { country = "AU,CA,FR,IE,JP,ES,GB,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +debit = { country = "AU,CA,FR,IE,JP,ES,GB,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } + +[pm_filters.iatapay] +upi_collect = { country = "IN", currency = "INR" } +upi_intent = { country = "IN", currency = "INR" } +ideal = { country = "NL", currency = "EUR" } +local_bank_redirect = { country = "AT,BE,EE,FI,FR,DE,IE,IT,LV,LT,LU,NL,PT,ES,GB,IN,HK,SG,TH,BR,MX,GH,VN,MY,PH,JO,AU,CO", currency = "EUR,GBP,INR,HKD,SGD,THB,BRL,MXN,GHS,VND,MYR,PHP,JOD,AUD,COP" } +duit_now = { country = "MY", currency = "MYR" } +fps = { country = "GB", currency = "GBP" } +prompt_pay = { country = "TH", currency = "THB" } +viet_qr = { country = "VN", currency = "VND" } + +[pm_filters.coinbase] +crypto_currency = { country = "ZA,US,BR,CA,TR,SG,VN,GB,DE,FR,ES,PT,IT,NL,AU" } + +[pm_filters.novalnet] +credit = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +debit = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +apple_pay = { country = "EG, MA, ZA, CN, HK, JP, MY, MN, SG, KR, VN, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, GE, DE, GR, GL, HU, IS, IE, IT, LV, LI, LT, LU, MT, MD, MC, ME, NL, NO, PL, PT, RO, SM, RS, SK, SI, ES, SE, CH, UA, GB, VA, CA, US, BH, IL, JO, KW, OM, QA, SA, AE, AR, BR, CL, CO, CR, SV, GT, MX, PY, PE, UY, BS, DO, AM, KZ, NZ", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, AMD, EUR, AZN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } +google_pay = { country = "AO, EG, KE, ZA, AR, BR, CL, CO, MX, PE, UY, AG, DO, AE, TR, SA, QA, OM, LB, KW, JO, IL, BH, KZ, VN, TH, SG, MY, JP, HK, LK, IN, US, CA, GB, UA, CH, SE, ES, SK, PT, RO, PL, NO, NL, LU, LT, LV, IE, IT, HU, GR, DE, FR, FI, EE, DK, CZ, HR, BG, BE, AT, AL", currency = "ALL, DZD, USD, XCD, ARS, AUD, EUR, AZN, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +paypal = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,BS,BW,BY,BZ,CA,CD,CH,CL,CN,CO,CR,CU,CY,CZ,DE,DJ,DK,DO,DZ,EE,EG,ET,ES,FI,FJ,FR,GB,GE,GH,GI,GM,GR,GT,GY,HK,HN,HR,HU,ID,IE,IL,IN,IS,IT,JM,JO,JP,KE,KH,KR,KW,KY,KZ,LB,LK,LT,LV,LY,MA,MC,MD,ME,MG,MK,MN,MO,MT,MV,MW,MX,MY,NG,NI,NO,NP,NL,NZ,OM,PA,PE,PG,PH,PK,PL,PT,PY,QA,RO,RS,RU,RW,SA,SB,SC,SE,SG,SH,SI,SK,SL,SO,SM,SR,ST,SV,SY,TH,TJ,TN,TO,TR,TW,TZ,UA,UG,US,UY,UZ,VE,VA,VN,VU,WS,CF,AG,DM,GD,KN,LC,VC,YE,ZA,ZM", currency = "AED,ALL,AMD,ARS,AUD,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,GBP,GEL,GHS,GIP,GMD,GTQ,GYD,HKD,HNL,HRK,HUF,IDR,ILS,INR,ISK,JMD,JOD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,LYD,MAD,MDL,MGA,MKD,MNT,MOP,MVR,MWK,MXN,MYR,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STN,SVC,SYP,THB,TJS,TND,TOP,TRY,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,YER,ZAR,ZMW" } +sepa = {country = "FR, IT, GR, BE, BG, FI, EE, HR, IE, DE, DK, LT, LV, MT, LU, AT, NL, PT, RO, PL, SK, SE, ES, SI, HU, CZ, CY, GB, LI, NO, IS, MC, CH, YT, PM, SM", currency="EUR"} +sepa_guaranteed_debit = {country = "AT, CH, DE", currency="EUR"} + +[pm_filters.braintree] +credit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} + +[pm_filters.facilitapay] +pix = { country = "BR", currency = "BRL" } + +[pm_filters.finix] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} +google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } + +[pm_filters.helcim] +credit = { country = "US, CA", currency = "USD, CAD" } +debit = { country = "US, CA", currency = "USD, CAD" } + +[pm_filters.globalpay] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,SZ,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AFN,DZD,ARS,AMD,AWG,AUD,AZN,BSD,BHD,THB,PAB,BBD,BYN,BZD,BMD,BOB,BRL,BND,BGN,BIF,CVE,CAD,CLP,COP,KMF,CDF,NIO,CRC,CUP,CZK,GMD,DKK,MKD,DJF,DOP,VND,XCD,EGP,SVC,ETB,EUR,FKP,FJD,HUF,GHS,GIP,HTG,PYG,GNF,GYD,HKD,UAH,ISK,INR,IRR,IQD,JMD,JOD,KES,PGK,HRK,KWD,AOA,MMK,LAK,GEL,LBP,ALL,HNL,SLL,LRD,LYD,SZL,LSL,MGA,MWK,MYR,MUR,MXN,MDL,MAD,MZN,NGN,ERN,NAD,NPR,ANG,ILS,TWD,NZD,BTN,KPW,NOK,TOP,PKR,MOP,UYU,PHP,GBP,BWP,QAR,GTQ,ZAR,OMR,KHR,RON,MVR,IDR,RUB,RWF,SHP,SAR,RSD,SCR,SGD,PEN,SBD,KGS,SOS,TJS,SSP,LKR,SDG,SRD,SEK,CHF,SYP,BDT,WST,TZS,KZT,TTD,MNT,TND,TRY,TMT,AED,UGX,USD,UZS,VUV,KRW,YER,JPY,CNY,ZMW,ZWL,PLN,CLF,STD,CUC" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,SZ,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AFN,DZD,ARS,AMD,AWG,AUD,AZN,BSD,BHD,THB,PAB,BBD,BYN,BZD,BMD,BOB,BRL,BND,BGN,BIF,CVE,CAD,CLP,COP,KMF,CDF,NIO,CRC,CUP,CZK,GMD,DKK,MKD,DJF,DOP,VND,XCD,EGP,SVC,ETB,EUR,FKP,FJD,HUF,GHS,GIP,HTG,PYG,GNF,GYD,HKD,UAH,ISK,INR,IRR,IQD,JMD,JOD,KES,PGK,HRK,KWD,AOA,MMK,LAK,GEL,LBP,ALL,HNL,SLL,LRD,LYD,SZL,LSL,MGA,MWK,MYR,MUR,MXN,MDL,MAD,MZN,NGN,ERN,NAD,NPR,ANG,ILS,TWD,NZD,BTN,KPW,NOK,TOP,PKR,MOP,UYU,PHP,GBP,BWP,QAR,GTQ,ZAR,OMR,KHR,RON,MVR,IDR,RUB,RWF,SHP,SAR,RSD,SCR,SGD,PEN,SBD,KGS,SOS,TJS,SSP,LKR,SDG,SRD,SEK,CHF,SYP,BDT,WST,TZS,KZT,TTD,MNT,TND,TRY,TMT,AED,UGX,USD,UZS,VUV,KRW,YER,JPY,CNY,ZMW,ZWL,PLN,CLF,STD,CUC" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,IT,NL", currency = "EUR" } + +[pm_filters.jpmorgan] +debit = { country = "CA, GB, US, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, EUR, GBP, AUD, NZD, SGD, CAD, JPY, HKD, KRW, TWD, MXN, BRL, DKK, NOK, ZAR, SEK, CHF, CZK, PLN, TRY, AFN, ALL, DZD, AOA, ARS, AMD, AWG, AZN, BSD, BDT, BBD, BYN, BZD, BMD, BOB, BAM, BWP, BND, BGN, BIF, BTN, XOF, XAF, XPF, KHR, CVE, KYD, CLP, CNY, COP, KMF, CDF, CRC, HRK, DJF, DOP, XCD, EGP, ETB, FKP, FJD, GMD, GEL, GHS, GIP, GTQ, GYD, HTG, HNL, HUF, ISK, INR, IDR, ILS, JMD, KZT, KES, LAK, LBP, LSL, LRD, MOP, MKD, MGA, MWK, MYR, MVR, MRU, MUR, MDL, MNT, MAD, MZN, MMK, NAD, NPR, ANG, PGK, NIO, NGN, PKR, PAB, PYG, PEN, PHP, QAR, RON, RWF, SHP, WST, STN, SAR, RSD, SCR, SLL, SBD, SOS, LKR, SRD, SZL, TJS, TZS, THB, TOP, TTD, UGX, UAH, AED, UYU, UZS, VUV, VND, YER, ZMW" } +credit = { country = "CA, GB, US, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, EUR, GBP, AUD, NZD, SGD, CAD, JPY, HKD, KRW, TWD, MXN, BRL, DKK, NOK, ZAR, SEK, CHF, CZK, PLN, TRY, AFN, ALL, DZD, AOA, ARS, AMD, AWG, AZN, BSD, BDT, BBD, BYN, BZD, BMD, BOB, BAM, BWP, BND, BGN, BIF, BTN, XOF, XAF, XPF, KHR, CVE, KYD, CLP, CNY, COP, KMF, CDF, CRC, HRK, DJF, DOP, XCD, EGP, ETB, FKP, FJD, GMD, GEL, GHS, GIP, GTQ, GYD, HTG, HNL, HUF, ISK, INR, IDR, ILS, JMD, KZT, KES, LAK, LBP, LSL, LRD, MOP, MKD, MGA, MWK, MYR, MVR, MRU, MUR, MDL, MNT, MAD, MZN, MMK, NAD, NPR, ANG, PGK, NIO, NGN, PKR, PAB, PYG, PEN, PHP, QAR, RON, RWF, SHP, WST, STN, SAR, RSD, SCR, SLL, SBD, SOS, LKR, SRD, SZL, TJS, TZS, THB, TOP, TTD, UGX, UAH, AED, UYU, UZS, VUV, VND, YER, ZMW" } + +[pm_filters.bitpay] +crypto_currency = { country = "US, CA, GB, AT, BE, BG, HR, CY, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IT, LV, LT, LU, MT, NL, PL, PT, RO, SK, SI, ES, SE", currency = "USD, AUD, CAD, GBP, MXN, NZD, CHF, EUR"} + +[pm_filters.paybox] +debit = { country = "FR", currency = "CAD, AUD, EUR, USD" } +credit = { country = "FR", currency = "CAD, AUD, EUR, USD" } + +[pm_filters.payload] +debit = { currency = "USD,CAD" } +credit = { currency = "USD,CAD" } +ach = { currency = "USD,CAD" } + + +[pm_filters.digitalvirgo] +direct_carrier_billing = {country = "MA, CM, ZA, EG, SN, DZ, TN, ML, GN, GH, LY, GA, CG, MG, BW, SD, NG, ID, SG, AZ, TR, FR, ES, PL, GB, PT, DE, IT, BE, IE, SK, GR, NL, CH, BR, MX, AR, CL, AE, IQ, KW, BH, SA, QA, PS, JO, OM, RU" , currency = "MAD, XOF, XAF, ZAR, EGP, DZD, TND, GNF, GHS, LYD, XAF, CDF, MGA, BWP, SDG, NGN, IDR, SGD, RUB, AZN, TRY, EUR, PLN, GBP, CHF, BRL, MXN, ARS, CLP, AED, IQD, KWD, BHD, SAR, QAR, ILS, JOD, OMR" } + +[pm_filters.payu] +debit = { country = "AE, AF, AL, AM, CW, AO, AR, AU, AW, AZ, BA, BB, BG, BH, BI, BM, BN, BO, BR, BS, BW, BY, BZ, CA, CD, LI, CL, CN, CO, CR, CV, CZ, DJ, DK, DO, DZ, EG, ET, AD, FJ, FK, GG, GE, GH, GI, GM, GN, GT, GY, HK, HN, HR, HT, HU, ID, IL, IQ, IS, JM, JO, JP, KG, KH, KM, KR, KW, KY, KZ, LA, LB, LR, LS, MA, MD, MG, MK, MN, MO, MR, MV, MW, MX, MY, MZ, NA, NG, NI, BV, CK, OM, PA, PE, PG, PL, PY, QA, RO, RS, RW, SA, SB, SC, SE, SG, SH, SO, SR, SV, SZ, TH, TJ, TM, TN, TO, TR, TT, TW, TZ, UG, AS, UY, UZ, VE, VN, VU, WS, CM, AI, BJ, PF, YE, ZA, ZM, ZW", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, IQD, ISK, JMD, JOD, JPY, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LRD, LSL, MAD, MDL, MGA, MKD, MNT, MOP, MRU, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NZD, OMR, PAB, PEN, PGK, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SOS, SRD, SVC, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +credit = { country = "AE, AF, AL, AM, CW, AO, AR, AU, AW, AZ, BA, BB, BG, BH, BI, BM, BN, BO, BR, BS, BW, BY, BZ, CA, CD, LI, CL, CN, CO, CR, CV, CZ, DJ, DK, DO, DZ, EG, ET, AD, FJ, FK, GG, GE, GH, GI, GM, GN, GT, GY, HK, HN, HR, HT, HU, ID, IL, IQ, IS, JM, JO, JP, KG, KH, KM, KR, KW, KY, KZ, LA, LB, LR, LS, MA, MD, MG, MK, MN, MO, MR, MV, MW, MX, MY, MZ, NA, NG, NI, BV, CK, OM, PA, PE, PG, PL, PY, QA, RO, RS, RW, SA, SB, SC, SE, SG, SH, SO, SR, SV, SZ, TH, TJ, TM, TN, TO, TR, TT, TW, TZ, UG, AS, UY, UZ, VE, VN, VU, WS, CM, AI, BJ, PF, YE, ZA, ZM, ZW", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, IQD, ISK, JMD, JOD, JPY, KGS, KHR, KMF, KRW, KWD, KYD, KZT, LAK, LBP, LRD, LSL, MAD, MDL, MGA, MKD, MNT, MOP, MRU, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NZD, OMR, PAB, PEN, PGK, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SOS, SRD, SVC, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" } +google_pay = { country = "AL, DZ, AS, AO, AR, AU, AZ, BH, BY, BR, BG, CA, CL, CO, HR, CZ, DK, DO, EG, HK, HU, ID, IN, IL, JP, JO, KZ, KW, LB, MY, MX, OM, PA, PE, PL, QA, RO, SA, SG, SE, TW, TH, TR, AE, UY, VN", currency = "ALL, DZD, USD, AOA, XCD, ARS, AUD, EUR, AZN, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PAB, PEN, PLN, QAR, RON, SAR, SGD, ZAR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } + +[pm_filters.klarna] +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,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } + +[pm_filters.flexiti] +flexiti = { country = "CA", currency = "CAD" } + +[pm_filters.mifinity] +mifinity = { country = "BR,CN,SG,MY,DE,CH,DK,GB,ES,AD,GI,FI,FR,GR,HR,IT,JP,MX,AR,CO,CL,PE,VE,UY,PY,BO,EC,GT,HN,SV,NI,CR,PA,DO,CU,PR,NL,NO,PL,PT,SE,RU,TR,TW,HK,MO,AX,AL,DZ,AS,AO,AI,AG,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BE,BZ,BJ,BM,BT,BQ,BA,BW,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CX,CC,KM,CG,CK,CI,CW,CY,CZ,DJ,DM,EG,GQ,ER,EE,ET,FK,FO,FJ,GF,PF,TF,GA,GM,GE,GH,GL,GD,GP,GU,GG,GN,GW,GY,HT,HM,VA,IS,IN,ID,IE,IM,IL,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LI,LT,LU,MK,MG,MW,MV,ML,MT,MH,MQ,MR,MU,YT,FM,MD,MC,MN,ME,MS,MA,MZ,NA,NR,NP,NC,NZ,NE,NG,NU,NF,MP,OM,PK,PW,PS,PG,PH,PN,QA,RE,RO,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SX,SK,SI,SB,SO,ZA,GS,KR,LK,SR,SJ,SZ,TH,TL,TG,TK,TO,TT,TN,TM,TC,TV,UG,UA,AE,UZ,VU,VN,VG,VI,WF,EH,ZM", currency = "AUD,CAD,CHF,CNY,CZK,DKK,EUR,GBP,INR,JPY,NOK,NZD,PLN,RUB,SEK,ZAR,USD,EGP,UYU,UZS" } + +[pm_filters.zen] +credit = { not_available_flows = { capture_method = "manual" } } +debit = { not_available_flows = { capture_method = "manual" } } +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pse = { country = "CO", currency = "COP" } +pix = { country = "BR", currency = "BRL" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[pm_filters.zsl] +local_bank_transfer = { country = "CN", currency = "CNY" } + +[pm_filters.aci] +credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" } +debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" } +mb_way = { country = "EE,ES,PT", currency = "EUR" } +ali_pay = { country = "CN", currency = "CNY" } +eps = { country = "AT", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +sofort = { country = "AT,BE,CH,DE,ES,GB,IT,NL,PL", currency = "CHF,EUR,GBP,HUF,PLN"} +interac = { country = "CA", currency = "CAD,USD"} +przelewy24 = { country = "PL", currency = "CZK,EUR,GBP,PLN" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +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 = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.gigadat] +interac = { currency = "CAD"} + +[pm_filters.loonio] +interac = { currency = "CAD"} + +[pm_filters.dlocal] +oxxo = { currency = "MXN" } + +[pm_filters.mollie] +eps = { country = "AT", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +przelewy24 = { country = "PL", currency = "PLN,EUR" } +klarna = { country = "DE,AT,NL,BE,FR,GB,IT,ES,PT,SE,DK,FI,NO,CH,IR,CZ,PL,GR,SK", currency = "EUR,GBP,DKK,SEK,NOK,CHF,PLN,CZK" } + +[pm_filters.redsys] +credit = { currency = "AUD,BGN,CAD,CHF,COP,CZK,DKK,EUR,GBP,HRK,HUF,ILS,INR,JPY,MYR,NOK,NZD,PEN,PLN,RUB,SAR,SEK,SGD,THB,USD,ZAR", country="ES" } +debit = { currency = "AUD,BGN,CAD,CHF,COP,CZK,DKK,EUR,GBP,HRK,HUF,ILS,INR,JPY,MYR,NOK,NZD,PEN,PLN,RUB,SAR,SEK,SGD,THB,USD,ZAR", country="ES" } + +[pm_filters.stax] +credit = { country = "US", currency = "USD" } +debit = { country = "US", currency = "USD" } +ach = { country = "US", currency = "USD" } + +[pm_filters.prophetpay] +card_redirect = { country = "US", currency = "USD" } + +[pm_filters.multisafepay] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AUD,BRL,CAD,CHF,CLP,CNY,COP,CZK,DKK,EUR,GBP,HKD,HRK,HUF,ILS,INR,ISK,JPY,KRW,MXN,MYR,NOK,NZD,PEN,PLN,RON,RUB,SEK,SGD,TRY,TWD,USD,ZAR", not_available_flows = { capture_method = "manual" } } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AUD,BRL,CAD,CHF,CLP,CNY,COP,CZK,DKK,EUR,GBP,HKD,HRK,HUF,ILS,INR,ISK,JPY,KRW,MXN,MYR,NOK,NZD,PEN,PLN,RON,RUB,SEK,SGD,TRY,TWD,USD,ZAR", not_available_flows = { capture_method = "manual" } } +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", currency = "AED, AUD, BRL, CAD, CHF, CLP, COP, CZK, DKK, EUR, GBP, HKD, HRK, HUF, ILS, INR, JPY, MXN, MYR, NOK, NZD, PEN, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, TWD, UAH, USD, ZAR" } +paypal = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,CV,KH,CM,CA,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,HRK,HUF,JPY,MXN,MYR,NOK,NZD,PHP,PLN,RUB,SEK,SGD,THB,TRY,TWD,USD" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IT,NL,NO,PT,ES,SE,GB", currency = "DKK,EUR,GBP,NOK,SEK" } +trustly = { country = "AT,CZ,DK,EE,FI,DE,LV,LT,NL,NO,PL,PT,ES,SE,GB" , currency = "EUR,GBP,SEK"} +ali_pay = { currency = "EUR,USD" } +we_chat_pay = { currency = "EUR"} +eps = { country = "AT" , currency = "EUR" } +mb_way = { country = "PT" , currency = "EUR" } +sofort = { country = "AT,BE,FR,DE,IT,PL,ES,CH,GB" , currency = "EUR"} + +[pm_filters.cashtocode] +classic = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +evoucher = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.wellsfargo] +credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,US,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,US,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "US", currency = "USD" } +apple_pay = { country = "US", currency = "USD" } +ach = { country = "US", currency = "USD" } + +[pm_filters.trustpay] +credit = { not_available_flows = { capture_method = "manual" } } +debit = { not_available_flows = { capture_method = "manual" } } +instant_bank_transfer = { country = "CZ,SK,GB,AT,DE,IT", currency = "CZK, EUR, GBP" } +instant_bank_transfer_poland = { country = "PL", currency = "PLN" } +instant_bank_transfer_finland = { country = "FI", currency = "EUR" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,GB", currency = "EUR" } + +[pm_filters.tesouro] +credit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AF,AL,DZ,AD,AO,AI,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BA,BW,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CO,KM,CD,CG,CK,CR,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MG,MW,MY,MV,ML,MT,MH,MR,MU,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PL,PT,PR,QA,CG,RO,RW,KN,LC,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SO,ZA,GS,ES,LK,SR,SJ,SZ,SE,CH,TW,TJ,TZ,TH,TL,TG,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UY,UZ,VU,VE,VN,VG,WF,YE,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +apple_pay = { currency = "USD" } +google_pay = { currency = "USD" } + +[pm_filters.authorizedotnet] +credit = {currency = "CAD,USD"} +debit = {currency = "CAD,USD"} +google_pay = {currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD"} +apple_pay = {currency = "EUR,GBP,ISK,USD,AUD,CAD,BRL,CLP,COP,CRC,CZK,DKK,EGP,GEL,GHS,GTQ,HNL,HKD,HUF,ILS,INR,JPY,KZT,KRW,KWD,MAD,MXN,MYR,NOK,NZD,PEN,PLN,PYG,QAR,RON,SAR,SEK,SGD,THB,TWD,UAH,AED,VND,ZAR"} +paypal = {currency = "AUD,BRL,CAD,CHF,CNY,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,MYR,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD"} + +[pm_filters.dwolla] +ach = { country = "US", currency = "USD" } + +[pm_filters.worldpay] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +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", currency = "DZD, AOA, USD, XCD, ARS, AUD, AZN, EUR, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "EG, MA, ZA, AU, CN, HK, JP, MO, MY, MN, NZ, SG, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, CZ, DK, EE, FO, FI, FR, GE, DE, GR, GL, GG, HU, IE, IS, 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, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US, PR", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } + +[pm_filters.worldpayxml] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +google_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +apple_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } + +[pm_filters.worldpayvantiv] +debit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +apple_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } +google_pay = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,KH,CA,CV,KY,CL,CO,KM,CD,CR,CZ,DK,DJ,ST,DO,EC,EG,SV,ER,ET,FK,FJ,GM,GE,GH,GI,GT,GN,GY,HT,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IL,IT,JM,JP,JO,KZ,KE,KW,LA,LB,LS,LR,LY,LT,MO,MK,MG,MW,MY,MV,MR,MU,MX,MD,MN,MA,MZ,MM,NA,NZ,NI,NG,KP,NO,AR,PK,PG,PY,PE,UY,PH,PL,GB,QA,OM,RO,RU,RW,WS,SG,ST,ZA,KR,LK,SH,SD,SR,SZ,SE,CH,SY,TW,TJ,TZ,TH,TT,TN,TR,UG,UA,US,UZ,VU,VE,VN,ZM,ZW", currency = "AFN,DZD,ANG,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BZD,BMD,BTN,BOB,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XOF,XAF,XPF,CLP,COP,KMF,CDF,CRC,EUR,CZK,DKK,DJF,DOP,XCD,EGP,SVC,ERN,ETB,EUR,FKP,FJD,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,NZD,NIO,NGN,KPW,NOK,ARS,PKR,PAB,PGK,PYG,PEN,UYU,PHP,PLN,GBP,QAR,OMR,RON,RUB,RWF,WST,SAR,RSD,SCR,SLL,SGD,STN,SBD,SOS,ZAR,KRW,LKR,SHP,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,AED,UGX,UAH,USD,UZS,VUV,VND,YER,CNY,ZMW,ZWL" } + +[pm_filters.worldpaymodular] +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", currency = "DZD, AOA, USD, XCD, ARS, AUD, AZN, EUR, BHD, BYN, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, JOD, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND" } +apple_pay = { country = "EG, MA, ZA, AU, CN, HK, JP, MO, MY, MN, NZ, SG, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, CZ, DK, EE, FO, FI, FR, GE, DE, GR, GL, GG, HU, IE, IS, 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, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US, PR", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MOP, MYR, MNT, NZD, SGD, KRW, TWD, VND, EUR, AZN, BYN, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, HNL, MXN, PAB, PYG, PEN, BSD, UYU, BHD, ILS, JOD, KWD, OMR, QAR, SAR, AED, CAD" } + +[pm_filters.calida] +bluecode = { country = "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IE,IT,LV,LT,LU,MT,NL,PL,PT,RO,SK,SI,ES,SE,IS,LI,NO", currency = "EUR" } + +[file_upload_config] +bucket_name = "" +region = "" + +[pm_filters.forte] +credit = { country = "US, CA", currency = "CAD,USD"} +debit = { country = "US, CA", currency = "CAD,USD"} + +[pm_filters.nordea] +sepa = { country = "DK,FI,NO,SE", currency = "DKK,EUR,NOK,SEK" } + +[pm_filters.fiuu] +duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +credit = { country = "CN,HK,ID,MY,PH,SG,TH,TW,VN", currency = "CNY,HKD,IDR,MYR,PHP,SGD,THB,TWD,VND" } +debit = { country = "CN,HK,ID,MY,PH,SG,TH,TW,VN", currency = "CNY,HKD,IDR,MYR,PHP,SGD,THB,TWD,VND" } + +[pm_filters.inespay] +sepa = { country = "ES", currency = "EUR"} + +[pm_filters.bluesnap] +credit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,ARS,AUD,AWG,BAM,BBD,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,CAD,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,DZD,EGP,EUR,FJD,GBP,GEL,GIP,GTQ,HKD,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KHR,KRW,KWD,KYD,KZT,LBP,LKR,MAD,MDL,MKD,MUR,MWK,MXN,MYR,NAD,NGN,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PLN,PKR,QAR,RON,RSD,RUB,SAR,SCR,SDG,SEK,SGD,THB,TND,TRY,TTD,TWD,TZS,UAH,USD,UYU,UZS,VND,XAF,XCD,XOF,ZAR"} +google_pay = { country = "AL, DZ, AS, AO, AG, AR, AU, AT, AZ, BH, BY, BE, BR, BG, 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", currency = "ALL, DZD, USD, XCD, ARS, AUD, EUR, BHD, BRL, BGN, CAD, CLP, COP, CZK, DKK, DOP, EGP, HKD, HUF, INR, IDR, ILS, JPY, KZT, KES, KWD, LBP, MYR, MXN, NZD, NOK, OMR, PKR, PAB, PEN, PHP, PLN, QAR, RON, RUB, SAR, SGD, ZAR, LKR, SEK, CHF, TWD, THB, TRY, UAH, AED, GBP, UYU, VND"} +apple_pay = { country = "EG, MA, ZA, AU, HK, JP, MO, MY, MN, NZ, SG, KR, TW, VN, AM, AT, AZ, BY, BE, BG, HR, CY, 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, SI, ES, SE, CH, UA, GB, VA, AR, BR, CL, CO, CR, DO, EC, SV, GT, HN, MX, PA, PY, PE, BS, UY, BH, IL, JO, KW, OM, PS, QA, SA, AE, CA, US", currency = "EGP, MAD, ZAR, AUD, CNY, HKD, JPY, MYR, NZD, SGD, KRW, TWD, VND, AMD, EUR, BGN, CZK, DKK, GEL, GBP, HUF, ISK, KZT, CHF, MDL, NOK, PLN, RON, RSD, SEK, UAH, GBP, ARS, BRL, CLP, COP, CRC, DOP, USD, GTQ, MXN, PAB, PEN, BSD, UYU, BHD, ILS, KWD, OMR, QAR, SAR, AED, CAD"} + +[pm_filters.fiserv] +credit = {country = "AU,NZ,CN,HK,IN,LK,KR,MY,SG,GB,BE,FR,DE,IT,ME,NL,PL,ES,ZA,AR,BR,CO,MX,PA,UY,US,CA", currency = "AFN,ALL,DZD,AOA,ARS,AMD,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BYN,BZD,BMD,BTN,BOB,VES,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XAF,CLP,CNY,COP,KMF,CDF,CRC,HRK,CUP,CZK,DKK,DJF,DOP,XCD,EGP,ERN,ETB,EUR,FKP,FJD,XPF,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KGS,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,ANG,NZD,NIO,NGN,VUV,KPW,NOK,OMR,PKR,PAB,PGK,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SHP,SVC,WST,STN,SAR,RSD,SCR,SLL,SGD,SBD,SOS,ZAR,KRW,SSP,LKR,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,UGX,UAH,AED,USD,UYU,UZS,VND,XOF,YER,ZMW,ZWL"} +debit = {country = "AU,NZ,CN,HK,IN,LK,KR,MY,SG,GB,BE,FR,DE,IT,ME,NL,PL,ES,ZA,AR,BR,CO,MX,PA,UY,US,CA", currency = "AFN,ALL,DZD,AOA,ARS,AMD,AWG,AUD,AZN,BSD,BHD,BDT,BBD,BYN,BZD,BMD,BTN,BOB,VES,BAM,BWP,BRL,BND,BGN,BIF,KHR,CAD,CVE,KYD,XAF,CLP,CNY,COP,KMF,CDF,CRC,HRK,CUP,CZK,DKK,DJF,DOP,XCD,EGP,ERN,ETB,EUR,FKP,FJD,XPF,GMD,GEL,GHS,GIP,GTQ,GNF,GYD,HTG,HNL,HKD,HUF,ISK,INR,IDR,IRR,IQD,ILS,JMD,JPY,JOD,KZT,KES,KGS,KWD,LAK,LBP,LSL,LRD,LYD,MOP,MKD,MGA,MWK,MYR,MVR,MRU,MUR,MXN,MDL,MNT,MAD,MZN,MMK,NAD,NPR,ANG,NZD,NIO,NGN,VUV,KPW,NOK,OMR,PKR,PAB,PGK,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SHP,SVC,WST,STN,SAR,RSD,SCR,SLL,SGD,SBD,SOS,ZAR,KRW,SSP,LKR,SDG,SRD,SZL,SEK,CHF,SYP,TWD,TJS,TZS,THB,TOP,TTD,TND,TRY,TMT,UGX,UAH,AED,USD,UYU,UZS,VND,XOF,YER,ZMW,ZWL"} +paypal = { currency = "AUD,EUR,BRL,CAD,CNY,EUR,EUR,EUR,GBP,HKD,INR,EUR,JPY,MYR,EUR,NZD,PHP,PLN,SGD,USD", country = "AU, BE, BR, CA, CN, DE, ES, FR, GB, HK, IN, IT, JP, MY, NL, NZ, PH, PL, SG, US" } +google_pay = { country = "AU,AT,BE,BR,CA,CN,HK,MY,NZ,SG,US", currency = "AUD,EUR,EUR,BRL,CAD,CNY,HKD,MYR,NZD,SGD,USD" } +apple_pay = { country = "AU,NZ,CN,HK,JP,SG,MY,KR,TW,VN,GB,IE,FR,DE,IT,ES,PT,NL,BE,LU,AT,CH,SE,FI,DK,NO,PL,CZ,SK,HU,LT,LV,EE,GR,RO,BG,HR,SI,MT,CY,IS,LI,MC,SM,VA,US,CA,MX,BR,AR,CL,CO,PE,UY,CR,PA,DO,EC,SV,GT,HN,BS,PR,AE,SA,QA,KW,BH,OM,IL,JO,PS,EG,MA,ZA,GE,AM,AZ,MD,ME,MK,AL,BA,RS,UA", currency = "AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,HUF,ILS,JPY,MXN,NOK,NZD,PHP,PLN,SEK,SGD,THB,TWD,USD" } + + +[pm_filters.amazonpay] +amazon_pay = { country = "US", currency = "USD" } + +[pm_filters.rapyd] +apple_pay = { country = "BR, CA, CL, CO, DO, SV, MX, PE, PT, US, AT, BE, BG, HR, CY, CZ, DO, DK, EE, FI, FR, GE, DE, GR, GL, HU, IS, IE, IL, IT, LV, LI, LT, LU, MT, MD, MC, ME, NL, NO, PL, RO, SM, SK, SI, ZA, ES, SE, CH, GB, VA, AU, HK, JP, MY, NZ, SG, KR, TW, VN", currency = "AMD, AUD, BGN, BRL, BYN, CAD, CHF, CLP, CNY, COP, CRC, CZK, DKK, DOP, EUR, GBP, GEL, GTQ, HUF, ISK, JPY, KRW, MDL, MXN, MYR, NOK, PAB, PEN, PLN, PYG, RON, RSD, SEK, SGD, TWD, UAH, USD, UYU, VND, ZAR" } +google_pay = { country = "BR, CA, CL, CO, DO, MX, PE, PT, US, AT, BE, BG, HR, CZ, DK, EE, FI, FR, DE, GR, HU, IE, IL, IT, LV, LT, LU, NZ, NO, GB, PL, RO, RU, SK, ZA, ES, SE, CH, TR, AU, HK, IN, ID, JP, MY, PH, SG, TW, TH, VN", currency = "AUD, BGN, BRL, BYN, CAD, CHF, CLP, COP, CZK, DKK, DOP, EUR, GBP, HUF, IDR, JPY, KES, MXN, MYR, NOK, PAB, PEN, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, TWD, UAH, USD, UYU, VND, ZAR" } +credit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } +debit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } + +[pm_filters.bamboraapac] +credit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } +debit = { country = "AD,AE,AG,AL,AM,AO,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BJ,BN,BO,BR,BS,BT,BW,BY,BZ,CA,CD,CF,CG,CH,CI,CL,CM,CN,CO,CR,CV,CY,CZ,DE,DK,DJ,DM,DO,DZ,EC,EE,EG,ER,ES,ET,FI,FJ,FM,FR,GA,GB,GD,GE,GG,GH,GM,GN,GQ,GR,GT,GW,GY,HN,HR,HT,HU,ID,IE,IL,IN,IS,IT,JM,JP,JO,KE,KG,KH,KI,KM,KN,KR,KW,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,MA,MC,MD,ME,MG,MH,MK,ML,MM,MN,MR,MT,MU,MV,MW,MX,MY,MZ,NA,NE,NG,NI,NL,NO,NP,NR,NZ,OM,PA,PE,PG,PH,PK,PL,PS,PT,PW,PY,QA,RO,RS,RW,SA,SB,SC,SE,SG,SI,SK,SL,SM,SN,SO,SR,SS,ST,SV,SZ,TD,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VN,VU,WS,ZA,ZM,ZW", currency = "AED,AUD,BDT,BGN,BND,BOB,BRL,BWP,CAD,CHF,CNY,COP,CZK,DKK,EGP,EUR,FJD,GBP,GEL,GHS,HKD,HRK,HUF,IDR,ILS,INR,IQD,IRR,ISK,JPY,KES,KRW,KWD,KZT,LAK,LKR,MAD,MDL,MMK,MOP,MXN,MYR,MZN,NAD,NGN,NOK,NPR,NZD,PEN,PHP,PKR,PLN,QAR,RON,RSD,RUB,RWF,SAR,SCR,SEK,SGD,SLL,THB,TRY,TWD,TZS,UAH,UGX,USD,UYU,VND,XAF,XOF,ZAR,ZMW,MWK" } + +[pm_filters.gocardless] +ach = { country = "US", currency = "USD" } +becs = { country = "AU", currency = "AUD" } +sepa = { country = "AU,AT,BE,BG,CA,HR,CY,CZ,DK,FI,FR,DE,HU,IT,LU,MT,NL,NZ,NO,PL,PT,IE,RO,SK,SI,ZA,ES,SE,CH,GB", currency = "GBP,EUR,SEK,DKK,AUD,NZD,CAD" } + +[pm_filters.powertranz] +credit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "BBD,BMD,BSD,CRC,GTQ,HNL,JMD,KYD,TTD,USD" } +debit = { country = "AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "BBD,BMD,BSD,CRC,GTQ,HNL,JMD,KYD,TTD,USD" } + +[pm_filters.worldline] +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +credit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.shift4] +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } +sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE", currency = "CHF,EUR" } +credit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLL,SOS,SRD,SSP,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +boleto = { country = "BR", currency = "BRL" } +trustly = { currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +ali_pay = { country = "CN", currency = "CNY" } +we_chat_pay = { country = "CN", currency = "CNY" } +klarna = { currency = "EUR,GBP,CHF,SEK" } +blik = { country = "PL", currency = "PLN" } +crypto_currency = { currency = "USD,GBP,AED" } +paysera = { currency = "EUR" } +skrill = { currency = "USD" } + +[pm_filters.placetopay] +credit = { country = "BE,CH,CO,CR,EC,HN,MX,PA,PR,UY", currency = "CLP,COP,USD"} +debit = { country = "BE,CH,CO,CR,EC,HN,MX,PA,PR,UY", currency = "CLP,COP,USD"} + +[pm_filters.coingate] +crypto_currency = { country = "AL, AD, AT, BE, BA, BG, HR, CZ, DK, EE, FI, FR, DE, GR, HU, IS, IE, IT, LV, LT, LU, MT, MD, NL, NO, PL, PT, RO, RS, SK, SI, ES, SE, CH, UA, GB, AR, BR, CL, CO, CR, DO, SV, GD, MX, PE, LC, AU, NZ, CY, HK, IN, IL, JP, KR, QA, SA, SG, EG", currency = "EUR, USD, GBP" } + +[pm_filters.paystack] +eft = { country = "NG, ZA, GH, KE, CI", currency = "NGN, GHS, ZAR, KES, USD" } + +[pm_filters.santander] +pix = { country = "BR", currency = "BRL" } +boleto = { country = "BR", currency = "BRL" } + +[pm_filters.boku] +dana = { country = "ID", currency = "IDR" } +gcash = { country = "PH", currency = "PHP" } +go_pay = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +momo = { country = "VN", currency = "VND" } + +[pm_filters.nmi] +credit = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +debit = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +apple_pay = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } +google_pay = { country = "EG,ZA,BH,CY,HK,IN,ID,IL,JP,JO,KW,MY,PK,PH,SA,SG,KR,TW,TH,TR,AE,VN,AL,AD,AM,AT,AZ,BY,BE,BA,BG,HR,CZ,DK,EE,FI,FR,GE,DE,GR,HU,IS,IE,IT,KZ,LV,LI,LT,LU,MT,MD,MC,ME,NL,MK,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,TR,GB,VA,AG,BS,BB,BZ,CA,SV,GT,HN,MX,PA,KN,LC,TT,US,AU,NZ,AR,BO,BR,CL,CO,EC,GY,PY,PE,SR,UY,VE", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } + +[pm_filters.paypal] +credit = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +debit = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +paypal = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +eps = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } +giropay = { currency = "EUR" } +ideal = { currency = "EUR" } +sofort = { country = "DZ,AO,BJ,BW,BF,BI,CM,CV,TD,KM,CI,CD,DJ,EG,ER,ET,GA,GM,GN,GW,KE,LS,MG,MW,ML,MR,MU,MA,MZ,NA,NE,NG,CG,RW,SH,ST,SN,SC,SL,SO,ZA,SZ,TZ,TG,TN,UG,ZM,ZW,AI,AG,AR,AW,BS,BB,BZ,BM,BO,BR,VG,CA,KY,CL,CO,CR,DM,DO,EC,SV,FK,GL,GD,GT,GY,HN,JM,MX,MS,NI,PA,PY,PE,KN,LC,PM,VC,SR,TT,TC,US,UY,VE,AM,AU,BH,BT,BN,KH,CN,CK,FJ,PF,HK,IN,ID,IL,JP,JO,KZ,KI,KW,KG,LA,MY,MV,MH,FM,MN,NR,NP,NC,NZ,NU,NF,OM,PW,PG,PH,PN,QA,WS,SA,SG,SB,KR,LK,TW,TJ,TH,TO,TM,TV,AE,VU,VN,WF,YE,AL,AD,AT,AZ,BY,BE,BA,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,HU,IS,IE,IT,LV,LI,LT,LU,MK,MT,MD,MC,ME,NL,NO,PL,PT,RO,RU,SM,RS,SK,SI,ES,SJ,SE,CH,UA,GB,VA", currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,SGD,SEK,CHF,THB,USD" } + +[pm_filters.datatrans] +credit = { country = "AL,AD,AM,AT,AZ,BY,BE,BA,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GE,GR,HR,HU,IE,IS,IT,KZ,LI,LT,LU,LV,MC,MD,ME,MK,MT,NL,NO,PL,PT,RO,RU,SE,SI,SK,SM,TR,UA,VA", currency = "BHD,BIF,CHF,DJF,EUR,GBP,GNF,IQD,ISK,JPY,JOD,KMF,KRW,KWD,LYD,OMR,PYG,RWF,TND,UGX,USD,VND,VUV,XAF,XOF,XPF" } +debit = { country = "AL,AD,AM,AT,AZ,BY,BE,BA,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GE,GR,HR,HU,IE,IS,IT,KZ,LI,LT,LU,LV,MC,MD,ME,MK,MT,NL,NO,PL,PT,RO,RU,SE,SI,SK,SM,TR,UA,VA", currency = "BHD,BIF,CHF,DJF,EUR,GBP,GNF,IQD,ISK,JPY,JOD,KMF,KRW,KWD,LYD,OMR,PYG,RWF,TND,UGX,USD,VND,VUV,XAF,XOF,XPF" } + +[pm_filters.payme] +credit = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } +debit = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } +apple_pay = { country = "US,CA,IL,GB", currency = "ILS,USD,EUR" } + +[pm_filters.paysafe] +apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,PM,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,NL,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,VG,VI,WF,EH,YE,ZM,ZW", currency = "ARS,AUD,AZN,BHD,BOB,BAM,BRL,BGN,CAD,CLP,CNY,COP,CRC,HRK,CZK,DKK,DOP,XCD,EGP,ETB,EUR,FJD,GEL,GTQ,HTG,HNL,HKD,HUF,INR,IDR,JMD,JPY,JOD,KZT,KES,KRW,KWD,LBP,LYD,MWK,MUR,MXN,MDL,MAD,ILS,NZD,NGN,NOK,OMR,PKR,PAB,PYG,PEN,PHP,PLN,GBP,QAR,RON,RUB,RWF,SAR,RSD,SGD,ZAR,LKR,SEK,CHF,SYP,TWD,THB,TTD,TND,TRY,UAH,AED,UYU,USD,VND" } + +[pm_filters.payjustnow] +payjustnow = { country = "ZA", currency = "ZAR" } + +[pm_filters.payjustnowinstore] +payjustnow = { country = "ZA", currency = "ZAR" } diff --git a/config/grafana-datasource.yaml b/config/grafana-datasource.yaml new file mode 100644 index 00000000..fbed2ef2 --- /dev/null +++ b/config/grafana-datasource.yaml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Metrics + type: prometheus + access: proxy + url: http://prometheus:9090/ + editable: true diff --git a/config/prometheus.yaml b/config/prometheus.yaml new file mode 100644 index 00000000..587b77c7 --- /dev/null +++ b/config/prometheus.yaml @@ -0,0 +1,30 @@ +# my global config +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +# alter manager if required +# Alertmanager configuration +# alerting: +# alertmanagers: +# - static_configs: +# - targets: +# - alertmanager:9093 + +# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: "open-router" + + metrics_path: /metrics + # scheme defaults to 'http'. + + static_configs: + - targets: ["open-router-pg:9094"] # this can be replaced by open-router-local-pg for local setup diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 00000000..5d7ee8dc --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,53 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + e2e: { + baseUrl: 'http://localhost:8080', + supportFile: 'cypress/support/e2e.js', + specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + viewportWidth: 1280, + viewportHeight: 720, + video: false, + screenshotOnRunFailure: true, + defaultCommandTimeout: 10000, + requestTimeout: 10000, + responseTimeout: 10000, + env: { + // Default configuration - can be overridden via environment variables + API_BASE_URL: 'http://localhost:8080', + DEFAULT_MERCHANT_ID_PREFIX: 'merc_', + DEFAULT_PAYMENT_ID_PREFIX: 'PAY_', + DEFAULT_CUSTOMER_ID_PREFIX: 'CUST', + // Test data configuration + DEFAULT_GATEWAYS: ['GatewayA', 'GatewayB', 'GatewayC'], + DEFAULT_AMOUNT: 100.50, + DEFAULT_CURRENCY: 'USD', + // Routing algorithm types + ROUTING_ALGORITHMS: { + SUCCESS_RATE: 'SR_BASED_ROUTING', + PAYMENT_LATENCY: 'PL_BASED_ROUTING', + COST_BASED: 'COST_BASED_ROUTING' + }, + // Payment method types for testing + PAYMENT_METHODS: { + UPI: { + type: 'UPI', + method: 'UPI_PAY' + }, + UPI_COLLECT: { + type: 'upi', + method: 'upi_collect' + }, + CARD: { + type: 'CARD', + method: 'CARD_PAY' + } + } + }, + setupNodeEvents(on, config) { + // implement node event listeners here + require('@cypress/grep/src/plugin')(config) + return config + }, + }, +}) diff --git a/cypress/README.md b/cypress/README.md new file mode 100644 index 00000000..da51a9ac --- /dev/null +++ b/cypress/README.md @@ -0,0 +1,318 @@ +# Decision Engine Cypress Testing Framework + +This directory contains comprehensive end-to-end tests for the Decision Engine routing flows using Cypress. The testing framework is designed to be generic and easily extensible for testing different routing algorithms and scenarios. + +## 📁 Directory Structure + +``` +cypress/ +├── e2e/ +│ └── routing-flows/ +│ ├── gateway-latency-scoring.cy.js # Gateway latency scoring tests +│ ├── success-rate-routing.cy.js # Success rate routing tests +├── support/ +│ ├── commands.js # Custom Cypress commands +│ ├── e2e.js # Global test configuration +│ └── test-data-factory.js # Test data generation utilities +└── README.md # This file +``` + +## 🚀 Getting Started + +### Prerequisites + +1. **Node.js** (v16 or higher) +2. **Decision Engine Service** running on `http://localhost:8082` +3. **Database** (MySQL/PostgreSQL) properly configured +4. **Redis** instance running + +### Installation + +1. Install dependencies: +```bash +npm install +``` + +2. Verify Cypress installation: +```bash +npx cypress verify +``` + +### Running Tests + +#### Interactive Mode (Cypress Test Runner) +```bash +npm run cypress:open +``` + +#### Headless Mode (CI/CD) +```bash +npm run cypress:run +``` + +#### Specific Test Suites +```bash +# Gateway latency scoring tests +npm run test:gateway-latency + +# Success rate routing tests +npm run test:success-rate + +# All routing flow tests +npm run test +``` + +#### Running Tests with Tags +```bash +# Run only smoke tests +npx cypress run --env grepTags="@smoke" + +# Run only gateway latency tests +npx cypress run --env grepTags="@gateway-latency" + +# Run performance tests +npx cypress run --env grepTags="@performance" +``` + +## 🛠️ Custom Commands + +The framework provides several custom Cypress commands for easy test creation: + +### Merchant Management +```javascript +cy.createMerchantAccount(merchantId) +``` + +### Rule Configuration +```javascript +cy.createRoutingRule(merchantId, ruleConfig) +cy.createSuccessRateRule(merchantId, options) +cy.createPaymentLatencyRule(merchantId, options) +``` + +### Gateway Operations +```javascript +cy.decideGateway(decisionRequest) +cy.updateGatewayScore(scoreUpdate) +``` + +### Complete Flows +```javascript +cy.completeGatewayLatencyFlow(options) +``` + +### Utility Commands +```javascript +cy.waitForService() +cy.cleanupTestData(merchantId) +``` + +## 📊 Test Data Factory + +The `TestDataFactory` class provides utilities for generating test data: + +```javascript +const TestDataFactory = require('../support/test-data-factory') + +// Generate unique IDs +const merchantId = TestDataFactory.generateMerchantId() +const paymentId = TestDataFactory.generatePaymentId() + +// Get rule configurations +const srRule = TestDataFactory.getSuccessRateRuleConfig({ + successRate: 0.8, + latencyThreshold: 100 +}) + +// Get test scenarios +const latencyScenarios = TestDataFactory.getLatencyTestScenarios() +const paymentMethods = TestDataFactory.getPaymentMethodTestCases() +``` + +## ⚙️ Configuration + +### Environment Variables + +Configure the testing environment in `cypress.config.js`: + +```javascript +env: { + API_BASE_URL: 'http://localhost:8082', + DEFAULT_GATEWAYS: ['GatewayA', 'GatewayB', 'GatewayC'], + DEFAULT_AMOUNT: 100.50, + DEFAULT_CURRENCY: 'USD' +} +``` + +### Override via Command Line +```bash +npx cypress run --env API_BASE_URL=http://localhost:8080 +``` + +## 🏷️ Test Tags + +Tests are organized using tags for easy filtering: + +- `@smoke` - Critical path tests +- `@gateway-latency` - Gateway latency specific tests +- `@success-rate` - Success rate routing tests +- `@payment-latency` - Payment latency routing tests +- `@performance` - Performance related tests +- `@edge-cases` - Edge case scenarios +- `@routing` - General routing tests + +## 📝 Writing New Tests + +### 1. Basic Test Structure + +```javascript +describe('New Routing Flow', () => { + let testData = {} + + beforeEach(() => { + cy.waitForService() + testData = { + merchantId: `merc_new_${Date.now()}`, + paymentId: `PAY_new_${Date.now()}` + } + }) + + afterEach(() => { + cy.cleanupTestData(testData.merchantId) + }) + + it('should test new routing logic', { tags: ['@new-feature'] }, () => { + // Test implementation + }) +}) +``` + +### 2. Using Test Data Factory + +```javascript +const TestDataFactory = require('../support/test-data-factory') + +it('should test with generated data', () => { + const merchantId = TestDataFactory.generateMerchantId() + const ruleConfig = TestDataFactory.getSuccessRateRuleConfig({ + successRate: 0.9 + }) + + cy.createMerchantAccount(merchantId) + .then(() => cy.createRoutingRule(merchantId, ruleConfig)) + .then(() => { + // Continue test + }) +}) +``` + +### 3. Adding New Custom Commands + +In `cypress/support/commands.js`: + +```javascript +Cypress.Commands.add('newCustomCommand', (parameters) => { + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/new-endpoint`, + body: parameters + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap(response.body) + }) +}) +``` + +## 🔧 Extending for New Routing Logic + +To add tests for a new routing algorithm: + +### 1. Create New Test File +```bash +touch cypress/e2e/routing-flows/new-algorithm-routing.cy.js +``` + +### 2. Add Rule Configuration Helper +In `cypress/support/commands.js`: +```javascript +Cypress.Commands.add('createNewAlgorithmRule', (merchantId, options = {}) => { + const ruleConfig = { + type: "newAlgorithm", + data: { + // Algorithm specific configuration + } + } + return cy.createRoutingRule(merchantId, ruleConfig) +}) +``` + +### 3. Add Test Data Factory Methods +In `cypress/support/test-data-factory.js`: +```javascript +static getNewAlgorithmRuleConfig(options = {}) { + return { + type: "newAlgorithm", + data: { + // Default configuration + } + } +} +``` + +### 4. Update Package.json Scripts +```json +{ + "scripts": { + "test:new-algorithm": "cypress run --spec 'cypress/e2e/routing-flows/new-algorithm-routing.cy.js'" + } +} +``` + +## 🐛 Debugging + +### 1. Enable Debug Logs +```bash +DEBUG=cypress:* npm run cypress:run +``` + +### 2. Screenshots and Videos +- Screenshots are automatically taken on test failures +- Videos can be enabled in `cypress.config.js` + +### 3. Browser DevTools +When running in interactive mode, use browser DevTools to inspect network requests and responses. + +## 📈 Performance Testing + +The framework includes utilities for performance testing: + +```javascript +const performanceConfig = TestDataFactory.getPerformanceTestConfig() +const loadTestData = TestDataFactory.generateLoadTestData(100) + +// Use in tests for load testing scenarios +``` + +## 🚨 Best Practices + +1. **Use unique test data** - Always generate unique merchant IDs and payment IDs +2. **Clean up after tests** - Use `afterEach` hooks to clean up test data +3. **Wait for service readiness** - Always call `cy.waitForService()` in `beforeEach` +4. **Use descriptive test names** - Make test purposes clear from the name +5. **Tag tests appropriately** - Use tags for easy test filtering +6. **Verify responses** - Always validate API response structure and data +7. **Log important data** - Use `cy.log()` for debugging information + +## 🤝 Contributing + +When adding new tests: + +1. Follow the existing file structure and naming conventions +2. Add appropriate tags to new tests +3. Update this README if adding new features +4. Ensure tests are independent and can run in any order +5. Add test data factory methods for reusable test data + +--- + +**Happy Testing! 🎉** diff --git a/cypress/e2e/routing-flows/gateway-latency-scoring.cy.js b/cypress/e2e/routing-flows/gateway-latency-scoring.cy.js new file mode 100644 index 00000000..c2bb36da --- /dev/null +++ b/cypress/e2e/routing-flows/gateway-latency-scoring.cy.js @@ -0,0 +1,224 @@ +describe('Gateway Latency Scoring Flow', () => { + let testData = {} + + beforeEach(() => { + // Wait for service to be ready + cy.waitForService() + + // Generate unique test data for each test + testData = { + merchantId: `merc_${Date.now()}`, + paymentId: `PAY_${Date.now()}`, + customerId: `CUST${Date.now()}` + } + }) + + afterEach(() => { + // Clean up test data if needed + cy.cleanupTestData(testData.merchantId) + }) + + it('should create merchant account successfully', { tags: ['@merchant'] }, () => { + cy.createMerchantAccount(testData.merchantId).then((result) => { + expect(result.merchantId).to.equal(testData.merchantId) + expect(result.response).to.exist + }) + }) + + it('should create success rate routing rule successfully', { tags: ['@routing-rule'] }, () => { + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId, { + latencyThreshold: 90, + successRate: 0.5, + bucketSize: 200, + hedgingPercent: 5, + gatewayLatency: 5000 + }) + }).then((result) => { + expect(result.ruleConfig).to.have.property('type', 'successRate') + expect(result.ruleConfig.data).to.have.property('defaultLatencyThreshold', 90) + expect(result.ruleConfig.data).to.have.property('defaultSuccessRate', 0.5) + expect(result.response).to.exist + }) + }) + + it('should decide gateway successfully', { tags: ['@gateway-decision'] }, () => { + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId) + }).then(() => { + return cy.decideGateway({ + merchantId: testData.merchantId, + eligibleGatewayList: ["GatewayA", "GatewayB", "GatewayC"], + rankingAlgorithm: "SR_BASED_ROUTING", + eliminationEnabled: true, + paymentInfo: { + paymentId: testData.paymentId, + amount: 100.50, + currency: "USD", + customerId: testData.customerId, + paymentMethodType: "UPI", + paymentMethod: "UPI_PAY" + } + }) + }).then((result) => { + expect(result.response).to.haveValidGatewayResponse() + expect(result.response.decided_gateway).to.be.oneOf(["GatewayA", "GatewayB", "GatewayC"]) + }) + }) + + it('should update gateway score successfully', { tags: ['@score-update'] }, () => { + let selectedGateway + + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId) + }).then(() => { + return cy.decideGateway({ + merchantId: testData.merchantId, + paymentInfo: { + paymentId: testData.paymentId + } + }) + }).then((result) => { + selectedGateway = result.response.decided_gateway + return cy.updateGatewayScore({ + merchantId: testData.merchantId, + gateway: selectedGateway, + paymentId: testData.paymentId, + status: "AUTHORIZED", + txnLatency: { + gatewayLatency: 6000 + } + }) + }).then((result) => { + expect(result.response).to.haveValidScoreUpdate() + }) + }) + + it('should show different gateway selection after score update', { tags: ['@score-impact'] }, () => { + let initialGateway, updatedGateway, initialGatewayScore, initialGatewayCurrentScore + + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createEliminationRule(testData.merchantId, { + gatewayLatency: 3000 // Lower threshold to make latency impact more visible + }) + }).then(() => { + // First gateway decision + return cy.decideGateway({ + merchantId: testData.merchantId, + paymentInfo: { + paymentId: testData.paymentId + } + }) + }).then((result) => { + initialGateway = result.response.decided_gateway + initialGatewayScore = result.response.gateway_priority_map[initialGateway] + + // Update score with high latency + return cy.updateGatewayScore({ + merchantId: testData.merchantId, + gateway: initialGateway, + paymentId: testData.paymentId, + status: "AUTHORIZED", + txnLatency: { + gatewayLatency: 8000 // High latency to impact scoring + } + }) + }).then(() => { + // Second gateway decision to see if scoring changed + return cy.decideGateway({ + merchantId: testData.merchantId, + eliminationEnabled: true, + paymentInfo: { + paymentId: `PAY_${Date.now()}_2` // Different payment ID + } + }) + }).then((result) => { + initialGatewayCurrentScore = result.response.gateway_priority_map[initialGateway] + updatedGateway = result.response.decided_gateway + + // Log both gateways for comparison + cy.log(`Initial Gateway: ${initialGateway}`) + cy.log(`Updated Gateway: ${updatedGateway}`) + + expect(updatedGateway).to.not.equal(initialGateway); + expect(initialGatewayCurrentScore).to.be.lessThan(initialGatewayScore); + }) + }) + + it('should handle different payment methods', { tags: ['@payment-methods'] }, () => { + const paymentMethods = [ + { type: "UPI", method: "UPI_PAY" }, + { type: "upi", method: "upi_collect" }, + { type: "CARD", method: "CARD_PAY" } + ] + + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId) + }).then(() => { + // Test each payment method + paymentMethods.forEach((paymentMethod, index) => { + cy.decideGateway({ + merchantId: testData.merchantId, + paymentInfo: { + paymentId: `${testData.paymentId}_${index}`, + paymentMethodType: paymentMethod.type, + paymentMethod: paymentMethod.method + } + }).then((result) => { + expect(result.response).to.haveValidGatewayResponse() + cy.log(`Payment Method ${paymentMethod.type}/${paymentMethod.method}: Gateway ${result.response.decided_gateway}`) + }) + }) + }) + }) + + it('legacy api: should show different gateway selection after score update', { tags: ['@score-impact'] }, () => { + let initialGateway, updatedGateway, initialGatewayScore, initialGatewayCurrentScore + + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId, { + gatewayLatency: 3000 // Lower threshold to make latency impact more visible + }) + }).then(() => { + // First gateway decision + return cy.decideGatewayLegacy({ + merchantId: testData.merchantId, + paymentInfo: { + paymentId: testData.paymentId + } + }) + }).then((result) => { + initialGateway = result.response.decided_gateway + initialGatewayScore = result.response.gateway_priority_map[initialGateway] + + // Update score with high latency + return cy.updateGatewayScore({ + merchantId: testData.merchantId, + gateway: initialGateway, + paymentId: testData.paymentId, + status: "AUTHORIZED", + txnLatency: { + gatewayLatency: 8000 // High latency to impact scoring + } + }) + }).then(() => { + // Second gateway decision to see if scoring changed + return cy.decideGateway({ + merchantId: testData.merchantId, + paymentInfo: { + paymentId: `PAY_${Date.now()}_2` // Different payment ID + } + }) + }).then((result) => { + initialGatewayCurrentScore = result.response.gateway_priority_map[initialGateway] + updatedGateway = result.response.decided_gateway + + // Log both gateways for comparison + cy.log(`Initial Gateway: ${initialGateway}`) + cy.log(`Updated Gateway: ${updatedGateway}`) + + expect(updatedGateway).to.not.equal(initialGateway); + expect(initialGatewayCurrentScore).to.be.lessThan(initialGatewayScore); + }) + }) +}) diff --git a/cypress/e2e/routing-flows/success-rate-routing.cy.js b/cypress/e2e/routing-flows/success-rate-routing.cy.js new file mode 100644 index 00000000..837bc698 --- /dev/null +++ b/cypress/e2e/routing-flows/success-rate-routing.cy.js @@ -0,0 +1,244 @@ +describe('Success Rate Routing Flow', () => { + let testData = {} + + beforeEach(() => { + cy.waitForService() + + testData = { + merchantId: `merc_sr_${Date.now()}`, + paymentId: `PAY_SR_${Date.now()}`, + customerId: `CUST_SR_${Date.now()}` + } + }) + + afterEach(() => { + cy.cleanupTestData(testData.merchantId) + }) + + it('should test success rate based routing', { tags: ['@success-rate', '@routing'] }, () => { + cy.createMerchantAccount(testData.merchantId).then(() => { + return cy.createSuccessRateRule(testData.merchantId, { + successRate: 0.8, + latencyThreshold: 100, + bucketSize: 300, + hedgingPercent: 10 + }) + }).then(() => { + // Define multiple transactions to simulate real-world scenario + const transactions = [ + { paymentId: `${testData.paymentId}_txn_1`, status: "AUTHORIZED", latency: 2000 }, + { paymentId: `${testData.paymentId}_txn_2`, status: "FAILURE", latency: 5000 }, + { paymentId: `${testData.paymentId}_txn_3`, status: "AUTHORIZED", latency: 1500 }, + { paymentId: `${testData.paymentId}_txn_4`, status: "AUTHORIZED", latency: 3000 }, + { paymentId: `${testData.paymentId}_txn_5`, status: "FAILURE", latency: 4500 }, + { paymentId: `${testData.paymentId}_txn_6`, status: "AUTHORIZED", latency: 2500 } + ] + + // Store gateway decisions for each transaction + const gatewayDecisions = [] + + // Process each transaction: decide gateway first, then update score + const processTransaction = (txn, index) => { + return cy.decideGateway({ + merchantId: testData.merchantId, + rankingAlgorithm: "SR_BASED_ROUTING", + paymentInfo: { + paymentId: txn.paymentId, + paymentMethodType: "UPI", + paymentMethod: "UPI_PAY", + customerId: `${testData.customerId}_${index}` + } + }).then((decisionResult) => { + expect(decisionResult.response).to.haveValidGatewayResponse() + + // Store the gateway decision + gatewayDecisions.push({ + paymentId: txn.paymentId, + gateway: decisionResult.response.decided_gateway, + gateway_priority_map: decisionResult.response.gateway_priority_map, + status: txn.status, + latency: txn.latency + }) + + cy.log(`Transaction ${index + 1}: Payment ${txn.paymentId} routed to ${decisionResult.response.decided_gateway}`) + + // Update gateway score only once per transaction + return cy.updateGatewayScore({ + merchantId: testData.merchantId, + gateway: decisionResult.response.decided_gateway, + paymentId: txn.paymentId, + status: txn.status, + txnLatency: { + gatewayLatency: txn.latency + } + }).then((updateResult) => { + expect(updateResult.response).to.haveValidScoreUpdate() + cy.log(`Score updated for ${txn.paymentId}: ${txn.status} with ${txn.latency}ms latency`) + return cy.wrap(decisionResult.response.decided_gateway) + }) + }) + } + + // Process transactions sequentially to ensure proper ordering + let chain = cy.wrap(null) + transactions.forEach((txn, index) => { + chain = chain.then(() => processTransaction(txn, index)) + }) + + return chain.then(() => { + // Verify that we have decisions for all transactions + expect(gatewayDecisions).to.have.length(transactions.length) + + // Log summary of gateway selections + const gatewaySummary = gatewayDecisions.reduce((acc, decision) => { + acc[decision.gateway] = (acc[decision.gateway] || 0) + 1 + return acc + }, {}) + cy.log('Gateway Decision', gatewayDecisions) + cy.log('Gateway Selection Summary:', gatewaySummary) + + // Test routing behavior after score updates by making additional decisions + return cy.decideGateway({ + merchantId: testData.merchantId, + rankingAlgorithm: "SR_BASED_ROUTING", + paymentInfo: { + paymentId: `${testData.paymentId}_final_test`, + paymentMethodType: "UPI", + paymentMethod: "UPI_PAY", + customerId: `${testData.customerId}_final` + } + }).then((finalResult) => { + expect(finalResult.response).to.haveValidGatewayResponse() + cy.log(`Final routing decision after score updates: ${finalResult.response.decided_gateway}`) + + // Verify that routing is influenced by the success rate data + // The gateway with better success rate should be preferred + const successfulGateways = gatewayDecisions + .filter(d => d.status === "AUTHORIZED") + .map(d => d.gateway) + + if (successfulGateways.length > 0) { + cy.log('Gateways with successful transactions:', [...new Set(successfulGateways)]) + } + }) + }) + }) + }) + + it('should handle multiple gateways with different success rates', { tags: ['@success-rate', '@multiple-gateways'] }, () => { + const merchantId = `${testData.merchantId}_multi_gw` + + cy.createMerchantAccount(merchantId).then(() => { + return cy.createSuccessRateRule(merchantId, { + successRate: 0.7, + latencyThreshold: 100, + bucketSize: 200, + hedgingPercent: 5 + }) + }).then(() => { + // Simulate transactions across multiple gateways with different success patterns + const gatewayTransactions = [ + // GatewayA - Good performance + { paymentId: `${testData.paymentId}_gwa_1`, gateway: 'GatewayA', status: "AUTHORIZED", latency: 1500 }, + { paymentId: `${testData.paymentId}_gwa_2`, gateway: 'GatewayA', status: "AUTHORIZED", latency: 1800 }, + { paymentId: `${testData.paymentId}_gwa_3`, gateway: 'GatewayA', status: "AUTHORIZED", latency: 1600 }, + + // GatewayB - Mixed performance + { paymentId: `${testData.paymentId}_gwb_1`, gateway: 'GatewayB', status: "AUTHORIZED", latency: 2500 }, + { paymentId: `${testData.paymentId}_gwb_2`, gateway: 'GatewayB', status: "FAILURE", latency: 4000 }, + { paymentId: `${testData.paymentId}_gwb_3`, gateway: 'GatewayB', status: "AUTHORIZED", latency: 2200 }, + + // GatewayC - Poor performance + { paymentId: `${testData.paymentId}_gwc_1`, gateway: 'GatewayC', status: "FAILURE", latency: 5000 }, + { paymentId: `${testData.paymentId}_gwc_2`, gateway: 'GatewayC', status: "FAILURE", latency: 4500 }, + { paymentId: `${testData.paymentId}_gwc_3`, gateway: 'GatewayC', status: "AUTHORIZED", latency: 3000 } + ] + + // Process each transaction: decide gateway first, then update score once + let chain = cy.wrap(null) + + gatewayTransactions.forEach((txn, index) => { + chain = chain.then(() => { + // First decide gateway for this transaction + return cy.decideGateway({ + merchantId: merchantId, + rankingAlgorithm: "SR_BASED_ROUTING", + eligibleGatewayList: ['GatewayA', 'GatewayB', 'GatewayC'], + paymentInfo: { + paymentId: txn.paymentId, + paymentMethodType: "UPI", + paymentMethod: "UPI_PAY", + customerId: `${testData.customerId}_multi_${index}` + } + }).then((decisionResult) => { + expect(decisionResult.response).to.haveValidGatewayResponse() + + const decidedGateway = decisionResult.response.decided_gateway + cy.log(`Transaction ${index + 1}: ${txn.paymentId} routed to ${decidedGateway}`) + + // Update score only once per transaction using the decided gateway + return cy.updateGatewayScore({ + merchantId: merchantId, + gateway: decidedGateway, + paymentId: txn.paymentId, + status: txn.status, + txnLatency: { + gatewayLatency: txn.latency + } + }).then((updateResult) => { + expect(updateResult.response).to.haveValidScoreUpdate() + cy.log(`Score updated for ${txn.paymentId}: ${txn.status} (${txn.latency}ms) on ${decidedGateway}`) + return cy.wrap({ decidedGateway, originalGateway: txn.gateway, status: txn.status }) + }) + }) + }) + }) + + return chain.then(() => { + // After all score updates, test final routing decisions + const finalTests = Array.from({ length: 3 }, (_, i) => ({ + paymentId: `${testData.paymentId}_final_${i}`, + customerId: `${testData.customerId}_final_${i}` + })) + + let finalChain = cy.wrap(null) + const finalDecisions = [] + + finalTests.forEach((test, index) => { + finalChain = finalChain.then(() => { + return cy.decideGateway({ + merchantId: merchantId, + rankingAlgorithm: "SR_BASED_ROUTING", + eligibleGatewayList: ['GatewayA', 'GatewayB', 'GatewayC'], + paymentInfo: { + paymentId: test.paymentId, + paymentMethodType: "UPI", + paymentMethod: "UPI_PAY", + customerId: test.customerId + } + }).then((finalResult) => { + expect(finalResult.response).to.haveValidGatewayResponse() + finalDecisions.push(finalResult.response.decided_gateway) + cy.log(`Final decision ${index + 1}: ${test.paymentId} → ${finalResult.response.decided_gateway}`) + return cy.wrap(finalResult.response.decided_gateway) + }) + }) + }) + + return finalChain.then(() => { + // Log summary of final routing decisions + const finalSummary = finalDecisions.reduce((acc, gateway) => { + acc[gateway] = (acc[gateway] || 0) + 1 + return acc + }, {}) + + cy.log('Final Routing Summary after score updates:', finalSummary) + + // Verify that routing is influenced by success rate data + expect(finalDecisions).to.have.length(3) + cy.log('All final routing decisions completed successfully') + }) + }) + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000..f82b5117 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,292 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +const { v4: uuidv4 } = require('uuid') + +// Helper function to generate unique IDs +function generateUniqueId(prefix = '') { + const timestamp = Date.now() + const random = Math.floor(Math.random() * 1000) + return `${prefix}${timestamp}${random}` +} + +// Helper function to get API base URL +function getApiBaseUrl() { + return Cypress.env('API_BASE_URL') || 'http://localhost:8082' +} + +/** + * Create a merchant account + * @param {string} merchantId - Optional merchant ID, will generate if not provided + */ +Cypress.Commands.add('createMerchantAccount', (merchantId = null) => { + const id = merchantId || generateUniqueId(Cypress.env('DEFAULT_MERCHANT_ID_PREFIX')) + + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/merchant-account/create`, + headers: { + 'Content-Type': 'application/json' + }, + body: { + merchant_id: id + } + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap({ merchantId: id, response: response.body }) + }) +}) + +/** + * Create a routing rule + * @param {string} merchantId - Merchant ID + * @param {object} ruleConfig - Rule configuration object + */ +Cypress.Commands.add('createRoutingRule', (merchantId, ruleConfig) => { + const defaultConfig = { + type: "successRate", + data: { + defaultLatencyThreshold: 90, + defaultSuccessRate: 0.5, + defaultBucketSize: 200, + defaultHedgingPercent: 5, + txnLatency: { + gatewayLatency: 5000 + }, + subLevelInputConfig: [ + { + paymentMethodType: "upi", + paymentMethod: "upi_collect", + bucketSize: 250, + hedgingPercent: 1 + } + ] + } + } + + const config = { ...defaultConfig, ...ruleConfig } + + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/rule/create`, + headers: { + 'Content-Type': 'application/json' + }, + body: { + merchant_id: merchantId, + config: config + } + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap({ ruleConfig: config, response: response.body }) + }) +}) + +/** + * Decide gateway for a payment + * @param {object} decisionRequest - Gateway decision request object + */ +Cypress.Commands.add('decideGateway', (decisionRequest) => { + const request = { + merchantId: decisionRequest.merchantId || generateUniqueId(Cypress.env('DEFAULT_MERCHANT_ID_PREFIX')), + eligibleGatewayList: decisionRequest.eligibleGatewayList || Cypress.env('DEFAULT_GATEWAYS'), + rankingAlgorithm: decisionRequest.rankingAlgorithm || Cypress.env('ROUTING_ALGORITHMS').SUCCESS_RATE, + eliminationEnabled: decisionRequest.eliminationEnabled || true, + paymentInfo: { + paymentId: decisionRequest.paymentInfo.paymentId || generateUniqueId(Cypress.env('DEFAULT_PAYMENT_ID_PREFIX')), + amount: decisionRequest.paymentInfo.amount || 100.50, + currency: decisionRequest.paymentInfo.currency || 'USD', + customerId: decisionRequest.paymentInfo.customerId || generateUniqueId(Cypress.env('DEFAULT_CUSTOMER_ID_PREFIX') || 'CUST'), + udfs: decisionRequest.paymentInfo.udfs || null, + preferredGateway: decisionRequest.paymentInfo.preferredGateway || null, + paymentType: decisionRequest.paymentInfo.paymentType || "ORDER_PAYMENT", + metadata: decisionRequest.paymentInfo.metadata || null, + internalMetadata: decisionRequest.paymentInfo.internalMetadata || null, + isEmi: decisionRequest.paymentInfo.isEmi || false, + emiBank: decisionRequest.paymentInfo.emiBank || null, + emiTenure: decisionRequest.paymentInfo.emiTenure || null, + paymentMethodType: decisionRequest.paymentInfo.paymentMethodType || Cypress.env('PAYMENT_METHODS').UPI.type, + paymentMethod: decisionRequest.paymentInfo.paymentMethod || Cypress.env('PAYMENT_METHODS').UPI.method, + paymentSource: decisionRequest.paymentInfo.paymentSource || null, + authType: decisionRequest.paymentInfo.authType || null, + cardIssuerBankName: decisionRequest.paymentInfo.cardIssuerBankName || null, + cardIsin: decisionRequest.paymentInfo.cardIsin || null, + cardType: decisionRequest.paymentInfo.cardType || null, + cardSwitchProvider: decisionRequest.paymentInfo.cardSwitchProvider || null + } + } + + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/decide-gateway`, + headers: { + 'Content-Type': 'application/json' + }, + body: request + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap({ request, response: response.body }) + }) +}) + +/** + * Legacy Decide gateway for a payment + * @param {object} decisionRequest - Gateway decision request object + */ +Cypress.Commands.add('decideGatewayLegacy', (decisionRequest) => { + const request = { + merchantId: decisionRequest.merchantId || generateUniqueId(Cypress.env('DEFAULT_MERCHANT_ID_PREFIX')), + eligibleGatewayList: decisionRequest.eligibleGatewayList || Cypress.env('DEFAULT_GATEWAYS'), + rankingAlgorithm: decisionRequest.rankingAlgorithm || Cypress.env('ROUTING_ALGORITHMS').SUCCESS_RATE, + eliminationEnabled: decisionRequest.eliminationEnabled || true, + paymentInfo: { + paymentId: decisionRequest.paymentInfo.paymentId || generateUniqueId(Cypress.env('DEFAULT_PAYMENT_ID_PREFIX')), + amount: decisionRequest.paymentInfo.amount || 100.50, + currency: decisionRequest.paymentInfo.currency || 'USD', + customerId: decisionRequest.paymentInfo.customerId || generateUniqueId(Cypress.env('DEFAULT_CUSTOMER_ID_PREFIX') || 'CUST'), + udfs: decisionRequest.paymentInfo.udfs || null, + preferredGateway: decisionRequest.paymentInfo.preferredGateway || null, + paymentType: decisionRequest.paymentInfo.paymentType || "ORDER_PAYMENT", + metadata: decisionRequest.paymentInfo.metadata || null, + internalMetadata: decisionRequest.paymentInfo.internalMetadata || null, + isEmi: decisionRequest.paymentInfo.isEmi || false, + emiBank: decisionRequest.paymentInfo.emiBank || null, + emiTenure: decisionRequest.paymentInfo.emiTenure || null, + paymentMethodType: decisionRequest.paymentInfo.paymentMethodType || Cypress.env('PAYMENT_METHODS').UPI.type, + paymentMethod: decisionRequest.paymentInfo.paymentMethod || Cypress.env('PAYMENT_METHODS').UPI.method, + paymentSource: decisionRequest.paymentInfo.paymentSource || null, + authType: decisionRequest.paymentInfo.authType || null, + cardIssuerBankName: decisionRequest.paymentInfo.cardIssuerBankName || null, + cardIsin: decisionRequest.paymentInfo.cardIsin || null, + cardType: decisionRequest.paymentInfo.cardType || null, + cardSwitchProvider: decisionRequest.paymentInfo.cardSwitchProvider || null + } + } + + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/decision_gateway`, + headers: { + 'Content-Type': 'application/json' + }, + body: request + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap({ request, response: response.body }) + }) +}) + +/** + * Update gateway score + * @param {object} scoreUpdate - Score update object + */ +Cypress.Commands.add('updateGatewayScore', (scoreUpdate) => { + const defaultUpdate = { + merchantId: scoreUpdate.merchantId || generateUniqueId(Cypress.env('DEFAULT_MERCHANT_ID_PREFIX')), + gateway: scoreUpdate.gateway || "GatewayC", + gatewayReferenceId: scoreUpdate.gatewayReferenceId || null, + status: scoreUpdate.status || "AUTHORIZED", + paymentId: scoreUpdate.paymentId || generateUniqueId(Cypress.env('DEFAULT_PAYMENT_ID_PREFIX')), + enforceDynamicRoutingFailure: null, + txnLatency: scoreUpdate.txnLatency.gatewayLatency || { + gatewayLatency: 6000 + } + } + + const update = { ...defaultUpdate, ...scoreUpdate } + + return cy.request({ + method: 'POST', + url: `${getApiBaseUrl()}/update-gateway-score`, + headers: { + 'Content-Type': 'application/json' + }, + body: update + }).then((response) => { + expect(response.status).to.eq(200) + return cy.wrap({ update, response: response.body }) + }) +}) + +/** + * Create a success rate routing rule + * @param {string} merchantId - Merchant ID + * @param {object} options - Configuration options + */ +Cypress.Commands.add('createSuccessRateRule', (merchantId, options = {}) => { + const ruleConfig = { + type: "successRate", + data: { + defaultLatencyThreshold: options.latencyThreshold || 90, + defaultSuccessRate: options.successRate || 0.5, + defaultBucketSize: options.bucketSize || 200, + defaultHedgingPercent: options.hedgingPercent || 5, + txnLatency: { + gatewayLatency: options.gatewayLatency || 5000 + }, + subLevelInputConfig: options.subLevelConfig || [ + { + paymentMethodType: "upi", + paymentMethod: "upi_collect", + bucketSize: 250, + hedgingPercent: 1 + } + ] + } + } + + return cy.createRoutingRule(merchantId, ruleConfig) +}) + +/** + * Create a elimination routing rule + * @param {string} merchantId - Merchant ID + * @param {object} options - Configuration options + */ +Cypress.Commands.add('createEliminationRule', (merchantId, options = {}) => { + const ruleConfig = { + type: "elimination", + data: { + threshold: 0.35, + txnLatency: { + gatewayLatency: options.gatewayLatency || 5000 + } + } + } + + return cy.createRoutingRule(merchantId, ruleConfig) +}) + +/** + * Wait for service to be ready + */ +Cypress.Commands.add('waitForService', () => { + return cy.request({ + method: 'GET', + url: `${getApiBaseUrl()}/health`, + failOnStatusCode: false, + timeout: 30000 + }).then((response) => { + if (response.status !== 200) { + cy.wait(2000) + cy.waitForService() + } + }) +}) + +/** + * Clean up test data (if cleanup endpoints exist) + * @param {string} merchantId - Merchant ID to clean up + */ +Cypress.Commands.add('cleanupTestData', (merchantId) => { + // This would depend on cleanup endpoints being available + // For now, just log the cleanup attempt + cy.log(`Cleaning up test data for merchant: ${merchantId}`) +}) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 00000000..f6028f4c --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,46 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' +import '@cypress/grep/src/support' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +// Global configuration +Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test on uncaught exceptions + return false +}) + +// Add custom assertions +chai.use(function (chai, utils) { + chai.Assertion.addMethod('haveValidGatewayResponse', function () { + const obj = this._obj + + expect(obj).to.have.property('decided_gateway') + expect(obj).to.have.property('gateway_priority_map') + expect(obj.gateway_priority_map).to.be.an('object') + }) + + chai.Assertion.addMethod('haveValidScoreUpdate', function () { + const obj = this._obj + + expect(obj).to.have.property('message') + expect(obj.message).to.equal('Success') + }) +}) diff --git a/cypress/support/test-data-factory.js b/cypress/support/test-data-factory.js new file mode 100644 index 00000000..cd37486d --- /dev/null +++ b/cypress/support/test-data-factory.js @@ -0,0 +1,220 @@ +// Test Data Factory for Decision Engine Tests +// This file provides utilities to generate test data for different routing scenarios + +const { v4: uuidv4 } = require('uuid') + +class TestDataFactory { + static generateMerchantId(prefix = 'merc_test_') { + return `${prefix}${Date.now()}_${Math.floor(Math.random() * 1000)}` + } + + static generatePaymentId(prefix = 'PAY_test_') { + return `${prefix}${Date.now()}_${Math.floor(Math.random() * 1000)}` + } + + static generateCustomerId(prefix = 'CUST_test_') { + return `${prefix}${Date.now()}_${Math.floor(Math.random() * 1000)}` + } + + // Success Rate Rule Configurations + static getSuccessRateRuleConfig(options = {}) { + return { + type: "successRate", + data: { + defaultLatencyThreshold: options.latencyThreshold || 90, + defaultSuccessRate: options.successRate || 0.5, + defaultBucketSize: options.bucketSize || 200, + defaultHedgingPercent: options.hedgingPercent || 5, + txnLatency: { + gatewayLatency: options.gatewayLatency || 5000 + }, + subLevelInputConfig: options.subLevelConfig || [ + { + paymentMethodType: "upi", + paymentMethod: "upi_collect", + bucketSize: 250, + hedgingPercent: 1 + } + ] + } + } + } + + // Payment Latency Rule Configurations + static getPaymentLatencyRuleConfig(options = {}) { + return { + type: "paymentLatency", + data: { + defaultLatencyThreshold: options.latencyThreshold || 90, + defaultBucketSize: options.bucketSize || 200, + defaultHedgingPercent: options.hedgingPercent || 5, + txnLatency: { + gatewayLatency: options.gatewayLatency || 3000, + paymentLatency: options.paymentLatency || 5000 + }, + subLevelInputConfig: options.subLevelConfig || [] + } + } + } + + // Cost Based Rule Configurations (if supported) + static getCostBasedRuleConfig(options = {}) { + return { + type: "costBased", + data: { + defaultCostThreshold: options.costThreshold || 2.5, + defaultBucketSize: options.bucketSize || 200, + defaultHedgingPercent: options.hedgingPercent || 5, + costConfig: { + baseCost: options.baseCost || 1.0, + variableCost: options.variableCost || 0.5 + }, + subLevelInputConfig: options.subLevelConfig || [] + } + } + } + + // Payment Info Configurations + static getPaymentInfo(options = {}) { + return { + paymentId: options.paymentId || this.generatePaymentId(), + amount: options.amount || 100.50, + currency: options.currency || "USD", + customerId: options.customerId || this.generateCustomerId(), + udfs: options.udfs || null, + preferredGateway: options.preferredGateway || null, + paymentType: options.paymentType || "ORDER_PAYMENT", + metadata: options.metadata || null, + internalMetadata: options.internalMetadata || null, + isEmi: options.isEmi || false, + emiBank: options.emiBank || null, + emiTenure: options.emiTenure || null, + paymentMethodType: options.paymentMethodType || "UPI", + paymentMethod: options.paymentMethod || "UPI_PAY", + paymentSource: options.paymentSource || null, + authType: options.authType || null, + cardIssuerBankName: options.cardIssuerBankName || null, + cardIsin: options.cardIsin || null, + cardType: options.cardType || null, + cardSwitchProvider: options.cardSwitchProvider || null + } + } + + // Gateway Decision Request + static getGatewayDecisionRequest(options = {}) { + return { + merchantId: options.merchantId || this.generateMerchantId(), + eligibleGatewayList: options.eligibleGatewayList || ["GatewayA", "GatewayB", "GatewayC"], + rankingAlgorithm: options.rankingAlgorithm || "SR_BASED_ROUTING", + eliminationEnabled: options.eliminationEnabled !== undefined ? options.eliminationEnabled : true, + paymentInfo: this.getPaymentInfo(options.paymentInfo || {}) + } + } + + // Score Update Request + static getScoreUpdateRequest(options = {}) { + return { + merchantId: options.merchantId || this.generateMerchantId(), + gateway: options.gateway || "GatewayA", + gatewayReferenceId: options.gatewayReferenceId || null, + status: options.status || "AUTHORIZED", + paymentId: options.paymentId || this.generatePaymentId(), + enforceDynamicRoutingFailure: options.enforceDynamicRoutingFailure || null, + txnLatency: { + gatewayLatency: options.gatewayLatency || 3000, + paymentLatency: options.paymentLatency || 4000, + ...options.txnLatency + } + } + } + + // Test Scenarios + static getLatencyTestScenarios() { + return [ + { name: "Low Latency", gatewayLatency: 1000, paymentLatency: 1500, status: "AUTHORIZED" }, + { name: "Medium Latency", gatewayLatency: 3000, paymentLatency: 4000, status: "AUTHORIZED" }, + { name: "High Latency", gatewayLatency: 6000, paymentLatency: 8000, status: "AUTHORIZED" }, + { name: "Failed Transaction", gatewayLatency: 2000, paymentLatency: 3000, status: "FAILED" }, + { name: "Timeout", gatewayLatency: 10000, paymentLatency: 12000, status: "TIMEOUT" } + ] + } + + static getSuccessRateTestScenarios() { + return [ + { name: "High Success", transactions: Array(8).fill("AUTHORIZED").concat(Array(2).fill("FAILED")) }, + { name: "Medium Success", transactions: Array(6).fill("AUTHORIZED").concat(Array(4).fill("FAILED")) }, + { name: "Low Success", transactions: Array(3).fill("AUTHORIZED").concat(Array(7).fill("FAILED")) }, + { name: "All Success", transactions: Array(10).fill("AUTHORIZED") }, + { name: "All Failed", transactions: Array(10).fill("FAILED") } + ] + } + + static getPaymentMethodTestCases() { + return [ + { type: "UPI", method: "UPI_PAY", description: "UPI Payment" }, + { type: "upi", method: "upi_collect", description: "UPI Collect" }, + { type: "CARD", method: "CARD_PAY", description: "Card Payment" }, + { type: "NETBANKING", method: "NETBANKING_PAY", description: "Net Banking" }, + { type: "WALLET", method: "WALLET_PAY", description: "Wallet Payment" } + ] + } + + static getRoutingAlgorithmTestCases() { + return [ + { algorithm: "SR_BASED_ROUTING", description: "Success Rate Based Routing" }, + { algorithm: "PL_BASED_ROUTING", description: "Payment Latency Based Routing" }, + { algorithm: "COST_BASED_ROUTING", description: "Cost Based Routing" } + ] + } + + // Edge Case Scenarios + static getEdgeCaseScenarios() { + return { + extremeLatency: { + gatewayLatency: 30000, + paymentLatency: 45000, + status: "TIMEOUT" + }, + zeroLatency: { + gatewayLatency: 0, + paymentLatency: 0, + status: "AUTHORIZED" + }, + negativeAmount: { + amount: -100, + currency: "USD" + }, + largeAmount: { + amount: 999999.99, + currency: "USD" + }, + invalidCurrency: { + amount: 100, + currency: "INVALID" + } + } + } + + // Load Testing Data + static generateLoadTestData(count = 100) { + return Array.from({ length: count }, (_, index) => ({ + merchantId: this.generateMerchantId(`load_test_${index}_`), + paymentId: this.generatePaymentId(`load_pay_${index}_`), + customerId: this.generateCustomerId(`load_cust_${index}_`), + amount: Math.floor(Math.random() * 1000) + 1, + latency: Math.floor(Math.random() * 5000) + 500 + })) + } + + // Performance Test Configurations + static getPerformanceTestConfig() { + return { + concurrentUsers: [1, 5, 10, 20, 50], + requestsPerSecond: [1, 5, 10, 25, 50], + testDuration: [30, 60, 120, 300], // seconds + latencyThresholds: [1000, 2000, 3000, 5000] // milliseconds + } + } +} + +module.exports = TestDataFactory diff --git a/decision-engine.postman_collection.json b/decision-engine.postman_collection.json new file mode 100644 index 00000000..3a03b1bd --- /dev/null +++ b/decision-engine.postman_collection.json @@ -0,0 +1,368 @@ +{ + "info": { + "name": "Decision Engine", + "description": "Postman collection for juspay/decision-engine — a payment gateway routing service.\n\nBase URL: http://localhost:8080\n\nQuick start:\n1. Create a merchant account (Merchant / Create)\n2. Call POST /decide-gateway with a payment payload\n3. Record the outcome via POST /update-gateway-score", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "merchantId", + "value": "test_merchant", + "type": "string" + } + ], + "item": [ + { + "name": "Health", + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/health", + "host": ["{{baseUrl}}"], + "path": ["health"] + }, + "description": "Returns {\"message\":\"Health is good\"} when the service is up." + } + } + ] + }, + { + "name": "Merchant Account", + "item": [ + { + "name": "Create Merchant", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/merchant-account/create", + "host": ["{{baseUrl}}"], + "path": ["merchant-account", "create"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"gateway_success_rate_based_decider_input\": null\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Register a new merchant. Must be done before calling /decide-gateway." + } + }, + { + "name": "Get Merchant", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/merchant-account/{{merchantId}}", + "host": ["{{baseUrl}}"], + "path": ["merchant-account", "{{merchantId}}"] + }, + "description": "Fetch a merchant's configuration by merchant_id." + } + }, + { + "name": "Delete Merchant", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/merchant-account/{{merchantId}}", + "host": ["{{baseUrl}}"], + "path": ["merchant-account", "{{merchantId}}"] + }, + "description": "Delete a merchant account." + } + } + ] + }, + { + "name": "Gateway Decision", + "item": [ + { + "name": "Decide Gateway (v2 — recommended)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/decide-gateway", + "host": ["{{baseUrl}}"], + "path": ["decide-gateway"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"paymentInfo\": {\n \"paymentId\": \"pay_001\",\n \"amount\": 1000.0,\n \"currency\": \"USD\",\n \"country\": \"US\",\n \"customerId\": \"cust_123\",\n \"paymentType\": \"ORDER_PAYMENT\",\n \"paymentMethodType\": \"CARD\",\n \"paymentMethod\": \"CREDIT\",\n \"authType\": \"THREE_DS\",\n \"cardIsin\": \"411111\",\n \"cardType\": \"CREDIT\",\n \"cardIssuerBankName\": \"HDFC\",\n \"preferredGateway\": null,\n \"paymentSource\": null,\n \"isEmi\": false,\n \"emiBank\": null,\n \"emiTenure\": null,\n \"metadata\": null,\n \"internalMetadata\": null,\n \"udfs\": null\n },\n \"eligibleGatewayList\": [\"stripe\", \"paypal\", \"adyen\"],\n \"rankingAlgorithm\": \"SrBasedRouting\",\n \"eliminationEnabled\": false\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Core routing decision API (v2). Returns the best gateway from eligible_gateway_list.\n\nranking_algorithm options: SrBasedRouting | PlBasedRouting | NtwBasedRouting\n\nResponse includes:\n- decided_gateway: the selected gateway name\n- routing_approach: e.g. SR_SELECTION_V3_ROUTING, PRIORITY_LOGIC\n- gateway_priority_map: scores for each eligible gateway\n- routing_dimension: dimension used for SR scoring (e.g. CARD_BRAND, AUTH_TYPE)" + } + }, + { + "name": "Decision Gateway (v1 — full payload)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/decision_gateway", + "host": ["{{baseUrl}}"], + "path": ["decision_gateway"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"orderReference\": {\n \"orderId\": \"order_001\",\n \"amount\": 1000.0,\n \"currency\": \"USD\",\n \"merchantId\": \"{{merchantId}}\",\n \"status\": \"NEW\",\n \"orderType\": \"ORDER_PAYMENT\",\n \"customerId\": \"cust_123\",\n \"preferredGateway\": null,\n \"metadata\": null,\n \"udfs\": {}\n },\n \"txnDetail\": {\n \"txnId\": \"txn_001\",\n \"orderId\": \"order_001\",\n \"merchantId\": \"{{merchantId}}\",\n \"status\": \"PENDING_VBV\",\n \"type\": \"ORDER_PAYMENT\",\n \"txnUuid\": \"pay_001\",\n \"gateway\": \"stripe\"\n },\n \"txnCardInfo\": {\n \"id\": \"card_001\",\n \"paymentMethodType\": \"CARD\",\n \"paymentMethod\": \"CREDIT\",\n \"authType\": \"THREE_DS\",\n \"cardIsin\": \"411111\",\n \"cardType\": \"CREDIT\",\n \"dateCreated\": \"2026-03-31T00:00:00Z\"\n },\n \"merchantAccount\": {\n \"merchantId\": \"{{merchantId}}\",\n \"gatewaySuccessRateBasedDeciderInput\": null\n },\n \"enforceGatewayList\": [\"stripe\", \"paypal\", \"adyen\"],\n \"priorityLogicScript\": null,\n \"priorityLogicOutput\": null\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Legacy full-payload decision endpoint. Prefer /decide-gateway (v2) for new integrations." + } + }, + { + "name": "Hybrid Routing", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/routing/hybrid", + "host": ["{{baseUrl}}"], + "path": ["routing", "hybrid"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"static_routing_request\": {\n \"created_by\": \"{{merchantId}}\",\n \"fallback_output\": null,\n \"parameters\": {\n \"payment_method\": \"card\",\n \"currency\": \"USD\",\n \"country\": \"US\"\n }\n },\n \"dynamic_routing_request\": {\n \"merchant_id\": \"{{merchantId}}\",\n \"payment_info\": {\n \"payment_id\": \"pay_001\",\n \"amount\": 1000.0,\n \"currency\": \"USD\",\n \"payment_type\": \"ORDER_PAYMENT\",\n \"payment_method_type\": \"CARD\",\n \"payment_method\": \"CREDIT\"\n },\n \"eligible_gateway_list\": [\"stripe\", \"paypal\", \"adyen\"],\n \"ranking_algorithm\": \"SrBasedRouting\",\n \"elimination_enabled\": false\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Combines static rule-based routing with dynamic SR-based routing. Returns merged results from both engines." + } + } + ] + }, + { + "name": "Score Feedback", + "item": [ + { + "name": "Update Gateway Score", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/update-gateway-score", + "host": ["{{baseUrl}}"], + "path": ["update-gateway-score"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"gateway\": \"stripe\",\n \"paymentId\": \"pay_001\",\n \"status\": \"CHARGED\",\n \"gatewayReferenceId\": \"stripe_ref_001\",\n \"enforceDynamicRoutingFailure\": false,\n \"txnLatency\": {\n \"gatewayLatency\": 120.5\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Feed transaction outcome back to update the success rate model.\n\nstatus options: CHARGED | AUTHENTICATION_FAILED | AUTHORIZATION_FAILED | JUSPAY_DECLINED | AUTO_REFUNDED | COD_INITIATED | STARTED | PENDING_VBV | CAPTURE_FAILED | VOID_FAILED | VOID_INITIATED | CAPTURE_INITIATED\n\nCall this after every transaction to keep SR scores accurate." + } + }, + { + "name": "Update Score (legacy)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/update-score", + "host": ["{{baseUrl}}"], + "path": ["update-score"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"txn_detail\": {\n \"txnId\": \"txn_001\",\n \"orderId\": \"order_001\",\n \"merchantId\": \"{{merchantId}}\",\n \"status\": \"CHARGED\",\n \"type\": \"ORDER_PAYMENT\",\n \"txnUuid\": \"pay_001\",\n \"gateway\": \"stripe\",\n \"dateCreated\": \"2026-03-31T00:00:00Z\",\n \"txnAmount\": { \"value\": 1000, \"currency\": \"USD\" },\n \"txnObjectType\": \"ORDER_PAYMENT\",\n \"sourceObject\": \"CREDIT\",\n \"isEmi\": false\n },\n \"txn_card_info\": {\n \"id\": \"card_001\",\n \"paymentMethodType\": \"CARD\",\n \"paymentMethod\": \"CREDIT\",\n \"authType\": \"THREE_DS\",\n \"cardIsin\": \"411111\",\n \"cardType\": \"CREDIT\",\n \"dateCreated\": \"2026-03-31T00:00:00Z\"\n },\n \"log_message\": \"Transaction completed\",\n \"enforce_dynaic_routing_failure\": false,\n \"txn_latency\": {\n \"gatewayLatency\": 120.5\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Legacy score update endpoint using the full txn_detail / txn_card_info structure. Prefer /update-gateway-score for new integrations." + } + } + ] + }, + { + "name": "Routing Rules (Euclid)", + "item": [ + { + "name": "Create Routing Rule", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/routing/create", + "host": ["{{baseUrl}}"], + "path": ["routing", "create"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"My Priority Rule\",\n \"description\": \"Route US card payments: Stripe first, Adyen fallback\",\n \"created_by\": \"{{merchantId}}\",\n \"algorithm_for\": \"Payment\",\n \"algorithm\": {\n \"type\": \"priority\",\n \"data\": [\n { \"connector\": \"stripe\", \"merchant_connector_id\": null },\n { \"connector\": \"adyen\", \"merchant_connector_id\": null }\n ]\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Create a routing rule in the Euclid rules engine.\n\nalgorithm.type options:\n- \"single\": always route to one connector\n- \"priority\": ordered fallback list\n- \"volume_split\": percentage-based split across connectors\n- \"advanced\": conditional AST-based logic\n\nalgorithm_for options: Payment | Payout | ThreeDsAuthentication" + } + }, + { + "name": "Activate Routing Rule", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/routing/activate", + "host": ["{{baseUrl}}"], + "path": ["routing", "activate"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"created_by\": \"{{merchantId}}\",\n \"routing_algorithm_id\": \"\"\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Activate a previously created routing rule for a merchant. Only one rule can be active at a time per merchant." + } + }, + { + "name": "List Routing Rules", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/routing/list/{{merchantId}}", + "host": ["{{baseUrl}}"], + "path": ["routing", "list", "{{merchantId}}"] + }, + "description": "List all routing rules created by a merchant." + } + }, + { + "name": "Get Active Routing Rule", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/routing/list/active/{{merchantId}}", + "host": ["{{baseUrl}}"], + "path": ["routing", "list", "active", "{{merchantId}}"] + }, + "description": "Get the currently active routing rule for a merchant." + } + }, + { + "name": "Evaluate Routing Rule", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/routing/evaluate", + "host": ["{{baseUrl}}"], + "path": ["routing", "evaluate"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"created_by\": \"{{merchantId}}\",\n \"fallback_output\": [\n { \"connector\": \"stripe\", \"merchant_connector_id\": null }\n ],\n \"parameters\": {\n \"payment_method\": \"card\",\n \"currency\": \"USD\",\n \"country\": \"US\",\n \"amount\": 1000\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Test/dry-run the active routing rule for a merchant with given payment parameters. Returns the connector(s) the rule would select." + } + }, + { + "name": "Configure SR Dimensions", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/config-sr-dimension", + "host": ["{{baseUrl}}"], + "path": ["config-sr-dimension"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"dimensions\": [\"CARD_BRAND\", \"AUTH_TYPE\", \"CURRENCY\"]\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Configure which dimensions to use when computing success rates for a merchant.\n\nPossible dimensions: CARD_BRAND | AUTH_TYPE | CURRENCY | PAYMENT_METHOD | CARD_TYPE | CARD_ISIN | COUNTRY" + } + } + ] + }, + { + "name": "Rule Configuration (service config)", + "item": [ + { + "name": "Create Rule Config", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/rule/create", + "host": ["{{baseUrl}}"], + "path": ["rule", "create"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"config\": {\n \"type\": \"successRate\",\n \"data\": {\n \"defaultBucketSize\": 20,\n \"defaultLatencyThreshold\": null,\n \"defaultHedgingPercent\": null\n }\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Create a service-level rule config (e.g. SR thresholds, elimination settings).\n\nconfig options: SuccessRate | Elimination | DebitRouting" + } + }, + { + "name": "Get Rule Config", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/rule/get", + "host": ["{{baseUrl}}"], + "path": ["rule", "get"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"algorithm\": \"successRate\"\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Retrieve a specific rule config for a merchant.\n\nalgorithm options: SuccessRate | Elimination | DebitRouting" + } + }, + { + "name": "Update Rule Config", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/rule/update", + "host": ["{{baseUrl}}"], + "path": ["rule", "update"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"config\": {\n \"type\": \"successRate\",\n \"data\": {\n \"defaultBucketSize\": 30,\n \"defaultHedgingPercent\": 0.1\n }\n }\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Update an existing rule config." + } + }, + { + "name": "Delete Rule Config", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/rule/delete", + "host": ["{{baseUrl}}"], + "path": ["rule", "delete"] + }, + "body": { + "mode": "raw", + "raw": "{\n \"merchant_id\": \"{{merchantId}}\",\n \"algorithm\": \"successRate\"\n}", + "options": { "raw": { "language": "json" } } + }, + "description": "Delete a rule config for a merchant." + } + } + ] + } + ] +} diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..c2720dc9 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,3 @@ +# Local profile variants are now defined directly in docker-compose.yaml. +# This override file is intentionally kept minimal for backward compatibility. +services: {} diff --git a/docker-compose.yaml b/docker-compose.yaml index 9639b552..97443feb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,25 +1,32 @@ services: - open-router: - image: ghcr.io/juspay/decision-engine:v1.2.0 + # ========================================== + # Decision Engine Runtime (GHCR Images) + # ========================================== + open-router-pg-ghcr: + image: ghcr.io/juspay/decision-engine/postgres:${DECISION_ENGINE_TAG:-v1.4} pull_policy: always platform: linux/amd64 - container_name: open-router + profiles: + - postgres-ghcr + - dashboard-postgres-ghcr restart: unless-stopped ports: - "8080:8080" + - "9094:9094" + - "9094:9094" depends_on: - mysql: + postgresql: condition: service_healthy redis: condition: service_healthy - groovy-runner: - condition: service_healthy - routing-config: + db-migrator-postgres: condition: service_completed_successfully volumes: - ./config/docker-configuration.toml:/local/config/development.toml networks: - - open-router-network + open-router-network: + aliases: + - decision-engine-api environment: - GROOVY_RUNNER_HOST=host.docker.internal:8085 @@ -36,7 +43,7 @@ services: - "com.docker.compose.watchfile=Cargo.lock" image: decision-engine-open-router-local:latest platform: linux/amd64 - container_name: open-router + container_name: open-router-local restart: unless-stopped ports: - "8080:8080" @@ -69,7 +76,7 @@ services: - "com.docker.compose.watchfile=Cargo.lock" image: decision-engine-open-router-local-pg:latest platform: linux/amd64 - container_name: open-router + container_name: open-router-local-pg restart: unless-stopped ports: - "8080:8080" @@ -79,8 +86,8 @@ services: condition: service_healthy redis: condition: service_healthy - groovy-runner-local: - condition: service_healthy + db-migrator-postgres: + condition: service_completed_successfully volumes: - ./config/docker-configuration.toml:/local/config/development.toml networks: @@ -88,32 +95,180 @@ services: environment: - GROOVY_RUNNER_HOST=host.docker.internal:8085 - open-router-pg: - image: ghcr.io/juspay/decision-engine/postgres:v1.2.0 + open-router-mysql-ghcr: + image: ghcr.io/juspay/decision-engine:${DECISION_ENGINE_TAG:-v1.4} pull_policy: always platform: linux/amd64 - container_name: open-router-pg + profiles: + - mysql-ghcr + - dashboard-mysql-ghcr restart: unless-stopped ports: - "8080:8080" - - "9090:9090" depends_on: - db-migrator-postgres: + mysql: + condition: service_healthy + redis: + condition: service_healthy + routing-config: condition: service_completed_successfully - groovy-runner-local: + db-migrator: + condition: service_completed_successfully + volumes: + - ./config/docker-configuration.toml:/local/config/development.toml + networks: + open-router-network: + aliases: + - decision-engine-api + environment: + - GROOVY_RUNNER_HOST=host.docker.internal:8085 + + # ========================================== + # Decision Engine Runtime (Local Build) + # ========================================== + open-router-pg-local: + build: + context: . + dockerfile: Dockerfile.postgres + platforms: + - linux/amd64 + image: decision-engine-pg:local + platform: linux/amd64 + profiles: + - postgres-local + - dashboard-postgres-local + restart: unless-stopped + ports: + - "8080:8080" + - "9094:9094" + depends_on: + postgresql: + condition: service_healthy + redis: condition: service_healthy + db-migrator-postgres: + condition: service_completed_successfully volumes: - ./config/docker-configuration.toml:/local/config/development.toml networks: - - open-router-network + open-router-network: + aliases: + - decision-engine-api environment: - GROOVY_RUNNER_HOST=host.docker.internal:8085 - groovy-runner: - image: ghcr.io/juspay/open-router/groovy-runner:main + open-router-mysql-local: + build: + context: . + dockerfile: Dockerfile + platforms: + - linux/amd64 + image: decision-engine-mysql:local + platform: linux/amd64 + profiles: + - mysql-local + - dashboard-mysql-local + restart: unless-stopped + ports: + - "8080:8080" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + routing-config: + condition: service_completed_successfully + db-migrator: + condition: service_completed_successfully + volumes: + - ./config/docker-configuration.toml:/local/config/development.toml + networks: + open-router-network: + aliases: + - decision-engine-api + environment: + - GROOVY_RUNNER_HOST=host.docker.internal:8085 + + # ========================================== + # Dashboard and Docs + # ========================================== + nginx: + image: nginx:alpine + profiles: + - dashboard-postgres-ghcr + - dashboard-postgres-local + - dashboard-mysql-ghcr + - dashboard-mysql-local + restart: unless-stopped + ports: + - "8081:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./website/dist:/usr/share/nginx/html/dashboard:ro + depends_on: + mintlify-docs: + condition: service_healthy + networks: + - open-router-network + + mintlify-docs: + build: + context: . + dockerfile: Dockerfile.docs + profiles: + - dashboard-postgres-ghcr + - dashboard-postgres-local + - dashboard-mysql-ghcr + - dashboard-mysql-local + restart: unless-stopped + expose: + - "3000" + networks: + - open-router-network + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3000 > /dev/null 2>&1 || exit 1"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 60s + + # ========================================== + # Supporting Services + # ========================================== + prometheus: + image: prom/prometheus:latest + profiles: + - monitoring + networks: + - open-router-network + volumes: + - ./config/prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + restart: unless-stopped + + grafana: + image: grafana/grafana:latest + profiles: + - monitoring + ports: + - "3000:3000" + networks: + - open-router-network + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + groovy-runner-ghcr: + image: ghcr.io/juspay/decision-engine/groovy-runner:${GROOVY_RUNNER_TAG:-v1.4} pull_policy: always platform: linux/amd64 - container_name: groovy-runner + profiles: + - groovy-ghcr restart: unless-stopped ports: - "8085:8085" @@ -137,8 +292,11 @@ services: labels: - "com.docker.compose.watchfile=groovy.Dockerfile" - "com.docker.compose.watchfile=src/Runner.groovy" + image: decision-engine-groovy-runner:local platform: linux/amd64 - container_name: groovy-runner + profiles: + - groovy-local + container_name: groovy-runner-local restart: unless-stopped ports: - "8085:8085" @@ -151,63 +309,93 @@ services: retries: 5 start_period: 10s + # ========================================== + # Core Infrastructure + # ========================================== mysql: image: mysql:8.0 - container_name: open-router-mysql + profiles: + - mysql-ghcr + - dashboard-mysql-ghcr + - mysql-local + - dashboard-mysql-local restart: unless-stopped environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_DATABASE=jdb - volumes: - - mysql-data:/var/lib/mysql + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: jdb + MYSQL_USER: db_user + MYSQL_PASSWORD: db_pass ports: - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql networks: - open-router-network healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"] - interval: 5s - timeout: 10s - retries: 10 + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s - postgresql: - image: postgres:latest - container_name: open-router-postgres - restart: unless-stopped + postgresql: + image: postgres:16 + profiles: + - postgres-ghcr + - dashboard-postgres-ghcr + - postgres-local + - dashboard-postgres-local + restart: unless-stopped + container_name: postgres-db environment: - - POSTGRES_USER=db_user - - POSTGRES_PASSWORD=db_pass - - POSTGRES_DB=decision_engine_db - volumes: - - postgres-data:/var/lib/postgresql/data - ports: + POSTGRES_USER: db_user + POSTGRES_PASSWORD: db_pass + POSTGRES_DB: decision_engine_db + ports: - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql networks: - - open-router-network + - open-router-network healthcheck: - test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] - interval: 5s - retries: 3 - start_period: 5s - timeout: 5s + test: ["CMD-SHELL", "pg_isready -U db_user -d decision_engine_db"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s redis: - image: redis:7 - container_name: open-router-redis + image: redis:7-alpine + profiles: + - postgres-ghcr + - dashboard-postgres-ghcr + - postgres-local + - dashboard-postgres-local + - mysql-ghcr + - dashboard-mysql-ghcr + - mysql-local + - dashboard-mysql-local + restart: unless-stopped ports: - "6379:6379" + volumes: + - redis-data:/data networks: - open-router-network healthcheck: - test: ["CMD-SHELL", "redis-cli ping | grep '^PONG$'"] - interval: 5s + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s retries: 5 start_period: 5s - timeout: 5s db-migrator: image: mysql:8.0 - container_name: db-migrator + profiles: + - mysql-ghcr + - dashboard-mysql-ghcr + - mysql-local + - dashboard-mysql-local depends_on: mysql: condition: service_healthy @@ -217,10 +405,15 @@ services: entrypoint: ["/bin/sh", "-c", "for sql_file in $$(find /app/migrations -name 'up.sql' -type f | sort); do echo \"Running migration: $$sql_file\"; mysql -h mysql -uroot -proot jdb < \"$$sql_file\"; done"] networks: - open-router-network - + db-migrator-postgres: image: rust:latest - container_name: db-migrator + profiles: + - postgres-ghcr + - dashboard-postgres-ghcr + - postgres-local + - dashboard-postgres-local + container_name: db-migrator-postgres depends_on: postgresql: condition: service_healthy @@ -230,13 +423,17 @@ services: volumes: - .:/app working_dir: /app - command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && cargo install just && just migrate-pg'" + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration run --database-url \"$$DATABASE_URL\" --migration-dir /app/migrations_pg --config-file /app/diesel_pg.toml'" networks: - open-router-network routing-config: image: python:3.10-slim - container_name: routing-config + profiles: + - mysql-ghcr + - dashboard-mysql-ghcr + - mysql-local + - dashboard-mysql-local depends_on: mysql: condition: service_healthy @@ -247,6 +444,90 @@ services: networks: - open-router-network + # Analytics Infrastructure + zookeeper: + image: confluentinc/cp-zookeeper:7.4.0 + platform: linux/amd64 + container_name: open-router-zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + networks: + - open-router-network + profiles: + - analytics + + kafka: + image: confluentinc/cp-kafka:7.4.0 + platform: linux/amd64 + container_name: open-router-kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + networks: + - open-router-network + profiles: + - analytics + healthcheck: + test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"] + interval: 10s + timeout: 10s + retries: 5 + + clickhouse: + image: clickhouse/clickhouse-server:latest + platform: linux/amd64 + container_name: open-router-clickhouse + ports: + - "8123:8123" + - "9000:9000" + environment: + CLICKHOUSE_DB: decision_engine_analytics + CLICKHOUSE_USER: analytics_user + CLICKHOUSE_PASSWORD: analytics_pass + volumes: + - clickhouse-data:/var/lib/clickhouse + - ./analytics/clickhouse/init:/docker-entrypoint-initdb.d + networks: + - open-router-network + depends_on: + - kafka + - zookeeper + profiles: + - analytics + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"] + interval: 10s + timeout: 5s + retries: 5 + + analytics-migrator: + image: clickhouse/clickhouse-server:latest + platform: linux/amd64 + container_name: analytics-migrator + depends_on: + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + volumes: + - ./analytics:/analytics + working_dir: /analytics + command: sh /analytics/run_migrations.sh + networks: + - open-router-network + profiles: + - analytics + networks: open-router-network: driver: bridge @@ -255,3 +536,4 @@ volumes: mysql-data: redis-data: postgres-data: + clickhouse-data: diff --git a/docs/analytics.mdx b/docs/analytics.mdx new file mode 100644 index 00000000..6036ca9e --- /dev/null +++ b/docs/analytics.mdx @@ -0,0 +1,88 @@ +--- +title: "Analytics" +description: "Routing metrics, score snapshots, and operational summaries" +--- + +## What It Shows + +Analytics is the operator-facing view for understanding how routing is behaving in practice. + +The page is split into these sections: + +- Overview +- Gateway Scoring +- Decisions +- Routing Stats +- Logs / Summaries + +For single-payment inspection, use the separate Payment Audit surface documented in [`payment-audit`](payment-audit). + +## Data Sources + +The first release uses the sources already present in the repository: + +- Redis for live SR and latency snapshots +- Prometheus for live counters and error-rate tiles +- Postgres for stored analytics events, snapshots, and summaries + +ClickHouse is intentionally not required for the first release. If the volume or retention profile grows beyond Postgres, it can be added later as a write sink. + +## Scope + +Analytics supports two views: + +- current merchant +- all merchants + +The current-merchant view uses the merchant selected in the top bar. The all-merchants view aggregates across the stored analytics events. + +Connector success-rate history is sourced from stored `score_snapshot` analytics events, not directly from Redis. Redis remains the live source for current score state. In the merchant-scoped view, the connector SR chart can be narrowed by: + +- `payment_method_type` +- `payment_method` +- connector + +In all-merchants mode, the chart intentionally stays connector-only so operators get a global SR trend without per-method slicing. + +## Generating Demo Traffic + +To populate Analytics and Decision Audit with real traffic, use the routing traffic generator. + +First apply the Postgres migration that creates `analytics_event`: + +```bash +just migrate-pg +``` + +Then generate traffic for `merchant_space`: + +```bash +bash scripts/generate-routing-traffic.sh +``` + +What the script does: + +- creates or reuses `merchant_space` +- enables success-rate routing for that merchant through the real `/rule/create` or `/rule/update` flow +- sends `100` real `POST /decide-gateway` requests with mixed payment-method combinations +- follows each decision with `POST /update-gateway-score` using a mix of successful and failed outcomes +- creates and activates a priority-based routing rule through `/routing/create` and `/routing/activate` +- sends rule-based `POST /routing/evaluate` traffic so the rule-evaluation analytics path is populated + +Useful overrides: + +```bash +MERCHANT_ID=merchant_space TOTAL_PAYMENTS=100 bash scripts/generate-routing-traffic.sh +RULE_EVALUATIONS=40 bash scripts/generate-routing-traffic.sh +CONNECTORS_CSV=stripe,adyen,checkout,paypal bash scripts/generate-routing-traffic.sh +``` + +After the script finishes, open `/dashboard/analytics` or `/dashboard/audit` and select `merchant_space` in the merchant picker. + +## Related Files + +- `website/src/components/pages/AnalyticsPage.tsx` +- `website/src/components/layout/Sidebar.tsx` +- `src/routes/analytics.rs` +- `src/analytics/` +- `src/metrics.rs` diff --git a/docs/api-reference.md b/docs/api-reference.md index 2dfeb000..8add4ea4 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,160 +1,42 @@ -# Dynamo API Reference +# API Reference -## Overview +The canonical API contract for the docs site is `docs/openapi.json`. -Dynamo provides both gRPC and HTTP APIs for its dynamic payment routing services. This reference documents the available endpoints, their parameters, and example usage. +Use this page to find the right endpoint family. Use the OpenAPI-backed endpoint pages for request and response schema details. ---- +## Endpoint Families -## Authentication and Headers +### Health -All API requests require appropriate authentication: +- [Health Check](api-reference/endpoint/healthCheck) -| Header | Required | Description | -|:-------|:--------:|:------------| -| `x-api-key` | ✅ | API key for authentication | -| `x-tenant-id` | ✅ | Tenant identifier (*required if multi-tenancy is enabled) | +### Merchant API ---- +- [Create Merchant](api-reference/endpoint/createMerchant) +- [Get Merchant](api-reference/endpoint/getMerchant) +- [Delete Merchant](api-reference/endpoint/deleteMerchant) -## Available Services +### Rule Based Routing -Dynamo offers three main routing services: +These are the `/routing/*` APIs for creating, activating, listing, and evaluating routing rules. The create endpoint supports `priority`, `single`, `volume_split`, and `advanced` Euclid programs. -### 1. **[Success Rate Calculator](api-reference/success-rate.md)** +- [Create Routing Rule](api-reference/endpoint/createRoutingRule) +- [Activate Routing Rule](api-reference/endpoint/activateRoutingRule) +- [List Routing Rules](api-reference/endpoint/listRoutingRules) +- [Get Active Routing Rule](api-reference/endpoint/getActiveRoutingRule) +- [Evaluate Routing Rule](api-reference/endpoint/evaluateRoutingRule) -Routes payments to processors with the highest historical success rates. +### Dynamic Routing APIs -| Endpoint | Description | -|:---------|:------------| -| **`FetchSuccessRate`** | Get success rates for processors | -| **`UpdateSuccessRateWindow`** | Update success/failure data | -| **`InvalidateWindows`** | Reset success rate data | -| **`FetchEntityAndGlobalSuccessRate`** | Get both entity and global success rates | +These are the dynamic routing APIs for gateway decisioning, gateway score updates, and `/rule/*` configuration. -### 2. **[Elimination Analyser](api-reference/elimination.md)** +- [POST /decide-gateway](api-reference/endpoint/decideGateway) +- [POST /update-gateway-score](api-reference/endpoint/updateGatewayScore) +- [POST /rule/create](api-reference/endpoint/createRuleConfig) +- [POST /rule/get](api-reference/endpoint/getRuleConfig) +- [POST /rule/update](api-reference/endpoint/updateRuleConfig) +- [POST /rule/delete](api-reference/endpoint/deleteRuleConfig) -Prevents routing to processors that meet failure criteria. - -| Endpoint | Description | -|:---------|:------------| -| **`GetEliminationStatus`** | Check if processors should be eliminated | -| **`UpdateEliminationBucket`** | Update failure data | -| **`InvalidateBucket`** | Reset elimination data | - -### 3. **[Contract Score Calculator](api-reference/contract.md)** - -Routes based on contractual obligations and targets. - -| Endpoint | Description | -|:---------|:------------| -| **`FetchContractScore`** | Get contract-based scores | -| **`UpdateContract`** | Update contract fulfillment data | -| **`InvalidateContract`** | Reset contract data | - -### 4. **Health** - -System health checks. - -| Endpoint | Description | -|:---------|:------------| -| **`Check`** | Verify system health status | - -**gRPC Method**: `grpc.health.v1.Health/Check` - -**HTTP Endpoint**: `POST /grpc.health.v1.Health/Check` - -**Request**: - -```json -{ - "service": "dynamo" -} -``` - -**Response**: - -```json -{ - "status": 1 // 1 = SERVING -} -``` - ---- - -## Protocol Support - -All services are available via both: - - - - - - - - - - -
gRPCEfficient binary protocol for direct integration
HTTP/JSONRESTful interface for broader compatibility
- ---- - -## Quick Start Examples - -### 🔹 gRPC Example (using grpcurl) - -```bash -grpcurl -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B"], - "config": { - "min_aggregates_size": 10, - "default_success_rate": 0.5 - } -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ --plaintext localhost:9000 success_rate.SuccessRateCalculator/FetchSuccessRate -``` - -### 🔹 HTTP Example (using curl) - -```bash -curl -X POST \ - -H "Content-Type: application/json" \ - -H "x-api-key: YOUR_API_KEY" \ - -H "x-tenant-id: tenant_001" \ - -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B"], - "config": { - "min_aggregates_size": 10, - "default_success_rate": 0.5 - } - }' \ - http://localhost:8080/success_rate.SuccessRateCalculator/FetchSuccessRate -``` - ---- - -## Error Handling - -All services use standard gRPC status codes: - -| Status Code | Description | Common Causes | -|:------------|:------------|:--------------| -| OK (0) | ✅ Operation completed successfully | Normal successful operation | -| INVALID_ARGUMENT (3) | ⚠️ Validation failed | Missing required fields, invalid parameters | -| NOT_FOUND (5) | 🔍 Required resource not found | Entity or configuration not found | -| UNAUTHENTICATED (16) | 🔒 Authentication failed | Invalid API key | -| PERMISSION_DENIED (7) | 🚫 Unauthorized access | Insufficient permissions | -| INTERNAL (13) | ❌ Internal server error | Unexpected server-side errors | - ---- - -## Related Documentation - -📥 [**Installation**](installation.md) ⚙️ [**Configuration**](configuration.md) 🔄 [**Dual Protocol**](dual-protocol-layer.md) +## Curl Examples +For local smoke-test examples, use [API Examples](/api-reference1). diff --git a/docs/api-reference/contract.md b/docs/api-reference/contract.md deleted file mode 100644 index fa056339..00000000 --- a/docs/api-reference/contract.md +++ /dev/null @@ -1,393 +0,0 @@ -# Contract Routing API Contracts - -## Overview - -The Contract Score Calculator service provides endpoints for routing payments based on contractual obligations and targets with payment processors. This service is a key component of the Dynamo routing system, helping ensure that payment volumes are distributed according to business agreements while maintaining efficient processing. - -## Service Definition - -```protobuf -service ContractScoreCalculator { - rpc FetchContractScore (CalContractScoreRequest) returns (CalContractScoreResponse); - rpc UpdateContract (UpdateContractRequest) returns (UpdateContractResponse); - rpc InvalidateContract (InvalidateContractRequest) returns (InvalidateContractResponse); -} -``` - -## Authentication - -All endpoints require authentication. Authentication is handled via metadata in the gRPC request. The service supports multi-tenancy, which means contract data is isolated per tenant. - -- Authentication requires an `x-api-key` header with a valid API key -- The `x-tenant-id` header is required to specify the tenant context -- Tenant ID is extracted from request metadata -- All operations verify tenant permissions before processing -- Multi-tenancy can be enabled/disabled via configuration - -## Endpoints - -### 1. FetchContractScore - -Calculates and returns contract-based routing scores for the provided processors. - -#### Request: `CalContractScoreRequest` - -```protobuf -message CalContractScoreRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters for contract calculation - repeated string labels = 3; // Labels (processors) to calculate scores for - CalContractScoreConfig config = 4; // Configuration for calculation -} - -message CalContractScoreConfig { - repeated double constants = 1; // Constants used in score calculation algorithm - TimeScale time_scale = 2; // Time scale for contract calculation -} - -message TimeScale { - enum Scale { - Day = 0; // Daily time scale - Month = 1; // Monthly time scale - } - Scale time_scale = 1; // Selected time scale -} -``` - -#### Response: `CalContractScoreResponse` - -```protobuf -message CalContractScoreResponse { - repeated ScoreData labels_with_score = 1; // Contract scores for each label -} - -message ScoreData { - double score = 1; // Contract score (higher values indicate higher priority) - string label = 2; // Label (processor) identifier - uint64 current_count = 3; // Current transaction count for this processor -} -``` - -#### Behavior - -- Calculates contract scores based on processor targets and current usage -- Scores are influenced by how far each processor is from meeting its target -- Higher scores indicate processors that need more transactions to meet targets -- Additional parameters can influence the scoring algorithm -- Returns current transaction counts alongside scores for transparency - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID, parameters, and processor labels -4. Convert the configuration to internal contract score settings -5. Calculate contract scores for each processor based on current transaction counts and targets -6. Return detailed score data for each processor - ---- - -### 2. UpdateContract - -Updates the contract information for specific processors, affecting future contract-based routing decisions. - -#### Request: `UpdateContractRequest` - -```protobuf -message UpdateContractRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters - repeated LabelInformation labels_information = 3; // Contract information for processors -} - -message LabelInformation { - string label = 1; // Processor identifier - uint64 target_count = 2; // Target transaction count in contract - uint64 target_time = 3; // Time period for the target (in seconds) - uint64 current_count = 4; // Current transaction count -} -``` - -#### Response: `UpdateContractResponse` - -```protobuf -message UpdateContractResponse { - enum UpdationStatus { - CONTRACT_UPDATION_SUCCEEDED = 0; - CONTRACT_UPDATION_FAILED = 1; - } - UpdationStatus status = 1; // Status of the update operation -} -``` - -#### Behavior - -- Updates contract information for the specified processors -- Stores target counts, time periods, and current transaction counts -- Enables the system to calculate routing scores based on contract fulfillment -- Contract data is tenant-specific for multi-tenant deployments -- Returns the status of the update operation - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID, parameters, and contract information for processors -4. Convert the provided data to internal contract map format -5. Update contract information for all specified processors -6. Return success/failure status of the operation - ---- - -### 3. InvalidateContract - -Invalidates all contract data for a specific entity, effectively resetting its contract-based routing state. - -#### Request: `InvalidateContractRequest` - -```protobuf -message InvalidateContractRequest { - string id = 1; // Entity identifier to invalidate -} -``` - -#### Response: `InvalidateContractResponse` - -```protobuf -message InvalidateContractResponse { - enum InvalidationStatus { - CONTRACT_INVALIDATION_SUCCEEDED = 0; - CONTRACT_INVALIDATION_FAILED = 1; - } - InvalidationStatus status = 1; // Status of the invalidation operation -} -``` - -#### Behavior - -- Removes all contract data for the specified entity -- This effectively resets the contract-based routing for this entity -- Useful when contracts are renewed or significantly changed -- Does not affect contract data for other entities -- Returns the status of the invalidation operation - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID to invalidate -4. Remove all contract data for the entity within the tenant context -5. Return success/failure status of the operation - -## Error Handling - -All endpoints return standard gRPC status codes: - -| Status Code | Description | When Used | -|-------------|-------------|-----------| -| `OK (0)` | Operation completed successfully | Normal successful operation | -| `NOT_FOUND (5)` | Required resource not found | Missing configuration, entity not found | -| `INVALID_ARGUMENT (3)` | Validation failed | Invalid or missing required parameters | -| `UNAUTHENTICATED (16)` | Authentication failed | Invalid or missing authentication credentials | -| `PERMISSION_DENIED (7)` | Authenticated user lacks permission | User not authorized for the operation | -| `INTERNAL (13)` | Internal server error | Unexpected errors during processing | - -## Multi-tenancy Support - -The service supports multi-tenancy with the following behavior: - -- All contract data is isolated per tenant with no cross-tenant data access -- Tenant ID is extracted from request metadata -- Each tenant has its own contract settings and transaction counts -- Multi-tenancy can be disabled in configuration via `is_multi_tenancy_enabled` flag -- When disabled, a default tenant ID is used - -## Performance Considerations - -- **Efficient Scoring Algorithm**: The contract score calculation is optimized for performance -- **Persistent Storage**: Contract data is stored efficiently for quick access -- **Parallel Processing**: The system can handle multiple contract score calculations simultaneously -- **Request Validation**: Validates requests early to fail fast and save processing resources -- **Rust Implementation**: Implementation is in Rust for maximum efficiency and safety -- **Error Handling**: Comprehensive error handling with detailed context - -## Example Usage - -### Fetch Contract Score Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_type\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], - "config": { - "constants": [0.75, 1.25, 0.5], - "time_scale": { - "time_scale": "Month" - } - } -} - -// Response -{ - "labels_with_score": [ - { - "score": 0.85, - "label": "processor_B", - "current_count": 7520 - }, - { - "score": 0.65, - "label": "processor_A", - "current_count": 12450 - }, - { - "score": 0.42, - "label": "processor_C", - "current_count": 9870 - } - ] -} -``` - -### Update Contract Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_type\":\"card\"}", - "labels_information": [ - { - "label": "processor_A", - "target_count": 15000, - "target_time": 2592000, - "current_count": 12450 - }, - { - "label": "processor_B", - "target_count": 10000, - "target_time": 2592000, - "current_count": 7520 - }, - { - "label": "processor_C", - "target_count": 12000, - "target_time": 2592000, - "current_count": 9870 - } - ] -} - -// Response -{ - "status": "CONTRACT_UPDATION_SUCCEEDED" -} -``` - -### Invalidate Contract Example - -```json -// Request -{ - "id": "merchant_123" -} - -// Response -{ - "status": "CONTRACT_INVALIDATION_SUCCEEDED" -} -``` - -## Integration Notes - -- All requests should include appropriate authentication metadata -- JSON parameters in the `params` field should be properly escaped -- Contract targets should be set realistically based on historical processing volumes -- Target times should align with billing cycles (typically monthly) -- Contract-based routing works best when combined with success rate and elimination routing -- Maintain proper error handling for all API calls -- Consider resetting contracts at the beginning of each billing cycle - -## gRPCurl Examples - -The following examples demonstrate how to call the Contract Score Calculator API endpoints using gRPCurl. These examples assume the service is running on localhost at port 9000. - -### FetchContractScore - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_type\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], - "config": { - "constants": [0.75, 1.25, 0.5], - "time_scale": { - "time_scale": "Month" - } - } -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 contract_routing.ContractScoreCalculator/FetchContractScore -``` - -### UpdateContract - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_type\":\"card\"}", - "labels_information": [ - { - "label": "processor_A", - "target_count": 15000, - "target_time": 2592000, - "current_count": 12450 - }, - { - "label": "processor_B", - "target_count": 10000, - "target_time": 2592000, - "current_count": 7520 - }, - { - "label": "processor_C", - "target_count": 12000, - "target_time": 2592000, - "current_count": 9870 - } - ] -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 contract_routing.ContractScoreCalculator/UpdateContract -``` - -### InvalidateContract - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123" -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 contract_routing.ContractScoreCalculator/InvalidateContract -``` - -### Listing Available Methods - -To discover available methods on the service: - -```bash -grpcurl -plaintext localhost:9000 list contract_routing.ContractScoreCalculator -``` - -### Viewing Method Details - -To see detailed information about a specific method: - -```bash -grpcurl -plaintext localhost:9000 describe contract_routing.ContractScoreCalculator.FetchContractScore -``` diff --git a/docs/api-reference/elimination.md b/docs/api-reference/elimination.md deleted file mode 100644 index 0a89a8a8..00000000 --- a/docs/api-reference/elimination.md +++ /dev/null @@ -1,404 +0,0 @@ -# Elimination Routing API Contracts - -## Overview - -The Elimination Analyser service provides endpoints for identifying and filtering out underperforming payment processors. This service is a critical component of the Dynamo routing system, helping improve payment success rates by preventing routing to processors with consistently high failure rates. - -## Service Definition - -```protobuf -service EliminationAnalyser { - rpc GetEliminationStatus (EliminationRequest) returns (EliminationResponse); - rpc UpdateEliminationBucket (UpdateEliminationBucketRequest) returns (UpdateEliminationBucketResponse); - rpc InvalidateBucket (InvalidateBucketRequest) returns (InvalidateBucketResponse); -} -``` - -## Authentication - -All endpoints require authentication. Authentication is handled via metadata in the gRPC request. The service supports multi-tenancy, which means elimination data is isolated per tenant. - -- Authentication requires an `x-api-key` header with a valid API key -- The `x-tenant-id` header is required to specify the tenant context -- Tenant ID is extracted from request metadata -- All operations verify tenant permissions before processing -- Multi-tenancy can be enabled/disabled via configuration - -## Endpoints - -### 1. GetEliminationStatus - -Determines which processors should be eliminated from routing consideration based on historical performance data. - -#### Request: `EliminationRequest` - -```protobuf -message EliminationRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters for elimination analysis - repeated string labels = 3; // Labels (processors) to check for elimination - EliminationBucketConfig config = 4; // Configuration for elimination buckets -} - -message EliminationBucketConfig { - uint64 bucket_size = 1; // Maximum failures allowed before elimination - uint64 bucket_leak_interval_in_secs = 2; // Time interval after which failures are "forgotten" -} -``` - -#### Response: `EliminationResponse` - -```protobuf -message EliminationResponse { - repeated LabelWithStatus labels_with_status = 1; // Elimination status for each label -} - -message LabelWithStatus { - string label = 1; // Label identifier - EliminationInformation elimination_information = 2; // Elimination details -} - -message EliminationInformation { - BucketInformation entity = 1; // Entity-specific elimination information - BucketInformation global = 2; // Global elimination information -} - -message BucketInformation { - bool is_eliminated = 1; // Whether the processor should be eliminated - repeated string bucket_name = 2; // Bucket identifiers that triggered elimination -} -``` - -#### Behavior - -- Evaluates each processor (label) against failure thresholds at both entity and global levels -- Returns elimination status for each processor along with the specific buckets triggering elimination -- A processor can be eliminated at entity level, global level, or both -- Bucket information provides context about which failure conditions were met - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID, parameters, and processor labels -4. Convert the configuration to internal elimination bucket settings -5. Perform elimination analysis for each processor -6. Return detailed elimination status for each processor - ---- - -### 2. UpdateEliminationBucket - -Updates the failure records for specific processors, affecting future elimination decisions. - -#### Request: `UpdateEliminationBucketRequest` - -```protobuf -message UpdateEliminationBucketRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters - repeated LabelWithBucketName labels_with_bucket_name = 3; // Processors with bucket information - EliminationBucketConfig config = 4; // Configuration for elimination buckets -} - -message LabelWithBucketName { - string label = 1; // Processor identifier - string bucket_name = 2; // Bucket to update (failure type) -} - -message EliminationBucketConfig { - uint64 bucket_size = 1; // Maximum failures allowed before elimination - uint64 bucket_leak_interval_in_secs = 2; // Time interval after which failures are "forgotten" -} -``` - -#### Response: `UpdateEliminationBucketResponse` - -```protobuf -message UpdateEliminationBucketResponse { - enum UpdationStatus { - BUCKET_UPDATION_SUCCEEDED = 0; - BUCKET_UPDATION_FAILED = 1; - } - UpdationStatus status = 1; // Status of the update operation -} -``` - -#### Behavior - -- Updates failure records for the specified processors and buckets -- Each bucket represents a specific failure type or condition -- Uses a "leaky bucket" algorithm where failures are counted until thresholds are met -- After the specified leak interval, failures are gradually removed from consideration -- Failure to update any processor's bucket will result in a failed status - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID, parameters, and processor-bucket mapping -4. Convert the configuration to internal elimination bucket settings -5. Update the elimination buckets for each processor -6. Return success/failure status of the operation - ---- - -### 3. InvalidateBucket - -Invalidates all elimination bucket data for a specific entity, effectively resetting its processor elimination history. - -#### Request: `InvalidateBucketRequest` - -```protobuf -message InvalidateBucketRequest { - string id = 1; // Entity identifier to invalidate -} -``` - -#### Response: `InvalidateBucketResponse` - -```protobuf -message InvalidateBucketResponse { - enum InvalidationStatus { - BUCKET_INVALIDATION_SUCCEEDED = 0; - BUCKET_INVALIDATION_FAILED = 1; - } - InvalidationStatus status = 1; // Status of the invalidation operation -} -``` - -#### Behavior - -- Removes all elimination bucket data for the specified entity -- This effectively resets the elimination status for all processors associated with the entity -- Useful for handling significant processor changes or clearing problematic data -- Does not affect global elimination data - -#### Process Flow - -1. Extract tenant ID from request metadata -2. Validate the request parameters -3. Extract entity ID to invalidate -4. Remove all elimination bucket data for the entity -5. Return success/failure status of the operation - -## Error Handling - -All endpoints return standard gRPC status codes: - -| Status Code | Description | When Used | -|-------------|-------------|-----------| -| `OK (0)` | Operation completed successfully | Normal successful operation | -| `NOT_FOUND (5)` | Required resource not found | Missing configuration, entity not found | -| `INVALID_ARGUMENT (3)` | Validation failed | Invalid or missing required parameters | -| `UNAUTHENTICATED (16)` | Authentication failed | Invalid or missing authentication credentials | -| `PERMISSION_DENIED (7)` | Authenticated user lacks permission | User not authorized for the operation | -| `INTERNAL (13)` | Internal server error | Unexpected errors during processing | - -## Multi-tenancy Support - -The service supports multi-tenancy with the following behavior: - -- All elimination data is isolated per tenant with no cross-tenant data access -- Tenant ID is extracted from request metadata -- Each tenant has its own elimination buckets and configurations -- Multi-tenancy can be disabled in configuration via `is_multi_tenancy_enabled` flag -- When disabled, a default tenant ID is used - -## Performance Considerations - -- **Leaky Bucket Algorithm**: Efficiently tracks failures without unlimited growth of data -- **Time-based Processing**: Automatically ages out old failures based on configuration -- **Optimized Storage**: Buckets are stored efficiently to minimize memory usage -- **Concurrent Processing**: Handles multiple elimination checks in parallel -- **Rust Implementation**: Implementation is in Rust for maximum efficiency and safety -- **Request Validation**: Validates requests early to fail fast and save processing resources - -## Example Usage - -### Get Elimination Status Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], - "config": { - "bucket_size": 5, - "bucket_leak_interval_in_secs": 3600 - } -} - -// Response -{ - "labels_with_status": [ - { - "label": "processor_A", - "elimination_information": { - "entity": { - "is_eliminated": false, - "bucket_name": [] - }, - "global": { - "is_eliminated": false, - "bucket_name": [] - } - } - }, - { - "label": "processor_B", - "elimination_information": { - "entity": { - "is_eliminated": true, - "bucket_name": ["authentication_failure"] - }, - "global": { - "is_eliminated": false, - "bucket_name": [] - } - } - }, - { - "label": "processor_C", - "elimination_information": { - "entity": { - "is_eliminated": true, - "bucket_name": ["network_error"] - }, - "global": { - "is_eliminated": true, - "bucket_name": ["network_error", "timeout"] - } - } - } - ] -} -``` - -### Update Elimination Bucket Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels_with_bucket_name": [ - { - "label": "processor_A", - "bucket_name": "authentication_failure" - }, - { - "label": "processor_B", - "bucket_name": "network_error" - } - ], - "config": { - "bucket_size": 5, - "bucket_leak_interval_in_secs": 3600 - } -} - -// Response -{ - "status": "BUCKET_UPDATION_SUCCEEDED" -} -``` - -### Invalidate Bucket Example - -```json -// Request -{ - "id": "merchant_123" -} - -// Response -{ - "status": "BUCKET_INVALIDATION_SUCCEEDED" -} -``` - -## Integration Notes - -- All requests should include appropriate authentication metadata -- JSON parameters in the `params` field should be properly escaped -- Bucket names should represent meaningful failure categories for better analytics -- Consider the bucket_size and leak interval carefully based on traffic patterns -- Very small bucket sizes may lead to premature elimination -- Very large leak intervals may keep processors eliminated for too long -- Maintain proper error handling for all API calls - -## gRPCurl Examples - -The following examples demonstrate how to call the Elimination Analyser API endpoints using gRPCurl. These examples assume the service is running on localhost at port 9000. - -### GetEliminationStatus - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], - "config": { - "bucket_size": 5, - "bucket_leak_interval_in_secs": 3600 - } -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 elimination.EliminationAnalyser/GetEliminationStatus -``` - -### UpdateEliminationBucket - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels_with_bucket_name": [ - { - "label": "processor_A", - "bucket_name": "authentication_failure" - }, - { - "label": "processor_B", - "bucket_name": "network_error" - } - ], - "config": { - "bucket_size": 5, - "bucket_leak_interval_in_secs": 3600 - } -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 elimination.EliminationAnalyser/UpdateEliminationBucket -``` - -### InvalidateBucket - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123" -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 elimination.EliminationAnalyser/InvalidateBucket -``` - -### Listing Available Methods - -To discover available methods on the service: - -```bash -grpcurl -plaintext localhost:9000 list elimination.EliminationAnalyser -``` - -### Viewing Method Details - -To see detailed information about a specific method: - -```bash -grpcurl -plaintext localhost:9000 describe elimination.EliminationAnalyser.GetEliminationStatus -``` diff --git a/docs/api-reference/endpoint/activateRoutingRule.mdx b/docs/api-reference/endpoint/activateRoutingRule.mdx new file mode 100644 index 00000000..dae80c51 --- /dev/null +++ b/docs/api-reference/endpoint/activateRoutingRule.mdx @@ -0,0 +1,7 @@ +--- +title: "Activate Routing Rule" +description: "Activate an existing routing algorithm for a given created_by scope." +openapi: "POST /routing/activate" +--- + +Use this endpoint to make a routing algorithm active for the supplied `created_by` value. diff --git a/docs/api-reference/endpoint/createMerchant.mdx b/docs/api-reference/endpoint/createMerchant.mdx new file mode 100644 index 00000000..5a67650a --- /dev/null +++ b/docs/api-reference/endpoint/createMerchant.mdx @@ -0,0 +1,7 @@ +--- +title: "Create Merchant" +description: "Create a merchant account used by later routing and scoring requests." +openapi: "POST /merchant-account/create" +--- + +Use this endpoint before calling routing or score-feedback APIs for a new merchant. diff --git a/docs/api-reference/endpoint/createRoutingRule.mdx b/docs/api-reference/endpoint/createRoutingRule.mdx new file mode 100644 index 00000000..ae6c07f9 --- /dev/null +++ b/docs/api-reference/endpoint/createRoutingRule.mdx @@ -0,0 +1,31 @@ +--- +title: "Create Routing Rule" +description: "Create a routing algorithm for a given created_by scope." +openapi: "POST /routing/create" +--- + +Use this endpoint to store a priority, single, volume-split, or advanced routing algorithm. + +## Supported algorithm shapes + +- `priority`: ordered connector fallback list. +- `single`: pin the route to exactly one connector. +- `volume_split`: distribute traffic by weighted percentages. +- `advanced`: Euclid program for conditional routing. + +For `advanced` rules: + +- each item in `statements` is an `OR` branch +- each item inside a single `condition` array is an `AND` block +- `nested` lets you build deeper hierarchical checks under a matched branch +- `default_selection` is the fallback when no rule matches + +The OpenAPI examples on this page include: + +- `priority` +- `single` +- `volume_split` +- `advanced` with top-level `OR` branches +- `advanced` with nested conditions and a volume-split output + +If you want curl-ready payloads for each of those variants, use [API Examples](/api-reference1). diff --git a/docs/api-reference/endpoint/createRuleConfig.mdx b/docs/api-reference/endpoint/createRuleConfig.mdx new file mode 100644 index 00000000..20ec9bc7 --- /dev/null +++ b/docs/api-reference/endpoint/createRuleConfig.mdx @@ -0,0 +1,7 @@ +--- +title: "Create Rule Config" +description: "Create service-level rule configuration such as success-rate settings." +openapi: "POST /rule/create" +--- + +Use this endpoint to create routing configuration tied to a merchant. diff --git a/docs/api-reference/endpoint/decideGateway.mdx b/docs/api-reference/endpoint/decideGateway.mdx new file mode 100644 index 00000000..bd264449 --- /dev/null +++ b/docs/api-reference/endpoint/decideGateway.mdx @@ -0,0 +1,7 @@ +--- +title: "Decide Gateway" +description: "Evaluate the current routing logic for a payment context and eligible gateway list." +openapi: "POST /decide-gateway" +--- + +This is the main routing decision endpoint exposed by the service. diff --git a/docs/api-reference/endpoint/deleteMerchant.mdx b/docs/api-reference/endpoint/deleteMerchant.mdx new file mode 100644 index 00000000..4408d8e3 --- /dev/null +++ b/docs/api-reference/endpoint/deleteMerchant.mdx @@ -0,0 +1,7 @@ +--- +title: "Delete Merchant" +description: "Delete a merchant account by ID." +openapi: "DELETE /merchant-account/{merchantId}" +--- + +Use this endpoint to remove a merchant record and its associated service-level configuration. diff --git a/docs/api-reference/endpoint/deleteRuleConfig.mdx b/docs/api-reference/endpoint/deleteRuleConfig.mdx new file mode 100644 index 00000000..0f2b8b1e --- /dev/null +++ b/docs/api-reference/endpoint/deleteRuleConfig.mdx @@ -0,0 +1,7 @@ +--- +title: "Delete Rule Config" +description: "Delete service-level routing configuration for a merchant." +openapi: "POST /rule/delete" +--- + +Use this endpoint when a stored rule configuration should no longer apply. diff --git a/docs/api-reference/endpoint/evaluateRoutingRule.mdx b/docs/api-reference/endpoint/evaluateRoutingRule.mdx new file mode 100644 index 00000000..01254a1c --- /dev/null +++ b/docs/api-reference/endpoint/evaluateRoutingRule.mdx @@ -0,0 +1,7 @@ +--- +title: "Evaluate Routing Rule" +description: "Evaluate the active routing rule without calling the gateway-selection API." +openapi: "POST /routing/evaluate" +--- + +Use this endpoint to inspect routing-rule output for a payment context. diff --git a/docs/api-reference/endpoint/getActiveRoutingRule.mdx b/docs/api-reference/endpoint/getActiveRoutingRule.mdx new file mode 100644 index 00000000..8453d0e8 --- /dev/null +++ b/docs/api-reference/endpoint/getActiveRoutingRule.mdx @@ -0,0 +1,7 @@ +--- +title: "Get Active Routing Rule" +description: "Fetch the currently active routing rule for a created_by scope." +openapi: "POST /routing/list/active/{created_by}" +--- + +Use this endpoint to inspect which routing algorithm is currently active. diff --git a/docs/api-reference/endpoint/getMerchant.mdx b/docs/api-reference/endpoint/getMerchant.mdx new file mode 100644 index 00000000..ec3e6976 --- /dev/null +++ b/docs/api-reference/endpoint/getMerchant.mdx @@ -0,0 +1,7 @@ +--- +title: "Get Merchant" +description: "Retrieve a merchant account by ID." +openapi: "GET /merchant-account/{merchantId}" +--- + +Use this endpoint to inspect the stored merchant account record. diff --git a/docs/api-reference/endpoint/getRuleConfig.mdx b/docs/api-reference/endpoint/getRuleConfig.mdx new file mode 100644 index 00000000..a00375a2 --- /dev/null +++ b/docs/api-reference/endpoint/getRuleConfig.mdx @@ -0,0 +1,7 @@ +--- +title: "Get Rule Config" +description: "Fetch service-level routing configuration for a merchant." +openapi: "POST /rule/get" +--- + +Use this endpoint to inspect the currently stored rule configuration for a merchant. diff --git a/docs/api-reference/endpoint/healthCheck.mdx b/docs/api-reference/endpoint/healthCheck.mdx new file mode 100644 index 00000000..c58182b5 --- /dev/null +++ b/docs/api-reference/endpoint/healthCheck.mdx @@ -0,0 +1,7 @@ +--- +title: "Health Check" +description: "Verify that the main API server is responding." +openapi: "GET /health" +--- + +Use this endpoint for the simplest local or deployment liveness check. diff --git a/docs/api-reference/endpoint/listRoutingRules.mdx b/docs/api-reference/endpoint/listRoutingRules.mdx new file mode 100644 index 00000000..c71d203e --- /dev/null +++ b/docs/api-reference/endpoint/listRoutingRules.mdx @@ -0,0 +1,7 @@ +--- +title: "List Routing Rules" +description: "List stored routing algorithms for a created_by scope." +openapi: "POST /routing/list/{created_by}" +--- + +Use this endpoint to inspect the routing algorithms stored for a merchant or platform scope. diff --git a/docs/api-reference/endpoint/updateGatewayScore.mdx b/docs/api-reference/endpoint/updateGatewayScore.mdx new file mode 100644 index 00000000..ff080b2e --- /dev/null +++ b/docs/api-reference/endpoint/updateGatewayScore.mdx @@ -0,0 +1,7 @@ +--- +title: "Update Gateway Score" +description: "Record a transaction outcome for later routing decisions." +openapi: "POST /update-gateway-score" +--- + +Use this endpoint after a payment outcome is known so score data can be updated. diff --git a/docs/api-reference/endpoint/updateRuleConfig.mdx b/docs/api-reference/endpoint/updateRuleConfig.mdx new file mode 100644 index 00000000..8c194563 --- /dev/null +++ b/docs/api-reference/endpoint/updateRuleConfig.mdx @@ -0,0 +1,7 @@ +--- +title: "Update Rule Config" +description: "Update stored service-level routing configuration for a merchant." +openapi: "POST /rule/update" +--- + +Use this endpoint to modify an existing rule configuration without recreating it. diff --git a/docs/api-reference/success-rate.md b/docs/api-reference/success-rate.md deleted file mode 100644 index baa073f2..00000000 --- a/docs/api-reference/success-rate.md +++ /dev/null @@ -1,485 +0,0 @@ -# Success Rate API Contracts - -## Overview - -The Success Rate Calculator service provides endpoints for calculating, updating, and managing success rates for payment routing decisions. This service is a core component of the Dynamo routing system, helping optimize payment flows by directing transactions to processors with the highest historical success rates. - -## Service Definition - -```protobuf -service SuccessRateCalculator { - rpc FetchSuccessRate (CalSuccessRateRequest) returns (CalSuccessRateResponse); - rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse); - rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse); - rpc FetchEntityAndGlobalSuccessRate (CalGlobalSuccessRateRequest) returns (CalGlobalSuccessRateResponse); -} -``` - -## Authentication - -All endpoints require authentication. Authentication is handled via metadata in the gRPC request. The service supports multi-tenancy, which means data and routing decisions are isolated per tenant. - -- Authentication is performed using the `Authenticate` trait -- Authentication requires an `x-api-key` header with a valid API key -- The `x-tenant-id` header is required to specify the tenant context -- Tenant and merchant IDs are extracted from authentication info -- All operations verify authentication before processing - -## Endpoints - -### 1. FetchSuccessRate - -Calculates and returns success rates for a specific entity and its associated labels. - -#### Request: `CalSuccessRateRequest` - -```protobuf -message CalSuccessRateRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters for success rate calculation - repeated string labels = 3; // Labels to calculate success rates for - CalSuccessRateConfig config = 4; // Configuration for calculation -} - -message CalSuccessRateConfig { - uint32 min_aggregates_size = 1; // Minimum number of data points required - double default_success_rate = 2; // Default rate to use if insufficient data - optional SuccessRateSpecificityLevel specificity_level = 3; // ENTITY or GLOBAL level -} - -enum SuccessRateSpecificityLevel { - ENTITY = 0; - GLOBAL = 1; -} -``` - -#### Response: `CalSuccessRateResponse` - -```protobuf -message CalSuccessRateResponse { - repeated LabelWithScore labels_with_score = 1; // Success rates for each label -} - -message LabelWithScore { - double score = 1; // Success rate score (0.0 to 1.0) - string label = 2; // Label identifier -} -``` - -#### Behavior - -- Returns calculated success rates for requested labels based on historical data -- If insufficient data exists for a label, uses the default success rate -- Applied specificity level determines if entity-specific or global data is used -- Success rates are returned as scores between 0.0 and 1.0 (or as percentages) -- Labels are returned in descending order of scores - -#### Process Flow - -1. Authenticate the request and extract tenant/merchant information -2. Fetch configuration settings for the tenant -3. Extract tenant ID from request (for multi-tenancy support) -4. Validate the request parameters -5. Extract entity ID, parameters, and labels -6. Calculate success rates based on historical data -7. Return the scores with associated labels - -#### Metrics - -- `SUCCESS_BASED_ROUTING_REQUEST`: Counts total requests -- `SUCCESS_BASED_ROUTING_DECISION_REQUEST_TIME`: Measures processing time -- `SUCCESS_BASED_ROUTING_SUCCESSFUL_RESPONSE_COUNT`: Counts successful responses - ---- - -### 2. UpdateSuccessRateWindow - -Updates the success/failure status for a set of labels, affecting future routing decisions. - -#### Request: `UpdateSuccessRateWindowRequest` - -```protobuf -message UpdateSuccessRateWindowRequest { - string id = 1; // Entity identifier - string params = 2; // Additional parameters - repeated LabelWithStatus labels_with_status = 3; // Entity-specific labels with success/failure status - UpdateSuccessRateWindowConfig config = 4; // Update configuration - repeated LabelWithStatus global_labels_with_status = 5; // Global labels with success/failure status -} - -message LabelWithStatus { - string label = 1; // Label identifier - bool status = 2; // Success (true) or failure (false) status -} - -message UpdateSuccessRateWindowConfig { - uint32 max_aggregates_size = 1; // Maximum size of aggregation window - CurrentBlockThreshold current_block_threshold = 2; // Threshold configuration -} - -message CurrentBlockThreshold { - optional uint64 duration_in_mins = 1; // Duration-based threshold in minutes - uint64 max_total_count = 2; // Count-based threshold -} -``` - -#### Response: `UpdateSuccessRateWindowResponse` - -```protobuf -message UpdateSuccessRateWindowResponse { - enum UpdationStatus { - WINDOW_UPDATION_SUCCEEDED = 0; - WINDOW_UPDATION_FAILED = 1; - } - UpdationStatus status = 1; // Status of the update operation -} -``` - -#### Behavior - -- Updates both entity-specific and global success rate windows -- Applies the provided status (success/failure) to each label -- Maintains a sliding window of transaction results based on configuration -- Will return failure status if either entity or global updates fail -- Uses configured thresholds to determine when to rotate windows - -#### Process Flow - -1. Authenticate the request and extract tenant/merchant information -2. Fetch configuration settings for the tenant -3. Extract tenant ID from request (for multi-tenancy support) -4. Validate the request parameters -5. Extract entity ID, parameters, and label status information -6. Update the entity-specific success rate window -7. Set the specificity level to Global -8. Update the global success rate window -9. Return success/failure status of the operation - -#### Metrics - -- `SUCCESS_BASED_ROUTING_UPDATE_WINDOW_DECISION_REQUEST_TIME`: Measures processing time -- `SUCCESS_BASED_ROUTING_UPDATE_WINDOW_COUNT`: Counts window update requests - ---- - -### 3. InvalidateWindows - -Invalidates success rate windows for a specific entity, effectively resetting its success rate history. - -#### Request: `InvalidateWindowsRequest` - -```protobuf -message InvalidateWindowsRequest { - string id = 1; // Entity identifier to invalidate -} -``` - -#### Response: `InvalidateWindowsResponse` - -```protobuf -message InvalidateWindowsResponse { - enum InvalidationStatus { - WINDOW_INVALIDATION_SUCCEEDED = 0; - WINDOW_INVALIDATION_FAILED = 1; - } - InvalidationStatus status = 1; // Status of the invalidation operation -} -``` - -#### Behavior - -- Removes all success rate window data for the specified entity -- This effectively resets the success rate calculation for this entity -- Useful for handling significant processor changes or clearing problematic data -- Does not affect global success rate data - -#### Process Flow - -1. Authenticate the request -2. Extract tenant ID from request (for multi-tenancy support) -3. Validate the request parameters -4. Extract entity ID to invalidate -5. Remove all success rate window data for the entity -6. Return success/failure status of the operation - ---- - -### 4. FetchEntityAndGlobalSuccessRate - -Fetches both entity-specific and global success rates in a single request. - -#### Request: `CalGlobalSuccessRateRequest` - -```protobuf -message CalGlobalSuccessRateRequest { - string entity_id = 1; // Entity identifier - string entity_params = 2; // Entity-specific parameters - repeated string entity_labels = 3; // Labels for entity-specific calculation - repeated string global_labels = 4; // Labels for global calculation - CalGlobalSuccessRateConfig config = 5; // Configuration -} - -message CalGlobalSuccessRateConfig { - uint32 entity_min_aggregates_size = 1; // Minimum aggregates for entity calculation - double entity_default_success_rate = 2; // Default success rate for entity -} -``` - -#### Response: `CalGlobalSuccessRateResponse` - -```protobuf -message CalGlobalSuccessRateResponse { - repeated LabelWithScore entity_scores_with_labels = 1; // Entity-specific success rates - repeated LabelWithScore global_scores_with_labels = 2; // Global success rates -} -``` - -#### Behavior - -- Performs parallel calculation of both entity and global success rates -- Allows for comparison between entity-specific and global performance -- Useful for analytics and decision-making about routing strategies -- Returns both sets of scores ordered by success rate - -#### Process Flow - -1. Authenticate the request -2. Extract tenant ID from request (for multi-tenancy support) -3. Validate the request parameters -4. Extract entity ID, parameters, and labels for both entity and global calculations -5. Create entity-specific and global configurations -6. Perform both calculations in parallel using tokio::try_join! -7. Format the results into the response structure -8. Return both sets of success rates - -#### Metrics - -- `SUCCESS_BASED_ROUTING_METRICS_REQUEST`: Counts metrics requests -- `SUCCESS_BASED_ROUTING_METRICS_DECISION_REQUEST_TIME`: Measures processing time -- `SUCCESS_BASED_ROUTING__METRICS_SUCCESSFUL_RESPONSE_COUNT`: Counts successful responses - -## Error Handling - -All endpoints return standard gRPC status codes: - -| Status Code | Description | When Used | -| ----------------------- | ----------------------------------- | --------------------------------------------- | -| `OK (0)` | Operation completed successfully | Normal successful operation | -| `NOT_FOUND (5)` | Required resource not found | Missing configuration, entity not found | -| `INVALID_ARGUMENT (3)` | Validation failed | Invalid or missing required parameters | -| `UNAUTHENTICATED (16)` | Authentication failed | Invalid or missing authentication credentials | -| `PERMISSION_DENIED (7)` | Authenticated user lacks permission | User not authorized for the operation | -| `INTERNAL (13)` | Internal server error | Unexpected errors during processing | - -## Multi-tenancy Support - -The service supports multi-tenancy with the following behavior: - -- All data is isolated per tenant with no cross-tenant data access -- Tenant ID is extracted from request metadata using authentication info -- Each tenant has its own success rate windows and calculations -- Multi-tenancy can be disabled in configuration via `is_multi_tenancy_enabled` flag -- When disabled, a default tenant ID is used - -## Performance Considerations - -- **Optimized Processing**: The service is optimized for high throughput and low latency -- **Caching**: Success rate calculations are cached for performance -- **Efficient Storage**: Windows are stored efficiently to minimize memory usage -- **Metrics Collection**: Comprehensive metrics are collected for performance monitoring -- **Rust Implementation**: Implementation is in Rust for maximum efficiency and safety -- **Parallel Processing**: Uses concurrent processing where appropriate (e.g., in FetchEntityAndGlobalSuccessRate) -- **Request Validation**: Validates requests early to fail fast and save processing resources - -## Example Usage - -### Fetch Success Rate Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], - "config": { - "min_aggregates_size": 10, - "default_success_rate": 0.5, - "specificity_level": "ENTITY" - } -} - -// Response -{ - "labels_with_score": [ - {"score": 0.95, "label": "processor_A"}, - {"score": 0.82, "label": "processor_B"}, - {"score": 0.73, "label": "processor_C"} - ] -} -``` - -### Update Success Rate Window Example - -```json -// Request -{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels_with_status": [ - {"label": "processor_A", "status": true}, - {"label": "processor_B", "status": false} - ], - "global_labels_with_status": [ - {"label": "processor_A", "status": true}, - {"label": "processor_B", "status": false} - ], - "config": { - "max_aggregates_size": 100, - "current_block_threshold": { - "duration_in_mins": 60, - "max_total_count": 1000 - } - } -} - -// Response -{ - "status": "WINDOW_UPDATION_SUCCEEDED" -} -``` - -### Invalidate Windows Example - -```json -// Request -{ - "id": "merchant_123" -} - -// Response -{ - "status": "WINDOW_INVALIDATION_SUCCEEDED" -} -``` - -### Fetch Entity and Global Success Rate Example - -```json -// Request -{ - "entity_id": "merchant_123", - "entity_params": "{\"payment_method\":\"card\"}", - "entity_labels": ["processor_A", "processor_B", "processor_C"], - "global_labels": ["processor_A", "processor_B", "processor_C", "processor_D"], - "config": { - "entity_min_aggregates_size": 10, - "entity_default_success_rate": 0.5 - } -} - -// Response -{ - "entity_scores_with_labels": [ - {"score": 0.95, "label": "processor_A"}, - {"score": 0.82, "label": "processor_B"}, - {"score": 0.73, "label": "processor_C"} - ], - "global_scores_with_labels": [ - {"score": 0.92, "label": "processor_A"}, - {"score": 0.85, "label": "processor_C"}, - {"score": 0.78, "label": "processor_B"}, - {"score": 0.70, "label": "processor_D"} - ] -} -``` - -## Integration Notes - -- All requests should include appropriate authentication metadata -- JSON parameters in the `params` field should be properly escaped -- Review metrics to monitor system performance -- Consider implementing client-side retries for transient failures -- Cache routing decisions where appropriate to improve performance -- Maintain proper error handling for all API calls - -## gRPCurl Examples - -The following examples demonstrate how to call the Success Rate API endpoints using gRPCurl. These examples assume the service is running on localhost at port 9000. -We wouldn't require configs as Dynamo will automatically fetch that from he specified profile. - -### FetchSuccessRate - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels": ["processor_A", "processor_B", "processor_C"], -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ --H 'x-profile-id: profile_id' \ -localhost:9000 success_rate.SuccessRateCalculator/FetchSuccessRate -``` - -### UpdateSuccessRateWindow - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123", - "params": "{\"payment_method\":\"card\"}", - "labels_with_status": [ - {"label": "processor_A", "status": true}, - {"label": "processor_B", "status": false} - ], - "global_labels_with_status": [ - {"label": "processor_A", "status": true}, - {"label": "processor_B", "status": false} - ] -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ --H 'x-profile-id: profile_id' \ -localhost:9000 success_rate.SuccessRateCalculator/UpdateSuccessRateWindow -``` - -### InvalidateWindows - -```bash -grpcurl -plaintext -d '{ - "id": "merchant_123" -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ -localhost:9000 success_rate.SuccessRateCalculator/InvalidateWindows -``` - -### FetchEntityAndGlobalSuccessRate - -```bash -grpcurl -plaintext -d '{ - "entity_id": "merchant_123", - "entity_params": "{\"payment_method\":\"card\"}", - "entity_labels": ["processor_A", "processor_B", "processor_C"], - "global_labels": ["processor_A", "processor_B", "processor_C", "processor_D"] -}' \ --H 'x-api-key: YOUR_API_KEY' \ --H 'x-tenant-id: tenant_001' \ --H 'x-profile-id: profile_id' \ -localhost:9000 success_rate.SuccessRateCalculator/FetchEntityAndGlobalSuccessRate -``` - -### Listing Available Methods - -To discover available methods on the service: - -```bash -grpcurl -plaintext localhost:9000 list success_rate.SuccessRateCalculator -``` - -### Viewing Method Details - -To see detailed information about a specific method: - -```bash -grpcurl -plaintext localhost:9000 describe success_rate.SuccessRateCalculator.FetchSuccessRate -``` diff --git a/docs/api-reference1.md b/docs/api-reference1.md index 75638e3e..811746a8 100644 --- a/docs/api-reference1.md +++ b/docs/api-reference1.md @@ -1,995 +1,379 @@ -# API REFERENCE +# API Examples -## Decision Gateway API +Use these examples for local smoke tests. For request and response schemas, use the OpenAPI-backed endpoint pages and `docs/openapi.json`. -### Sample curl for decide-gateway +Base URL: -#### Request: SR BASED ROUTING ```bash -curl --location 'http://localhost:8080/decide-gateway' \ ---header 'Content-Type: application/json' \ ---data '{ - "merchantId": "test_merchant1", - "eligibleGatewayList": ["GatewayA", "GatewayB", "GatewayC"], - "rankingAlgorithm": "SR_BASED_ROUTING", - "eliminationEnabled": true, - "paymentInfo": { - "paymentId": "PAY12359", - "amount": 100.50, - "currency": "USD", - "customerId": "CUST12345", - "udfs": null, - "preferredGateway": null, - "paymentType": "ORDER_PAYMENT", - "metadata": null, - "internalMetadata": null, - "isEmi": false, - "emiBank": null, - "emiTenure": null, - "paymentMethodType": "UPI", - "paymentMethod": "UPI_PAY", - "paymentSource": null, - "authType": null, - "cardIssuerBankName": null, - "cardIsin": null, - "cardType": null, - "cardSwitchProvider": null - } -}' -``` - -#### Response: -```json -{ - "decided_gateway": "GatewayA", - "gateway_priority_map": { - "GatewayA": 1.0, - "GatewayB": 1.0, - "GatewayC": 1.0 - }, - "filter_wise_gateways": null, - "priority_logic_tag": null, - "routing_approach": "SR_SELECTION_V3_ROUTING", - "gateway_before_evaluation": "GatewayA", - "priority_logic_output": { - "isEnforcement": false, - "gws": [ - "GatewayA", - "GatewayB", - "GatewayC" - ], - "priorityLogicTag": null, - "gatewayReferenceIds": {}, - "primaryLogic": null, - "fallbackLogic": null - }, - "reset_approach": "NO_RESET", - "routing_dimension": "ORDER_PAYMENT, UPI, UPI_PAY", - "routing_dimension_level": "PM_LEVEL", - "is_scheduled_outage": false, - "is_dynamic_mga_enabled": false, - "gateway_mga_id_map": null -} -``` -##### Routing Approach -This field available in the response for the decide-gateway api call provides visibility into the routing logic applied by the decision engine for a transaction. The possible values are as follows: - -- **SR_SELECTION_V3_ROUTING** : Routing is based on the gateway with the highest Success Rate (SR) score for the merchant, evaluated at the dimension on which routing is happening - -- **SR_V3_DOWNTIME_ROUTING** : Routing uses SR-based selection, but one or more (not all) eligible gateways have been deprioritized due to downtime (i.e., having a score below the elimination threshold). The system selects the best available gateway (based on SR) amongst the gateways which are not facing downtime. - -- **SR_V3_ALL_DOWNTIME_ROUTING** : All eligible gateways are facing downtime and have been deprioritized via elimination. Routing still uses SR scores at the configured dimension level to select the best among the degraded options. - -- **SR_V3_HEDGING** : Routing is done across all eligible gateways irrespective of their SR performance. This mode is used for exploration and evaluation of gateway SR performance and is controlled via configuration in the SR routing setup. - -- **SR_V3_DOWNTIME_HEDGING** : Routing follows the hedging strategy, where SR performance is not a strict criterion. However, one or more (but not all) eligible gateways are facing downtime. The system prefers gateways that are currently healthy while maintaining the exploration objective. - -- **SR_V3_ALL_DOWNTIME_HEDGING** : Routing follows the configured hedging strategy, but all eligible gateways are experiencing downtime. In this scenario, routing proceeds without reprioritization, in accordance with the defined hedging configuration. - -#### Request: DEBIT ROUTING -```bash -curl --location 'http://localhost:8080/decide-gateway' \ ---header 'Content-Type: application/json' \ ---data '{ - "merchantId": "pro_OiJkBiFuCYbYAkCG9X02", - "eligibleGatewayList": ["PAYU", "RAZORPAY", "PAYTM_V2"], - "rankingAlgorithm": "NTW_BASED_ROUTING", - "eliminationEnabled": true, - "paymentInfo": { - "paymentId": "PAY12345", - "amount": 100.50, - "currency": "USD", - "customerId": "CUST12345", - "udfs": null, - "preferredGateway": null, - "paymentType": "ORDER_PAYMENT", - "metadata": "{\"merchant_category_code\":\"merchant_category_code_0001\",\"acquirer_country\":\"US\"}", - "internalMetadata": null, - "isEmi": false, - "emiBank": null, - "emiTenure": null, - "paymentMethodType": "UPI", - "paymentMethod": "UPI_PAY", - "paymentSource": null, - "authType": null, - "cardIssuerBankName": null, - "cardIsin": "440000", - "cardType": null, - "cardSwitchProvider": null - } -}' -``` - -#### Response: -```json -{ - "decided_gateway": "PAYU", - "gateway_priority_map": null, - "filter_wise_gateways": null, - "priority_logic_tag": null, - "routing_approach": "NONE", - "gateway_before_evaluation": null, - "priority_logic_output": null, - "debit_routing_output": { - "co_badged_card_networks": [ - "STAR", - "VISA" - ], - "issuer_country": "US", - "is_regulated": false, - "regulated_name": "GOVERNMENT EXEMPT INTERCHANGE FEE", - "card_type": "debit" - }, - "reset_approach": "NO_RESET", - "routing_dimension": null, - "routing_dimension_level": null, - "is_scheduled_outage": false, - "is_dynamic_mga_enabled": false, - "gateway_mga_id_map": null -} +export BASE_URL=http://localhost:8080 ``` -## Update Gateway Score API +## Health -### Sample curl for update-gateway-score - -#### Request: ```bash -curl --location 'http://localhost:8080/update-gateway-score' \ ---header 'Content-Type: application/json' \ ---data '{ - "merchantId" : "test_merchant1", - "gateway": "RAZORPAY", - "gatewayReferenceId": null, - "status": "FAILURE", - "paymentId": "PAY12359", - "enforceDynamicRoutingFailure" : null -}' +curl "$BASE_URL/health" ``` -#### Response: -``` -Success -``` +## Create Merchant -## Config APIs - -#### Request: Success Rate Config Create ```bash -curl -X POST http://localhost:8080/rule/create \ +curl -X POST "$BASE_URL/merchant-account/create" \ -H "Content-Type: application/json" \ -d '{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "successRate", - "data": { - "defaultLatencyThreshold": 90, - "defaultSuccessRate": 0.5, - "defaultBucketSize": 200, - "defaultHedgingPercent": 5, - "subLevelInputConfig": [ - { - "paymentMethodType": "upi", - "paymentMethod": "upi_collect", - "bucketSize": 250, - "hedgingPercent": 1 - } - ] - } - } + "merchant_id": "demo_merchant", + "gateway_success_rate_based_decider_input": null }' ``` -#### Response: -```json -{ - "Success Rate Configuration created successfully" -} -``` +## Decide Gateway -#### Request: Success Rate Config retrieve ```bash -curl -X POST http://localhost:8080/rule/get \ - -H "Content-Type: application/json" \ - -d '{ - "merchant_id": "test_merchant_123423", - "algorithm": "successRate" - }' +curl -X POST "$BASE_URL/decide-gateway" \ + -H "Content-Type: application/json" \ + -d '{ + "merchantId": "demo_merchant", + "paymentInfo": { + "paymentId": "pay_001", + "amount": 1000.0, + "currency": "USD", + "paymentType": "ORDER_PAYMENT", + "paymentMethodType": "CARD", + "paymentMethod": "CREDIT" + }, + "eligibleGatewayList": ["stripe", "paypal", "adyen"], + "rankingAlgorithm": "SrBasedRouting", + "eliminationEnabled": false + }' ``` -#### Response: -```json -{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "successRate", - "data": { - "defaultLatencyThreshold": 90, - "defaultSuccessRate": 0.5, - "defaultBucketSize": 200, - "defaultHedgingPercent": 5, - "subLevelInputConfig": [ - { - "paymentMethodType": "upi", - "paymentMethod": "upi_collect", - "bucketSize": 250, - "hedgingPercent": 1 - } - ] - } - } -} -``` +## Update Gateway Score -#### Request: Success Rate Config update ```bash -curl -X POST http://localhost:8080/rule/update \ +curl -X POST "$BASE_URL/update-gateway-score" \ -H "Content-Type: application/json" \ -d '{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "successRate", - "data": { - "defaultLatencyThreshold": 90, - "defaultSuccessRate": 0.5, - "defaultBucketSize": 200, - "defaultHedgingPercent": 5, - "subLevelInputConfig": [ - { - "paymentMethodType": "upi", - "paymentMethod": "upi_collect", - "bucketSize": 250, - "hedgingPercent": 1 - } - ] - } - } + "merchantId": "demo_merchant", + "gateway": "stripe", + "paymentId": "pay_001", + "status": "CHARGED", + "gatewayReferenceId": null, + "enforceDynamicRoutingFailure": null }' ``` -#### Response: -```json -{ - "Success Rate Configuration updated successfully" -} -``` - -#### Request: Success Rate Config delete -```bash -curl -X POST http://localhost:8080/rule/delete \ - -H "Content-Type: application/json" \ - -d '{ - "merchant_id": "test_merchant_123423", - "algorithm": "successRate" - }' -``` +## Create Routing Rule -#### Response: -```json -{ - "Success Rate Configuration deleted successfully" -} -``` +### Priority rule -#### Request: Elimination Config Create ```bash -curl -X POST http://localhost:8080/rule/create \ +curl -X POST "$BASE_URL/routing/create" \ -H "Content-Type: application/json" \ -d '{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "elimination", - "data": { - "threshold": 0.35 - } + "name": "default-priority", + "description": "route to stripe first", + "created_by": "demo_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "priority", + "data": [ + { "gateway_name": "stripe", "gateway_id": null }, + { "gateway_name": "paypal", "gateway_id": null } + ] } }' ``` -#### Response: -```json -{ - "Elimination Configuration created successfully" -} -``` +### Single connector rule -#### Request: Elimination Config retrieve ```bash -curl -X POST http://localhost:8080/rule/get \ - -H "Content-Type: application/json" \ - -d '{ - "merchant_id": "test_merchant_123423", - "algorithm": "elimination" - }' -``` - -#### Response: -```json -{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "elimination", - "data": { - "threshold": 0.35 - } - } -} -``` - -#### Request: Elimination Config update -```bash -curl -X POST http://localhost:8080/rule/update \ +curl -X POST "$BASE_URL/routing/create" \ -H "Content-Type: application/json" \ -d '{ - "merchant_id": "test_merchant_123423", - "config": { - "type": "elimination", + "name": "always-stripe", + "description": "Pin all traffic to stripe", + "created_by": "demo_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "single", "data": { - "threshold": 0.35 + "gateway_name": "stripe", + "gateway_id": null } } }' ``` -#### Response: -```json -{ - "Elimination Configuration updated successfully" -} -``` - -#### Request: Elimination Config delete -```bash -curl -X POST http://localhost:8080/rule/delete \ - -H "Content-Type: application/json" \ - -d '{ - "merchant_id": "test_merchant_123423", - "algorithm": "elimination" - }' -``` - -#### Response: -```json -{ - "Elimination Configuration deleted successfully" -} -``` - -#### Request: Merchant account create -```bash -curl --location --request POST 'http://localhost:8080/merchant-account/create' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "merchant_id": "test_merchant_123423" -}' -``` - -#### Response: -```json -{ - "Merchant account created successfully" -} -``` - -#### Request: Merchant account retrieve -```bash -curl -X GET http://localhost:8080/merchant-account/test_merchant_123423 -``` - -#### Response: -```json -{ - "merchant_id": "test_merchant_123423", - "gateway_success_rate_based_decider_input": null -} -``` - -#### Request: Merchant account delete -```bash -curl -X DELETE http://localhost:8080/merchant-account/test_merchant_123423 -``` - -#### Response: -```json -{ - "Merchant account deleted successfully" -} -``` - -# Priority Logic V2 ---- - -**A rule engine to enable merchants to create complex logical expressions based on various payment related [parameters](https://github.com/juspay/decision-engine/blob/main/config/development.toml). These rules are executed on the payment payload to evaluate the gateway to be used.** - -## Table of Contents -1. [API Components](#components) -2. [API Reference](#PL-api-reference) -   2.1 [Create](#create) -   2.2 [Evaluate](#evaluate) -   2.3 [Operations](#operations) -    2.3.1 [List](#list) -    2.3.2 [Activate](#activate) -    2.3.e [List-Activated](#list_activated) -3. [Algorithm Types](#algorithm-types) -   3.1 Advanced Logic -     3.1.1 [AND](#and-rule) -     3.1.2 [OR](#or-rule) -     3.1.3 [AND-OR (Nested)](#and-or-rule) -     3.1.4 [Enum variant](#enum-variant) -     3.1.5 [Number array](#number-array) -     3.1.6 [Number comparison array](#number-comparison-array) -   3.2 [Priority](#priority) -   3.3 [Single Connector](#single) -   3.4 [Volume Split](#volume-split) - ---- - -## 1 · API Components - -| Components | Description & Accepted Values | -|------------------------------|----------------------------------------------------------------------------------------------------------------| -| `name` | Name of the rule (**string**, required) | -| `created_by` | Merchant or platform ID (**string**, required) | -| `description` | Rule description (**string**) | -| `algorithm_for` | Routing scope (**enum**): `payment` (default), `payout`, `three_ds_authentication` | -| `algorithm.type` | Routing algorithm type (**enum**): [`advanced`](#advanced), [`single`](#single), [`priority`](#priority), [`volume_split`](#volume-split) | -| `algorithm.data.globals` | Optional constants (**object**): reusable values for expressions | -| `default_selection` | Fallback connectors if no rule matches (**object**) with `priority: [{ gateway_name, gateway_id }]` | -| `rules[].name` | Name of an individual rule (**string**) | -| `rules[].routing_type` | Rule behavior (**enum**): `priority`, `volume_split`, Required only in advanced rule. | -| `output.priority[]` | Priority connector list (**array**): `[ { gateway_name, gateway_id } ]` | -| `output.volume_split[]` | Volume split rule (**array**): `[ { split: number, output: { gateway_name, gateway_id } } ]`, either priority or volume can be present at once in output. | -| `statements[].condition[]` | AND logic conditions (**array**): each with `lhs`, `comparison`, `value`, and optional `metadata` | -| `condition.lhs` | Field to evaluate (**string**): e.g., `amount`, `payment_method`, `card_network`. Can be checked from development.toml. | -| `condition.comparison` | Comparator (**enum**): `equal`, `greater_than`, `less_than`, `greater_than_equals`, etc. | -| `condition.value.type` | Value type (**enum**): `number`, [`number_array`](#number-array), [`enum_variant`](#enum-variant), `str_value`, [`number_comparison_array`](#number-comparison-array), etc. | -| `condition.value.value` | Value being compared (**any**): e.g., `100`, `"card"`, `[1000, 2000]` | -| `condition.metadata` | Optional metadata for tracing/debug (**object**) | -| `statements[].nested` | Nested OR conditions (**array**) of `condition[]` blocks | -| `algorithm.metadata` | Algorithm-level metadata (**object**, optional) | -| `rules[].metadata` | Rule-level metadata (**object**, optional) | - - -**Tip**: Use multiple `statements[]` blocks for OR logic. Use `nested` inside a `statement` for AND+OR nesting. - ---- - - -## 2 · API Reference - -### 2.1 Create Routing Algorithm +### Volume split rule ```bash -curl --location 'http://127.0.0.1:8082/routing/create' \ ---header 'Content-Type: application/json' \ ---data ' -{ - "name": "Priority rule", - "created_by": "merchant_1234", - "description": "this is my priority rule", +curl -X POST "$BASE_URL/routing/create" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "card-ab-test", + "description": "Split card traffic between stripe and checkout", + "created_by": "demo_merchant", "algorithm_for": "payment", "algorithm": { - "type": "advanced", - "data": { - "globals": {}, - "default_selection": { - "priority": [ - { - "gateway_name": "stripe", - "gateway_id": "mca_111" - }, - { - "gateway_name": "adyen", - "gateway_id": "mca_112" - }, - { - "gateway_name": "checkout", - "gateway_id": "mca_113" - } - ] - }, - "rules": [ - { - "name": "Card Rule", - "routingType": "priority", - "output": { - "priority": [ - { - "gateway_name": "Paytm", - "gateway_id": "mca_114" - }, - { - "gateway_name": "adyen", - "gateway_id": "mca_112" - } - ] - }, - "statements": [ - { - "condition": [ - { - "lhs": "payment_method", - "comparison": "equal", - "value": { - "type": "enum_variant", - "value": "card" - }, - "metadata": {} - } - ] - }, - { - "condition": [ - { - "lhs": "amount", - "comparison": "greater_than", - "value": { - "type": "number", - "value": 100 - }, - "metadata": {} - } - ] - } - ] - } - ] - } - }, - "metadata": {} -} -' -``` - -**Success Response** -```json -{ - "rule_id": "routing_e641380c-6f24-4405-8454-5ae6cbceb7a0", - "name": "Priority rule", - "created_at": "2025-04-22 11:45:03.411134513", - "modified_at": "2025-04-22 11:45:03.411134513" -} -``` - ---- - -### 2.2 Evaluate Routing Algorithm - -```bash -curl --location 'http://127.0.0.1:8082/routing/evaluate' \ ---header 'Content-Type: application/json' \ ---data '{ - "created_by": "merchant_1234", - "parameters": { - "payment_method": { "type": "enum_variant", "value": "upi" }, - "amount": { "type": "number", "value": 10 } - } -}' -``` - -**Example Response** -```json -{ - "status": "default_selection", - "output": { - "type": "priority", - "connectors": [ - { "gateway_name": "stripe", "gateway_id": "mca_111" }, - { "gateway_name": "adyen", "gateway_id": "mca_112" }, - { "gateway_name": "checkout", "gateway_id": "mca_113" } - ] - }, - "evaluated_output": [ - { "gateway_name": "stripe", "gateway_id": "mca_111" } - ], - "eligible_connectors": [] -} -``` - ---- - -## 2.3 Operations - -### 2.3.1 List Algorithms - -```bash -curl --request POST 'http://127.0.0.1:8082/routing/list/merchant_1234' -``` - -Returns an array of algorithms for `merchant_1234`. - -### 2.3.2 Activate Algorithm - -```bash -curl --location 'http://127.0.0.1:8082/routing/activate' \ ---header 'Content-Type: application/json' \ ---data '{ - "created_by": "merchant_1234", - "routing_algorithm_id": "routing_8711ce52-33e2-473f-9c8f-91a406acb850" -}' -``` -At a given time one algorithm for each transaction_type (`payment`, `payout`, `three_ds_authentication`) can be active for one created_by id. -HTTP 200 ⇒ algorithm is now active. - -### 2.3.3 List Activated algorithm - -```bash -curl --location --request POST 'http://127.0.0.1:8082/routing/list/active/merchant_31' \ ---header 'Content-Type: application/json' -``` - -Returns algorithms currently active for the merchant. - ---- - -## 3 · Algorithm Types - -### 3.1 Advanced Logic (AND / OR / AND-OR) - -| Use-case | Description | -|---------------------|--------------------------------------| -| **AND** | All conditions must be true | -| **OR** | Any one condition may be true | -| **AND-OR (nested)** | Parent condition + any nested match | - ---- -Note: for advanced algorithm kinds we always require statements to be evaluated upoun, unlike the below priority, single and volume_split, which donot requires any statements and directly provide output. -
-AND Rule - -```json -{ - "name": "HDFC Rule", - "routing_type": "volume_split", - "output": { - "volume_split": [ - { - "split": 60, - "output": { "gateway_name": "hdfc", "gateway_id": "mca_114" } - }, - { - "split": 40, - "output": { "gateway_name": "instamojo", "gateway_id": "mca_115" } - } - ] - }, - "statements": [ - { - "condition": [ + "type": "volume_split", + "data": [ { - "lhs": "amount", - "comparison": "greater_than", - "value": { "type": "number", "value": 100 } + "split": 70, + "output": { "gateway_name": "stripe", "gateway_id": null } }, { - "lhs": "billing_country", - "comparison": "equal", - "value": { "type": "enum_variant", "value": "Netherlands" } + "split": 30, + "output": { "gateway_name": "checkout", "gateway_id": null } } ] } - ] -} + }' ``` -All conditions must match → volume split applies -
- ---- - -
-OR Rule - -```json -{ - "name": "Card Rule", - "routing_type": "priority", - "output": { - "priority": [ - { "gateway_name": "Paytm", "gateway_id": "mca_114" }, - { "gateway_name": "adyen", "gateway_id": "mca_112" } - ] - }, - "statements": [ - { - "condition": [ - { - "lhs": "payment_method", - "comparison": "equal", - "value": { "type": "enum_variant", "value": "card" } - } - ] - }, - { - "condition": [ - { - "lhs": "amount", - "comparison": "greater_than", - "value": { "type": "number", "value": 100 } - } - ] - } - ] -} -``` +### Advanced rule with OR branches -Any one condition match triggers the rule -
- ---- - -
-AND + OR (Nested) - -```json -{ - "name": "RBL Rule", - "routing_type": "priority", - "output": { - "priority": [ - { "gateway_name": "rbl", "gateway_id": "mca_114" }, - { "gateway_name": "instamojo", "gateway_id": "mca_115" } - ] - }, - "statements": [ - { - "condition": [ - { - "lhs": "amount", - "comparison": "greater_than", - "value": { "type": "number", "value": 10 } - } - ], - "nested": [ - { - "condition": [ - { - "lhs": "card_network", - "comparison": "equal", - "value": { "type": "enum_variant", "value": "Visa" } - } +`statements` are OR branches. Each `condition` array is an AND block. + +```bash +curl -X POST "$BASE_URL/routing/create" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "wallet-or-credit-card", + "description": "Route wallet traffic to checkout and credit cards to stripe first", + "created_by": "demo_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "advanced", + "data": { + "globals": {}, + "default_selection": { + "priority": [ + { "gateway_name": "adyen", "gateway_id": null } ] }, - { - "condition": [ - { - "lhs": "billing_country", - "comparison": "equal", - "value": { "type": "enum_variant", "value": "India" } - } - ] - } - ] - } - ] -} -``` - -Main condition must match + any one nested condition -
- ---- - -
-Enum variant - -```json -{ - "name": "Card Rule", - "routing_type": "priority", - "output": { - "priority": [ - { - "gateway_name": "rbl", - "gateway_id": "mca_114" - } - ] - }, - "statements": [ - { - "condition": [ - { - "lhs": "card_network", + "rules": [ + { + "name": "wallet_or_credit_card", + "routing_type": "priority", + "output": { + "priority": [ + { "gateway_name": "checkout", "gateway_id": null }, + { "gateway_name": "stripe", "gateway_id": null } + ] + }, + "statements": [ + { + "condition": [ + { + "lhs": "payment_method", "comparison": "equal", - "value": { - "type": "enum_variant_array", - "value": [ - "Visa", - "Mastercard" - ] - }, + "value": { "type": "enum_variant", "value": "wallet" }, "metadata": {} - } - ] - } - ] -} -``` - -The input for evaluation parameter must be one of the mentioned types in array. -
- ---- - -
-Number array - -```json -{ - "name": "Card Rule", - "routing_type": "priority", - "output": { - "priority": [ - { - "gateway_name": "rbl", - "gateway_id": "mca_114" - } - ] - }, - "statements": [ - { - "condition": [ - { - "lhs": "amount", + } + ], + "nested": null + }, + { + "condition": [ + { + "lhs": "payment_method", "comparison": "equal", - "value": { - "type": "number_array", - "value": [ - 1000, - 2000, - 5000 - ] - }, + "value": { "type": "enum_variant", "value": "card" }, "metadata": {} - } + }, + { + "lhs": "card_type", + "comparison": "equal", + "value": { "type": "enum_variant", "value": "credit" }, + "metadata": {} + } + ], + "nested": null + } ] - } - ] -} + } + ], + "metadata": {} + } + } + }' ``` -The input for evaluation parameter must be one of the mentioned values in array. -
+### Advanced rule with nested conditions ---- -
-Number comparison array +Use `nested` when the second block should only run after the parent condition matched. -```json -{ - "name": "Card Rule", - "routing_type": "priority", - "output": { - "priority": [ +```bash +curl -X POST "$BASE_URL/routing/create" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "visa-usd-nested-routing", + "description": "Use nested card checks, then split matched traffic by percentage", + "created_by": "demo_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "advanced", + "data": { + "globals": {}, + "default_selection": { + "volume_split": [ { - "gateway_name": "rbl", - "gateway_id": "mca_114" + "split": 60, + "output": { "gateway_name": "stripe", "gateway_id": null } + }, + { + "split": 40, + "output": { "gateway_name": "checkout", "gateway_id": null } } - ] - }, - "statements": [ - { - "condition": [ + ] + }, + "rules": [ + { + "name": "card_credit_visa", + "routing_type": "volume_split", + "output": { + "volume_split": [ { - "lhs": "amount", + "split": 80, + "output": { "gateway_name": "stripe", "gateway_id": null } + }, + { + "split": 20, + "output": { "gateway_name": "adyen", "gateway_id": null } + } + ] + }, + "statements": [ + { + "condition": [ + { + "lhs": "payment_method", "comparison": "equal", - "value": { - "type": "number_comparison_array", - "value": [ - { - "comparison_type": "greater_than", - "number": 1000 - }, - { - "comparison_type": "less_than_equal", - "number": 5000 - } - ] - }, + "value": { "type": "enum_variant", "value": "card" }, "metadata": {} - } + } + ], + "nested": [ + { + "condition": [ + { + "lhs": "card_type", + "comparison": "equal", + "value": { "type": "enum_variant", "value": "credit" }, + "metadata": {} + } + ], + "nested": [ + { + "condition": [ + { + "lhs": "card_network", + "comparison": "equal", + "value": { "type": "enum_variant", "value": "visa" }, + "metadata": {} + }, + { + "lhs": "currency", + "comparison": "equal", + "value": { "type": "enum_variant", "value": "USD" }, + "metadata": {} + } + ], + "nested": null + } + ] + } + ] + } ] - } - ] -} + } + ], + "metadata": {} + } + } + }' ``` -The input for evaluation parameter must be in the specifed thresholds. -
- ---- +## List Routing Rules +```bash +curl -X POST "$BASE_URL/routing/list/demo_merchant" +``` -### 3.2 Priority Routing +## Activate Routing Rule ```bash -curl --location 'http://127.0.0.1:8082/routing/create' \ ---header 'Content-Type: application/json' \ ---data '{ - "name": "priority rule test", - "created_by": "merchant_123", - "algorithm": { - "type": "priority", - "data": [ - { "gateway_name": "stripe", "gateway_id": "mca_001" }, - { "gateway_name": "razorpay", "gateway_id": "mca_002" } - ] - } -}' +curl -X POST "$BASE_URL/routing/activate" \ + -H "Content-Type: application/json" \ + -d '{ + "created_by": "demo_merchant", + "routing_algorithm_id": "rule_id_here" + }' ``` -Always returns the connectors **in the given order**. +## Get Active Routing Rule ---- +```bash +curl -X POST "$BASE_URL/routing/list/active/demo_merchant" +``` -### 3.3 Single Connector (straight-through) +## Create Euclid Rule Config ```bash -curl --location 'http://127.0.0.1:8082/routing/create' \ ---header 'Content-Type: application/json' \ ---data '{ - "name": "single connector rule", - "created_by": "merchant_123", - "algorithm": { - "type": "single", - "data": { "gateway_name": "stripe", "gateway_id": "mca_00123" } - } -}' +curl -X POST "$BASE_URL/rule/create" \ + -H "Content-Type: application/json" \ + -d '{ + "merchant_id": "demo_merchant", + "config": { + "type": "successRate", + "data": { + "defaultLatencyThreshold": 90, + "defaultSuccessRate": 0.5, + "defaultBucketSize": 200, + "defaultHedgingPercent": 5, + "subLevelInputConfig": [] + } + } + }' ``` -Regardless of parameters, Routing decision will always be **Stripe (mca_00123)**. +## Get Euclid Rule Config ---- +```bash +curl -X POST "$BASE_URL/rule/get" \ + -H "Content-Type: application/json" \ + -d '{ + "merchant_id": "demo_merchant", + "algorithm": "successRate" + }' +``` -### 3.4 Volume Split +## Update Euclid Rule Config ```bash -curl --location 'http://127.0.0.1:8082/routing/create' \ ---header 'Content-Type: application/json' \ ---data '{ - "name": "volume split test rule", - "created_by": "merchant_31", - "algorithm_for": "payout", - "algorithm": { - "type": "volume_split", - "data": [ - { - "split": 70, - "output": { "gateway_name": "stripe", "gateway_id": "mca_001" } - }, - { - "split": 30, - "output": { "gateway_name": "paytm", "gateway_id": "mca_002" } +curl -X POST "$BASE_URL/rule/update" \ + -H "Content-Type: application/json" \ + -d '{ + "merchant_id": "demo_merchant", + "config": { + "type": "successRate", + "data": { + "defaultLatencyThreshold": 95, + "defaultSuccessRate": 0.5, + "defaultBucketSize": 250, + "defaultHedgingPercent": 5, + "subLevelInputConfig": [] } - ] - } -}' + } + }' ``` -Provides **70 %** of decisions as Stripe and **30 %** as Paytm. - ---- +## Delete Euclid Rule Config -**Note:** Full routing rule example is provided in the [initial request section](#create). Use that as template to compose complex rules (AND / OR / AND-OR). - ---- +```bash +curl -X POST "$BASE_URL/rule/delete" \ + -H "Content-Type: application/json" \ + -d '{ + "merchant_id": "demo_merchant", + "algorithm": "successRate" + }' +``` diff --git a/docs/configuration.md b/docs/configuration.md index fc4d0f28..5ab4636f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,176 +1,118 @@ # Configuration Guide -Dynamo uses TOML configuration files to customize its behavior. This guide explains the available configuration options and how to use them effectively. +This page describes the runtime configuration model used by Decision Engine. -## Configuration File Location +## How Configuration Is Loaded -By default, Dynamo looks for configuration files in the following locations: +The application loads config in this order: -- Development mode: `config/development.toml` -- Production mode: `config/production.toml` +1. file selected by `APP_ENV` +2. environment overrides with the `DECISION_ENGINE__` prefix -You can also specify a custom configuration file using environment variables. +From `src/config.rs`: -## Configuration Format +- `APP_ENV=dev` or unset -> `config/development.toml` +- `APP_ENV=sandbox` -> `config/sandbox.toml` +- `APP_ENV=production` -> `config/production.toml` -Below is an explanation of the main configuration sections: +Environment overrides use `__` as the separator. -### Server Configuration +Example: -```toml -[server] -host = "127.0.0.1" # The address to bind the server to -port = 8000 # The port to listen on -type = "grpc" # Server type: "grpc" or "http" +```bash +DECISION_ENGINE__SERVER__PORT=8080 +DECISION_ENGINE__METRICS__PORT=9090 ``` -### Metrics Server Configuration +## Primary Files -```toml -[metrics] -host = "127.0.0.1" # The address to bind the metrics server to -port = 9000 # The port for the metrics server -``` +- `config.example.toml`: sample config +- `config/development.toml`: source-run default +- `config/docker-configuration.toml`: Compose-mounted config +- `src/config.rs`: actual config structs and load rules -### Logging Configuration +For deployment behavior, also inspect: -```toml -[log.console] -enabled = true # Whether to enable console logging -level = "DEBUG" # Log level: DEBUG, INFO, WARN, ERROR -log_format = "default" # Log format: "default" or "json" -``` +- `docker-compose.yaml` +- `helm-charts/templates/*` -### Redis Configuration +## Main Config Sections -```toml -[redis] -host = "127.0.0.1" -port = 6379 -pool_size = 5 # Number of connections to keep open -reconnect_max_attempts = 5 # Maximum reconnection attempts -reconnect_delay = 5 # Delay between attempts in milliseconds -use_legacy_version = false # Use RESP2 protocol for Redis < 6 -``` +The runtime config model in `src/config.rs` includes: -### TTL for Keys +- `log` +- `server` +- `metrics` +- `database` or `pg_database` +- `redis` +- `cache_config` +- `tenant_secrets` +- `tls` +- `api_client` +- `routing_config` +- `pm_filters` +- `debit_routing_config` +- `compression_filepath` -```toml -[ttl_for_keys] -aggregates = 300 # Time to live for aggregates keys (seconds) -current_block = 900 # Time to live for current_block keys (seconds) -elimination_bucket = 900 # Time to live for elimination buckets (seconds) -contract_ttl = 900 # Time to live for contracts (seconds) -``` +## Common Areas -### Global Routing Configurations +### Server ```toml -[global_routing_configs.success_rate] -min_aggregates_size = 5 # Minimum number of buckets for SR calculation -default_success_rate = 100 # Default SR when insufficient data -max_aggregates_size = 10 # Maximum number of aggregates to store - -[global_routing_configs.success_rate.current_block_threshold] -duration_in_mins = 10 # Current block duration in minutes -max_total_count = 5 # Maximum transaction count for current block - -[global_routing_configs.elimination_rate] -bucket_size = 5 # Capacity of buckets -bucket_leak_interval_in_secs = 300 # Leak rate of buckets in seconds +[server] +host = "0.0.0.0" +port = 8080 ``` -### Multi-Tenancy +### Metrics ```toml -[multi_tenancy] -enabled = true # Enable multi-tenant mode -``` - -## Environment Variables - -You can override configuration values using environment variables with the prefix `DYNAMO__`: - -```bash -# Override Redis host and port -export DYNAMO__REDIS__HOST=redis-server -export DYNAMO__REDIS__PORT=6380 - -# Override server port -export DYNAMO__SERVER__PORT=9000 +[metrics] +host = "0.0.0.0" +port = 9090 ``` -## Configuration Examples - -### Development Configuration +### Logging ```toml [log.console] enabled = true level = "DEBUG" log_format = "default" - -[server] -host = "127.0.0.1" -port = 8000 -type = "grpc" - -[redis] -host = "127.0.0.1" -port = 6379 ``` -### Production Configuration +### Database -```toml -[log.console] -enabled = true -level = "INFO" -log_format = "json" +Use one backend path: -[server] -host = "0.0.0.0" # Listen on all interfaces -port = 8000 -type = "grpc" +- MySQL via `[database]` +- PostgreSQL via `[pg_database]` -[redis] -host = "redis-server" # Use service name in production -port = 6379 -pool_size = 20 -reconnect_max_attempts = 10 -``` - -### High-Availability Configuration +### Redis ```toml -[server] -host = "0.0.0.0" -port = 8000 -type = "grpc" - [redis] -host = "redis-master" +host = "127.0.0.1" port = 6379 -pool_size = 30 -reconnect_max_attempts = 15 -reconnect_delay = 2 ``` -## Advanced Configuration +### TLS -### Using Multiple Redis Instances +TLS is optional and configured through the `tls` section. -While not directly supported in the configuration file, you can set up Redis Sentinel or Redis Cluster for high availability and implement a custom client. +### Tenant Config -### Configuring Routing Strategies +Tenant-aware behavior is driven by `tenant_secrets` and tenant-specific app-state wiring in `src/tenant.rs`. -Each routing strategy has specific configuration parameters: +## Deployment Notes -1. **Success Rate Routing**: Configure weight factors, time windows, and default values -2. **Elimination Routing**: Configure bucket sizes, leak rates, and thresholds -3. **Contract Routing**: Configure contract scores, targets, and time scales +- Source runs default to `config/development.toml` +- Compose mounts `config/docker-configuration.toml` at `/local/config/development.toml` +- Helm behavior should be verified against the chart templates directly -## Next Steps +## Related Docs -- [Installation Guide](setup-guide.md) -- [API Reference](api-reference.md) +- [Local Setup Guide](local-setup.md) +- [PostgreSQL Setup Guide](setup-guide-postgres.md) +- [MySQL Setup Guide](setup-guide-mysql.md) +- [API Overview](api-reference.md) diff --git a/docs/dashboard.mdx b/docs/dashboard.mdx new file mode 100644 index 00000000..91b6a953 --- /dev/null +++ b/docs/dashboard.mdx @@ -0,0 +1,50 @@ +--- +title: "Dashboard" +description: "Local dashboard routes and how they are served" +--- + +## Availability + +The dashboard is available in the `dashboard-*` Docker Compose profiles. + +Example: + +```bash +docker compose --profile dashboard-postgres-ghcr up -d +``` + +Then open: + +- `http://localhost:8081/dashboard/` + +The dashboard is served by Nginx from the built assets in `website/dist`. + +## What It Includes + +Based on the React routes in `website/src/App.tsx`, the dashboard exposes: + +- `/dashboard/` +- `/dashboard/routing` +- `/dashboard/routing/sr` +- `/dashboard/routing/rules` +- `/dashboard/routing/volume` +- `/dashboard/routing/debit` +- `/dashboard/decisions` +- `/dashboard/analytics` +- `/dashboard/audit` + +## Docs And API In The Same Profile + +The same `dashboard-*` profiles also expose: + +- Mintlify docs at `http://localhost:8081/introduction` +- the API at `http://localhost:8080` + +Proxy behavior is defined in `nginx/nginx.conf`. + +## Related Files + +- `website/src/App.tsx` +- `website/src/components/pages/*` +- `docker-compose.yaml` +- `nginx/nginx.conf` diff --git a/docs/dual-protocol-layer.md b/docs/dual-protocol-layer.md deleted file mode 100644 index a897e432..00000000 --- a/docs/dual-protocol-layer.md +++ /dev/null @@ -1,79 +0,0 @@ -# Dual Protocol Layer: gRPC and HTTP - -## Overview - -Dynamo implements a unique dual-protocol layer that allows services to be exposed via both gRPC and HTTP simultaneously. This enables clients to interact with Dynamo using their preferred protocol without requiring separate service implementations. - -## Architecture - -The implementation uses a custom generator that extends Tonic's standard gRPC code generation to automatically create corresponding HTTP endpoints for each gRPC service. This is done through a specialized WebGenerator in the build process. - -### Key Components - -- **Protocol Buff Definition**: Service interfaces are defined once in `.proto` files -- **Code Generation**: Build process generates both gRPC and HTTP handlers -- **Axum Integration**: HTTP endpoints are implemented using the Axum framework -- **Automatic Conversion**: Seamless serialization/deserialization between gRPC and HTTP formats - -## How It Works - -1. **Define Once**: Services are defined once using Protocol Buffers -2. **Generate Both**: Build scripts generate both gRPC service code and HTTP routes -3. **Single Implementation**: Service logic is implemented only once -4. **Configure at Runtime**: Choose protocol through configuration settings - -The system automatically handles conversion between HTTP and gRPC formats, including: -- Converting gRPC metadata to HTTP headers and vice versa -- Mapping gRPC status codes to HTTP status codes -- Handling serialization differences between the protocols - -## Benefits - -1. **Protocol Flexibility**: Clients can choose between gRPC or HTTP based on their needs -2. **Unified Codebase**: Service logic is implemented only once -3. **No Duplication**: Avoid duplicating endpoint logic and type definitions -4. **Simple Configuration**: Switch between protocols with a single configuration option -5. **Testing Ease**: Test services using simple HTTP clients like cURL - -## Using the Dual Protocol Layer - -### Configuration - -In your configuration file, specify the protocol: - -```toml -[server] -type = "grpc" # or "http" -``` - -### Client Options - -Clients can interact with the service using either protocol: - -**gRPC Client**: -- Better performance for high-throughput scenarios -- Efficient binary serialization -- Built-in streaming support -- Strong typing via generated client code - -**HTTP Client**: -- Works in environments where gRPC isn't supported (e.g., browsers) -- Simpler to debug and inspect with standard tools -- Easier integration with existing HTTP-based systems -- No need for protocol buffer compilation - -## Technical Considerations - -1. **Performance**: gRPC generally offers better performance for service-to-service communication -2. **Compatibility**: HTTP offers broader compatibility but without some gRPC advantages -3. **Streaming**: gRPC excels at bidirectional streaming; HTTP is more limited -4. **Headers/Metadata**: Slight differences in how metadata is handled between protocols - -## Future Directions - -- Enhanced streaming support for HTTP endpoints -- WebSocket alternatives for bidirectional communication in HTTP mode -- Automatic OpenAPI/Swagger documentation generation -- Support for protocol-specific optimizations - -This innovative approach allows Dynamo to be more accessible to a wider range of clients while maintaining a clean and maintainable codebase. diff --git a/docs/favicon.svg b/docs/favicon.svg new file mode 100644 index 00000000..5ae09a6b --- /dev/null +++ b/docs/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/installation.md b/docs/installation.md index 675014e1..f5a6433a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,168 +1,30 @@ # Installation Guide -This guide provides detailed instructions for installing Dynamo on different platforms. +Use this page to choose the right installation path. For actual commands, use the linked guides. -## Prerequisites +## Supported Paths -- Rust 1.78.0 or later -- Redis server 6.0 or later -- Git +| Path | Best for | Primary doc | +|------|----------|-------------| +| Docker Compose with published images | fastest local or on-prem style bring-up | [Local Setup Guide](local-setup.md) | +| Docker Compose with local builds | validating local source changes | [Local Setup Guide](local-setup.md) | +| Source build | backend debugging and feature-specific runs | [Local Setup Guide](local-setup.md) | +| Helm | Kubernetes and on-prem deployment work | [Local Setup Guide](local-setup.md) and `helm-charts/README.md` | -## From Source +## Database-Specific Shortcuts -### 1. Clone the Repository +- PostgreSQL commands: [PostgreSQL Setup Guide](setup-guide-postgres.md) +- MySQL commands: [MySQL Setup Guide](setup-guide-mysql.md) -```bash -git clone https://github.com/yourusername/dynamo.git -cd dynamo -``` +## Dashboard And Docs -### 2. Install Dependencies +If you want the React dashboard and Mintlify docs, use one of the `dashboard-*` compose profiles from [Local Setup Guide](local-setup.md). -#### Ubuntu/Debian +That exposes: -```bash -# Install system dependencies -sudo apt-get update -sudo apt-get install -y pkg-config libssl-dev protobuf-compiler libpq-dev +- Dashboard: `http://localhost:8081/dashboard/` +- Docs: `http://localhost:8081/introduction` -# Install Rust (if not already installed) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -``` +## Canonical Source -#### macOS - -```bash -# Using Homebrew -brew install pkg-config openssl protobuf postgresql - -# Install Rust (if not already installed) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -``` - -### 3. Build Dynamo - -```bash -# Build in release mode -cargo build --release -``` - -### 4. Set Up Configuration - -```bash -# Copy example configuration -cp config/config.example.toml config/development.toml - -# Edit configuration as needed -# vi config/development.toml -``` - -### 5. Run Redis - -Make sure Redis is running on your system: - -```bash -# Install Redis if needed -sudo apt-get install redis-server # Ubuntu/Debian -brew install redis # macOS - -# Start Redis server -redis-server -``` - -### 6. Run Dynamo - -```bash -# Run with development configuration -./target/release/dynamo -``` - -## Using Docker - -### 1. Pull the Docker Image - -```bash -docker pull yourusername/dynamo:latest -``` - -Or build it yourself: - -```bash -docker build -t dynamo:latest . -``` - -### 2. Run with Docker - -```bash -# Run with default configuration -docker run -p 8000:8000 -p 9000:9000 dynamo:latest - -# Run with custom configuration -docker run -p 8000:8000 -p 9000:9000 \ - -v $(pwd)/config:/app/config \ - dynamo:latest -``` - -### 3. Docker Compose Setup - -Create a `docker-compose.yml` file: - -```yaml -version: '3' -services: - dynamo: - image: yourusername/dynamo:latest - ports: - - "8000:8000" - - "9000:9000" - volumes: - - ./config:/app/config - depends_on: - - redis - - redis: - image: redis:latest - ports: - - "6379:6379" -``` - -Run with Docker Compose: - -```bash -docker-compose up -d -``` - -## Building the WebAssembly Module - -The `procesmo` module can be built for web use: - -```bash -# Install wasm-pack if needed -cargo install wasm-pack - -# Build the WebAssembly module -cd crates/procesmo -wasm-pack build --target web -``` - -## Verifying Installation - -To verify that Dynamo is running correctly: - -```bash -# Check health status -curl http://localhost:8000/grpc.health.v1.Health/Check - -# Or using grpcurl -grpcurl -plaintext localhost:8000 grpc.health.v1.Health/Check -``` - -You should see a response indicating the service is running. - -## Next Steps - -- [Configure Dynamo](configuration.md) for your environment -- [Explore the API](api-reference.md) -- [See usage examples](examples.md) +Treat [Local Setup Guide](local-setup.md) as the canonical installation and startup document for this repository. diff --git a/docs/introduction.mdx b/docs/introduction.mdx new file mode 100644 index 00000000..0e23b67d --- /dev/null +++ b/docs/introduction.mdx @@ -0,0 +1,98 @@ +--- +title: "Decision Engine" +description: "Routing API, local run paths, dashboard, and docs entrypoints" +--- + +
+ + + Juspay Decision Engine + +

+ Juspay Decision Engine +
+ Routing infrastructure docs for setup, operations, analytics, and API usage. +

+
+ +## What It Is + +Decision Engine is a Rust service that picks a gateway from an eligible list and records transaction outcomes for later routing decisions. + +The repository currently ships: + +- the routing API +- merchant and routing configuration APIs +- Docker Compose profiles for local bring-up +- Helm chart assets +- a React dashboard under `/dashboard/` +- an Analytics surface under `/analytics` +- Mintlify docs served in the dashboard profiles + +## Main API Surface + +The main endpoint groups are: + +- `GET /health` +- `POST /decide-gateway` +- `POST /update-gateway-score` +- `POST /merchant-account/create` +- `GET /merchant-account/{merchantId}` +- `DELETE /merchant-account/{merchantId}` +- `POST /routing/*` +- `POST /rule/*` +- `GET /analytics/*` + +Use the OpenAPI-backed endpoint pages for request and response schemas. + +## Run Locally + +For API only: + +```bash +docker compose --profile postgres-ghcr up -d +curl http://localhost:8080/health +``` + +For API + dashboard + docs: + +```bash +docker compose --profile dashboard-postgres-ghcr up -d +``` + +URLs: + +- API: `http://localhost:8080` +- Dashboard: `http://localhost:8081/dashboard/` +- Docs: `http://localhost:8081/introduction` + +Use [Local Setup](local-setup) for the full Compose, source-build, and Helm matrix. + +## Dashboard Routes + +When the dashboard profile is running, the React app is available at: + +- `/dashboard/` +- `/dashboard/routing` +- `/dashboard/routing/sr` +- `/dashboard/routing/rules` +- `/dashboard/routing/volume` +- `/dashboard/routing/debit` +- `/dashboard/decisions` +- `/dashboard/analytics` + +Use [Dashboard](dashboard) for route-by-route details. + +## Where To Go Next + +- [Installation](installation) +- [Local Setup](local-setup) +- [Configuration](configuration) +- [Dashboard](dashboard) +- [Analytics](analytics) +- [API Overview](api-reference) +- [API Examples](api-reference1) diff --git a/docs/local-setup.md b/docs/local-setup.md new file mode 100644 index 00000000..d3f86c71 --- /dev/null +++ b/docs/local-setup.md @@ -0,0 +1,182 @@ +# Local Setup Guide + +This is the canonical local startup guide for Decision Engine. + +## Prerequisites + +- Docker 20+ +- Docker Compose v2+ +- Git 2+ + +Optional for source runs: + +- Rust 1.85+ +- `just` +- PostgreSQL or MySQL +- Redis + +## Runtime Tracks + +Decision Engine supports two local tracks: + +1. published-image track: pull existing images +2. local-build track: build images or binaries from the current source tree + +Default tags used in this repo: + +- `DECISION_ENGINE_TAG=v1.4` +- `GROOVY_RUNNER_TAG=v1.4` + +## Docker Compose Profiles + +You must pass at least one profile. + +### Core runtime profiles + +| Profile | DB | Includes | +|---|---|---| +| `postgres-ghcr` | PostgreSQL | API + PostgreSQL + Redis + PG migrations | +| `postgres-local` | PostgreSQL | API + PostgreSQL + Redis + PG migrations | +| `mysql-ghcr` | MySQL | API + MySQL + Redis + MySQL migrations + routing-config | +| `mysql-local` | MySQL | API + MySQL + Redis + MySQL migrations + routing-config | + +### Dashboard profiles + +| Profile | DB | Includes | +|---|---|---| +| `dashboard-postgres-ghcr` | PostgreSQL | core PG stack + dashboard + Mintlify docs | +| `dashboard-postgres-local` | PostgreSQL | core PG stack + dashboard + Mintlify docs | +| `dashboard-mysql-ghcr` | MySQL | core MySQL stack + dashboard + Mintlify docs | +| `dashboard-mysql-local` | MySQL | core MySQL stack + dashboard + Mintlify docs | + +### Optional profiles + +| Profile | Adds | +|---|---| +| `monitoring` | Prometheus + Grafana | +| `groovy-ghcr` | Groovy runner image | +| `groovy-local` | Groovy runner built from local source | + +## Fastest Bring-Up + +### API Only + +```bash +docker compose --profile postgres-ghcr up -d +``` + +### API + Dashboard + Docs + +```bash +docker compose --profile dashboard-postgres-ghcr up -d +``` + +### With Monitoring + +```bash +docker compose --profile postgres-ghcr --profile monitoring up -d +``` + +## Make Targets + +Common wrappers: + +```bash +make init-pg-ghcr +make init-pg-local +make init-mysql-ghcr +make init-mysql-local +make run-pg-ghcr +make run-mysql-local +make stop +``` + +## Source Build And Run + +### PostgreSQL + +```bash +cargo build --release --no-default-features --features middleware,kms-aws,postgres +just migrate-pg +RUSTFLAGS="-Awarnings" cargo run --no-default-features --features postgres +``` + +### MySQL + +```bash +cargo build --release --features release +RUSTFLAGS="-Awarnings" cargo run --features release +``` + +## Docker Builds Without Compose + +```bash +docker build --platform=linux/amd64 -t decision-engine-mysql:local -f Dockerfile . +docker build --platform=linux/amd64 -t decision-engine-pg:local -f Dockerfile.postgres . +``` + +Example container run: + +```bash +docker run --platform=linux/amd64 \ + -v $(pwd)/config/docker-configuration.toml:/local/config/development.toml \ + -p 8080:8080 \ + decision-engine-pg:local +``` + +## Helm + +Chart location: `helm-charts/` + +```bash +cd helm-charts +helm dependency build +helm install my-release . +``` + +For image overrides, use `image.repository`, `image.version`, and `image.pullPolicy`. Verify the rendered templates directly when troubleshooting chart behavior. + +## Verification + +```bash +curl http://localhost:8080/health +``` + +Expected response: + +```json +{"message":"Health is good"} +``` + +Dashboard profiles also expose: + +- Dashboard: `http://localhost:8081/dashboard/` +- Docs: `http://localhost:8081/introduction` + +Monitoring profile also exposes: + +- Prometheus: `http://localhost:9090` +- Grafana: `http://localhost:3000` + +## Troubleshooting + +### Recreate a profile with clean volumes + +```bash +docker compose --profile postgres-ghcr down -v +docker compose --profile postgres-ghcr up -d +``` + +### Inspect migration jobs + +```bash +docker compose logs db-migrator-postgres +docker compose logs db-migrator +``` + +### Common next files to inspect + +- `docker-compose.yaml` +- `config/docker-configuration.toml` +- `src/config.rs` +- `src/app.rs` diff --git a/docs/logo/dark.svg b/docs/logo/dark.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/docs/logo/dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/logo/decision-engine-dark.svg b/docs/logo/decision-engine-dark.svg new file mode 100644 index 00000000..d96f4aed --- /dev/null +++ b/docs/logo/decision-engine-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/docs/logo/decision-engine-docs-dark.svg b/docs/logo/decision-engine-docs-dark.svg new file mode 100644 index 00000000..5230088e --- /dev/null +++ b/docs/logo/decision-engine-docs-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/docs/logo/decision-engine-docs-light.svg b/docs/logo/decision-engine-docs-light.svg new file mode 100644 index 00000000..ffba1dc0 --- /dev/null +++ b/docs/logo/decision-engine-docs-light.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/docs/logo/decision-engine-light.svg b/docs/logo/decision-engine-light.svg new file mode 100644 index 00000000..5c18998b --- /dev/null +++ b/docs/logo/decision-engine-light.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/docs/logo/light.svg b/docs/logo/light.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/docs/logo/light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json new file mode 100644 index 00000000..c2a73111 --- /dev/null +++ b/docs/mint.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "name": "Juspay Decision Engine", + "logo": { + "dark": "/logo/decision-engine-docs-dark.svg", + "light": "/logo/decision-engine-docs-light.svg" + }, + "favicon": "/favicon.svg", + "colors": { + "primary": "#0069ED", + "light": "#4D9CFF", + "dark": "#0050B4" + }, + "openapi": [ + "openapi.json" + ], + "api": { + "baseUrl": "http://localhost:8080", + "playground": { + "mode": "show" + } + }, + "navigation": [ + { + "group": "Overview", + "pages": [ + "introduction", + "installation", + "local-setup", + "configuration", + "dashboard", + "analytics", + "payment-audit", + "api-reference" + ] + }, + { + "group": "Database Setup", + "pages": [ + "setup-guide-postgres", + "setup-guide-mysql" + ] + }, + { + "group": "Health", + "pages": [ + "api-reference/endpoint/healthCheck" + ] + }, + { + "group": "Merchant API", + "pages": [ + "api-reference/endpoint/createMerchant", + "api-reference/endpoint/getMerchant", + "api-reference/endpoint/deleteMerchant" + ] + }, + { + "group": "Rule Based Routing", + "pages": [ + "api-reference/endpoint/createRoutingRule", + "api-reference/endpoint/activateRoutingRule", + "api-reference/endpoint/listRoutingRules", + "api-reference/endpoint/getActiveRoutingRule", + "api-reference/endpoint/evaluateRoutingRule" + ] + }, + { + "group": "Dynamic Routing APIs", + "pages": [ + "api-reference/endpoint/decideGateway", + "api-reference/endpoint/updateGatewayScore", + "api-reference/endpoint/createRuleConfig", + "api-reference/endpoint/getRuleConfig", + "api-reference/endpoint/updateRuleConfig", + "api-reference/endpoint/deleteRuleConfig" + ] + } + ], + "footerSocials": { + "github": "https://github.com/juspay/decision-engine" + } +} diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 00000000..a886da84 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,1407 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Decision Engine", + "description": "HTTP API for gateway decisions, score feedback, merchant configuration, and routing rule management.", + "version": "1.2.1", + "contact": { + "name": "Juspay", + "url": "https://github.com/juspay/decision-engine" + }, + "license": { + "name": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.html" + } + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Local development" + } + ], + "tags": [ + { + "name": "Health", + "description": "Service liveness check" + }, + { + "name": "Dynamic Routing APIs", + "description": "Dynamic routing APIs for gateway decisioning, score updates, and /rule/* configuration." + }, + { + "name": "Merchant API", + "description": "Merchant account management APIs." + }, + { + "name": "Rule Based Routing", + "description": "APIs under /routing/* for rule-based routing creation, activation, listing, and evaluation." + } + ], + "paths": { + "/health": { + "get": { + "operationId": "healthCheck", + "tags": [ + "Health" + ], + "summary": "Health check", + "description": "Returns a simple health status. Use this to verify the service is running.", + "responses": { + "200": { + "description": "Service is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + }, + "example": { + "message": "Health is good" + } + } + } + } + } + } + }, + "/decide-gateway": { + "post": { + "operationId": "decideGateway", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /decide-gateway", + "description": "Core routing decision API. Given a payment context and a list of eligible gateways, returns the optimal gateway to route to.\n\nThe engine applies a sequence of filters (currency, card brand, auth type, EMI, etc.) then scores remaining gateways using success rate history, elimination status, and contract obligations.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DecideGatewayRequest" + }, + "examples": { + "sr_routing": { + "summary": "SR-based routing", + "value": { + "merchantId": "test_merchant", + "paymentInfo": { + "paymentId": "pay_001", + "amount": 1000.0, + "currency": "USD", + "country": "US", + "customerId": "cust_123", + "paymentType": "ORDER_PAYMENT", + "paymentMethodType": "CARD", + "paymentMethod": "CREDIT", + "authType": "THREE_DS", + "cardIsin": "411111" + }, + "eligibleGatewayList": [ + "stripe", + "paypal", + "adyen" + ], + "rankingAlgorithm": "SrBasedRouting", + "eliminationEnabled": false + } + }, + "debit_routing": { + "summary": "Debit/network-based routing", + "value": { + "merchantId": "test_merchant", + "paymentInfo": { + "paymentId": "pay_002", + "amount": 500.0, + "currency": "USD", + "paymentType": "ORDER_PAYMENT", + "paymentMethodType": "CARD", + "paymentMethod": "DEBIT" + }, + "eligibleGatewayList": [ + "stripe", + "braintree" + ], + "rankingAlgorithm": "NtwBasedRouting", + "eliminationEnabled": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Gateway decision result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DecidedGateway" + }, + "example": { + "decided_gateway": "stripe", + "routing_approach": "SR_SELECTION_V3_ROUTING", + "gateway_priority_map": { + "stripe": 0.94, + "adyen": 0.87, + "paypal": 0.72 + }, + "routing_dimension": "CARD_BRAND", + "routing_dimension_level": "visa", + "reset_approach": "NoReset", + "is_scheduled_outage": false, + "is_rust_based_decider": true, + "latency": 8 + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/update-gateway-score": { + "post": { + "operationId": "updateGatewayScore", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /update-gateway-score", + "description": "Feed a transaction outcome back into the success-rate model. Call this after every transaction so the engine has accurate SR data for future routing decisions.\n\nA `CHARGED` status increases the gateway's SR; failure statuses (`AUTHENTICATION_FAILED`, `AUTHORIZATION_FAILED`, etc.) decrease it.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGatewayScoreRequest" + }, + "example": { + "merchantId": "test_merchant", + "gateway": "stripe", + "paymentId": "pay_001", + "status": "CHARGED", + "gatewayReferenceId": "stripe_ref_001", + "enforceDynamicRoutingFailure": false + } + } + } + }, + "responses": { + "200": { + "description": "Score updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScoreResponse" + }, + "example": { + "message": "Score updated", + "merchant_id": "test_merchant", + "gateway": "stripe", + "payment_id": "pay_001" + } + } + } + } + } + } + }, + "/merchant-account/create": { + "post": { + "operationId": "createMerchant", + "tags": [ + "Merchant API" + ], + "summary": "Create merchant", + "description": "Register a new merchant account. The merchant ID is the primary identifier used in all subsequent routing and scoring calls.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMerchantRequest" + }, + "example": { + "merchant_id": "my_merchant", + "gateway_success_rate_based_decider_input": null + } + } + } + }, + "responses": { + "200": { + "description": "Merchant created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantAccount" + }, + "example": { + "message": "Merchant created", + "merchant_id": "my_merchant", + "gateway_success_rate_based_decider_input": null + } + } + } + } + } + } + }, + "/merchant-account/{merchantId}": { + "get": { + "operationId": "getMerchant", + "tags": [ + "Merchant API" + ], + "summary": "Get merchant", + "description": "Retrieve a merchant account by ID.", + "parameters": [ + { + "name": "merchantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "my_merchant" + } + ], + "responses": { + "200": { + "description": "Merchant account", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantAccount" + } + } + } + }, + "404": { + "description": "Merchant not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteMerchant", + "tags": [ + "Merchant API" + ], + "summary": "Delete merchant", + "description": "Delete a merchant account and all associated routing configuration.", + "parameters": [ + { + "name": "merchantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "my_merchant" + } + ], + "responses": { + "200": { + "description": "Merchant deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteResponse" + } + } + } + } + } + } + }, + "/routing/create": { + "post": { + "operationId": "createRoutingRule", + "tags": [ + "Rule Based Routing" + ], + "summary": "POST /routing/create", + "description": "Create a routing rule for a merchant. Supported algorithm types are `priority`, `single`, `volume_split`, and `advanced`. For `advanced`, same-level `statements` act as OR branches, each `condition` array is an AND block, and `nested` allows deeper hierarchical matching.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRoutingRuleRequest" + }, + "examples": { + "priority": { + "summary": "Priority-based rule", + "value": { + "name": "default-priority", + "description": "Route to stripe first, fallback to paypal", + "created_by": "test_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "priority", + "data": [ + { + "gateway_name": "stripe", + "gateway_id": null + }, + { + "gateway_name": "paypal", + "gateway_id": null + }, + { + "gateway_name": "adyen", + "gateway_id": null + } + ] + } + } + }, + "volume_split": { + "summary": "Volume split rule", + "value": { + "name": "ab-test-split", + "description": "", + "created_by": "test_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "volume_split", + "data": [ + { + "split": 70, + "output": { + "gateway_name": "stripe", + "gateway_id": null + } + }, + { + "split": 30, + "output": { + "gateway_name": "paypal", + "gateway_id": null + } + } + ] + } + } + }, + "single": { + "summary": "Single connector rule", + "value": { + "name": "always-stripe", + "description": "Always route to stripe", + "created_by": "test_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "single", + "data": { + "gateway_name": "stripe", + "gateway_id": null + } + } + } + }, + "advanced_or": { + "summary": "Advanced rule with OR branches", + "value": { + "name": "wallet-or-credit-card", + "description": "Route wallet traffic to checkout and credit cards to stripe first", + "created_by": "test_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "advanced", + "data": { + "globals": {}, + "default_selection": { + "priority": [ + { + "gateway_name": "adyen", + "gateway_id": null + } + ] + }, + "rules": [ + { + "name": "wallet_or_credit_card", + "routing_type": "priority", + "output": { + "priority": [ + { + "gateway_name": "checkout", + "gateway_id": null + }, + { + "gateway_name": "stripe", + "gateway_id": null + } + ] + }, + "statements": [ + { + "condition": [ + { + "lhs": "payment_method", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "wallet" + }, + "metadata": {} + } + ], + "nested": null + }, + { + "condition": [ + { + "lhs": "payment_method", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "card" + }, + "metadata": {} + }, + { + "lhs": "card_type", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "credit" + }, + "metadata": {} + } + ], + "nested": null + } + ] + } + ], + "metadata": {} + } + } + } + }, + "advanced_nested": { + "summary": "Advanced rule with nested conditions", + "value": { + "name": "visa-usd-nested-routing", + "description": "Use nested card checks, then split matched traffic by percentage", + "created_by": "test_merchant", + "algorithm_for": "payment", + "algorithm": { + "type": "advanced", + "data": { + "globals": {}, + "default_selection": { + "volume_split": [ + { + "split": 60, + "output": { + "gateway_name": "stripe", + "gateway_id": null + } + }, + { + "split": 40, + "output": { + "gateway_name": "checkout", + "gateway_id": null + } + } + ] + }, + "rules": [ + { + "name": "card_credit_visa", + "routing_type": "volume_split", + "output": { + "volume_split": [ + { + "split": 80, + "output": { + "gateway_name": "stripe", + "gateway_id": null + } + }, + { + "split": 20, + "output": { + "gateway_name": "adyen", + "gateway_id": null + } + } + ] + }, + "statements": [ + { + "condition": [ + { + "lhs": "payment_method", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "card" + }, + "metadata": {} + } + ], + "nested": [ + { + "condition": [ + { + "lhs": "card_type", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "credit" + }, + "metadata": {} + } + ], + "nested": [ + { + "condition": [ + { + "lhs": "card_network", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "visa" + }, + "metadata": {} + }, + { + "lhs": "currency", + "comparison": "equal", + "value": { + "type": "enum_variant", + "value": "USD" + }, + "metadata": {} + } + ], + "nested": null + } + ] + } + ] + } + ] + } + ], + "metadata": {} + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Routing rule created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingRule" + } + } + } + } + } + } + }, + "/routing/activate": { + "post": { + "operationId": "activateRoutingRule", + "tags": [ + "Rule Based Routing" + ], + "summary": "POST /routing/activate", + "description": "Activate a routing rule for a merchant.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateRoutingRuleRequest" + }, + "example": { + "created_by": "test_merchant", + "routing_algorithm_id": "rule_abc123" + } + } + } + }, + "responses": { + "200": { + "description": "Rule activated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingRule" + } + } + } + } + } + } + }, + "/routing/list/{created_by}": { + "post": { + "operationId": "listRoutingRules", + "tags": [ + "Rule Based Routing" + ], + "summary": "POST /routing/list/{created_by}", + "description": "List routing rules for a merchant.", + "parameters": [ + { + "name": "created_by", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "test_merchant" + } + ], + "responses": { + "200": { + "description": "List of routing rules", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingRule" + } + } + } + } + } + } + } + }, + "/routing/list/active/{created_by}": { + "post": { + "operationId": "getActiveRoutingRule", + "tags": [ + "Rule Based Routing" + ], + "summary": "POST /routing/list/active/{created_by}", + "description": "Get the active routing rule for a merchant.", + "parameters": [ + { + "name": "created_by", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "test_merchant" + } + ], + "responses": { + "200": { + "description": "Active routing rule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingRule" + } + } + } + }, + "404": { + "description": "No active rule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/routing/evaluate": { + "post": { + "operationId": "evaluateRoutingRule", + "tags": [ + "Rule Based Routing" + ], + "summary": "POST /routing/evaluate", + "description": "Evaluate routing output for the provided payment context.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvaluateRoutingRequest" + }, + "example": { + "merchantId": "test_merchant", + "paymentInfo": { + "paymentId": "pay_001", + "amount": 1000.0, + "currency": "USD", + "paymentType": "ORDER_PAYMENT", + "paymentMethodType": "CARD", + "paymentMethod": "CREDIT" + } + } + } + } + }, + "responses": { + "200": { + "description": "Evaluation result with ordered gateway list", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/rule/create": { + "post": { + "operationId": "createRuleConfig", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /rule/create", + "description": "Create Euclid rule configuration for success-rate, elimination, or debit-routing settings.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigRequest" + }, + "examples": { + "success_rate": { + "summary": "Success rate config", + "value": { + "merchant_id": "test_merchant", + "config": { + "type": "successRate", + "data": { + "defaultBucketSize": 20, + "defaultLatencyThreshold": null, + "defaultHedgingPercent": null + } + } + } + }, + "elimination": { + "summary": "Elimination config", + "value": { + "merchant_id": "test_merchant", + "config": { + "type": "elimination", + "data": { + "bucketSize": 5, + "eliminationThreshold": 0.2 + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Rule config created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigResponse" + } + } + } + } + } + } + }, + "/rule/get": { + "post": { + "operationId": "getRuleConfig", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /rule/get", + "description": "Fetch Euclid rule configuration for a merchant and algorithm type.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigGetRequest" + }, + "example": { + "merchant_id": "test_merchant", + "algorithm": "successRate" + } + } + } + }, + "responses": { + "200": { + "description": "Rule config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigResponse" + } + } + } + } + } + } + }, + "/rule/update": { + "post": { + "operationId": "updateRuleConfig", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /rule/update", + "description": "Update Euclid rule configuration for a merchant.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigRequest" + }, + "example": { + "merchant_id": "test_merchant", + "config": { + "type": "successRate", + "data": { + "defaultBucketSize": 30, + "defaultHedgingPercent": 0.1 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Rule config updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigResponse" + } + } + } + } + } + } + }, + "/rule/delete": { + "post": { + "operationId": "deleteRuleConfig", + "tags": [ + "Dynamic Routing APIs" + ], + "summary": "POST /rule/delete", + "description": "Delete Euclid rule configuration for a merchant and algorithm type.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleConfigGetRequest" + }, + "example": { + "merchant_id": "test_merchant", + "algorithm": "successRate" + } + } + } + }, + "responses": { + "200": { + "description": "Rule config deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HealthResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Health is good" + } + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "DeleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "PaymentInfo": { + "type": "object", + "required": [ + "paymentId", + "amount", + "currency", + "paymentType", + "paymentMethodType", + "paymentMethod" + ], + "properties": { + "paymentId": { + "type": "string", + "example": "pay_001" + }, + "amount": { + "type": "number", + "format": "float", + "example": 1000.0 + }, + "currency": { + "type": "string", + "example": "USD" + }, + "country": { + "type": "string", + "example": "US" + }, + "customerId": { + "type": "string", + "example": "cust_123" + }, + "paymentType": { + "type": "string", + "enum": [ + "ORDER_PAYMENT", + "MANDATE_PAYMENT" + ], + "example": "ORDER_PAYMENT" + }, + "paymentMethodType": { + "type": "string", + "enum": [ + "CARD", + "UPI", + "WALLET", + "NETBANKING" + ], + "example": "CARD" + }, + "paymentMethod": { + "type": "string", + "enum": [ + "CREDIT", + "DEBIT" + ], + "example": "CREDIT" + }, + "authType": { + "type": "string", + "enum": [ + "THREE_DS", + "NO_THREE_DS" + ], + "example": "THREE_DS" + }, + "cardIsin": { + "type": "string", + "example": "411111" + } + } + }, + "DecideGatewayRequest": { + "type": "object", + "required": [ + "merchantId", + "paymentInfo", + "eligibleGatewayList", + "rankingAlgorithm" + ], + "properties": { + "merchantId": { + "type": "string", + "example": "test_merchant" + }, + "paymentInfo": { + "$ref": "#/components/schemas/PaymentInfo" + }, + "eligibleGatewayList": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "stripe", + "paypal", + "adyen" + ] + }, + "rankingAlgorithm": { + "type": "string", + "enum": [ + "SrBasedRouting", + "PlBasedRouting", + "NtwBasedRouting" + ], + "example": "SrBasedRouting" + }, + "eliminationEnabled": { + "type": "boolean", + "default": false + } + } + }, + "DecidedGateway": { + "type": "object", + "properties": { + "decided_gateway": { + "type": "string", + "example": "stripe" + }, + "routing_approach": { + "type": "string", + "example": "SR_SELECTION_V3_ROUTING" + }, + "gateway_priority_map": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "example": { + "stripe": 0.94, + "adyen": 0.87, + "paypal": 0.72 + } + }, + "routing_dimension": { + "type": "string" + }, + "routing_dimension_level": { + "type": "string" + }, + "reset_approach": { + "type": "string" + }, + "is_scheduled_outage": { + "type": "boolean" + }, + "is_rust_based_decider": { + "type": "boolean" + }, + "latency": { + "type": "number" + } + } + }, + "UpdateGatewayScoreRequest": { + "type": "object", + "required": [ + "merchantId", + "gateway", + "paymentId", + "status" + ], + "properties": { + "merchantId": { + "type": "string", + "example": "test_merchant" + }, + "gateway": { + "type": "string", + "example": "stripe" + }, + "paymentId": { + "type": "string", + "example": "pay_001" + }, + "status": { + "type": "string", + "enum": [ + "CHARGED", + "AUTHENTICATION_FAILED", + "AUTHORIZATION_FAILED", + "JUSPAY_DECLINED", + "FAILURE" + ], + "example": "CHARGED" + }, + "gatewayReferenceId": { + "type": "string", + "example": "stripe_ref_001" + }, + "enforceDynamicRoutingFailure": { + "type": "boolean", + "default": false + } + } + }, + "UpdateScoreResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "merchant_id": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "payment_id": { + "type": "string" + } + } + }, + "CreateMerchantRequest": { + "type": "object", + "required": [ + "merchant_id" + ], + "properties": { + "merchant_id": { + "type": "string", + "example": "my_merchant" + }, + "gateway_success_rate_based_decider_input": { + "type": "string", + "nullable": true + } + } + }, + "MerchantAccount": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "merchant_id": { + "type": "string", + "example": "my_merchant" + }, + "gateway_success_rate_based_decider_input": { + "type": "string", + "nullable": true + } + } + }, + "CreateRoutingRuleRequest": { + "type": "object", + "required": [ + "name", + "created_by", + "algorithm" + ], + "properties": { + "name": { + "type": "string", + "example": "default-priority" + }, + "description": { + "type": "string", + "example": "" + }, + "created_by": { + "type": "string", + "example": "test_merchant" + }, + "algorithm_for": { + "type": "string", + "enum": [ + "payment", + "payout", + "three_ds_authentication" + ], + "default": "payment" + }, + "algorithm": { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + } + }, + "RoutingAlgorithm": { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority", + "single", + "volume_split", + "advanced" + ], + "example": "priority" + }, + "data": { + "description": "Depends on type: array of ConnectorInfo ({gateway_name, gateway_id}) for `priority`; single ConnectorInfo for `single`; array of {split, connectors} for `volume_split`; Euclid AST Program for `advanced`" + } + } + }, + "ActivateRoutingRuleRequest": { + "type": "object", + "required": [ + "created_by", + "routing_algorithm_id" + ], + "properties": { + "created_by": { + "type": "string", + "example": "test_merchant" + }, + "routing_algorithm_id": { + "type": "string", + "example": "rule_abc123" + } + } + }, + "RoutingRule": { + "type": "object", + "properties": { + "rule_id": { + "type": "string", + "nullable": true, + "example": "rule_abc123" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_by": { + "type": "string", + "example": "test_merchant" + }, + "algorithm_for": { + "type": "string" + }, + "algorithm": { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + } + }, + "EvaluateRoutingRequest": { + "type": "object", + "required": [ + "merchantId", + "paymentInfo" + ], + "properties": { + "merchantId": { + "type": "string", + "example": "test_merchant" + }, + "paymentInfo": { + "$ref": "#/components/schemas/PaymentInfo" + } + } + }, + "RuleConfigRequest": { + "type": "object", + "required": [ + "merchant_id", + "config" + ], + "properties": { + "merchant_id": { + "type": "string", + "example": "test_merchant" + }, + "config": { + "type": "object", + "description": "Tagged config variant. `type` is `successRate`, `elimination`, or `debitRouting`. `data` holds the variant-specific fields." + } + } + }, + "RuleConfigGetRequest": { + "type": "object", + "required": [ + "merchant_id", + "algorithm" + ], + "properties": { + "merchant_id": { + "type": "string", + "example": "test_merchant" + }, + "algorithm": { + "type": "string", + "enum": [ + "successRate", + "elimination", + "debitRouting" + ], + "example": "successRate" + } + } + }, + "RuleConfigResponse": { + "type": "object", + "properties": { + "merchant_id": { + "type": "string" + }, + "config": { + "type": "object" + } + } + } + } + } +} diff --git a/docs/payment-audit.mdx b/docs/payment-audit.mdx new file mode 100644 index 00000000..371fa1a8 --- /dev/null +++ b/docs/payment-audit.mdx @@ -0,0 +1,83 @@ +--- +title: "Payment Audit" +description: "Search a payment or request and inspect the full routing timeline" +--- + +## What It Shows + +Payment Audit is the operator-facing timeline view for a single payment or request. + +Use it when you want to answer questions like: + +- which gateway was decided for this payment +- which priority rule was hit +- whether a score update succeeded or failed +- what structured event payload was captured at each step + +The page is designed for exact lookups first and broad operator filtering second. + +## Search Inputs + +The page supports these filters: + +- `payment_id` +- `request_id` +- `gateway` +- `route` +- `status` +- `event_type` +- time window: `15m`, `1h`, `24h` +- scope: current merchant or all merchants + +Use an exact `payment_id` when you have it. If you only have a request trace, use `request_id`. + +## Timeline Stages + +The timeline is built from analytics events captured by the live request flows. + +Current stages include: + +- `gateway_decided` +- `rule_applied` +- `score_updated` +- `request_failed` +- `validation_failed` +- `request_parse_failed` +- `score_update_failed` + +The raw event type remains visible as well, for example `decision`, `rule_hit`, `score_snapshot`, or `error`. + +## Data Sources + +Payment Audit reads from the same `analytics_event` store used by the analytics surface. + +Relevant fields include: + +- `payment_id` +- `request_id` +- `event_stage` +- `route` +- `gateway` +- `routing_approach` +- `rule_name` +- `status` +- `error_code` +- `error_message` +- `details` + +The `details` field is shown as structured JSON when available. + +## Route + +When the dashboard profile is running, open: + +- `http://localhost:8081/dashboard/audit` + +## Related Files + +- `website/src/components/pages/PaymentAuditPage.tsx` +- `src/routes/analytics.rs` +- `src/analytics/models.rs` +- `src/analytics/service.rs` +- `src/decider/gatewaydecider/` +- `src/feedback/gateway_scoring_service.rs` diff --git a/docs/readme/analytics-overview.gif b/docs/readme/analytics-overview.gif new file mode 100644 index 00000000..426597ff Binary files /dev/null and b/docs/readme/analytics-overview.gif differ diff --git a/docs/readme/analytics-overview.png b/docs/readme/analytics-overview.png new file mode 100644 index 00000000..690ac212 Binary files /dev/null and b/docs/readme/analytics-overview.png differ diff --git a/docs/setup-guide-mysql.md b/docs/setup-guide-mysql.md index 7b17c83e..cd3b051a 100644 --- a/docs/setup-guide-mysql.md +++ b/docs/setup-guide-mysql.md @@ -1,247 +1,55 @@ -# Setup Instructions: +# MySQL Setup Guide -Follow the steps below to set up and run the project locally. +Use this page when the task is explicitly MySQL-specific. For the full local matrix, use [Local Setup Guide](local-setup.md). -## 1. Clone the Repository +## Compose Commands -```bash -git clone https://github.com/juspay/decision-engine.git -cd decision-engine -``` ---- - -## 2. Install Docker - -Make sure Docker is installed on your system. -You can download and install Docker Desktop from the below links. - -- Mac - https://docs.docker.com/desktop/setup/install/mac-install/ -- Windows - https://docs.docker.com/desktop/setup/install/windows-install/ -- Linux - https://docs.docker.com/desktop/setup/install/linux/ - ---- - -## 3. Run the Project - -### a. First-Time Setup - -If you're setting up the environment for the first time, run: +### Published-image track ```bash -make init +export DECISION_ENGINE_TAG=v1.4 +docker compose --profile mysql-ghcr up -d ``` -To build it with postgres DB use this instead -``` -make init-pg -``` - -This command performs the following under the hood: +### Published-image track with dashboard + docs ```bash -docker-compose run --rm db-migrator && docker-compose up open-router +docker compose --profile dashboard-mysql-ghcr up -d ``` -This will: -- Set up the environment -- Set up the database with the required schema -- Sets up redis and the server for running the application -- Push the configs defined in the config.yaml & the static rules defined for routing in priority_logic.txt to the DB -### b. Start the Server (without resetting DB) - -If the DB schema is already set up and you don't want to reset the DB, use: +### Local-build track ```bash -make run +docker compose --profile mysql-local up -d --build ``` -**System Requirements:** Approximately 2GB of disk space - -After successful setup, the server will start running. -### c. Update Configs / Static Rules -To update the configs (from the config.yaml file) or the static rules (from priority_logic.txt), run: +### Local-build track with dashboard + docs ```bash -make update-config +docker compose --profile dashboard-mysql-local up -d --build ``` -### d. Stop Running Instances - -To stop the running Docker instances: +## Make Targets ```bash -make stop +make init-mysql-ghcr +make init-mysql-local ``` ---- - -## 4. Running Local Code Changes - -If you've made changes to the code locally and want to test them: - -### a. Initialize Local Environment +## Source Run ```bash -make init-local +cargo build --release --features release +RUSTFLAGS="-Awarnings" cargo run --features release ``` -This command performs the following under the hood: +## Verify ```bash -docker-compose run --rm db-migrator && docker-compose up open-router-local +curl http://localhost:8080/health ``` -### b. Run Locally - -```bash -make run-local -``` - -## Using the Decision Engine APIs - -image - -### 1. Get the Gateway Decision - -Use the following cURL with payment info to get the gateway-decision: - -```bash -curl --location 'http://localhost:8080/decide-gateway' \ ---header 'Content-Type: application/json' \ ---data '{ - "merchantId": "test_merchant1", - "eligibleGatewayList": ["PAYU", "RAZORPAY", "PAYTM_V2"], - "rankingAlgorithm": "SR_BASED_ROUTING", - "eliminationEnabled": true, - "paymentInfo": { - "paymentId": "PAY12345", - "amount": 100.50, - "currency": "USD", - "customerId": "CUST12345", - "udfs": null, - "preferredGateway": null, - "paymentType": "ORDER_PAYMENT", - "metadata": null, - "internalMetadata": null, - "isEmi": false, - "emiBank": null, - "emiTenure": null, - "paymentMethodType": "UPI", - "paymentMethod": "UPI_PAY", - "paymentSource": null, - "authType": null, - "cardIssuerBankName": null, - "cardIsin": null, - "cardType": null, - "cardSwitchProvider": null - } -}' -``` - -#### Response Example - -```json -{ - "decided_gateway": "PAYTM_V2", - "gateway_priority_map": { - "PAYU": 1.0, - "RAZORPAY": 1.0, - "PAYTM_V2": 1.0 - }, - "filter_wise_gateways": null, - "priority_logic_tag": "PL_TEST", - "routing_approach": "PRIORITY_LOGIC", - "gateway_before_evaluation": "RAZORPAY", - "priority_logic_output": { - "isEnforcement": false, - "gws": [], - "priorityLogicTag": "PL_TEST", - "gatewayReferenceIds": {}, - "primaryLogic": { - "name": "PL_TEST", - "status": "SUCCESS", - "failure_reason": "NO_ERROR" - }, - "fallbackLogic": null - }, - "reset_approach": "NO_RESET", - "routing_dimension": "ORDER_PAYMENT, UPI, UPI_PAY", - "routing_dimension_level": "PM_LEVEL", - "is_scheduled_outage": false, - "gateway_mga_id_map": null -} -``` - -### 2. Update Gateway Score - -This will update the decision-engine with the transaction status to optimize for future decisions: - -```bash -curl --location 'http://localhost:8080/update-gateway-score' \ ---header 'Content-Type: application/json' \ ---data '{ - "merchantId": "test_merchant1", - "gateway": "PAYU", - "gatewayReferenceId": null, - "status": "PENDING_VBV", - "paymentId": "123" -}' -``` +Dashboard profiles also expose: -## Glossary - -### Gateway Decision API Parameters - -| Parameter | Description | -|-----------|-------------| -| `merchantId` | Unique identifier assigned to the merchant using the API | -| `eligibleGatewayList` | List of gateways eligible to process the transaction | -| `rankingAlgorithm` | Specifies the routing algorithm to use (`SR_BASED_ROUTING` or `PL_BASED_ROUTING`) | -| `eliminationEnabled` | Boolean flag to enable/disable downtime detection in routing decisions | - -#### Payment Info Parameters - -| Parameter | Description | -|-----------|-------------| -| `paymentId` | Unique identifier for the transaction (mandatory) | -| `amount` | Transaction amount to be processed | -| `currency` | Currency code for the transaction (e.g., INR, USD) | -| `paymentType` | Indicates payment purpose (e.g., `ORDER_PAYMENT`, `MANDATE_REGISTER`, `EMANDATE_REGISTER`) | -| `paymentMethodType` | Type of payment method (e.g., `CARD`, `UPI`, `WALLET`, `NET BANKING`) | -| `paymentMethod` | Specific subcategory within the chosen paymentMethodType | - -### Response Fields - -| Field | Description | -|-------|-------------| -| `decided_gateway` | The gateway chosen by the decision engine for routing the transaction | -| `gateway_priority_map` | Scores for each gateway used in making the routing decision | -| `filter_wise_gateways` | List of eligible connectors (if Eligibility Check/Orchestration is used) | -| `priority_logic_tag` | Unique identifier for the specific Static Rule defined in the YAML file | -| `routing_approach` | The specific routing approach used for processing the transaction | -| `gateway_before_evaluation` | The gateway decided before downtime evaluation | -| `routing_dimension` | The dimensions on which routing decisions are made | -| `routing_dimension_level` | The level at which routing decisions are made (e.g., `PM_LEVEL`) | -| `is_scheduled_outage` | Returns true if the routing decision is impacted by scheduled outages | - -### Update Gateway Score Parameters - -| Parameter | Description | -|-----------|-------------| -| `merchantId` | Unique identifier assigned to the merchant using the API | -| `gateway` | The gateway to which the transaction was routed | -| `gatewayReferenceId` | Reference ID from the gateway | -| `status` | Transaction status used to update the routing score | -| `paymentId` | Unique identifier for the transaction | - -### Configuration YAML Parameters - -| Parameter | Description | -|-----------|-------------| -| `merchant_id` | Unique identifier assigned to the merchant | -| `priority_logic.script` | The file name in which static rules are defined | -| `priority_logic.tag` | Unique identifier for a static rule defined | -| `elimination_config.threshold` | PG health threshold (PGs below this are deprioritized) | -| `defaultBucketSize` | Last 'n' transactions to consider for computing SR scores | -| `defaultHedgingPercent` | Percentage of traffic for exploration of lower-ranked gateways | -| `subLevelInputConfig` | Define granular configs at PMT/PM level | +- `http://localhost:8081/dashboard/` +- `http://localhost:8081/introduction` diff --git a/docs/setup-guide-postgres.md b/docs/setup-guide-postgres.md index 187fd616..d6a47758 100644 --- a/docs/setup-guide-postgres.md +++ b/docs/setup-guide-postgres.md @@ -1,195 +1,56 @@ -# PostgreSQL Setup Guide for Decision Engine +# PostgreSQL Setup Guide -This guide provides instructions on how to set up the Decision Engine with PostgreSQL as the database. There are several ways to achieve this, depending on your preference for using Docker or a local PostgreSQL installation. +Use this page when the task is explicitly PostgreSQL-specific. For the complete local matrix, use [Local Setup Guide](local-setup.md). -## Prerequisites +## Compose Commands -Before you begin, ensure you have the necessary tools installed based on your chosen setup method. +### Published-image track -**Common Tools (potentially needed for all methods):** +```bash +export DECISION_ENGINE_TAG=v1.4 +docker compose --profile postgres-ghcr up -d +``` -* A text editor or IDE for viewing/editing configuration files. -* Git for cloning the project repository. -* Rust and Cargo: Essential to build and run the Decision Engine application from source directly on your host machine. +### Published-image track with dashboard + docs -**For All Docker-Based Setups:** +```bash +docker compose --profile dashboard-postgres-ghcr up -d +``` -* **Docker and Docker Compose**: Essential for running the application and database in containers. +### Local-build track +```bash +docker compose --profile postgres-local up -d --build +``` -**For Full Local Development Setup:** +### Local-build track with dashboard + docs -* **PostgreSQL**: Needed if you are managing a local PostgreSQL instance directly (e.g., for creating databases, running `just resurrect`). -* **Just**: A command runner used for some of the local setup scripts (e.g., `just resurrect`, `just migrate-pg`). You can install it by following the instructions [here](https://github.com/casey/just#installation). -* **Diesel CLI (with PostgreSQL feature)**: Required for managing database migrations when running against a local PostgreSQL database. Install using: +```bash +docker compose --profile dashboard-postgres-local up -d --build +``` - ```bash - cargo install diesel_cli --no-default-features --features postgres - ``` +## Make Targets -## Setup Options +```bash +make init-pg-ghcr +make init-pg-local +``` -Choose one of the following methods to set up the Decision Engine with PostgreSQL: +## Source Run -### 1. Using Pre-built Docker Image (Recommended for quick setup) +```bash +cargo build --release --no-default-features --features middleware,kms-aws,postgres +just migrate-pg +RUSTFLAGS="-Awarnings" cargo run --no-default-features --features postgres +``` -This method uses pre-built Docker images for both the application and the PostgreSQL database. It's the simplest way to get started. +## Verify -**Steps:** +```bash +curl http://localhost:8080/health +``` -1. Navigate to the root directory of the `decision-engine` project. -2. Run the following command: +Dashboard profiles also expose: - ```bash - make init-pg - ``` - - This command will: - * Pull the necessary Docker images. - * Start a PostgreSQL container. - * Run database migrations using a `db-migrator-postgres` service defined in `docker-compose.yaml`. - * Start the Decision Engine application container (`open-router-pg`), configured to connect to the PostgreSQL database. - -The application should now be running and accessible. - -### 2. Using Local Changes with PostgreSQL in Docker - -This method is useful if you are making changes to the Decision Engine codebase and want to test them with a PostgreSQL database running in Docker. - -**Steps:** - -1. Navigate to the root directory of the `decision-engine` project. -2. Run the following command: - - ```bash - make init-local-pg - ``` - - This command will: - * Start a PostgreSQL container. - * Run database migrations using the `db-migrator-postgres` service. - * Build the Decision Engine application from your local source code within a Docker container (`open-router-local-pg`). - * Start the newly built application container, connected to the PostgreSQL database. - -This allows you to test your local code changes in an environment where PostgreSQL is managed by Docker. - -### 3. Using Local Changes with a Local PostgreSQL Installation - -This method is for developers who have PostgreSQL installed and running directly on their local machine (not in Docker). - -**Steps:** - -1. **Ensure PostgreSQL is Running:** - Make sure your local PostgreSQL server is running and accessible. - -2. **Set up Environment Variables (Optional but Recommended):** - The application and `diesel` CLI use environment variables to connect to the database. - - The `Justfile` provides defaults if these are not set: - * `DB_USER` (default: `db_user`) - * `DB_PASSWORD` (default: `db_pass`) - * `DB_HOST` (default: `localhost`) - * `DB_PORT` (default: `5432`) - * `DB_NAME` (default: `decision_engine_db`) - * `DATABASE_URL` (derived default: `postgresql://db_user:db_pass@localhost:5432/decision_engine_db`) - - **Example of exporting variables in your shell (using default values):** - ```bash - export DB_USER="db_user" - export DB_PASSWORD="db_pass" - export DB_HOST="localhost" - export DB_PORT="5432" - export DB_NAME="decision_engine_db" - # DATABASE_URL will be constructed by the application or Justfile if not set, - ``` -3. **Drop Database if Exist:** - - ```bash - just resurrect - ``` - This command will drop the Database and create a new one. -4. **Run Database Migrations:** - Apply the database schema migrations to your local PostgreSQL database: - - ```bash - just migrate-pg - ``` - This command uses `diesel migration run` with the PostgreSQL specific migration directory (`migrations_pg`) and configuration file (`diesel_pg.toml`). - -5. **Run the Application:** - Compile and run the Decision Engine application, ensuring it's built with PostgreSQL features: - ```bash - RUSTFLAGS="-Awarnings" cargo run --no-default-features --features postgres - ``` - * `RUSTFLAGS="-Awarnings"`: Suppresses warnings during compilation (optional). - * `--no-default-features --features postgres`: Ensures the application is compiled specifically for PostgreSQL, excluding default features (like MySQL support if it's a default) and including the `postgres` feature. - -The application will start and connect to your local PostgreSQL database. - -## Configuration - -The database connection URL is typically configured in: - -* `config/development.toml` (for local cargo runs) -* `config/docker-configuration.toml` (often mapped into Docker containers) - -Ensure the `[database.url]` points to your PostgreSQL instance. For example: -`url = "postgresql://db_user:db_pass@localhost:5432/decision_engine_db"` - -For Docker setups (`make init-pg`, `make init-local-pg`), the `docker-compose.yaml` file handles the service linking and environment variables to ensure the application container can connect to the PostgreSQL container (usually aliased as `postgres` or a similar hostname within the Docker network). - -## Troubleshooting - -* **Connection Issues:** - * Verify `DATABASE_URL` is correct and the PostgreSQL server is accessible from where the application is running (your local machine or Docker container). - * Check PostgreSQL logs for any connection errors. - * Ensure firewalls are not blocking the connection. -* **Migration Failures:** - * Check `diesel_pg.toml` for correct configuration. - * Ensure the migration files in `migrations_pg/` are correctly formatted. - * If you encounter issues with a "dirty" database state after a failed migration, you might need to manually resolve it in the database or use `diesel migration redo` (with caution). -* **`just` command not found:** - * Install `just` by following its official installation guide. -* **`diesel` command not found:** - * Install `diesel_cli` with PostgreSQL support: `cargo install diesel_cli --no-default-features --features postgres`. - -This guide should help you get the Decision Engine up and running with PostgreSQL. - - -# Metrics - -This document provides an overview of the metrics implementation in the routing layer. - -## Overview - -The metrics server is responsible for exposing key performance indicators of the application. It uses the `prometheus` crate to register and expose metrics in a format that can be scraped by a Prometheus server. - -## How it works - -The metrics server is built using `axum` and runs on a separate port from the main application server. It exposes a `/metrics` endpoint that returns the current state of all registered metrics. - -The server is initialized in the `metrics_server_builder` function in `src/metrics.rs`. This function creates a new `axum` router and binds it to the address specified in the configuration. - -## Available Metrics - -The following metrics are exposed by the server: - -### Counters - -- `api_requests_total`: A counter that tracks the total number of API requests received by the application. It has a single label, `endpoint`, which is the path of the API endpoint that was called. - -- `api_requests_by_status`: A counter that tracks the number of API requests grouped by endpoint and result status. It has two labels: `endpoint` and `status`. - -### Histograms - -- `api_latency_seconds`: A histogram that measures the latency of API calls. It has a single label, `endpoint`, and uses exponential buckets to provide a detailed view of the latency distribution. - -## How to check metrics - -To check the metrics, you can hit the following endpoint: - -```sh -curl http://127.0.0.1:9090/metrics -```` - -This will return a text-based representation of the current metrics, which can be ingested by a Prometheus server. +- `http://localhost:8081/dashboard/` +- `http://localhost:8081/introduction` diff --git a/docs/setup-guide.md b/docs/setup-guide.md index 89012211..f0ce7bc8 100644 --- a/docs/setup-guide.md +++ b/docs/setup-guide.md @@ -1,167 +1,18 @@ -# Installation Guide +# Setup Guide -This guide provides detailed instructions for installing Dynamo on different platforms. +Use this page as an index, not as the primary source. -## Prerequisites +## Canonical Guide -- Rust 1.78.0 or later -- Redis server 6.0 or later -- Git +- [Local Setup Guide](local-setup.md) -## From Source +## Database-Specific Guides -### 1. Clone the Repository +- [PostgreSQL Setup Guide](setup-guide-postgres.md) +- [MySQL Setup Guide](setup-guide-mysql.md) -```bash -git clone https://github.com/yourusername/dynamo.git -cd dynamo -``` +## When To Use Which Page -### 2. Install Dependencies - -#### Ubuntu/Debian - -```bash -# Install system dependencies -sudo apt-get update -sudo apt-get install -y pkg-config libssl-dev protobuf-compiler libpq-dev - -# Install Rust (if not already installed) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -``` - -#### macOS - -```bash -# Using Homebrew -brew install pkg-config openssl protobuf postgresql - -# Install Rust (if not already installed) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -``` - -### 3. Build Dynamo - -```bash -# Build in release mode -cargo build --release -``` - -### 4. Set Up Configuration - -```bash -# Copy example configuration -cp config/config.example.toml config/development.toml - -# Edit configuration as needed -# vi config/development.toml -``` - -### 5. Run Redis - -Make sure Redis is running on your system: - -```bash -# Install Redis if needed -sudo apt-get install redis-server # Ubuntu/Debian -brew install redis # macOS - -# Start Redis server -redis-server -``` - -### 6. Run Dynamo - -```bash -# Run with development configuration -./target/release/dynamo -``` - -## Using Docker - -### 1. Pull the Docker Image - -```bash -docker pull yourusername/dynamo:latest -``` - -Or build it yourself: - -```bash -docker build -t dynamo:latest . -``` - -### 2. Run with Docker - -```bash -# Run with default configuration -docker run -p 8000:8000 -p 9000:9000 dynamo:latest - -# Run with custom configuration -docker run -p 8000:8000 -p 9000:9000 \ - -v $(pwd)/config:/app/config \ - dynamo:latest -``` - -### 3. Docker Compose Setup - -Create a `docker-compose.yml` file: - -```yaml -version: '3' -services: - dynamo: - image: yourusername/dynamo:latest - ports: - - "8000:8000" - - "9000:9000" - volumes: - - ./config:/app/config - depends_on: - - redis - - redis: - image: redis:latest - ports: - - "6379:6379" -``` - -Run with Docker Compose: - -```bash -docker-compose up -d -``` - -## Building the WebAssembly Module - -The `procesmo` module can be built for web use: - -```bash -# Install wasm-pack if needed -cargo install wasm-pack - -# Build the WebAssembly module -cd crates/procesmo -wasm-pack build --target web -``` - -## Verifying Installation - -To verify that Dynamo is running correctly: - -```bash -# Check health status -curl http://localhost:8000/grpc.health.v1.Health/Check - -# Or using grpcurl -grpcurl -plaintext localhost:8000 grpc.health.v1.Health/Check -``` - -You should see a response indicating the service is running. - -## Next Steps - -- [Configure Dynamo](configuration.md) for your environment -- [Explore the API](api-reference.md) +- full local bring-up, Compose profiles, source builds, and Helm: `local-setup.md` +- PostgreSQL-specific shortcuts: `setup-guide-postgres.md` +- MySQL-specific shortcuts: `setup-guide-mysql.md` diff --git a/helm-charts/Chart.lock b/helm-charts/Chart.lock index d2744324..ec96fdf4 100644 --- a/helm-charts/Chart.lock +++ b/helm-charts/Chart.lock @@ -1,12 +1,12 @@ dependencies: - name: postgresql repository: https://charts.bitnami.com/bitnami - version: 12.5.9 + version: 18.5.14 - name: mysql repository: https://charts.bitnami.com/bitnami - version: 13.0.0 + version: 13.0.4 - name: redis repository: https://charts.bitnami.com/bitnami - version: 17.11.8 -digest: sha256:a30a5d9f1cd2bdc84f9f36f3909d75b6702b996ad454cff2737cd738303b6420 -generated: "2025-06-04T21:08:21.265499+05:30" + version: 25.3.9 +digest: sha256:9d54be26fed3d4599bdc5426e4a22ede83361404744e227266b41bee4b7d3ffd +generated: "2026-03-31T14:29:48.785567+05:30" diff --git a/helm-charts/Chart.yaml b/helm-charts/Chart.yaml index aa8323d7..ecb6082d 100644 --- a/helm-charts/Chart.yaml +++ b/helm-charts/Chart.yaml @@ -6,14 +6,14 @@ version: 0.1.0 appVersion: "1.0.0" dependencies: - name: postgresql - version: ~12.5.5 + version: ~18.5.14 repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: mysql - version: ~13.0.0 + version: ~13.0.4 repository: https://charts.bitnami.com/bitnami condition: mysql.enabled - name: redis - version: ~17.11.3 + version: ~25.3.9 repository: https://charts.bitnami.com/bitnami condition: redis.enabled diff --git a/helm-charts/README.md b/helm-charts/README.md index e2531b15..6a3c457c 100644 --- a/helm-charts/README.md +++ b/helm-charts/README.md @@ -60,6 +60,24 @@ Then, to install the chart with the release name `my-release`: helm install my-release . ``` +To pin a specific on-prem image version (for example `v1.4`): + +```bash +helm install my-release . \ + --set image.repository=ghcr.io/juspay/decision-engine/postgres \ + --set image.version=v1.4 \ + --set image.pullPolicy=Always +``` + +To use an internal/private registry: + +```bash +helm install my-release . \ + --set image.repository=/decision-engine/postgres \ + --set image.version= \ + --set image.pullPolicy=IfNotPresent +``` + The command deploys Decision Engine on the Kubernetes cluster with the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. ## Uninstalling the Chart @@ -77,8 +95,8 @@ helm delete my-release | Name | Description | Value | |---------------------|---------------------------------------------------------------------------------------|-----------------| | `replicaCount` | Number of Decision Engine replicas | `1` | -| `image.repository` | Decision Engine image repository | `decision-engine` | -| `image.version` | Decision Engine image tag | `latest` | +| `image.repository` | Decision Engine image repository | `ghcr.io/juspay/decision-engine/postgres` | +| `image.version` | Decision Engine image tag | `v1.4` | | `image.pullPolicy` | Decision Engine image pull policy | `Always` | | `imagePullSecrets` | Image pull secrets | `[]` | | `nameOverride` | Override the name of the chart | `""` | @@ -136,8 +154,8 @@ helm delete my-release | Name | Description | Value | |-----------------------------------------|------------------------------------------------------------|-----------------| | `groovyRunner.enabled` | Deploy Groovy Runner | `true` | -| `groovyRunner.image.repository` | Groovy Runner image repository | `"groovy-runner"` | -| `groovyRunner.image.version` | Groovy Runner image tag | `"latest"` | +| `groovyRunner.image.repository` | Groovy Runner image repository | `"ghcr.io/juspay/decision-engine/groovy-runner"` | +| `groovyRunner.image.version` | Groovy Runner image tag | `"v1.4"` | | `groovyRunner.image.pullPolicy` | Groovy Runner image pull policy | `"Always"` | | `groovyRunner.service.port` | Groovy Runner service port | `8085` | | `groovyRunner.resources` | Groovy Runner resources | `{}` | diff --git a/helm-charts/charts/mysql-13.0.0.tgz b/helm-charts/charts/mysql-13.0.0.tgz deleted file mode 100644 index ab18090b..00000000 Binary files a/helm-charts/charts/mysql-13.0.0.tgz and /dev/null differ diff --git a/helm-charts/charts/mysql-13.0.4.tgz b/helm-charts/charts/mysql-13.0.4.tgz new file mode 100644 index 00000000..12c0e3b6 Binary files /dev/null and b/helm-charts/charts/mysql-13.0.4.tgz differ diff --git a/helm-charts/charts/postgresql-12.5.9.tgz b/helm-charts/charts/postgresql-12.5.9.tgz deleted file mode 100644 index 490097c4..00000000 Binary files a/helm-charts/charts/postgresql-12.5.9.tgz and /dev/null differ diff --git a/helm-charts/charts/postgresql-18.5.14.tgz b/helm-charts/charts/postgresql-18.5.14.tgz new file mode 100644 index 00000000..41fb11ae Binary files /dev/null and b/helm-charts/charts/postgresql-18.5.14.tgz differ diff --git a/helm-charts/charts/redis-17.11.8.tgz b/helm-charts/charts/redis-17.11.8.tgz deleted file mode 100644 index 1aed4703..00000000 Binary files a/helm-charts/charts/redis-17.11.8.tgz and /dev/null differ diff --git a/helm-charts/charts/redis-25.3.9.tgz b/helm-charts/charts/redis-25.3.9.tgz new file mode 100644 index 00000000..4a388783 Binary files /dev/null and b/helm-charts/charts/redis-25.3.9.tgz differ diff --git a/helm-charts/config/development.toml b/helm-charts/config/development.toml new file mode 120000 index 00000000..7f052ec6 --- /dev/null +++ b/helm-charts/config/development.toml @@ -0,0 +1 @@ +../../config/development.toml \ No newline at end of file diff --git a/helm-charts/templates/configmap.yaml b/helm-charts/templates/configmap.yaml index deb33f26..dfa1a13d 100644 --- a/helm-charts/templates/configmap.yaml +++ b/helm-charts/templates/configmap.yaml @@ -6,159 +6,4 @@ metadata: {{- include "decision-engine.labels" . | nindent 4 }} data: development.toml: |- - {{- include "decision-engine.configFile" . | nindent 4 }} - - [routing_config.keys] - billing_country = {type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe"} - business_country = {type = "enum", values = "Afghanistan, AlandIslands, Albania, Algeria, AmericanSamoa, Andorra, Angola, Anguilla, Antarctica, AntiguaAndBarbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, BoliviaPlurinationalState, BonaireSintEustatiusAndSaba, BosniaAndHerzegovina, Botswana, BouvetIsland, Brazil, BritishIndianOceanTerritory, BruneiDarussalam, Bulgaria, BurkinaFaso, Burundi, CaboVerde, Cambodia, Cameroon, Canada, CaymanIslands, CentralAfricanRepublic, Chad, Chile, China, ChristmasIsland, CocosKeelingIslands, Colombia, Comoros, Congo, CongoDemocraticRepublic, CookIslands, CostaRica, CotedIvoire, Croatia, Cuba, Curacao, Cyprus, Czechia, Denmark, Djibouti, Dominica, DominicanRepublic, Ecuador, Egypt, ElSalvador, EquatorialGuinea, Eritrea, Estonia, Ethiopia, FalklandIslandsMalvinas, FaroeIslands, Fiji, Finland, France, FrenchGuiana, FrenchPolynesia, FrenchSouthernTerritories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, GuineaBissau, Guyana, Haiti, HeardIslandAndMcDonaldIslands, HolySee, Honduras, HongKong, Hungary, Iceland, India, Indonesia, IranIslamicRepublic, Iraq, Ireland, IsleOfMan, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, KoreaDemocraticPeoplesRepublic, KoreaRepublic, Kuwait, Kyrgyzstan, LaoPeoplesDemocraticRepublic, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao, MacedoniaTheFormerYugoslavRepublic, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, MarshallIslands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, MicronesiaFederatedStates, MoldovaRepublic, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar, Namibia, Nauru, Nepal, Netherlands, NewCaledonia, NewZealand, Nicaragua, Niger, Nigeria, Niue, NorfolkIsland, NorthernMarianaIslands, Norway, Oman, Pakistan, Palau, PalestineState, Panama, PapuaNewGuinea, Paraguay, Peru, Philippines, Pitcairn, Poland, Portugal, PuertoRico, Qatar, Reunion, Romania, RussianFederation, Rwanda, SaintBarthelemy, SaintHelenaAscensionAndTristandaCunha, SaintKittsAndNevis, SaintLucia, SaintMartinFrenchpart, SaintPierreAndMiquelon, SaintVincentAndTheGrenadines, Samoa, SanMarino, SaoTomeAndPrincipe, SaudiArabia, Senegal, Serbia, Seychelles, SierraLeone, Singapore, SintMaartenDutchpart, Slovakia, Slovenia, SolomonIslands, Somalia, SouthAfrica, SouthGeorgiaAndTheSouthSandwichIslands, SouthSudan, Spain, SriLanka, Sudan, Suriname, SvalbardAndJanMayen, Swaziland, Sweden, Switzerland, SyrianArabRepublic, TaiwanProvinceOfChina, Tajikistan, TanzaniaUnitedRepublic, Thailand, TimorLeste, Togo, Tokelau, Tonga, TrinidadAndTobago, Tunisia, Turkey, Turkmenistan, TurksAndCaicosIslands, Tuvalu, Uganda, Ukraine, UnitedArabEmirates, UnitedKingdomOfGreatBritainAndNorthernIreland, UnitedStatesOfAmerica, UnitedStatesMinorOutlyingIslands, Uruguay, Uzbekistan, Vanuatu, VenezuelaBolivarianRepublic, Vietnam, VirginIslandsBritish, VirginIslandsUS, WallisAndFutuna, WesternSahara, Yemen, Zambia, Zimbabwe"} - business_label = { type = "udf"} - metadata = {type = "udf"} - pay_later = {type = "enum", values = "Affirm, Alma, AfterpayClearpay, Klarna, PayBright, Atome, Walley"} - gift_card = {type = "enum", values = "Givex, PaySafeCard"} - wallet = {type = "enum", values = "AmazonPay, ApplePay, GooglePay, Paypal, AliPay, AliPayHk, Dana, MbWay, MobilePay, SamsungPay, Twint, Vipps, TouchNGo, Swish, WeChatPay, GoPay, Gcash, Momo, KakaoPay, Cashapp, Mifinity, Paze"} - upi = {type = "enum", values = "UpiCollect, UpiIntent"} - voucher = {type = "enum", values = "Boleto, Efecty, PagoEfectivo, RedCompra, RedPagos, Indomaret, Alfamart, Oxxo, SevenEleven, Lawson, MiniStop, FamilyMart, Seicomart, PayEasy"} - bank_transfer = {type = "enum", values = "Ach, SepaBankTransfer, Bacs, Multibanco, Pix, Pse, PermataBankTransfer, BcaBankTransfer, BniVa, BriVa, CimbVa, DanamonVa, MandiriVa, LocalBankTransfer, InstantBankTransfer"} - bank_redirect = {type = "enum", values = "Giropay, Ideal, Sofort, Eft, Eps, BancontactCard, Blik, LocalBankRedirect, OnlineBankingThailand, OnlineBankingCzechRepublic, OnlineBankingFinland, OnlineBankingFpx, OnlineBankingPoland, OnlineBankingSlovakia, Przelewy24, Trustly, Bizum, Interac, OpenBankingUk, OpenBankingPIS"} - bank_debit = {type = "enum", values = "Ach, Sepa, Bacs, Becs"} - crypto = {type = "enum", values = "CryptoCurrency"} - reward = {type = "enum", values = "Evoucher, ClassicReward"} - card_redirect = {type = "enum", values = "Knet, Benefit, MomoAtm, CardRedirect"} - real_time_payment = {type = "enum", values = "Fps, DuitNow, PromptPay, VietQr"} - open_banking = {type = "enum", values = "OpenBankingPIS"} - mobile_payment = {type = "enum", values = "DirectCarrierBilling"} - payment_method = {type = "enum", values = "card, card_redirect, pay_later, wallet, bank_redirect, bank_transfer, crypto, bank_debit, reward, real_time_payment, upi, voucher, gift_card, open_banking, mobile_payment"} - payment_card_bin = {type = "udf"} - payment_card_type = {type = "enum", values = "CREDIT, DEBIT"} - mandate_acceptance_type = {type = "enum", values= "Online, Offline"} - card_network = {type = "enum", values = "Visa, Mastercard, AmericanExpress, JCB, DinersClub, Discover, CartesBancaires, UnionPay, Interac, RuPay, Maestro, Star, Pulse, Accel, Nyce"} - mandate_type = {type = "enum", values= "SingleUse, MultiUse"} - payment_type = {type = "enum", values = "normal, new_mandate, setup_mandate, recurring_mandate, non_mandate"} - payment_method_type = {type = "enum", values = "ach, affirm, afterpay_clearpay, alfamart, ali_pay, ali_pay_hk, alma, amazon_pay, apple_pay, atome, bacs, bancontact_card, becs, benefit, bizum, blik, boleto, bca_bank_transfer, bni_va, bri_va, card, card_redirect, cimb_va, classic_reward, credit, crypto_currency, cashapp, dana, danamon_va, debit, duit_now, efecty, eft, eps, fps, evoucher, giropay, givex, google_pay, go_pay, gcash, ideal, interac, indomaret, klarna, kakao_pay, local_bank_redirect, mandiri_va, knet, mb_way, mobile_pay, momo, momo_atm, multibanco, online_banking_thailand, online_banking_czech_republic, online_banking_finland, online_banking_fpx, online_banking_poland, online_banking_slovakia, oxxo, pago_efectivo, permata_bank_transfer, open_banking_uk, pay_bright, paypal, paze, pix, pay_safe_card, przelewy24, prompt_pay, pse, red_compra, red_pagos, samsung_pay, sepa, sepa_bank_transfer, sofort, swish, touch_n_go, trustly, twint, upi_collect, upi_intent, vipps, viet_qr, venmo, walley, we_chat_pay, seven_eleven, lawson, mini_stop, family_mart, seicomart, pay_easy, local_bank_transfer, mifinity, open_banking_pis, direct_carrier_billing, instant_bank_transfer"} - authentication_type = {type = "enum", values = "three_ds, no_three_ds"} - capture_methods = {type = "enum", values = "automatic, manual, manual_multiple, scheduled, sequential_automatic"} - setup_future_usage = {type = "enum", values = "on_session, off_session"} - payment_card_network = {type = "enum", values = "visa, mastercard, american_express, jcb, diners_club, discover, cartes_bancaires, union_pay, interac, rupay, maestro"} - amount = {type = "integer"} - login_date = {type = "str"} - currency = {type = "enum", values = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SLL, SOS, SRD, SSP, STD, STN, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL"} - payment_card_issuer_country = { type = "enum", values = "AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, AZ, BS, BH, BD, BB, BY, BE, BZ, BJ, BM, BT, BO, BQ, BA, BW, BV, BR, IO, BN, BG, BF, BI, KH, CM, CA, CV, KY, CF, TD, CL, CN, CX, CC, CO, KM, CG, CD, CK, CR, CI, HR, CU, CW, CY, CZ, DK, DJ, DM, DO, EC, EG, SV, GQ, ER, EE, ET, FK, FO, FJ, FI, FR, GF, PF, TF, GA, GM, GE, DE, GH, GI, GR, GL, GD, GP, GU, GT, GG, GN, GW, GY, HT, HM, VA, HN, HK, HU, IS, IN, ID, IR, IQ, IE, IM, IL, IT, JM, JP, JE, JO, KZ, KE, KI, KP, KR, KW, KG, LA, LV, LB, LS, LR, LY, LI, LT, LU, MO, MK, MG, MW, MY, MV, ML, MT, MH, MQ, MR, MU, YT, MX, FM, MD, MC, MN, ME, MS, MA, MZ, MM, NA, NR, NP, NL, NC, NZ, NI, NE, NG, NU, NF, MP, NO, OM, PK, PW, PS, PA, PG, PY, PE, PH, PN, PL, PT, PR, QA, RE, RO, RU, RW, BL, SH, KN, LC, MF, PM, VC, WS, SM, ST, SA, SN, RS, SC, SL, SG, SX, SK, SI, SB, SO, ZA, GS, SS, ES, LK, SD, SR, SJ, SZ, SE, CH, SY, TW, TJ, TZ, TH, TL, TG, TK, TO, TT, TN, TR, TM, TC, TV, UG, UA, AE, GB, UM, UY, UZ, VU, VE, VN, VG, VI, WF, EH, YE, ZM, ZW, US"} - card_bin = {type = "str"} - capture_method = {type = "enum", values = "automatic, manual"} - new_customer = {type = "udf"} - - - udf1 = {type = "str"} - order_udf1 = {type = "global_ref"} - payment_payment_method = {type = "enum", values = "NB_HDFC, NB_ICICI, NB_SBI"} - payment_payment_source = {type = "enum", values = "net.one97.paytm, @paytm"} - txn_is_emi = {type = "enum", values = "true, false"} - - [routing_config.default] - output = ["stripe", "adyen"] - - [[routing_config.constraint_graph.nodes]] - preds = [] - succs = [0] - - [routing_config.constraint_graph.nodes.kind] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data.data] - key = "payment_method" - comparison = "equal" - - [routing_config.constraint_graph.nodes.kind.data.data.value] - type = "enum_variant" - value = "card" - - [[routing_config.constraint_graph.nodes]] - preds = [] - succs = [1] - - [routing_config.constraint_graph.nodes.kind] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data.data] - key = "payment_method" - comparison = "equal" - - [routing_config.constraint_graph.nodes.kind.data.data.value] - type = "enum_variant" - value = "bank_debit" - - [[routing_config.constraint_graph.nodes]] - preds = [0] - succs = [] - - [routing_config.constraint_graph.nodes.kind] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data.data] - key = "output" - comparison = "equal" - - [routing_config.constraint_graph.nodes.kind.data.data.value] - type = "enum_variant" - value = "stripe" - - [[routing_config.constraint_graph.nodes]] - preds = [1] - succs = [] - - [routing_config.constraint_graph.nodes.kind] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data] - kind = "value" - - [routing_config.constraint_graph.nodes.kind.data.data] - key = "output" - comparison = "equal" - - [routing_config.constraint_graph.nodes.kind.data.data.value] - type = "enum_variant" - value = "adyen" - - [[routing_config.constraint_graph.edges]] - strength = "strong" - relation = "positive" - pred = 0 - succ = 2 - - [[routing_config.constraint_graph.edges]] - strength = "strong" - relation = "positive" - pred = 1 - succ = 3 - - [debit_routing_config] - fraud_check_fee = 0.01 - - [debit_routing_config.network_fee] - visa = { percentage = 0.1375, fixed_amount = 0.020 } - mastercard = { percentage = 0.15, fixed_amount = 0.40 } - accel = { percentage = 0.0, fixed_amount = 0.040 } - nyce = { percentage = 0.10, fixed_amount = 0.015 } - pulse = { percentage = 0.10, fixed_amount = 0.03 } - star = { percentage = 0.10, fixed_amount = 0.015 } - - [debit_routing_config.interchange_fee] - regulated = { percentage = 0.05, fixed_amount = 0.21 } - - [debit_routing_config.interchange_fee.non_regulated] - merchant_category_code_0001.visa = { percentage = 1.65, fixed_amount = 0.15 } - merchant_category_code_0001.mastercard = { percentage = 1.65, fixed_amount = 0.15 } - merchant_category_code_0001.accel = { percentage = 1.55, fixed_amount = 0.04 } - merchant_category_code_0001.nyce = { percentage = 1.30, fixed_amount = 0.213125 } - merchant_category_code_0001.pulse = { percentage = 1.60, fixed_amount = 0.15 } - merchant_category_code_0001.star = { percentage = 1.63, fixed_amount = 0.15 } + {{ .Files.Get "config/development.toml" | nindent 4 }} \ No newline at end of file diff --git a/helm-charts/templates/deployment.yaml b/helm-charts/templates/deployment.yaml index e579c6a2..c5dbea8f 100644 --- a/helm-charts/templates/deployment.yaml +++ b/helm-charts/templates/deployment.yaml @@ -28,25 +28,31 @@ spec: serviceAccountName: {{ include "decision-engine.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.initContainers.enabled }} initContainers: {{- if .Values.decisionEngine.usePostgreSQL }} - name: wait-for-postgresql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.postgresqlHost" . }} 5432; do echo waiting for postgresql; sleep 2; done;'] {{- end }} {{- if .Values.decisionEngine.useRedis }} - name: wait-for-redis - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.redisHost" . }} 6379; do echo waiting for redis; sleep 2; done;'] {{- end }} - name: wait-for-groovy-runner - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.groovyRunnerName" . }} {{ .Values.groovyRunner.service.port }}; do echo waiting for groovy-runner; sleep 2; done;'] {{- if .Values.decisionEngine.useMySQL }} - name: wait-for-mysql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.mysqlHost" . }} 3306; do echo waiting for mysql; sleep 2; done;'] {{- end }} + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -54,15 +60,24 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.version | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - - name: http - containerPort: {{ .Values.decisionEngine.server.port }} - protocol: TCP - - name: metrics - containerPort: {{ .Values.decisionEngine.metrics.port }} - protocol: TCP + {{- range .Values.service.ports }} + - name: {{ .name }} + containerPort: {{ .targetPort }} + protocol: {{ .protocol | default "TCP" }} + {{- end }} env: - name: GROOVY_RUNNER_HOST value: "{{ include "decision-engine.groovyRunnerName" . }}:{{ .Values.groovyRunner.service.port }}" + {{- range $name, $value := .Values.extraEnvVars }} + - name: {{ $name }} + {{- if and (kindIs "map" $value) $value.valueFrom }} + valueFrom: + {{- toYaml $value.valueFrom | nindent 16 }} + {{- else }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + livenessProbe: httpGet: path: /health diff --git a/helm-charts/templates/istio-destinationrule.yaml b/helm-charts/templates/istio-destinationrule.yaml new file mode 100644 index 00000000..a5ab60f8 --- /dev/null +++ b/helm-charts/templates/istio-destinationrule.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.istio.enabled .Values.istio.destinationRule.enabled }} +apiVersion: networking.istio.io/v1 +kind: DestinationRule +metadata: + name: {{ include "decision-engine.fullname" . }}-dr + namespace: {{ .Release.Namespace }} + labels: + {{- include "decision-engine.labels" . | nindent 4 }} + app.kubernetes.io/component: istio-destination-rule +spec: + host: {{ include "decision-engine.fullname" . }} + {{- if .Values.istio.destinationRule.trafficPolicy }} + trafficPolicy: + {{- toYaml .Values.istio.destinationRule.trafficPolicy | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-charts/templates/istio-virtualservice.yaml b/helm-charts/templates/istio-virtualservice.yaml new file mode 100644 index 00000000..9d2994f5 --- /dev/null +++ b/helm-charts/templates/istio-virtualservice.yaml @@ -0,0 +1,52 @@ +{{- if and .Values.istio.enabled .Values.istio.virtualService.enabled }} +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: {{ include "decision-engine.fullname" . }}-vs + namespace: {{ .Release.Namespace }} + labels: + {{- include "decision-engine.labels" . | nindent 4 }} + app.kubernetes.io/component: istio-virtual-service +spec: + {{- with .Values.istio.virtualService.hosts }} + hosts: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.istio.virtualService.gateways }} + gateways: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.istio.virtualService.http }} + http: + {{- range .Values.istio.virtualService.http }} + - {{- if .name }} + name: {{ .name | quote }} + {{- end }} + {{- if .match }} + match: + {{- toYaml .match | nindent 6 }} + {{- end }} + {{- if .rewrite }} + rewrite: + {{- toYaml .rewrite | nindent 6 }} + {{- end }} + {{- if .timeout }} + timeout: {{ .timeout }} + {{- end }} + {{- if .retries }} + retries: + {{- toYaml .retries | nindent 6 }} + {{- end }} + route: + - destination: + host: {{ include "decision-engine.fullname" $ }} + port: + number: 80 + {{- if .weight }} + weight: {{ .weight }} + {{- else }} + weight: 100 + {{- end }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-charts/templates/mysql-migration-job.yaml b/helm-charts/templates/mysql-migration-job.yaml index 97c412e3..85d52d5a 100644 --- a/helm-charts/templates/mysql-migration-job.yaml +++ b/helm-charts/templates/mysql-migration-job.yaml @@ -26,10 +26,13 @@ spec: serviceAccountName: {{ include "decision-engine.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.initContainers.enabled }} initContainers: - name: wait-for-mysql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.mysqlHost" . }} 3306; do echo waiting for mysql; sleep 2; done;'] + {{- end }} containers: - name: migration image: "debian:stable-slim" diff --git a/helm-charts/templates/postgresql-migration-job.yaml b/helm-charts/templates/postgresql-migration-job.yaml index ae561780..28d6488d 100644 --- a/helm-charts/templates/postgresql-migration-job.yaml +++ b/helm-charts/templates/postgresql-migration-job.yaml @@ -36,10 +36,13 @@ spec: serviceAccountName: {{ include "decision-engine.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.initContainers.enabled }} initContainers: - name: wait-for-postgresql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.postgresqlHost" . }} 5432; do echo waiting for postgresql; sleep 2; done;'] + {{- end }} containers: - name: migration image: rust:latest diff --git a/helm-charts/templates/routing-config-job.yaml b/helm-charts/templates/routing-config-job.yaml index 1a6becb4..467d4e24 100644 --- a/helm-charts/templates/routing-config-job.yaml +++ b/helm-charts/templates/routing-config-job.yaml @@ -26,17 +26,21 @@ spec: serviceAccountName: {{ include "decision-engine.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.initContainers.enabled }} initContainers: {{- if .Values.decisionEngine.usePostgreSQL }} - name: wait-for-postgresql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.postgresqlHost" . }} 5432; do echo waiting for postgresql; sleep 2; done;'] {{- end }} {{- if .Values.decisionEngine.useMySQL }} - name: wait-for-mysql - image: busybox:1.28 + image: "{{ .Values.initContainers.busybox.repository }}:{{ .Values.initContainers.busybox.tag }}" + imagePullPolicy: {{ .Values.initContainers.busybox.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "decision-engine.mysqlHost" . }} 3306; do echo waiting for mysql; sleep 2; done;'] {{- end }} + {{- end }} containers: - name: routing-config image: "{{ .Values.routingConfig.image.repository }}:{{ .Values.routingConfig.image.tag }}" diff --git a/helm-charts/templates/service.yaml b/helm-charts/templates/service.yaml index c0e9a419..b97106ec 100644 --- a/helm-charts/templates/service.yaml +++ b/helm-charts/templates/service.yaml @@ -8,10 +8,12 @@ metadata: spec: type: {{ .Values.service.type }} ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http + {{- range .Values.service.ports }} + - name: {{ .name }} + port: {{ .port }} + targetPort: {{ .targetPort }} + protocol: {{ .protocol | default "TCP" }} + {{- end }} selector: {{- include "decision-engine.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: {{ .Values.decisionEngine.metadata.labels.component }} diff --git a/helm-charts/values.yaml b/helm-charts/values.yaml index 2fc07263..98999855 100644 --- a/helm-charts/values.yaml +++ b/helm-charts/values.yaml @@ -5,10 +5,10 @@ image: # Updated to include the registry URL repository: ghcr.io/juspay/decision-engine/postgres pullPolicy: Always - version: "v1.2.0" + version: "v1.4" # Configure these if your images are in a private registry -imagePullSecrets: +imagePullSecrets: # - name: regcred nameOverride: "" fullnameOverride: "" @@ -27,6 +27,14 @@ podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 +initContainers: + # Set to false to disable all init containers + enabled: true + busybox: + repository: public.ecr.aws/docker/library/busybox + tag: "1.28" + pullPolicy: IfNotPresent + securityContext: {} # capabilities: # drop: @@ -37,7 +45,15 @@ securityContext: {} service: type: ClusterIP - port: 8080 + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + - name: metrics + port: 9090 + targetPort: 9090 + protocol: TCP ingress: enabled: false @@ -156,7 +172,7 @@ groovyRunner: enabled: true image: repository: "ghcr.io/juspay/decision-engine/groovy-runner" - version: "v1.0.0" + version: "v1.4" pullPolicy: "Always" service: port: 8085 @@ -206,3 +222,55 @@ routingConfig: enabled: true # Path to mount routing config files mountPath: "/app" + +# Additional environment variables to inject into the main container (map format for proper merging) +# Supports both simple values and valueFrom references for secrets/configmaps +extraEnvVars: {} + # Simple value example: + # MY_VAR: "my-value" + # Secret reference example: + # DB_PASSWORD: + # valueFrom: + # secretKeyRef: + # name: decision-engine-secrets + # key: LOCKER__PG_DATABASE__PG_PASSWORD + # ConfigMap reference example: + # APP_CONFIG: + # valueFrom: + # configMapKeyRef: + # name: app-config + # key: config.json + +# Istio configuration (optional) +istio: + enabled: false + virtualService: + enabled: false + hosts: [] + gateways: [] + # HTTP routing rules - route destination will be automatically set to control center service + http: [] + # Example configuration: + # http: + # - name: "control-center-routes" + # match: + # - uri: + # prefix: / + # rewrite: + # uri: "/dashboard" + # weight: 100 + # timeout: 30s + # retries: + # attempts: 3 + # perTryTimeout: 10s + destinationRule: + enabled: false + trafficPolicy: {} + # Example traffic policy: + # trafficPolicy: + # loadBalancer: + # simple: ROUND_ROBIN + # connectionPool: + # tcp: + # maxConnections: 50 + # connectTimeout: 30s \ No newline at end of file diff --git a/migrations/2026-04-14-analytics_events/down.sql b/migrations/2026-04-14-analytics_events/down.sql new file mode 100644 index 00000000..611d9b5b --- /dev/null +++ b/migrations/2026-04-14-analytics_events/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS analytics_event; diff --git a/migrations/2026-04-14-analytics_events/up.sql b/migrations/2026-04-14-analytics_events/up.sql new file mode 100644 index 00000000..442f8c11 --- /dev/null +++ b/migrations/2026-04-14-analytics_events/up.sql @@ -0,0 +1,26 @@ +-- Your SQL goes here +CREATE TABLE analytics_event ( + id SERIAL PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + merchant_id VARCHAR(255), + payment_method_type VARCHAR(255), + payment_method VARCHAR(255), + gateway VARCHAR(255), + routing_approach VARCHAR(128), + rule_name VARCHAR(255), + status VARCHAR(64), + error_code VARCHAR(64), + error_message TEXT, + score_value DOUBLE, + sigma_factor DOUBLE, + average_latency DOUBLE, + tp99_latency DOUBLE, + transaction_count BIGINT, + route VARCHAR(128), + details TEXT, + created_at_ms BIGINT NOT NULL +); + +CREATE INDEX idx_analytics_event_created_at_ms ON analytics_event (created_at_ms); +CREATE INDEX idx_analytics_event_merchant_id ON analytics_event (merchant_id); +CREATE INDEX idx_analytics_event_type ON analytics_event (event_type); diff --git a/migrations/2026-04-15-000001_payment_audit_fields/down.sql b/migrations/2026-04-15-000001_payment_audit_fields/down.sql new file mode 100644 index 00000000..2b515b7b --- /dev/null +++ b/migrations/2026-04-15-000001_payment_audit_fields/down.sql @@ -0,0 +1,8 @@ +DROP INDEX idx_analytics_event_stage ON analytics_event; +DROP INDEX idx_analytics_event_request_id ON analytics_event; +DROP INDEX idx_analytics_event_payment_id ON analytics_event; + +ALTER TABLE analytics_event + DROP COLUMN event_stage, + DROP COLUMN request_id, + DROP COLUMN payment_id; diff --git a/migrations/2026-04-15-000001_payment_audit_fields/up.sql b/migrations/2026-04-15-000001_payment_audit_fields/up.sql new file mode 100644 index 00000000..b1bfd884 --- /dev/null +++ b/migrations/2026-04-15-000001_payment_audit_fields/up.sql @@ -0,0 +1,8 @@ +ALTER TABLE analytics_event + ADD COLUMN payment_id VARCHAR(255) NULL, + ADD COLUMN request_id VARCHAR(255) NULL, + ADD COLUMN event_stage VARCHAR(128) NULL; + +CREATE INDEX idx_analytics_event_payment_id ON analytics_event (payment_id); +CREATE INDEX idx_analytics_event_request_id ON analytics_event (request_id); +CREATE INDEX idx_analytics_event_stage ON analytics_event (event_stage); diff --git a/migrations/2026-04-16-000001_analytics_sr_dimensions/down.sql b/migrations/2026-04-16-000001_analytics_sr_dimensions/down.sql new file mode 100644 index 00000000..9f253e4b --- /dev/null +++ b/migrations/2026-04-16-000001_analytics_sr_dimensions/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE analytics_event + DROP COLUMN auth_type, + DROP COLUMN country, + DROP COLUMN currency, + DROP COLUMN card_is_in, + DROP COLUMN card_network; diff --git a/migrations/2026-04-16-000001_analytics_sr_dimensions/up.sql b/migrations/2026-04-16-000001_analytics_sr_dimensions/up.sql new file mode 100644 index 00000000..32608d64 --- /dev/null +++ b/migrations/2026-04-16-000001_analytics_sr_dimensions/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE analytics_event + ADD COLUMN card_network VARCHAR(255), + ADD COLUMN card_is_in VARCHAR(255), + ADD COLUMN currency VARCHAR(64), + ADD COLUMN country VARCHAR(64), + ADD COLUMN auth_type VARCHAR(64); diff --git a/migrations_pg/2026-04-14-analytics_events/down.sql b/migrations_pg/2026-04-14-analytics_events/down.sql new file mode 100644 index 00000000..6ca66a8b --- /dev/null +++ b/migrations_pg/2026-04-14-analytics_events/down.sql @@ -0,0 +1 @@ +DROP TABLE analytics_event; diff --git a/migrations_pg/2026-04-14-analytics_events/up.sql b/migrations_pg/2026-04-14-analytics_events/up.sql new file mode 100644 index 00000000..25774a8a --- /dev/null +++ b/migrations_pg/2026-04-14-analytics_events/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE analytics_event ( + id SERIAL PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + merchant_id VARCHAR(255), + payment_method_type VARCHAR(255), + payment_method VARCHAR(255), + gateway VARCHAR(255), + routing_approach VARCHAR(128), + rule_name VARCHAR(255), + status VARCHAR(64), + error_code VARCHAR(64), + error_message TEXT, + score_value DOUBLE PRECISION, + sigma_factor DOUBLE PRECISION, + average_latency DOUBLE PRECISION, + tp99_latency DOUBLE PRECISION, + transaction_count BIGINT, + route VARCHAR(128), + details TEXT, + created_at_ms BIGINT NOT NULL +); + +CREATE INDEX idx_analytics_event_created_at_ms ON analytics_event (created_at_ms); +CREATE INDEX idx_analytics_event_merchant_id ON analytics_event (merchant_id); +CREATE INDEX idx_analytics_event_type ON analytics_event (event_type); diff --git a/migrations_pg/2026-04-15-000001_payment_audit_fields/down.sql b/migrations_pg/2026-04-15-000001_payment_audit_fields/down.sql new file mode 100644 index 00000000..49ea4f44 --- /dev/null +++ b/migrations_pg/2026-04-15-000001_payment_audit_fields/down.sql @@ -0,0 +1,8 @@ +DROP INDEX IF EXISTS idx_analytics_event_stage; +DROP INDEX IF EXISTS idx_analytics_event_request_id; +DROP INDEX IF EXISTS idx_analytics_event_payment_id; + +ALTER TABLE analytics_event + DROP COLUMN IF EXISTS event_stage, + DROP COLUMN IF EXISTS request_id, + DROP COLUMN IF EXISTS payment_id; diff --git a/migrations_pg/2026-04-15-000001_payment_audit_fields/up.sql b/migrations_pg/2026-04-15-000001_payment_audit_fields/up.sql new file mode 100644 index 00000000..503624cf --- /dev/null +++ b/migrations_pg/2026-04-15-000001_payment_audit_fields/up.sql @@ -0,0 +1,8 @@ +ALTER TABLE analytics_event + ADD COLUMN payment_id VARCHAR(255), + ADD COLUMN request_id VARCHAR(255), + ADD COLUMN event_stage VARCHAR(128); + +CREATE INDEX idx_analytics_event_payment_id ON analytics_event (payment_id); +CREATE INDEX idx_analytics_event_request_id ON analytics_event (request_id); +CREATE INDEX idx_analytics_event_stage ON analytics_event (event_stage); diff --git a/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/down.sql b/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/down.sql new file mode 100644 index 00000000..e1c4a75b --- /dev/null +++ b/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE analytics_event + DROP COLUMN IF EXISTS auth_type, + DROP COLUMN IF EXISTS country, + DROP COLUMN IF EXISTS currency, + DROP COLUMN IF EXISTS card_is_in, + DROP COLUMN IF EXISTS card_network; diff --git a/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/up.sql b/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/up.sql new file mode 100644 index 00000000..32608d64 --- /dev/null +++ b/migrations_pg/2026-04-16-000001_analytics_sr_dimensions/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE analytics_event + ADD COLUMN card_network VARCHAR(255), + ADD COLUMN card_is_in VARCHAR(255), + ADD COLUMN currency VARCHAR(64), + ADD COLUMN country VARCHAR(64), + ADD COLUMN auth_type VARCHAR(64); diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..e46aa9a7 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,54 @@ +events {} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + upstream decision_engine { + server decision-engine-api:8080; + } + + upstream mintlify_docs { + server mintlify-docs:3000; + } + + server { + listen 80; + + # Mintlify Next.js static assets — must be proxied to the docs server + location /_next/ { + proxy_pass http://mintlify_docs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # Mintlify internal routes + location ~* ^/(api-reference|introduction|favicon|logo) { + proxy_pass http://mintlify_docs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Redirect /dashboard → /dashboard/ + location = /dashboard { + return 301 /dashboard/; + } + + # Dashboard SPA (React app served from static files) + location /dashboard/ { + root /usr/share/nginx/html; + try_files $uri $uri/ /dashboard/index.html; + } + + # Decision Engine API + location / { + proxy_pass http://decision_engine; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } +} diff --git a/oneclick.sh b/oneclick.sh new file mode 100755 index 00000000..55f90e67 --- /dev/null +++ b/oneclick.sh @@ -0,0 +1,206 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +OPENAPI_PATH="$SCRIPT_DIR/docs/openapi.json" +DOCS_PORT="${DOCS_PORT:-3000}" +DOCS_URL="http://localhost:${DOCS_PORT}" +DOCS_HOME_URL="${DOCS_URL}/introduction" +API_REF_URL="${DOCS_URL}/api-reference" +API_EXAMPLES_URL="${DOCS_URL}/api-reference1" +DOCS_LOG_PATH="${SCRIPT_DIR}/.mintlify-dev.log" + +PORTS=(8080 5173 "$DOCS_PORT") + +check_and_kill_ports() { + local pids_to_kill=() + local ports_in_use=() + + echo "Checking for processes on ports ${PORTS[*]}..." + echo "" + + for port in "${PORTS[@]}"; do + local pids + pids=$(lsof -t -iTCP:$port -sTCP:LISTEN 2>/dev/null || true) + if [ -n "$pids" ]; then + ports_in_use+=("$port") + while IFS= read -r pid; do + [ -z "$pid" ] && continue + local cmd + cmd=$(ps -p "$pid" -o command= 2>/dev/null || echo "unknown process") + pids_to_kill+=("$pid") + echo " [!] Port $port is in use by PID $pid" + echo " Command: $cmd" + done <<< "$pids" + fi + done + + if [ ${#pids_to_kill[@]} -gt 0 ]; then + echo "" + echo "==========================================" + echo " WARNING: Found processes on ports ${ports_in_use[*]}" + echo " These processes will be killed to proceed." + echo "==========================================" + echo "" + echo "Press Enter to continue and kill these processes, or Ctrl+C to abort..." + read -r + + echo "" + echo "Killing processes..." + for pid in "${pids_to_kill[@]}"; do + kill $pid 2>/dev/null || true + echo " Killed PID $pid" + done + + sleep 1 + + for port in "${PORTS[@]}"; do + local pid + pid=$(lsof -t -iTCP:$port -sTCP:LISTEN 2>/dev/null || true) + if [ -n "$pid" ]; then + kill -9 $pid 2>/dev/null || true + echo " Force killed PID $pid on port $port" + fi + done + + echo "Done. All ports cleared." + echo "" + else + echo "No processes found on ports ${PORTS[*]}." + echo "" + fi +} + +cleanup() { + local exit_code="${1:-0}" + echo "" + echo "Stopping services..." + if [ -n "$SERVER_PID" ]; then + kill $SERVER_PID 2>/dev/null || true + fi + if [ -n "$DASHBOARD_PID" ]; then + kill $DASHBOARD_PID 2>/dev/null || true + fi + if [ -n "$DOCS_PID" ]; then + kill $DOCS_PID 2>/dev/null || true + fi + exit "$exit_code" +} + +trap cleanup SIGINT SIGTERM + +wait_for_backend() { + local attempts=0 + local max_attempts=90 + + echo "Waiting for Decision Engine API on http://localhost:8080/health..." + + while [ $attempts -lt $max_attempts ]; do + if curl -fsS http://localhost:8080/health >/dev/null 2>&1; then + echo "Decision Engine API is healthy." + echo "" + return 0 + fi + + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Decision Engine server exited before becoming healthy." + return 1 + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + echo "Decision Engine API did not become healthy within ${max_attempts}s." + return 1 +} + +wait_for_docs() { + local attempts=0 + local max_attempts=120 + + echo "Waiting for docs preview on ${DOCS_HOME_URL}..." + + while [ $attempts -lt $max_attempts ]; do + if curl -fsS "${DOCS_HOME_URL}" >/dev/null 2>&1; then + echo "Docs preview is healthy." + echo "" + return 0 + fi + + if ! kill -0 "$DOCS_PID" 2>/dev/null; then + echo "Docs preview exited before becoming healthy." + echo "Check ${DOCS_LOG_PATH} for details." + return 1 + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + echo "Docs preview did not become healthy within ${max_attempts}s." + echo "Check ${DOCS_LOG_PATH} for details." + return 1 +} + +check_and_kill_ports + +echo "Running Postgres migrations..." +just migrate-pg + +echo "Starting Decision Engine server..." +cargo run --no-default-features --features postgres & +SERVER_PID=$! + +if ! wait_for_backend; then + cleanup 1 +fi + +echo "Installing dashboard dependencies..." +cd "$SCRIPT_DIR/website" +npm install --silent + +echo "Starting docs preview..." +cd "$SCRIPT_DIR/docs" +rm -f "$DOCS_LOG_PATH" +if [ "${DOCS_PORT}" != "3000" ]; then + echo "Mint preview uses port 3000 in this environment; overriding DOCS_PORT=${DOCS_PORT} to 3000." + DOCS_PORT="3000" + DOCS_URL="http://localhost:${DOCS_PORT}" + DOCS_HOME_URL="${DOCS_URL}/introduction" + API_REF_URL="${DOCS_URL}/api-reference" + API_EXAMPLES_URL="${DOCS_URL}/api-reference1" +fi +PORT="$DOCS_PORT" mint dev --no-open >"$DOCS_LOG_PATH" 2>&1 & +DOCS_PID=$! + +if ! wait_for_docs; then + cleanup 1 +fi + +cd "$SCRIPT_DIR/website" +echo "Starting dashboard..." +npm run dev & +DASHBOARD_PID=$! + +cd "$SCRIPT_DIR" + +echo "" +echo "==========================================" +echo " Decision Engine is starting up!" +echo "==========================================" +echo "" +echo " Server: http://localhost:8080" +echo " Dashboard: http://localhost:5173/dashboard/" +echo " Docs: $DOCS_HOME_URL" +echo " API Ref: $API_REF_URL" +echo " API Examples:$API_EXAMPLES_URL" +echo " OpenAPI: $OPENAPI_PATH" +echo "" +echo "==========================================" +echo "" + +wait $SERVER_PID $DASHBOARD_PID diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..e8dcadc4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3152 @@ +{ + "name": "decision-engine-cypress-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decision-engine-cypress-tests", + "version": "1.0.0", + "dependencies": { + "uuid": "^9.0.1" + }, + "devDependencies": { + "@cypress/grep": "^4.0.1", + "cypress": "^13.6.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/grep": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@cypress/grep/-/grep-4.1.0.tgz", + "integrity": "sha512-yUscMiUgM28VDPrNxL19/BhgHZOVrAPrzVsuEcy6mqPqDYt8H8fIaHeeGQPW4CbMu/ry9sehjH561WDDBIXOIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "find-test-names": "^1.28.18", + "globby": "^11.0.4" + }, + "peerDependencies": { + "cypress": ">=10" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.6", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-test-names": { + "version": "1.29.17", + "resolved": "https://registry.npmjs.org/find-test-names/-/find-test-names-1.29.17.tgz", + "integrity": "sha512-JyZJ1EqH6+3/eXtViY7A79BTggAmcKu0rmXu89oJPobwbk8MmFhVz06sF1L2r6TLT7477iVtCmftBQ0pl2K/kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.2", + "@babel/plugin-syntax-jsx": "^7.27.1", + "acorn-walk": "^8.2.0", + "debug": "^4.3.3", + "globby": "^11.0.4", + "simple-bin-help": "^1.8.0" + }, + "bin": { + "find-test-names": "bin/find-test-names.js", + "print-tests": "bin/print-tests.js", + "update-test-count": "bin/update-test-count.js" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-bin-help": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/simple-bin-help/-/simple-bin-help-1.8.0.tgz", + "integrity": "sha512-0LxHn+P1lF5r2WwVB/za3hLRIsYoLaNq1CXqjbrs3ZvLuvlWnRKrUjEWzV7umZL7hpQ7xULiQMV+0iXdRa5iFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..f31aa140 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "decision-engine-cypress-tests", + "version": "1.0.0", + "description": "Cypress tests for Decision Engine routing flows", + "scripts": { + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "cypress:run:headless": "cypress run --headless", + "test:gateway-latency": "cypress run --spec 'cypress/e2e/routing-flows/gateway-latency-scoring.cy.js'", + "test:success-rate": "cypress run --spec 'cypress/e2e/routing-flows/success-rate-routing.cy.js'", + "test": "cypress run --spec 'cypress/e2e/routing-flows/*.cy.js'" + }, + "devDependencies": { + "cypress": "^13.6.0", + "@cypress/grep": "^4.0.1" + }, + "dependencies": { + "uuid": "^9.0.1" + } +} diff --git a/scripts/test_analytics.sh b/scripts/test_analytics.sh new file mode 100755 index 00000000..69b3facb --- /dev/null +++ b/scripts/test_analytics.sh @@ -0,0 +1,197 @@ +#!/bin/bash + +# Test script for Decision Engine Analytics +set -e + +echo "🚀 Testing Decision Engine Analytics Setup" +echo "==========================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}✓${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_error() { + echo -e "${RED}✗${NC} $1" +} + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + print_error "Docker is not running. Please start Docker first." + exit 1 +fi + +print_status "Docker is running" + +# Check if docker-compose is available +if ! command -v docker-compose &> /dev/null; then + print_error "docker-compose is not installed" + exit 1 +fi + +print_status "docker-compose is available" + +echo "" +echo "🔧 Starting Analytics Infrastructure..." +echo "======================================" + +# Clean up any existing containers to avoid conflicts +print_status "Cleaning up existing containers..." +docker-compose --profile analytics down --remove-orphans 2>/dev/null || true + +# Start analytics infrastructure +print_status "Starting Zookeeper, Kafka, ClickHouse and Analytics Migrator..." +docker-compose up -d kafka zookeeper clickhouse analytics-migrator + +# Wait for services to be ready +echo "" +echo "⏳ Waiting for services to be ready..." +sleep 10 + +# Check Kafka +echo "" +echo "🔍 Checking Kafka..." +if docker exec open-router-kafka kafka-topics --bootstrap-server localhost:9092 --list > /dev/null 2>&1; then + print_status "Kafka is running" +else + print_error "Kafka is not responding" + exit 1 +fi + +# Check ClickHouse +echo "" +echo "🔍 Checking ClickHouse..." +if docker exec open-router-clickhouse clickhouse-client --query "SELECT 1" > /dev/null 2>&1; then + print_status "ClickHouse is running" +else + print_error "ClickHouse is not responding" + exit 1 +fi + +# Check if analytics database exists +echo "" +echo "🔍 Checking Analytics Database..." +if docker exec open-router-clickhouse clickhouse-client --query "SHOW DATABASES" | grep -q "decision_engine_analytics"; then + print_status "Analytics database exists" +else + print_warning "Analytics database not found, running migrations..." + + # Run migrations manually if needed + docker exec open-router-clickhouse clickhouse-client --multiquery < analytics/migrations/001_routing_events.sql + + if docker exec open-router-clickhouse clickhouse-client --query "SHOW DATABASES" | grep -q "decision_engine_analytics"; then + print_status "Analytics database created successfully" + else + print_error "Failed to create analytics database" + exit 1 + fi +fi + +# Check analytics tables +echo "" +echo "🔍 Checking Analytics Tables..." +TABLES=$(docker exec open-router-clickhouse clickhouse-client --query "SHOW TABLES FROM decision_engine_analytics") + +if echo "$TABLES" | grep -q "routing_events"; then + print_status "routing_events table exists" +else + print_error "routing_events table not found" + exit 1 +fi + +if echo "$TABLES" | grep -q "routing_events_queue"; then + print_status "routing_events_queue table exists" +else + print_error "routing_events_queue table not found" + exit 1 +fi + +# Test Kafka topic creation +echo "" +echo "🔍 Checking Kafka Topics..." +if docker exec open-router-kafka kafka-topics --bootstrap-server localhost:9092 --list | grep -q "decision-engine-routing-events"; then + print_status "Routing events topic exists" +else + print_warning "Creating routing events topic..." + docker exec open-router-kafka kafka-topics --bootstrap-server localhost:9092 --create --topic decision-engine-routing-events --partitions 3 --replication-factor 1 + print_status "Routing events topic created" +fi + +# Test sending a sample event to Kafka +echo "" +echo "🧪 Testing Event Publishing..." +SAMPLE_EVENT='{"event_id":"test-event-1","merchant_id":"test-merchant","request_id":"test-req-1","endpoint":"/routing/evaluate","method":"POST","request_payload":"{}","response_payload":"{}","status_code":200,"processing_time_ms":100,"gateway_selected":"stripe","routing_algorithm_id":"algo-1","error_message":null,"user_agent":"test-agent","ip_address":"127.0.0.1","created_at":"2024-01-01T12:00:00Z","sign_flag":1}' + +echo "$SAMPLE_EVENT" | docker exec -i open-router-kafka kafka-console-producer --bootstrap-server localhost:9092 --topic decision-engine-routing-events + +print_status "Sample event sent to Kafka" + +# Wait a moment for processing +sleep 5 + +# Check if event was processed by ClickHouse +echo "" +echo "🔍 Checking Event Processing..." +EVENT_COUNT=$(docker exec open-router-clickhouse clickhouse-client --query "SELECT count(*) FROM decision_engine_analytics.routing_events WHERE event_id = 'test-event-1'") + +if [ "$EVENT_COUNT" -gt 0 ]; then + print_status "Event successfully processed by ClickHouse" +else + print_warning "Event not yet processed (this might be normal for new setups)" +fi + +# Show sample queries +echo "" +echo "📊 Sample Analytics Queries" +echo "==========================" + +echo "" +echo "Total events in the last hour:" +docker exec open-router-clickhouse clickhouse-client --query " +SELECT count(*) as total_events +FROM decision_engine_analytics.routing_events +WHERE created_at >= now() - INTERVAL 1 HOUR +" + +echo "" +echo "Events by endpoint:" +docker exec open-router-clickhouse clickhouse-client --query " +SELECT + endpoint, + count(*) as event_count +FROM decision_engine_analytics.routing_events +GROUP BY endpoint +ORDER BY event_count DESC +" + +echo "" +echo "🎉 Analytics Setup Test Complete!" +echo "=================================" +print_status "All analytics components are running correctly" + +echo "" +echo "📝 Next Steps:" +echo "1. Enable analytics in your config: Set analytics.enabled = true" +echo "2. Start the decision engine: docker-compose up open-router-local" +echo "3. Send test requests to /routing/evaluate or /decide-gateway" +echo "4. Query analytics data using the sample queries above" + +echo "" +echo "🔗 Useful Commands:" +echo "- View ClickHouse logs: docker logs open-router-clickhouse" +echo "- View Kafka logs: docker logs open-router-kafka" +echo "- Connect to ClickHouse: docker exec -it open-router-clickhouse clickhouse-client" +echo "- List Kafka topics: docker exec open-router-kafka kafka-topics --bootstrap-server localhost:9092 --list" + +echo "" +echo "📚 For more information, see analytics/README.md" diff --git a/scripts/test_kafka_connection.sh b/scripts/test_kafka_connection.sh new file mode 100755 index 00000000..23ca2ea7 --- /dev/null +++ b/scripts/test_kafka_connection.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo "Testing Kafka connection from application container..." + +# Test 1: Check if the application can reach Kafka +echo "1. Testing network connectivity to Kafka..." +docker exec open-router-kafka sh -c "nc -z kafka 29092 && echo 'Kafka is reachable' || echo 'Kafka is NOT reachable'" + +# Test 2: Send a test message to the topic using kafka tools from within the kafka container +echo "2. Sending test message to decision-engine-routing-events topic..." +docker exec open-router-kafka kafka-console-producer --bootstrap-server localhost:9092 --topic decision-engine-routing-events <>, + event_sender: Option>, +} + +impl AnalyticsClient { + pub fn new(config: AnalyticsConfig) -> AnalyticsResult { + if !config.enabled { + info!("Analytics is disabled"); + return Ok(Self { + config, + kafka_producer: None, + event_sender: None, + }); + } + + // Initialize Kafka producer + let kafka_producer = Arc::new(KafkaProducer::new(config.kafka.clone())?); + + // Create event channel for batching + let (event_sender, event_receiver) = mpsc::channel::(1000); + + // Start batch processor + let _batch_processor = kafka_producer.start_batch_processor(event_receiver); + + info!("Analytics client initialized successfully"); + + Ok(Self { + config, + kafka_producer: Some(kafka_producer), + event_sender: Some(event_sender), + }) + } + + pub async fn track_routing_event(&self, event: RoutingEvent) -> AnalyticsResult<()> { + if !self.config.enabled { + return Ok(()); + } + + let event_data = event.to_event_data(); + + if let Some(sender) = &self.event_sender { + if let Err(e) = sender.try_send(event_data) { + match e { + mpsc::error::TrySendError::Full(_) => { + warn!("Analytics event queue is full, dropping event"); + } + mpsc::error::TrySendError::Closed(_) => { + error!("Analytics event channel is closed"); + return Err(AnalyticsError::Configuration( + "Event channel is closed".to_string(), + )); + } + } + } + } + + Ok(()) + } + + pub async fn track_routing_event_sync(&self, event: RoutingEvent) -> AnalyticsResult<()> { + if !self.config.enabled { + return Ok(()); + } + + let event_data = event.to_event_data(); + + if let Some(producer) = &self.kafka_producer { + producer.send_event(&event_data).await?; + } + + Ok(()) + } + + pub async fn flush_events(&self) -> AnalyticsResult<()> { + if !self.config.enabled { + return Ok(()); + } + + // Close the sender to trigger flush in batch processor + if let Some(sender) = &self.event_sender { + sender.closed().await; + } + + Ok(()) + } + + pub fn is_enabled(&self) -> bool { + self.config.enabled + } + + pub async fn health_check(&self) -> AnalyticsResult<()> { + if !self.config.enabled { + return Ok(()); + } + + // Test Kafka connectivity by sending a test event + if let Some(producer) = &self.kafka_producer { + let test_event = RoutingEventData { + event_id: "health-check".to_string(), + merchant_id: "health-check".to_string(), + request_id: "health-check".to_string(), + endpoint: "/health".to_string(), + method: "GET".to_string(), + request_payload: "{}".to_string(), + response_payload: r#"{"status": "ok"}"#.to_string(), + status_code: 200, + processing_time_ms: 1, + gateway_selected: None, + routing_algorithm_id: None, + error_message: None, + user_agent: Some("health-check".to_string()), + ip_address: Some("127.0.0.1".to_string()), + created_at: time::OffsetDateTime::now_utc(), + sign_flag: 1, + }; + + producer.send_event(&test_event).await?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::analytics::{ClickhouseConfig, KafkaConfig}; + + #[tokio::test] + async fn test_analytics_client_disabled() { + let config = AnalyticsConfig { + enabled: false, + kafka: KafkaConfig::default(), + clickhouse: ClickhouseConfig::default(), + }; + + let client = AnalyticsClient::new(config).unwrap(); + assert!(!client.is_enabled()); + + let event = RoutingEvent::new( + "merchant-123".to_string(), + "req-456".to_string(), + "/routing/evaluate".to_string(), + "POST".to_string(), + ); + + // Should not error when disabled + let result = client.track_routing_event(event).await; + assert!(result.is_ok()); + } + + #[test] + fn test_analytics_config_default() { + let config = AnalyticsConfig::default(); + assert!(!config.enabled); + assert_eq!(config.kafka.brokers, vec!["localhost:9092"]); + assert_eq!(config.kafka.topic_prefix, "decision-engine"); + assert_eq!(config.clickhouse.host, "http://localhost:8123"); + assert_eq!(config.clickhouse.username, "analytics_user"); + } +} diff --git a/src/analytics/events.rs b/src/analytics/events.rs new file mode 100644 index 00000000..46cd8a3b --- /dev/null +++ b/src/analytics/events.rs @@ -0,0 +1,284 @@ +use crate::analytics::{AnalyticsResult, RoutingEventData}; +use axum::extract::Request; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Clone)] +pub struct RoutingEvent { + pub event_id: String, + pub merchant_id: String, + pub request_id: String, + pub endpoint: String, + pub method: String, + pub request_payload: String, + pub response_payload: String, + pub status_code: u16, + pub processing_time_ms: u32, + pub gateway_selected: Option, + pub routing_algorithm_id: Option, + pub error_message: Option, + pub user_agent: Option, + pub ip_address: Option, + pub created_at: OffsetDateTime, +} + +impl RoutingEvent { + pub fn new(merchant_id: String, request_id: String, endpoint: String, method: String) -> Self { + Self { + event_id: Uuid::new_v4().to_string(), + merchant_id, + request_id, + endpoint, + method, + request_payload: String::new(), + response_payload: String::new(), + status_code: 0, + processing_time_ms: 0, + gateway_selected: None, + routing_algorithm_id: None, + error_message: None, + user_agent: None, + ip_address: None, + created_at: OffsetDateTime::now_utc(), + } + } + + pub fn from_request(request: &Request, merchant_id: String) -> Self { + let request_id = extract_request_id(request); + let endpoint = request.uri().path().to_string(); + let method = request.method().to_string(); + let user_agent = extract_user_agent(request); + let ip_address = extract_ip_address(request); + + Self { + event_id: Uuid::new_v4().to_string(), + merchant_id, + request_id, + endpoint, + method, + request_payload: String::new(), + response_payload: String::new(), + status_code: 0, + processing_time_ms: 0, + gateway_selected: None, + routing_algorithm_id: None, + error_message: None, + user_agent, + ip_address, + created_at: OffsetDateTime::now_utc(), + } + } + + pub fn with_request_payload(mut self, payload: &str) -> Self { + self.request_payload = payload.to_string(); + self + } + + pub fn with_response_payload(mut self, payload: &str) -> Self { + self.response_payload = payload.to_string(); + self + } + + pub fn with_status_code(mut self, status_code: u16) -> Self { + self.status_code = status_code; + self + } + + pub fn with_processing_time(mut self, processing_time_ms: u32) -> Self { + self.processing_time_ms = processing_time_ms; + self + } + + pub fn with_gateway_selected(mut self, gateway: Option) -> Self { + self.gateway_selected = gateway; + self + } + + pub fn with_routing_algorithm_id(mut self, algorithm_id: Option) -> Self { + self.routing_algorithm_id = algorithm_id; + self + } + + pub fn with_error_message(mut self, error: Option) -> Self { + self.error_message = error; + self + } + + pub fn to_event_data(&self) -> RoutingEventData { + RoutingEventData { + event_id: self.event_id.clone(), + merchant_id: self.merchant_id.clone(), + request_id: self.request_id.clone(), + endpoint: self.endpoint.clone(), + method: self.method.clone(), + request_payload: self.request_payload.clone(), + response_payload: self.response_payload.clone(), + status_code: self.status_code, + processing_time_ms: self.processing_time_ms, + gateway_selected: self.gateway_selected.clone(), + routing_algorithm_id: self.routing_algorithm_id.clone(), + error_message: self.error_message.clone(), + user_agent: self.user_agent.clone(), + ip_address: self.ip_address.clone(), + created_at: self.created_at, + sign_flag: 1, // Always 1 for new events + } + } + + /// Extract gateway information from response payload + pub fn extract_gateway_from_response(&mut self) -> AnalyticsResult<()> { + if !self.response_payload.is_empty() { + if let Ok(response_json) = serde_json::from_str::(&self.response_payload) { + // Try to extract gateway from various possible response structures + if let Some(gateway) = response_json + .get("gateway") + .or_else(|| response_json.get("selected_gateway")) + .or_else(|| response_json.get("connector")) + .and_then(|v| v.as_str()) + { + self.gateway_selected = Some(gateway.to_string()); + } + + // Try to extract routing algorithm ID + if let Some(algo_id) = response_json + .get("routing_algorithm_id") + .or_else(|| response_json.get("algorithm_id")) + .and_then(|v| v.as_str()) + { + self.routing_algorithm_id = Some(algo_id.to_string()); + } + } + } + Ok(()) + } + + /// Extract error information from response payload + pub fn extract_error_from_response(&mut self) -> AnalyticsResult<()> { + if self.status_code >= 400 && !self.response_payload.is_empty() { + if let Ok(response_json) = serde_json::from_str::(&self.response_payload) { + if let Some(error) = response_json + .get("error") + .or_else(|| response_json.get("message")) + .or_else(|| response_json.get("error_message")) + .and_then(|v| v.as_str()) + { + self.error_message = Some(error.to_string()); + } + } + } + Ok(()) + } +} + +fn extract_request_id(request: &Request) -> String { + request + .headers() + .get("x-request-id") + .or_else(|| request.headers().get("request-id")) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_else(|| Uuid::new_v4().to_string()) +} + +fn extract_user_agent(request: &Request) -> Option { + request + .headers() + .get("user-agent") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) +} + +fn extract_ip_address(request: &Request) -> Option { + // Try various headers for IP address + request + .headers() + .get("x-forwarded-for") + .or_else(|| request.headers().get("x-real-ip")) + .or_else(|| request.headers().get("cf-connecting-ip")) + .and_then(|v| v.to_str().ok()) + .map(|s| { + // Take the first IP if there are multiple (comma-separated) + s.split(',') + .next() + .map(|ip| ip.trim().to_string()) + .unwrap_or_else(String::new) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routing_event_creation() { + let event = RoutingEvent::new( + "merchant-123".to_string(), + "req-456".to_string(), + "/routing/evaluate".to_string(), + "POST".to_string(), + ); + + assert_eq!(event.merchant_id, "merchant-123"); + assert_eq!(event.request_id, "req-456"); + assert_eq!(event.endpoint, "/routing/evaluate"); + assert_eq!(event.method, "POST"); + assert!(!event.event_id.is_empty()); + } + + #[test] + fn test_event_builder_pattern() { + let event = RoutingEvent::new( + "merchant-123".to_string(), + "req-456".to_string(), + "/routing/evaluate".to_string(), + "POST".to_string(), + ) + .with_request_payload(r#"{"test": "data"}"#) + .with_response_payload(r#"{"gateway": "stripe"}"#) + .with_status_code(200) + .with_processing_time(150) + .with_gateway_selected(Some("stripe".to_string())); + + assert_eq!(event.request_payload, r#"{"test": "data"}"#); + assert_eq!(event.response_payload, r#"{"gateway": "stripe"}"#); + assert_eq!(event.status_code, 200); + assert_eq!(event.processing_time_ms, 150); + assert_eq!(event.gateway_selected, Some("stripe".to_string())); + } + + #[test] + fn test_extract_gateway_from_response() { + let mut event = RoutingEvent::new( + "merchant-123".to_string(), + "req-456".to_string(), + "/routing/evaluate".to_string(), + "POST".to_string(), + ) + .with_response_payload(r#"{"gateway": "stripe", "routing_algorithm_id": "algo-123"}"#); + + event.extract_gateway_from_response().unwrap(); + + assert_eq!(event.gateway_selected, Some("stripe".to_string())); + assert_eq!(event.routing_algorithm_id, Some("algo-123".to_string())); + } + + #[test] + fn test_extract_error_from_response() { + let mut event = RoutingEvent::new( + "merchant-123".to_string(), + "req-456".to_string(), + "/routing/evaluate".to_string(), + "POST".to_string(), + ) + .with_response_payload(r#"{"error": "Gateway not available"}"#) + .with_status_code(500); + + event.extract_error_from_response().unwrap(); + + assert_eq!( + event.error_message, + Some("Gateway not available".to_string()) + ); + } +} diff --git a/src/analytics/kafka_producer.rs b/src/analytics/kafka_producer.rs new file mode 100644 index 00000000..17014660 --- /dev/null +++ b/src/analytics/kafka_producer.rs @@ -0,0 +1,248 @@ +use crate::analytics::{AnalyticsError, AnalyticsResult, KafkaConfig, RoutingEventData}; +use kafka::producer::{Producer, Record, RequiredAcks}; +use std::time::Duration; +use tokio::sync::mpsc; +use tracing::{debug, error, info, warn}; + +#[derive(Clone)] +pub struct KafkaProducer { + config: KafkaConfig, + topic: String, +} + +impl KafkaProducer { + pub fn new(config: KafkaConfig) -> AnalyticsResult { + let topic = format!("{}-routing-events", config.topic_prefix); + + // Validate broker configuration + if config.brokers.is_empty() { + return Err(AnalyticsError::Configuration( + "No Kafka brokers configured".to_string(), + )); + } + + debug!( + "Initializing Kafka producer with brokers: {:?}", + config.brokers + ); + + Ok(Self { config, topic }) + } + + /// Test Kafka connectivity + pub async fn test_connection(&self) -> AnalyticsResult<()> { + debug!( + "Testing Kafka connection to brokers: {:?}", + self.config.brokers + ); + + Producer::from_hosts(self.config.brokers.clone()) + .with_ack_timeout(Duration::from_secs(5)) + .with_required_acks(RequiredAcks::One) + .create() + .map_err(|e| { + error!( + "Failed to create Kafka producer for connection test: {:?}", + e + ); + AnalyticsError::Kafka(e) + })?; + + info!("Kafka connection test successful"); + Ok(()) + } + + pub async fn send_event(&self, event: &RoutingEventData) -> AnalyticsResult<()> { + let json_data = serde_json::to_string(event)?; + + // Create producer with configuration + let mut producer = Producer::from_hosts(self.config.brokers.clone()) + .with_ack_timeout(Duration::from_secs(1)) + .with_required_acks(RequiredAcks::One) + .create() + .map_err(AnalyticsError::Kafka)?; + + // Send the record + let record = + Record::from_key_value(&self.topic, event.event_id.as_bytes(), json_data.as_bytes()); + + producer.send(&record).map_err(AnalyticsError::Kafka)?; + + Ok(()) + } + + pub async fn send_events_batch(&self, events: &[RoutingEventData]) -> AnalyticsResult<()> { + if events.is_empty() { + return Ok(()); + } + + let mut producer = Producer::from_hosts(self.config.brokers.clone()) + .with_ack_timeout(Duration::from_secs(5)) + .with_required_acks(RequiredAcks::One) + .create() + .map_err(|e| { + error!("Failed to create Kafka producer for batch send: {:?}", e); + warn!("Kafka brokers configured: {:?}", self.config.brokers); + AnalyticsError::Kafka(e) + })?; + + for (index, event) in events.iter().enumerate() { + let json_data = serde_json::to_string(event)?; + let record = Record::from_key_value( + &self.topic, + event.event_id.as_bytes(), + json_data.as_bytes(), + ); + + if let Err(e) = producer.send(&record) { + error!( + "Failed to send event {} of {} to Kafka: {:?}", + index + 1, + events.len(), + e + ); + return Err(AnalyticsError::Kafka(e)); + } + } + + info!( + "Successfully sent {} events to Kafka topic: {}", + events.len(), + self.topic + ); + Ok(()) + } + + /// Send events batch with graceful error handling + pub async fn send_events_batch_graceful(&self, events: &[RoutingEventData]) -> bool { + match self.send_events_batch(events).await { + Ok(()) => true, + Err(e) => { + warn!( + "Failed to send events batch to Kafka, continuing without analytics: {:?}", + e + ); + false + } + } + } + + pub fn start_batch_processor( + &self, + mut receiver: mpsc::Receiver, + ) -> tokio::task::JoinHandle<()> { + let producer = self.clone(); + let batch_size = self.config.batch_size; + let batch_timeout = Duration::from_millis(self.config.batch_timeout_ms); + let max_consecutive_failures = self.config.max_consecutive_failures; + + tokio::spawn(async move { + let mut batch = Vec::with_capacity(batch_size); + let mut last_flush = tokio::time::Instant::now(); + let mut consecutive_failures = 0; + info!("Starting Kafka batch processor with batch_size: {}, timeout: {}ms, max_consecutive_failures: {}", + batch_size, batch_timeout.as_millis(), max_consecutive_failures); + + loop { + tokio::select! { + // Receive new events + event = receiver.recv() => { + match event { + Some(event) => { + batch.push(event); + + // Flush if batch is full + if batch.len() >= batch_size { + let success = producer.send_events_batch_graceful(&batch).await; + if success { + consecutive_failures = 0; + } else { + consecutive_failures += 1; + if consecutive_failures >= max_consecutive_failures { + warn!("Too many consecutive Kafka failures ({}), continuing to collect events but not sending", + consecutive_failures); + } + } + batch.clear(); + last_flush = tokio::time::Instant::now(); + } + } + None => { + // Channel closed, flush remaining events and exit + if !batch.is_empty() { + info!("Channel closed, sending final batch of {} events", batch.len()); + producer.send_events_batch_graceful(&batch).await; + } + info!("Kafka batch processor shutting down"); + break; + } + } + } + + // Timeout-based flush + _ = tokio::time::sleep_until(last_flush + batch_timeout) => { + if !batch.is_empty() { + let success = producer.send_events_batch_graceful(&batch).await; + if success { + consecutive_failures = 0; + } else { + consecutive_failures += 1; + if consecutive_failures >= max_consecutive_failures { + warn!("Too many consecutive Kafka failures ({}), continuing to collect events but not sending", + consecutive_failures); + } + } + batch.clear(); + last_flush = tokio::time::Instant::now(); + } + } + } + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use time::OffsetDateTime; + + #[tokio::test] + async fn test_kafka_producer_creation() { + let config = KafkaConfig { + brokers: vec!["localhost:9092".to_string()], + topic_prefix: "test".to_string(), + batch_size: 10, + batch_timeout_ms: 1000, + max_consecutive_failures: 5, + }; + + let producer = KafkaProducer::new(config); + assert!(producer.is_ok()); + } + + #[test] + fn test_event_serialization() { + let event = RoutingEventData { + event_id: "test-event-1".to_string(), + merchant_id: "merchant-123".to_string(), + request_id: "req-456".to_string(), + endpoint: "/routing/evaluate".to_string(), + method: "POST".to_string(), + request_payload: r#"{"test": "data"}"#.to_string(), + response_payload: r#"{"result": "success"}"#.to_string(), + status_code: 200, + processing_time_ms: 150, + gateway_selected: Some("stripe".to_string()), + routing_algorithm_id: Some("algo-789".to_string()), + error_message: None, + user_agent: Some("test-agent".to_string()), + ip_address: Some("127.0.0.1".to_string()), + created_at: OffsetDateTime::now_utc(), + sign_flag: 1, + }; + + let json = serde_json::to_string(&event); + assert!(json.is_ok()); + } +} diff --git a/src/analytics/middleware.rs b/src/analytics/middleware.rs new file mode 100644 index 00000000..43323336 --- /dev/null +++ b/src/analytics/middleware.rs @@ -0,0 +1,159 @@ +use crate::analytics::RoutingEvent; +use crate::tenant::GlobalAppState; +use axum::{ + body::Body, + extract::{Request, State}, + middleware::Next, + response::Response, +}; +use bytes::Bytes; +use http_body_util::BodyExt; +use std::{sync::Arc, time::Instant}; +use tracing::{error, warn}; + +/// Analytics middleware to track routing events for specific endpoints +pub async fn analytics_middleware( + State(global_app_state): State>, + request: Request, + next: Next, +) -> Response { + // Only track analytics for routing endpoints + let path = request.uri().path(); + if !global_app_state.global_config.analytics.enabled || !should_track_endpoint(path) { + return next.run(request).await; + } + + let start_time = Instant::now(); + + // Extract merchant ID from request headers or body (simplified for now) + let merchant_id = extract_merchant_id(&request).unwrap_or("public".to_string()); + + // Get the tenant app state to access analytics client + let tenant_app_state = match global_app_state.get_app_state_of_tenant(&merchant_id).await { + Ok(state) => state, + Err(_) => { + // If tenant not found, try with default "public" tenant + match global_app_state.get_app_state_of_tenant("public").await { + Ok(state) => state, + Err(_) => { + // If analytics client is not available, just proceed without analytics + return next.run(request).await; + } + } + } + }; + + // Create routing event + let mut routing_event = RoutingEvent::from_request(&request, merchant_id.clone()); + + // Extract request body for logging + let (request_parts, body) = request.into_parts(); + let body_bytes = match body.collect().await { + Ok(collected) => collected.to_bytes(), + Err(_) => Bytes::new(), + }; + + let request_payload = String::from_utf8_lossy(&body_bytes).to_string(); + routing_event = routing_event.with_request_payload(&request_payload); + + // Reconstruct request with body + let request = Request::from_parts(request_parts, Body::from(body_bytes)); + + // Process the request + let response = next.run(request).await; + + // Calculate processing time + let processing_time = start_time.elapsed().as_millis() as u32; + + // Extract response information + let status_code = response.status().as_u16(); + + // Extract response body for logging. Note: This operation can lead to high memory usage + let (response_parts, body) = response.into_parts(); + let body_bytes = match body.collect().await { + Ok(collected) => collected.to_bytes(), + Err(_) => Bytes::new(), + }; + + let response_payload = String::from_utf8_lossy(&body_bytes).to_string(); + + // Complete the routing event + routing_event = routing_event + .with_response_payload(&response_payload) + .with_status_code(status_code) + .with_processing_time(processing_time); + + // Extract gateway information from response + if let Err(e) = routing_event.extract_gateway_from_response() { + warn!("Failed to extract gateway from response: {:?}", e); + } + + // Extract error information if status indicates failure + if let Err(e) = routing_event.extract_error_from_response() { + warn!("Failed to extract error from response: {:?}", e); + } + + // Send event to analytics (async, non-blocking) + if let Err(e) = tenant_app_state + .analytics_client + .track_routing_event(routing_event) + .await + { + error!("Failed to track routing event: {:?}", e); + } + + // Reconstruct response + Response::from_parts(response_parts, Body::from(body_bytes)) +} + +/// Determine if an endpoint should be tracked for analytics +fn should_track_endpoint(path: &str) -> bool { + matches!(path, "/routing/evaluate" | "/decide-gateway") +} + +/// Extract merchant ID from request (simplified implementation) +fn extract_merchant_id(request: &Request) -> Option { + // Try to extract from headers first + if let Some(merchant_id) = request.headers().get("x-merchant-id") { + if let Ok(merchant_id_str) = merchant_id.to_str() { + return Some(merchant_id_str.to_string()); + } + } + + // Try x-tenant-id header as fallback + if let Some(tenant_id) = request.headers().get("x-tenant-id") { + if let Ok(tenant_id_str) = tenant_id.to_str() { + return Some(tenant_id_str.to_string()); + } + } + + // Default to "public" tenant + Some("public".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_should_track_endpoint() { + assert!(should_track_endpoint("/routing/evaluate")); + assert!(should_track_endpoint("/decide-gateway")); + assert!(!should_track_endpoint("/health")); + assert!(!should_track_endpoint("/rule/create")); + } + + #[test] + fn test_extract_merchant_id_from_header() { + let request = Request::builder() + .uri("/routing/evaluate") + .header("x-merchant-id", "merchant-123") + .body(Body::empty()) + .unwrap(); + + assert_eq!( + extract_merchant_id(&request).as_deref(), + Some("merchant-123") + ); + } +} diff --git a/src/analytics/mod.rs b/src/analytics/mod.rs new file mode 100644 index 00000000..427265d0 --- /dev/null +++ b/src/analytics/mod.rs @@ -0,0 +1,15 @@ +pub mod client; +pub mod events; +pub mod kafka_producer; +pub mod middleware; +pub mod models; +pub mod service; +pub mod types; + +pub use client::AnalyticsClient; +pub use events::RoutingEvent; +pub use kafka_producer::KafkaProducer; +pub use middleware::analytics_middleware; +pub use models::*; +pub use service::*; +pub use types::*; diff --git a/src/analytics/models.rs b/src/analytics/models.rs new file mode 100644 index 00000000..f979648b --- /dev/null +++ b/src/analytics/models.rs @@ -0,0 +1,317 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsQuery { + pub merchant_id: Option, + pub scope: AnalyticsScope, + pub range: AnalyticsRange, + pub start_ms: Option, + pub end_ms: Option, + pub page: usize, + pub page_size: usize, + pub payment_method_type: Option, + pub payment_method: Option, + pub card_network: Option, + pub card_is_in: Option, + pub currency: Option, + pub country: Option, + pub auth_type: Option, + pub gateways: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AnalyticsScope { + Current, + All, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AnalyticsRange { + M15, + H1, + H24, +} + +impl AnalyticsRange { + pub fn from_query(value: Option<&str>) -> Self { + match value { + Some("15m") => Self::M15, + Some("24h") => Self::H24, + _ => Self::H1, + } + } + + pub fn window_ms(&self) -> i64 { + match self { + Self::M15 => 15 * 60 * 1000, + Self::H1 => 60 * 60 * 1000, + Self::H24 => 24 * 60 * 60 * 1000, + } + } + + pub fn bucket_ms(&self) -> i64 { + match self { + Self::M15 => 60 * 1000, + Self::H1 => 5 * 60 * 1000, + Self::H24 => 15 * 60 * 1000, + } + } +} + +impl AnalyticsScope { + pub fn from_query(value: Option<&str>) -> Self { + match value { + Some("all") => Self::All, + _ => Self::Current, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Current => "current", + Self::All => "all", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsKpi { + pub label: String, + pub value: String, + pub subtitle: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsOverviewResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub kpis: Vec, + pub route_hits: Vec, + pub top_scores: Vec, + pub top_errors: Vec, + pub top_rules: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsRouteHit { + pub route: String, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GatewayScoreSnapshot { + pub merchant_id: String, + pub payment_method_type: String, + pub payment_method: String, + pub gateway: String, + pub score_value: f64, + pub sigma_factor: f64, + pub average_latency: f64, + pub tp99_latency: f64, + pub transaction_count: i64, + pub last_updated_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GatewayScoreSeriesPoint { + pub bucket_ms: i64, + pub merchant_id: String, + pub payment_method_type: String, + pub payment_method: String, + pub gateway: String, + pub score_value: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsGatewayScoresResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub range: String, + pub snapshots: Vec, + pub series: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsDecisionPoint { + pub bucket_ms: i64, + pub routing_approach: String, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsDecisionResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub range: String, + pub tiles: Vec, + pub series: Vec, + pub approaches: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsGatewaySharePoint { + pub bucket_ms: i64, + pub gateway: String, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsRoutingStatsResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub range: String, + pub gateway_share: Vec, + pub top_rules: Vec, + pub sr_trend: Vec, + pub available_filters: RoutingFilterOptions, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoutingFilterOptions { + pub dimensions: Vec, + pub missing_dimensions: Vec, + pub gateways: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoutingFilterDimension { + pub key: String, + pub label: String, + pub values: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoutingFilterDimensionHint { + pub key: String, + pub label: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsErrorSummary { + pub route: String, + pub error_code: String, + pub error_message: String, + pub count: i64, + pub last_seen_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsLogSample { + pub route: String, + pub merchant_id: Option, + pub payment_id: Option, + pub request_id: Option, + pub gateway: Option, + pub routing_approach: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub event_type: Option, + pub created_at_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsLogSummariesResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub range: String, + pub total_errors: i64, + pub errors: Vec, + pub samples: Vec, + pub page: usize, + pub page_size: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalyticsRuleHit { + pub rule_name: String, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentAuditQuery { + pub merchant_id: Option, + pub scope: AnalyticsScope, + pub range: AnalyticsRange, + pub start_ms: Option, + pub end_ms: Option, + pub page: usize, + pub page_size: usize, + pub payment_id: Option, + pub request_id: Option, + pub gateway: Option, + pub route: Option, + pub status: Option, + pub event_type: Option, + pub error_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentAuditSummary { + pub lookup_key: String, + pub payment_id: Option, + pub request_id: Option, + pub merchant_id: Option, + pub first_seen_ms: i64, + pub last_seen_ms: i64, + pub event_count: usize, + pub latest_status: Option, + pub latest_gateway: Option, + pub latest_stage: Option, + pub gateways: Vec, + pub routes: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentAuditEvent { + pub id: i32, + pub event_type: String, + pub event_stage: Option, + pub route: Option, + pub merchant_id: Option, + pub payment_id: Option, + pub request_id: Option, + pub payment_method_type: Option, + pub payment_method: Option, + pub gateway: Option, + pub routing_approach: Option, + pub rule_name: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub score_value: Option, + pub sigma_factor: Option, + pub average_latency: Option, + pub tp99_latency: Option, + pub transaction_count: Option, + pub details: Option, + pub details_json: Option, + pub created_at_ms: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentAuditResponse { + pub generated_at_ms: i64, + pub scope: String, + pub merchant_id: Option, + pub range: String, + pub payment_id: Option, + pub request_id: Option, + pub gateway: Option, + pub route: Option, + pub status: Option, + pub event_type: Option, + pub error_code: Option, + pub page: usize, + pub page_size: usize, + pub total_results: usize, + pub results: Vec, + pub timeline: Vec, +} diff --git a/src/analytics/service.rs b/src/analytics/service.rs new file mode 100644 index 00000000..a96b49d4 --- /dev/null +++ b/src/analytics/service.rs @@ -0,0 +1,2025 @@ +use crate::analytics::models::*; +use crate::error; +use crate::metrics::{ANALYTICS_EVENT_COUNTER, ROUTING_DECISION_COUNTER, ROUTING_RULE_HIT_COUNTER}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::prelude::*; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use time::OffsetDateTime; + +#[cfg(feature = "mysql")] +use crate::storage::schema; +#[cfg(feature = "postgres")] +use crate::storage::schema_pg; + +#[cfg(feature = "mysql")] +use crate::storage::schema::analytics_event::dsl as analytics_dsl; +#[cfg(feature = "postgres")] +use crate::storage::schema_pg::analytics_event::dsl as analytics_dsl; + +#[derive(Debug, Clone, Insertable)] +#[cfg_attr(feature = "mysql", diesel(table_name = schema::analytics_event))] +#[cfg_attr(feature = "postgres", diesel(table_name = schema_pg::analytics_event))] +pub struct NewAnalyticsEvent { + pub event_type: String, + pub merchant_id: Option, + pub payment_id: Option, + pub request_id: Option, + pub payment_method_type: Option, + pub payment_method: Option, + pub card_network: Option, + pub card_is_in: Option, + pub currency: Option, + pub country: Option, + pub auth_type: Option, + pub gateway: Option, + pub event_stage: Option, + pub routing_approach: Option, + pub rule_name: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub score_value: Option, + pub sigma_factor: Option, + pub average_latency: Option, + pub tp99_latency: Option, + pub transaction_count: Option, + pub route: Option, + pub details: Option, + pub created_at_ms: i64, +} + +#[derive(Debug, Clone, Queryable, Selectable)] +#[cfg_attr(feature = "mysql", diesel(table_name = schema::analytics_event))] +#[cfg_attr(feature = "postgres", diesel(table_name = schema_pg::analytics_event))] +pub struct AnalyticsEvent { + pub id: i32, + pub event_type: String, + pub merchant_id: Option, + pub payment_id: Option, + pub request_id: Option, + pub payment_method_type: Option, + pub payment_method: Option, + pub card_network: Option, + pub card_is_in: Option, + pub currency: Option, + pub country: Option, + pub auth_type: Option, + pub gateway: Option, + pub event_stage: Option, + pub routing_approach: Option, + pub rule_name: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub score_value: Option, + pub sigma_factor: Option, + pub average_latency: Option, + pub tp99_latency: Option, + pub transaction_count: Option, + pub route: Option, + pub details: Option, + pub created_at_ms: i64, +} + +pub fn now_ms() -> i64 { + (OffsetDateTime::now_utc() + .unix_timestamp_nanos() + .div_euclid(1_000_000)) as i64 +} + +fn event_type_label(kind: &str) -> &'static str { + match kind { + "decision" => "decision", + "gateway_update" => "gateway_update", + "score_snapshot" => "score_snapshot", + "rule_hit" => "rule_hit", + "rule_evaluation_preview" => "rule_evaluation_preview", + "error" => "error", + "request_hit" => "request_hit", + _ => "other", + } +} + +fn spawn_persist(event: NewAnalyticsEvent) { + let label = event_type_label(event.event_type.as_str()); + ANALYTICS_EVENT_COUNTER.with_label_values(&[label]).inc(); + + tokio::spawn(async move { + if let Err(err) = persist_event(event).await { + crate::logger::debug!(error = %err, "Failed to persist analytics event"); + } + }); +} + +pub fn record_decision_event( + merchant_id: Option, + routing_approach: Option, + gateway: Option, + status: Option, + route: &str, + rule_name: Option, + details: Option, + payment_id: Option, + request_id: Option, + event_stage: Option, + payment_method_type: Option, + payment_method: Option, +) { + let approach = routing_approach + .clone() + .unwrap_or_else(|| "UNKNOWN".to_string()); + let status_label = status.clone().unwrap_or_else(|| "success".to_string()); + ROUTING_DECISION_COUNTER + .with_label_values(&[approach.as_str(), status_label.as_str()]) + .inc(); + spawn_persist(NewAnalyticsEvent { + event_type: "decision".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type, + payment_method, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway, + event_stage, + routing_approach, + rule_name, + status, + error_code: None, + error_message: None, + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some(route.to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_score_snapshot_event( + merchant_id: Option, + payment_method_type: Option, + payment_method: Option, + card_network: Option, + card_is_in: Option, + currency: Option, + country: Option, + auth_type: Option, + gateway: Option, + score_value: Option, + sigma_factor: Option, + average_latency: Option, + tp99_latency: Option, + transaction_count: Option, + route: &str, + details: Option, + payment_id: Option, + request_id: Option, + event_stage: Option, +) { + spawn_persist(NewAnalyticsEvent { + event_type: "score_snapshot".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type, + payment_method, + card_network, + card_is_in, + currency, + country, + auth_type, + gateway, + event_stage, + routing_approach: None, + rule_name: None, + status: Some("snapshot".to_string()), + error_code: None, + error_message: None, + score_value, + sigma_factor, + average_latency, + tp99_latency, + transaction_count, + route: Some(route.to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_gateway_update_event( + merchant_id: Option, + gateway: Option, + status: Option, + route: &str, + details: Option, + payment_id: Option, + request_id: Option, + event_stage: Option, +) { + spawn_persist(NewAnalyticsEvent { + event_type: "gateway_update".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type: None, + payment_method: None, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway, + event_stage, + routing_approach: None, + rule_name: None, + status, + error_code: None, + error_message: None, + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some(route.to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_rule_hit_event( + merchant_id: Option, + rule_name: String, + gateway: Option, + routing_approach: Option, + details: Option, + payment_id: Option, + request_id: Option, + event_stage: Option, +) { + ROUTING_RULE_HIT_COUNTER + .with_label_values(&[rule_name.as_str()]) + .inc(); + spawn_persist(NewAnalyticsEvent { + event_type: "rule_hit".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type: None, + payment_method: None, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway, + event_stage, + routing_approach, + rule_name: Some(rule_name), + status: Some("hit".to_string()), + error_code: None, + error_message: None, + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some("routing".to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_rule_evaluation_preview_event( + merchant_id: Option, + payment_id: Option, + gateway: Option, + rule_name: Option, + status: Option, + details: Option, +) { + spawn_persist(NewAnalyticsEvent { + event_type: "rule_evaluation_preview".to_string(), + merchant_id, + payment_id, + request_id: None, + payment_method_type: None, + payment_method: None, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway, + event_stage: Some("preview_evaluated".to_string()), + routing_approach: Some("RULE_EVALUATE_PREVIEW".to_string()), + rule_name, + status, + error_code: None, + error_message: None, + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some("routing_evaluate".to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_error_event( + route: &str, + merchant_id: Option, + payment_id: Option, + request_id: Option, + gateway: Option, + routing_approach: Option, + error_code: String, + error_message: String, + details: Option, + event_stage: Option, +) { + spawn_persist(NewAnalyticsEvent { + event_type: "error".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type: None, + payment_method: None, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway, + event_stage, + routing_approach, + rule_name: None, + status: Some("failure".to_string()), + error_code: Some(error_code), + error_message: Some(error_message), + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some(route.to_string()), + details, + created_at_ms: now_ms(), + }); +} + +pub fn record_request_hit_event( + route: &str, + merchant_id: Option, + payment_id: Option, + request_id: Option, +) { + spawn_persist(NewAnalyticsEvent { + event_type: "request_hit".to_string(), + merchant_id, + payment_id, + request_id, + payment_method_type: None, + payment_method: None, + card_network: None, + card_is_in: None, + currency: None, + country: None, + auth_type: None, + gateway: None, + event_stage: Some("request_received".to_string()), + routing_approach: None, + rule_name: None, + status: Some("received".to_string()), + error_code: None, + error_message: None, + score_value: None, + sigma_factor: None, + average_latency: None, + tp99_latency: None, + transaction_count: None, + route: Some(route.to_string()), + details: None, + created_at_ms: now_ms(), + }); +} + +async fn persist_event(event: NewAnalyticsEvent) -> Result<(), error::ApiError> { + let state = crate::app::get_tenant_app_state().await; + let conn = &state + .db + .get_conn() + .await + .map_err(|_| error::ApiError::DatabaseError)?; + + diesel::insert_into(analytics_dsl::analytics_event) + .values(event) + .execute_async(&**conn) + .await + .map_err(|_| error::ApiError::DatabaseError)?; + + Ok(()) +} + +async fn load_events( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, + event_types: &[&str], +) -> Result, error::ApiError> { + let conn = &state + .db + .get_conn() + .await + .map_err(|_| error::ApiError::DatabaseError)?; + + let mut builder = analytics_dsl::analytics_event + .select(AnalyticsEvent::as_select()) + .into_boxed(); + let (start_ms, end_ms) = effective_window_bounds(query); + builder = builder + .filter(analytics_dsl::created_at_ms.ge(start_ms)) + .filter(analytics_dsl::created_at_ms.le(end_ms)); + + if query.scope == AnalyticsScope::Current { + if let Some(merchant_id) = &query.merchant_id { + builder = builder.filter(analytics_dsl::merchant_id.eq(merchant_id.clone())); + } + } + + let event_types: Vec = event_types + .iter() + .map(|event_type| (*event_type).to_string()) + .collect(); + + if !event_types.is_empty() { + builder = builder.filter(analytics_dsl::event_type.eq_any(event_types)); + } + + builder + .order((analytics_dsl::created_at_ms.asc(), analytics_dsl::id.asc())) + .load_async::(&**conn) + .await + .map_err(|err| { + crate::logger::error!( + error = ?err, + merchant_id = ?query.merchant_id, + scope = query.scope.as_str(), + "Analytics read failed; returning empty analytics state" + ); + err + }) + .or_else(|_| Ok(Vec::new())) +} + +async fn load_payment_audit_events( + state: &crate::app::TenantAppState, + query: &PaymentAuditQuery, +) -> Result, error::ApiError> { + let conn = &state + .db + .get_conn() + .await + .map_err(|_| error::ApiError::DatabaseError)?; + + let mut builder = analytics_dsl::analytics_event + .select(AnalyticsEvent::as_select()) + .into_boxed(); + let (start_ms, end_ms) = effective_payment_audit_window_bounds(query); + builder = builder + .filter(analytics_dsl::created_at_ms.ge(start_ms)) + .filter(analytics_dsl::created_at_ms.le(end_ms)); + builder = builder.filter(analytics_dsl::event_type.eq_any(vec![ + "decision".to_string(), + "gateway_update".to_string(), + "rule_hit".to_string(), + "error".to_string(), + ])); + + if query.scope == AnalyticsScope::Current { + if let Some(merchant_id) = &query.merchant_id { + builder = builder.filter(analytics_dsl::merchant_id.eq(merchant_id.clone())); + } + } + + if let Some(payment_id) = &query.payment_id { + builder = builder.filter(analytics_dsl::payment_id.eq(payment_id.clone())); + } + + if query.payment_id.is_none() { + if let Some(request_id) = &query.request_id { + builder = builder.filter(analytics_dsl::request_id.eq(request_id.clone())); + } + } + + if let Some(gateway) = &query.gateway { + builder = builder.filter(analytics_dsl::gateway.eq(gateway.clone())); + } + + if let Some(route) = &query.route { + builder = builder.filter(analytics_dsl::route.eq(route.clone())); + } + + if let Some(status) = &query.status { + if status.eq_ignore_ascii_case("success") { + builder = builder.filter( + analytics_dsl::status + .eq("success".to_string()) + .or(analytics_dsl::status + .eq("SUCCESS".to_string()) + .or(analytics_dsl::status + .eq("CHARGED".to_string()) + .or(analytics_dsl::status.eq("AUTHORIZED".to_string())))), + ); + } else if status.eq_ignore_ascii_case("failure") || status.eq_ignore_ascii_case("FAILURE") { + builder = builder.filter( + analytics_dsl::status + .eq("FAILURE".to_string()) + .or(analytics_dsl::status + .like("%FAILED%") + .or(analytics_dsl::status.like("%DECLINED%"))), + ); + } else { + builder = builder.filter(analytics_dsl::status.eq(status.clone())); + } + } + + if let Some(event_type) = &query.event_type { + builder = builder.filter(analytics_dsl::event_type.eq(event_type.clone())); + } + + if let Some(error_code) = &query.error_code { + builder = builder.filter(analytics_dsl::error_code.eq(error_code.clone())); + } + + builder + .order(( + analytics_dsl::created_at_ms.desc(), + analytics_dsl::id.desc(), + )) + .load_async::(&**conn) + .await + .map_err(|err| { + crate::logger::error!( + error = ?err, + merchant_id = ?query.merchant_id, + payment_id = ?query.payment_id, + request_id = ?query.request_id, + "Payment audit read failed; returning empty audit state" + ); + err + }) + .or_else(|_| Ok(Vec::new())) +} + +async fn load_preview_trace_events( + state: &crate::app::TenantAppState, + query: &PaymentAuditQuery, +) -> Result, error::ApiError> { + let conn = &state + .db + .get_conn() + .await + .map_err(|_| error::ApiError::DatabaseError)?; + + let mut builder = analytics_dsl::analytics_event + .select(AnalyticsEvent::as_select()) + .into_boxed(); + let (start_ms, end_ms) = effective_payment_audit_window_bounds(query); + builder = builder + .filter(analytics_dsl::created_at_ms.ge(start_ms)) + .filter(analytics_dsl::created_at_ms.le(end_ms)); + builder = builder.filter(analytics_dsl::route.eq("routing_evaluate".to_string())); + builder = builder.filter(analytics_dsl::event_type.eq_any(vec![ + "rule_evaluation_preview".to_string(), + "error".to_string(), + ])); + + if query.scope == AnalyticsScope::Current { + if let Some(merchant_id) = &query.merchant_id { + builder = builder.filter(analytics_dsl::merchant_id.eq(merchant_id.clone())); + } + } + + if let Some(payment_id) = &query.payment_id { + builder = builder.filter(analytics_dsl::payment_id.eq(payment_id.clone())); + } + + if query.payment_id.is_none() { + if let Some(request_id) = &query.request_id { + builder = builder.filter(analytics_dsl::request_id.eq(request_id.clone())); + } + } + + if let Some(gateway) = &query.gateway { + builder = builder.filter(analytics_dsl::gateway.eq(gateway.clone())); + } + + if let Some(status) = &query.status { + if status.eq_ignore_ascii_case("success") { + builder = builder.filter( + analytics_dsl::status + .eq("success".to_string()) + .or(analytics_dsl::status.eq("default_selection".to_string())), + ); + } else if status.eq_ignore_ascii_case("failure") || status.eq_ignore_ascii_case("FAILURE") { + builder = builder.filter( + analytics_dsl::status + .eq("FAILURE".to_string()) + .or(analytics_dsl::status + .like("%FAILED%") + .or(analytics_dsl::status.like("%DECLINED%"))), + ); + } else { + builder = builder.filter(analytics_dsl::status.eq(status.clone())); + } + } + + if let Some(event_type) = &query.event_type { + builder = builder.filter(analytics_dsl::event_type.eq(event_type.clone())); + } + + if let Some(error_code) = &query.error_code { + builder = builder.filter(analytics_dsl::error_code.eq(error_code.clone())); + } + + builder + .order(( + analytics_dsl::created_at_ms.desc(), + analytics_dsl::id.desc(), + )) + .load_async::(&**conn) + .await + .map_err(|err| { + crate::logger::error!( + error = ?err, + merchant_id = ?query.merchant_id, + payment_id = ?query.payment_id, + "Preview trace read failed; returning empty preview state" + ); + err + }) + .or_else(|_| Ok(Vec::new())) +} + +fn parse_details_json(details: &Option) -> Option { + details + .as_ref() + .and_then(|value| serde_json::from_str::(value).ok()) +} + +fn transaction_status_from_details(event: &AnalyticsEvent) -> Option { + let details = parse_details_json(&event.details)?; + details + .get("request") + .and_then(|request| request.get("status")) + .and_then(|status| status.as_str()) + .map(str::to_string) + .or_else(|| { + details + .get("selection_reason") + .and_then(|reason| reason.get("transaction_status")) + .and_then(|status| status.as_str()) + .map(str::to_string) + }) +} + +fn effective_window_bounds(query: &AnalyticsQuery) -> (i64, i64) { + let now = now_ms(); + let end_ms = query.end_ms.unwrap_or(now).min(now); + let start_ms = query + .start_ms + .filter(|start_ms| *start_ms >= 0 && *start_ms < end_ms) + .unwrap_or_else(|| end_ms.saturating_sub(query.range.window_ms())); + + (start_ms, end_ms) +} + +fn effective_payment_audit_window_bounds(query: &PaymentAuditQuery) -> (i64, i64) { + let now = now_ms(); + let end_ms = query.end_ms.unwrap_or(now).min(now); + let start_ms = query + .start_ms + .filter(|start_ms| *start_ms >= 0 && *start_ms < end_ms) + .unwrap_or_else(|| end_ms.saturating_sub(query.range.window_ms())); + + (start_ms, end_ms) +} + +fn query_bucket_size_ms(query: &AnalyticsQuery) -> i64 { + let (start_ms, end_ms) = effective_window_bounds(query); + let window_ms = end_ms.saturating_sub(start_ms); + + match window_ms { + 0..=900_000 => 60 * 1000, + 900_001..=3_600_000 => 5 * 60 * 1000, + 3_600_001..=86_400_000 => 15 * 60 * 1000, + 86_400_001..=259_200_000 => 60 * 60 * 1000, + _ => 3 * 60 * 60 * 1000, + } +} + +fn bucket_ms(created_at_ms: i64, query: &AnalyticsQuery) -> i64 { + let bucket = query_bucket_size_ms(query).max(1); + created_at_ms - (created_at_ms.rem_euclid(bucket)) +} + +fn normalise_gateways(raw: Option) -> Vec { + raw.into_iter() + .flat_map(|value| value.split(',').map(str::to_owned).collect::>()) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .collect() +} + +fn normalise_payment_audit_route_filter(route: Option) -> Option { + route.and_then(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + + Some(match trimmed { + "Decide Gateway" => "decide_gateway".to_string(), + "Update Gateway" => "update_gateway_score".to_string(), + "Rule Evaluate" => "routing_evaluate".to_string(), + other => other.to_string(), + }) + }) +} + +fn normalise_payment_audit_status_filter(status: Option) -> Option { + status.and_then(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + + Some(match trimmed.to_ascii_lowercase().as_str() { + "success" => "success".to_string(), + "failure" => "FAILURE".to_string(), + _ => trimmed.to_string(), + }) + }) +} + +fn payment_audit_stage_label(event: &AnalyticsEvent) -> &'static str { + match event.event_type.as_str() { + "decision" => "Decide Gateway", + "gateway_update" => "Update Gateway", + "rule_hit" => "Rule Evaluate", + "rule_evaluation_preview" => "Preview Result", + "error" => "Errors", + _ => "Errors", + } +} + +fn payment_audit_route_label(route: &str) -> String { + match route { + "decision_gateway" | "decide_gateway" => "Decide Gateway".to_string(), + "update_gateway_score" => "Update Gateway".to_string(), + "routing_evaluate" => "Rule Evaluate".to_string(), + other => other.to_string(), + } +} + +fn payment_dimension_filters_enabled(query: &AnalyticsQuery) -> bool { + query.scope == AnalyticsScope::Current +} + +struct RoutingDimensionSpec { + key: &'static str, + label: &'static str, +} + +const ROUTING_DIMENSION_SPECS: [RoutingDimensionSpec; 7] = [ + RoutingDimensionSpec { + key: "payment_method_type", + label: "Payment Method Type", + }, + RoutingDimensionSpec { + key: "payment_method", + label: "Payment Method", + }, + RoutingDimensionSpec { + key: "card_network", + label: "Card Network", + }, + RoutingDimensionSpec { + key: "card_is_in", + label: "Card ISIN", + }, + RoutingDimensionSpec { + key: "currency", + label: "Currency", + }, + RoutingDimensionSpec { + key: "country", + label: "Country", + }, + RoutingDimensionSpec { + key: "auth_type", + label: "Auth Type", + }, +]; + +fn event_dimension_value<'a>(event: &'a AnalyticsEvent, key: &str) -> Option<&'a str> { + match key { + "payment_method_type" => event.payment_method_type.as_deref(), + "payment_method" => event.payment_method.as_deref(), + "card_network" => event.card_network.as_deref(), + "card_is_in" => event.card_is_in.as_deref(), + "currency" => event.currency.as_deref(), + "country" => event.country.as_deref(), + "auth_type" => event.auth_type.as_deref(), + _ => None, + } +} + +fn query_dimension_value<'a>(query: &'a AnalyticsQuery, key: &str) -> Option<&'a str> { + match key { + "payment_method_type" => query.payment_method_type.as_deref(), + "payment_method" => query.payment_method.as_deref(), + "card_network" => query.card_network.as_deref(), + "card_is_in" => query.card_is_in.as_deref(), + "currency" => query.currency.as_deref(), + "country" => query.country.as_deref(), + "auth_type" => query.auth_type.as_deref(), + _ => None, + } +} + +fn score_events_for_dimensions<'a>(events: &'a [AnalyticsEvent]) -> Vec<&'a AnalyticsEvent> { + events + .iter() + .filter(|event| event.event_type == "score_snapshot") + .collect() +} + +fn active_routing_dimension_specs( + events: &[&AnalyticsEvent], +) -> Vec<&'static RoutingDimensionSpec> { + ROUTING_DIMENSION_SPECS + .iter() + .filter(|spec| { + events.iter().any(|event| { + event_dimension_value(event, spec.key).is_some_and(|value| !value.is_empty()) + }) + }) + .collect() +} + +fn score_event_matches_dimension_filters( + event: &AnalyticsEvent, + query: &AnalyticsQuery, + ignored_key: Option<&str>, +) -> bool { + if !payment_dimension_filters_enabled(query) { + return true; + } + + for spec in ROUTING_DIMENSION_SPECS.iter() { + if ignored_key == Some(spec.key) { + continue; + } + + let Some(selected_value) = query_dimension_value(query, spec.key) else { + continue; + }; + + if event_dimension_value(event, spec.key) != Some(selected_value) { + return false; + } + } + + true +} + +fn score_event_matches_filters(event: &AnalyticsEvent, query: &AnalyticsQuery) -> bool { + if !score_event_matches_dimension_filters(event, query, None) { + return false; + } + + if !query.gateways.is_empty() { + let Some(gateway) = event.gateway.as_deref() else { + return false; + }; + if !query.gateways.iter().any(|selected| selected == gateway) { + return false; + } + } + + true +} + +fn derive_routing_filter_options( + events: &[AnalyticsEvent], + query: &AnalyticsQuery, +) -> RoutingFilterOptions { + let score_events = score_events_for_dimensions(events); + let active_dimensions = active_routing_dimension_specs(&score_events); + let missing_dimensions = ROUTING_DIMENSION_SPECS + .iter() + .filter(|spec| { + !active_dimensions + .iter() + .any(|active| active.key == spec.key) + }) + .map(|spec| RoutingFilterDimensionHint { + key: spec.key.to_string(), + label: spec.label.to_string(), + }) + .collect(); + + let dimensions = active_dimensions + .into_iter() + .map(|spec| { + let values = score_events + .iter() + .filter(|event| score_event_matches_dimension_filters(event, query, Some(spec.key))) + .filter_map(|event| event_dimension_value(event, spec.key).map(str::to_string)) + .filter(|value| !value.is_empty()) + .collect::>() + .into_iter() + .collect(); + + RoutingFilterDimension { + key: spec.key.to_string(), + label: spec.label.to_string(), + values, + } + }) + .collect(); + + let gateways = score_events + .iter() + .filter(|event| score_event_matches_dimension_filters(event, query, None)) + .filter_map(|event| event.gateway.clone()) + .filter(|value| !value.is_empty()) + .collect::>() + .into_iter() + .collect(); + + RoutingFilterOptions { + dimensions, + missing_dimensions, + gateways, + } +} + +fn summarise_filtered_scores( + events: &[AnalyticsEvent], + query: &AnalyticsQuery, +) -> Vec { + let mut by_bucket_gateway: BTreeMap<(i64, String), (f64, i64, String, String, String)> = + BTreeMap::new(); + + for event in events + .iter() + .filter(|event| event.event_type == "score_snapshot") + .filter(|event| score_event_matches_filters(event, query)) + { + let gateway = event + .gateway + .clone() + .unwrap_or_else(|| "unknown".to_string()); + let bucket = bucket_ms(event.created_at_ms, query); + let entry = by_bucket_gateway + .entry((bucket, gateway.clone())) + .or_insert(( + 0.0, + 0, + event.merchant_id.clone().unwrap_or_default(), + event.payment_method_type.clone().unwrap_or_default(), + event.payment_method.clone().unwrap_or_default(), + )); + entry.0 += event.score_value.unwrap_or_default(); + entry.1 += 1; + } + + by_bucket_gateway + .into_iter() + .map( + |( + (bucket_ms, gateway), + (score_total, score_count, merchant_id, payment_method_type, payment_method), + )| { + GatewayScoreSeriesPoint { + bucket_ms, + merchant_id, + payment_method_type, + payment_method, + gateway, + score_value: if score_count > 0 { + score_total / score_count as f64 + } else { + 0.0 + }, + } + }, + ) + .collect() +} + +fn summarise_errors(events: &[AnalyticsEvent]) -> Vec { + let mut grouped: HashMap<(String, String, String), AnalyticsErrorSummary> = HashMap::new(); + + for event in events.iter().filter(|event| event.event_type == "error") { + let route = event.route.clone().unwrap_or_else(|| "unknown".to_string()); + let error_code = event + .error_code + .clone() + .unwrap_or_else(|| "unknown".to_string()); + let error_message = event + .error_message + .clone() + .unwrap_or_else(|| "unknown".to_string()); + let key = (route.clone(), error_code.clone(), error_message.clone()); + grouped + .entry(key) + .and_modify(|summary| { + summary.count += 1; + summary.last_seen_ms = summary.last_seen_ms.max(event.created_at_ms); + }) + .or_insert_with(|| AnalyticsErrorSummary { + route, + error_code, + error_message, + count: 1, + last_seen_ms: event.created_at_ms, + }); + } + + let mut rows: Vec<_> = grouped.into_values().collect(); + rows.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| right.last_seen_ms.cmp(&left.last_seen_ms)) + }); + rows +} + +fn summarise_rule_hits(events: &[AnalyticsEvent]) -> Vec { + let mut grouped: HashMap = HashMap::new(); + for event in events.iter().filter(|event| event.event_type == "rule_hit") { + let rule_name = event + .rule_name + .clone() + .unwrap_or_else(|| "unknown".to_string()); + *grouped.entry(rule_name).or_insert(0) += 1; + } + + let mut rows: Vec<_> = grouped + .into_iter() + .map(|(rule_name, count)| AnalyticsRuleHit { rule_name, count }) + .collect(); + rows.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.rule_name.cmp(&right.rule_name)) + }); + rows +} + +fn summarise_scores( + events: &[AnalyticsEvent], +) -> (Vec, Vec) { + let mut latest: HashMap<(String, String, String, String), AnalyticsEvent> = HashMap::new(); + let mut series = Vec::new(); + + for event in events + .iter() + .filter(|event| event.event_type == "score_snapshot") + { + let merchant_id = event.merchant_id.clone().unwrap_or_default(); + let payment_method_type = event.payment_method_type.clone().unwrap_or_default(); + let payment_method = event.payment_method.clone().unwrap_or_default(); + let gateway = event.gateway.clone().unwrap_or_default(); + let key = ( + merchant_id.clone(), + payment_method_type.clone(), + payment_method.clone(), + gateway.clone(), + ); + series.push(GatewayScoreSeriesPoint { + bucket_ms: event.created_at_ms, + merchant_id: merchant_id.clone(), + payment_method_type: payment_method_type.clone(), + payment_method: payment_method.clone(), + gateway: gateway.clone(), + score_value: event.score_value.unwrap_or_default(), + }); + + latest + .entry(key) + .and_modify(|current| { + if event.created_at_ms >= current.created_at_ms { + *current = event.clone(); + } + }) + .or_insert_with(|| event.clone()); + } + + let mut snapshots: Vec = latest + .into_iter() + .map( + |((merchant_id, payment_method_type, payment_method, gateway), event)| { + GatewayScoreSnapshot { + merchant_id, + payment_method_type, + payment_method, + gateway, + score_value: event.score_value.unwrap_or_default(), + sigma_factor: event.sigma_factor.unwrap_or_default(), + average_latency: event.average_latency.unwrap_or_default(), + tp99_latency: event.tp99_latency.unwrap_or_default(), + transaction_count: event.transaction_count.unwrap_or_default(), + last_updated_ms: event.created_at_ms, + } + }, + ) + .collect(); + snapshots.sort_by(|left, right| { + right + .score_value + .partial_cmp(&left.score_value) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| right.last_updated_ms.cmp(&left.last_updated_ms)) + }); + + (snapshots, series) +} + +fn summarise_decisions( + events: &[AnalyticsEvent], + query: &AnalyticsQuery, +) -> ( + Vec, + Vec, + Vec, +) { + let mut by_bucket: BTreeMap<(i64, String), i64> = BTreeMap::new(); + let mut by_approach: HashMap = HashMap::new(); + let mut total = 0_i64; + let mut failures = 0_i64; + + for event in events.iter().filter(|event| event.event_type == "decision") { + total += 1; + if event.status.as_deref() == Some("failure") { + failures += 1; + } + let bucket = bucket_ms(event.created_at_ms, query); + let approach = event + .routing_approach + .clone() + .unwrap_or_else(|| "UNKNOWN".to_string()); + *by_bucket.entry((bucket, approach.clone())).or_insert(0) += 1; + *by_approach.entry(approach).or_insert(0) += 1; + } + + let mut series: Vec = by_bucket + .into_iter() + .map( + |((bucket_ms, routing_approach), count)| AnalyticsDecisionPoint { + bucket_ms, + routing_approach, + count, + }, + ) + .collect(); + series.sort_by(|left, right| { + left.bucket_ms + .cmp(&right.bucket_ms) + .then_with(|| left.routing_approach.cmp(&right.routing_approach)) + }); + + let mut approaches: Vec = by_approach + .into_iter() + .map(|(rule_name, count)| AnalyticsRuleHit { rule_name, count }) + .collect(); + approaches.sort_by(|left, right| right.count.cmp(&left.count)); + + let error_rate = if total > 0 { + (failures as f64 / total as f64) * 100.0 + } else { + 0.0 + }; + + let tiles = vec![ + AnalyticsKpi { + label: "Decisions".to_string(), + value: total.to_string(), + subtitle: Some(format!("Failures: {}", failures)), + }, + AnalyticsKpi { + label: "Error rate".to_string(), + value: format!("{:.2}%", error_rate), + subtitle: Some("From recorded decision events".to_string()), + }, + ]; + + (series, approaches, tiles) +} + +fn summarise_gateway_share( + events: &[AnalyticsEvent], + query: &AnalyticsQuery, +) -> Vec { + let mut by_bucket_gateway: BTreeMap<(i64, String), i64> = BTreeMap::new(); + + for event in events.iter().filter(|event| event.event_type == "decision") { + let bucket = bucket_ms(event.created_at_ms, query); + let gateway = event + .gateway + .clone() + .unwrap_or_else(|| "unknown".to_string()); + *by_bucket_gateway.entry((bucket, gateway)).or_insert(0) += 1; + } + + let mut points: Vec<_> = by_bucket_gateway + .into_iter() + .map(|((bucket_ms, gateway), count)| AnalyticsGatewaySharePoint { + bucket_ms, + gateway, + count, + }) + .collect(); + points.sort_by(|left, right| { + left.bucket_ms + .cmp(&right.bucket_ms) + .then_with(|| left.gateway.cmp(&right.gateway)) + }); + points +} + +fn overview_kpis(events: &[AnalyticsEvent], query: &AnalyticsQuery) -> Vec { + let total = events + .iter() + .filter(|event| event.event_type == "decision") + .count() as i64; + let score_count = events + .iter() + .filter(|event| event.event_type == "score_snapshot") + .count() as i64; + let rule_hit_count = events + .iter() + .filter(|event| event.event_type == "rule_hit") + .count() as i64; + let error_count = events + .iter() + .filter(|event| event.event_type == "error") + .count() as i64; + + vec![ + AnalyticsKpi { + label: format!("Decisions / {}", format_range(query)), + value: total.to_string(), + subtitle: Some("Recorded decision events".to_string()), + }, + AnalyticsKpi { + label: "Score snapshots".to_string(), + value: score_count.to_string(), + subtitle: Some("Latest gateway score updates".to_string()), + }, + AnalyticsKpi { + label: "Rule hits".to_string(), + value: rule_hit_count.to_string(), + subtitle: Some("Priority-logic hits".to_string()), + }, + AnalyticsKpi { + label: "Errors".to_string(), + value: error_count.to_string(), + subtitle: Some("Structured failure summaries".to_string()), + }, + ] +} + +fn summarise_route_hits(events: &[AnalyticsEvent]) -> Vec { + let mut counts: HashMap = HashMap::new(); + for event in events + .iter() + .filter(|event| event.event_type == "request_hit") + { + let route = event.route.clone().unwrap_or_else(|| "unknown".to_string()); + *counts.entry(route).or_insert(0) += 1; + } + + [ + ("decide_gateway", "/decide_gateway"), + ("update_gateway_score", "/update_gateway"), + ("routing_evaluate", "/rule_evaluate"), + ] + .into_iter() + .map(|(stored_route, display_route)| AnalyticsRouteHit { + route: display_route.to_string(), + count: counts.get(stored_route).copied().unwrap_or(0), + }) + .collect() +} + +fn empty_overview_response(query: &AnalyticsQuery) -> AnalyticsOverviewResponse { + AnalyticsOverviewResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + kpis: vec![ + AnalyticsKpi { + label: format!("Decisions / {}", format_range(query)), + value: "0".to_string(), + subtitle: Some("Global mode is limited to connector-level analytics".to_string()), + }, + AnalyticsKpi { + label: "Score snapshots".to_string(), + value: "0".to_string(), + subtitle: Some("Global mode hides merchant-specific data".to_string()), + }, + AnalyticsKpi { + label: "Rule hits".to_string(), + value: "0".to_string(), + subtitle: Some("Global mode hides merchant-specific data".to_string()), + }, + AnalyticsKpi { + label: "Errors".to_string(), + value: "0".to_string(), + subtitle: Some("Global mode hides merchant-specific data".to_string()), + }, + ], + route_hits: vec![ + AnalyticsRouteHit { + route: "/decide_gateway".to_string(), + count: 0, + }, + AnalyticsRouteHit { + route: "/update_gateway".to_string(), + count: 0, + }, + AnalyticsRouteHit { + route: "/rule_evaluate".to_string(), + count: 0, + }, + ], + top_scores: Vec::new(), + top_errors: Vec::new(), + top_rules: Vec::new(), + } +} + +fn empty_gateway_scores_response(query: &AnalyticsQuery) -> AnalyticsGatewayScoresResponse { + AnalyticsGatewayScoresResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + snapshots: Vec::new(), + series: Vec::new(), + } +} + +fn empty_decisions_response(query: &AnalyticsQuery) -> AnalyticsDecisionResponse { + AnalyticsDecisionResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + tiles: vec![ + AnalyticsKpi { + label: "Decisions".to_string(), + value: "0".to_string(), + subtitle: Some("Global mode hides merchant-specific traffic volumes".to_string()), + }, + AnalyticsKpi { + label: "Error rate".to_string(), + value: "0.00%".to_string(), + subtitle: Some("Global mode hides merchant-specific traffic volumes".to_string()), + }, + ], + series: Vec::new(), + approaches: Vec::new(), + } +} + +fn empty_log_summaries_response(query: &AnalyticsQuery) -> AnalyticsLogSummariesResponse { + AnalyticsLogSummariesResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + total_errors: 0, + errors: Vec::new(), + samples: Vec::new(), + page: query.page.max(1), + page_size: query.page_size.clamp(1, 50), + } +} + +fn empty_payment_audit_response(query: &PaymentAuditQuery) -> PaymentAuditResponse { + PaymentAuditResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_payment_audit_range(query), + payment_id: query.payment_id.clone(), + request_id: query.request_id.clone(), + gateway: query.gateway.clone(), + route: query.route.clone(), + status: query.status.clone(), + event_type: query.event_type.clone(), + error_code: query.error_code.clone(), + page: query.page.max(1), + page_size: query.page_size.clamp(1, 50), + total_results: 0, + results: Vec::new(), + timeline: Vec::new(), + } +} + +fn summarise_payment_audit_results(events: &[AnalyticsEvent]) -> Vec { + let mut grouped: HashMap> = HashMap::new(); + + for event in events { + let Some(lookup_key) = event + .payment_id + .clone() + .or_else(|| event.request_id.clone()) + else { + continue; + }; + grouped.entry(lookup_key).or_default().push(event); + } + + let mut rows: Vec = grouped + .into_iter() + .filter_map(|(lookup_key, events)| { + let mut sorted = events; + sorted.sort_by(|left, right| { + left.created_at_ms + .cmp(&right.created_at_ms) + .then_with(|| left.id.cmp(&right.id)) + }); + let (first, last) = match (sorted.first().copied(), sorted.last().copied()) { + (Some(first), Some(last)) => (first, last), + _ => return None, + }; + let gateways = sorted + .iter() + .filter_map(|event| event.gateway.clone()) + .fold(Vec::::new(), |mut acc, gateway| { + if !acc.contains(&gateway) { + acc.push(gateway); + } + acc + }); + let routes = sorted.iter().filter_map(|event| event.route.clone()).fold( + Vec::::new(), + |mut acc, route| { + let route_label = payment_audit_route_label(&route); + if !acc.contains(&route_label) { + acc.push(route_label); + } + acc + }, + ); + + Some(PaymentAuditSummary { + lookup_key, + payment_id: last.payment_id.clone().or_else(|| first.payment_id.clone()), + request_id: last.request_id.clone().or_else(|| first.request_id.clone()), + merchant_id: last + .merchant_id + .clone() + .or_else(|| first.merchant_id.clone()), + first_seen_ms: first.created_at_ms, + last_seen_ms: last.created_at_ms, + event_count: sorted.len(), + latest_status: sorted + .iter() + .rev() + .find_map(|event| transaction_status_from_details(event)) + .or_else(|| { + sorted + .iter() + .rev() + .find_map(|event| match event.event_type.as_str() { + "error" => Some("FAILURE".to_string()), + "decision" | "gateway_update" | "rule_evaluation_preview" => { + event.status.clone() + } + _ => None, + }) + }), + latest_gateway: last.gateway.clone(), + latest_stage: Some(payment_audit_stage_label(last).to_string()), + gateways, + routes, + }) + }) + .collect(); + + rows.sort_by(|left, right| { + right + .last_seen_ms + .cmp(&left.last_seen_ms) + .then_with(|| right.event_count.cmp(&left.event_count)) + }); + rows +} + +fn build_payment_timeline( + events: &[AnalyticsEvent], + selected_payment_id: Option<&str>, + selected_request_id: Option<&str>, + selected_lookup_key: Option<&str>, +) -> Vec { + let mut timeline: Vec = events + .iter() + .filter(|event| { + if let Some(payment_id) = selected_payment_id { + return event.payment_id.as_deref() == Some(payment_id); + } + if let Some(request_id) = selected_request_id { + return event.request_id.as_deref() == Some(request_id); + } + if let Some(lookup_key) = selected_lookup_key { + return event.payment_id.as_deref() == Some(lookup_key) + || event.request_id.as_deref() == Some(lookup_key); + } + false + }) + .map(|event| PaymentAuditEvent { + id: event.id, + event_type: event.event_type.clone(), + event_stage: event.event_stage.clone(), + route: event.route.clone(), + merchant_id: event.merchant_id.clone(), + payment_id: event.payment_id.clone(), + request_id: event.request_id.clone(), + payment_method_type: event.payment_method_type.clone(), + payment_method: event.payment_method.clone(), + gateway: event.gateway.clone(), + routing_approach: event.routing_approach.clone(), + rule_name: event.rule_name.clone(), + status: event.status.clone(), + error_code: event.error_code.clone(), + error_message: event.error_message.clone(), + score_value: event.score_value, + sigma_factor: event.sigma_factor, + average_latency: event.average_latency, + tp99_latency: event.tp99_latency, + transaction_count: event.transaction_count, + details: event.details.clone(), + details_json: parse_details_json(&event.details), + created_at_ms: event.created_at_ms, + }) + .collect(); + + timeline.sort_by(|left, right| { + left.created_at_ms + .cmp(&right.created_at_ms) + .then_with(|| left.id.cmp(&right.id)) + }); + timeline +} + +pub async fn overview( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_overview_response(query)); + } + let events = load_events( + state, + query, + &[ + "request_hit", + "decision", + "score_snapshot", + "rule_hit", + "error", + ], + ) + .await?; + let (top_scores, _) = summarise_scores(&events); + let top_errors = summarise_errors(&events); + let top_rules = summarise_rule_hits(&events); + + Ok(AnalyticsOverviewResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + kpis: overview_kpis(&events, query), + route_hits: summarise_route_hits(&events), + top_scores: top_scores.into_iter().take(5).collect(), + top_errors: top_errors.into_iter().take(5).collect(), + top_rules: top_rules.into_iter().take(5).collect(), + }) +} + +pub async fn gateway_scores( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_gateway_scores_response(query)); + } + let events = load_events(state, query, &["score_snapshot"]).await?; + let (snapshots, series) = summarise_scores(&events); + Ok(AnalyticsGatewayScoresResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + snapshots, + series, + }) +} + +pub async fn decisions( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_decisions_response(query)); + } + let events = load_events(state, query, &["decision"]).await?; + let (series, approaches, tiles) = summarise_decisions(&events, query); + Ok(AnalyticsDecisionResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + tiles, + series, + approaches, + }) +} + +pub async fn routing_stats( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, +) -> Result { + let events = load_events(state, query, &["decision", "score_snapshot", "rule_hit"]).await?; + let gateway_share = summarise_gateway_share(&events, query); + let top_rules = summarise_rule_hits(&events); + let available_filters = derive_routing_filter_options(&events, query); + let series = summarise_filtered_scores(&events, query); + + Ok(AnalyticsRoutingStatsResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + gateway_share, + top_rules: top_rules.into_iter().take(10).collect(), + sr_trend: series, + available_filters, + }) +} + +pub async fn log_summaries( + state: &crate::app::TenantAppState, + query: &AnalyticsQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_log_summaries_response(query)); + } + let events = load_events( + state, + query, + &["error", "decision", "score_snapshot", "rule_hit"], + ) + .await?; + let mut errors = summarise_errors(&events); + let total_errors = errors.iter().map(|entry| entry.count).sum(); + errors.truncate(10); + + let mut samples: Vec = events + .into_iter() + .filter(|event| event.event_type == "error") + .map(|event| AnalyticsLogSample { + route: event.route.unwrap_or_else(|| "unknown".to_string()), + merchant_id: event.merchant_id, + payment_id: event.payment_id, + request_id: event.request_id, + gateway: event.gateway, + routing_approach: event.routing_approach, + status: event.status, + error_code: event.error_code, + error_message: event.error_message, + event_type: Some(event.event_type), + created_at_ms: event.created_at_ms, + }) + .collect(); + samples.sort_by(|left, right| right.created_at_ms.cmp(&left.created_at_ms)); + + let page_size = query.page_size.clamp(1, 50); + let page = query.page.max(1); + let start = (page - 1) * page_size; + let samples = samples.into_iter().skip(start).take(page_size).collect(); + + Ok(AnalyticsLogSummariesResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_range(query), + total_errors, + errors, + samples, + page, + page_size, + }) +} + +pub async fn payment_audit( + state: &crate::app::TenantAppState, + query: &PaymentAuditQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_payment_audit_response(query)); + } + let events = load_payment_audit_events(state, query).await?; + let audit_events: Vec = events + .into_iter() + .filter(|event| event.event_type != "request_hit" && event.event_type != "score_snapshot") + .collect(); + let results = summarise_payment_audit_results(&audit_events); + + let page_size = query.page_size.clamp(1, 50); + let page = query.page.max(1); + let start = (page - 1) * page_size; + let paged_results: Vec = results + .iter() + .skip(start) + .take(page_size) + .cloned() + .collect(); + + let selected_payment_id = query.payment_id.as_deref().or_else(|| { + paged_results + .first() + .and_then(|row| row.payment_id.as_deref()) + }); + let selected_request_id = query.request_id.as_deref().or_else(|| { + paged_results + .first() + .and_then(|row| row.request_id.as_deref()) + }); + let selected_lookup_key = paged_results.first().map(|row| row.lookup_key.as_str()); + let timeline = build_payment_timeline( + &audit_events, + selected_payment_id, + selected_request_id, + selected_lookup_key, + ); + + Ok(PaymentAuditResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_payment_audit_range(query), + payment_id: query + .payment_id + .clone() + .or_else(|| paged_results.first().and_then(|row| row.payment_id.clone())), + request_id: query + .request_id + .clone() + .or_else(|| paged_results.first().and_then(|row| row.request_id.clone())), + gateway: query.gateway.clone(), + route: query.route.clone(), + status: query.status.clone(), + event_type: query.event_type.clone(), + error_code: query.error_code.clone(), + page, + page_size, + total_results: results.len(), + results: paged_results, + timeline, + }) +} + +pub async fn preview_trace( + state: &crate::app::TenantAppState, + query: &PaymentAuditQuery, +) -> Result { + if query.scope == AnalyticsScope::All { + return Ok(empty_payment_audit_response(query)); + } + + let preview_events = load_preview_trace_events(state, query).await?; + let results = summarise_payment_audit_results(&preview_events); + + let page_size = query.page_size.clamp(1, 50); + let page = query.page.max(1); + let start = (page - 1) * page_size; + let paged_results: Vec = results + .iter() + .skip(start) + .take(page_size) + .cloned() + .collect(); + + let selected_payment_id = query.payment_id.as_deref().or_else(|| { + paged_results + .first() + .and_then(|row| row.payment_id.as_deref()) + }); + let selected_request_id = query.request_id.as_deref().or_else(|| { + paged_results + .first() + .and_then(|row| row.request_id.as_deref()) + }); + let selected_lookup_key = paged_results.first().map(|row| row.lookup_key.as_str()); + let timeline = build_payment_timeline( + &preview_events, + selected_payment_id, + selected_request_id, + selected_lookup_key, + ); + + Ok(PaymentAuditResponse { + generated_at_ms: now_ms(), + scope: query.scope.as_str().to_string(), + merchant_id: query.merchant_id.clone(), + range: format_payment_audit_range(query), + payment_id: query + .payment_id + .clone() + .or_else(|| paged_results.first().and_then(|row| row.payment_id.clone())), + request_id: query + .request_id + .clone() + .or_else(|| paged_results.first().and_then(|row| row.request_id.clone())), + gateway: query.gateway.clone(), + route: Some("routing_evaluate".to_string()), + status: query.status.clone(), + event_type: query.event_type.clone(), + error_code: query.error_code.clone(), + page, + page_size, + total_results: results.len(), + results: paged_results, + timeline, + }) +} + +pub fn parse_query( + merchant_id: Option, + scope: Option, + range: Option, + start_ms: Option, + end_ms: Option, + page: Option, + page_size: Option, + payment_method_type: Option, + payment_method: Option, + card_network: Option, + card_is_in: Option, + currency: Option, + country: Option, + auth_type: Option, + gateways: Option, +) -> AnalyticsQuery { + let scope = AnalyticsScope::from_query(scope.as_deref()); + let range = AnalyticsRange::from_query(range.as_deref()); + let (start_ms, end_ms) = match (start_ms, end_ms) { + (Some(start_ms), Some(end_ms)) if start_ms >= 0 && end_ms > start_ms => { + (Some(start_ms), Some(end_ms)) + } + _ => (None, None), + }; + let page = page.unwrap_or(1).max(1) as usize; + let page_size = page_size.unwrap_or(10).clamp(1, 50) as usize; + let gateways = normalise_gateways(gateways); + let payment_method_type = if scope == AnalyticsScope::Current { + payment_method_type.filter(|value| !value.is_empty()) + } else { + None + }; + let payment_method = if scope == AnalyticsScope::Current { + payment_method.filter(|value| !value.is_empty()) + } else { + None + }; + let card_network = if scope == AnalyticsScope::Current { + card_network.filter(|value| !value.is_empty()) + } else { + None + }; + let card_is_in = if scope == AnalyticsScope::Current { + card_is_in.filter(|value| !value.is_empty()) + } else { + None + }; + let currency = if scope == AnalyticsScope::Current { + currency.filter(|value| !value.is_empty()) + } else { + None + }; + let country = if scope == AnalyticsScope::Current { + country.filter(|value| !value.is_empty()) + } else { + None + }; + let auth_type = if scope == AnalyticsScope::Current { + auth_type.filter(|value| !value.is_empty()) + } else { + None + }; + + AnalyticsQuery { + merchant_id, + scope, + range, + start_ms, + end_ms, + page, + page_size, + payment_method_type, + payment_method, + card_network, + card_is_in, + currency, + country, + auth_type, + gateways, + } +} + +pub fn parse_payment_audit_query( + merchant_id: Option, + scope: Option, + range: Option, + start_ms: Option, + end_ms: Option, + page: Option, + page_size: Option, + payment_id: Option, + request_id: Option, + gateway: Option, + route: Option, + status: Option, + event_type: Option, + error_code: Option, +) -> PaymentAuditQuery { + let scope = AnalyticsScope::from_query(scope.as_deref()); + let range = AnalyticsRange::from_query(range.as_deref()); + let (start_ms, end_ms) = match (start_ms, end_ms) { + (Some(start_ms), Some(end_ms)) if start_ms >= 0 && end_ms > start_ms => { + (Some(start_ms), Some(end_ms)) + } + _ => (None, None), + }; + let page = page.unwrap_or(1).max(1) as usize; + let page_size = page_size.unwrap_or(12).clamp(1, 50) as usize; + + PaymentAuditQuery { + merchant_id, + scope, + range, + start_ms, + end_ms, + page, + page_size, + payment_id, + request_id, + gateway, + route: normalise_payment_audit_route_filter(route), + status: normalise_payment_audit_status_filter(status), + event_type, + error_code, + } +} + +pub fn format_range(query: &AnalyticsQuery) -> String { + if query.start_ms.is_some() && query.end_ms.is_some() { + return "custom".to_string(); + } + + match query.range { + AnalyticsRange::M15 => "15m".to_string(), + AnalyticsRange::H1 => "1h".to_string(), + AnalyticsRange::H24 => "24h".to_string(), + } +} + +fn format_payment_audit_range(query: &PaymentAuditQuery) -> String { + if query.start_ms.is_some() && query.end_ms.is_some() { + return "custom".to_string(); + } + + match query.range { + AnalyticsRange::M15 => "15m".to_string(), + AnalyticsRange::H1 => "1h".to_string(), + AnalyticsRange::H24 => "24h".to_string(), + } +} diff --git a/src/analytics/types.rs b/src/analytics/types.rs new file mode 100644 index 00000000..df4bda6a --- /dev/null +++ b/src/analytics/types.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AnalyticsConfig { + pub enabled: bool, + pub kafka: KafkaConfig, + pub clickhouse: ClickhouseConfig, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct KafkaConfig { + pub brokers: Vec, + pub topic_prefix: String, + pub batch_size: usize, + pub batch_timeout_ms: u64, + pub max_consecutive_failures: u32, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ClickhouseConfig { + pub host: String, + pub username: String, + pub password: Option, + pub database: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RoutingEventData { + pub event_id: String, + pub merchant_id: String, + pub request_id: String, + pub endpoint: String, + pub method: String, + pub request_payload: String, + pub response_payload: String, + pub status_code: u16, + pub processing_time_ms: u32, + pub gateway_selected: Option, + pub routing_algorithm_id: Option, + pub error_message: Option, + pub user_agent: Option, + pub ip_address: Option, + #[serde(with = "clickhouse_datetime")] + pub created_at: OffsetDateTime, + pub sign_flag: i8, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self { + enabled: false, + kafka: KafkaConfig { + brokers: vec!["localhost:9092".to_string()], + topic_prefix: "decision-engine".to_string(), + batch_size: 100, + batch_timeout_ms: 1000, + max_consecutive_failures: 5, + }, + clickhouse: ClickhouseConfig { + host: "http://localhost:8123".to_string(), + username: "analytics_user".to_string(), + password: Some("analytics_pass".to_string()), + database: "decision_engine_analytics".to_string(), + }, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AnalyticsError { + #[error("Kafka error: {0}")] + Kafka(#[from] kafka::Error), + #[error("ClickHouse error: {0}")] + ClickHouse(String), + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + #[error("Configuration error: {0}")] + Configuration(String), +} + +pub type AnalyticsResult = Result; + +// Custom datetime serialization for ClickHouse compatibility +mod clickhouse_datetime { + use serde::{self, Deserialize, Deserializer, Serializer}; + use time::format_description::well_known::Rfc3339; + use time::OffsetDateTime; + + pub fn serialize(date: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + // Format as Unix timestamp for ClickHouse compatibility + let timestamp = date.unix_timestamp(); + serializer.serialize_i64(timestamp) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + // Try parsing as Unix timestamp first + if let Ok(timestamp) = s.parse::() { + return OffsetDateTime::from_unix_timestamp(timestamp) + .map_err(serde::de::Error::custom); + } + + // Fallback to RFC3339 parsing + OffsetDateTime::parse(&s, &Rfc3339).map_err(serde::de::Error::custom) + } +} diff --git a/src/app.rs b/src/app.rs index da215adf..1ada801e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,15 +1,22 @@ use crate::redis::commands::RedisConnectionWrapper; +use axum::http::HeaderValue; use axum::{ + body::Body, extract::Request, + middleware::{self, Next}, + response::Response, routing::{delete, get, post}, }; use axum_server::{tls_rustls::RustlsConfig, Handle}; use error_stack::ResultExt; use std::sync::Arc; use tokio::signal::unix::{signal, SignalKind}; +use tokio::sync::OnceCell as TokioOnceCell; +use tower::ServiceBuilder; use tower_http::trace as tower_trace; use crate::{ + analytics::{analytics_middleware, AnalyticsClient}, api_client::ApiClient, config::{self, GlobalConfig, TenantConfig}, error, logger, routes, storage, @@ -31,6 +38,35 @@ pub async fn get_tenant_app_state() -> Arc { type Storage = storage::Storage; +async fn ensure_request_id(mut request: Request, next: Next) -> Response { + let header_value = request + .headers() + .get(storage::consts::X_REQUEST_ID) + .filter(|value| !value.as_bytes().is_empty()) + .cloned() + .unwrap_or_else(generate_request_id_header_value); + + request + .headers_mut() + .insert(storage::consts::X_REQUEST_ID, header_value.clone()); + + let mut response = next.run(request).await; + response + .headers_mut() + .insert(storage::consts::X_REQUEST_ID, header_value); + + response +} + +fn generate_request_id_header_value() -> HeaderValue { + loop { + let request_id = storage::utils::generate_uuid(); + if let Ok(value) = HeaderValue::from_str(&request_id) { + return value; + } + } +} + /// /// TenantAppState: /// @@ -43,6 +79,9 @@ pub struct TenantAppState { pub redis_conn: Arc, pub config: config::TenantConfig, pub api_client: ApiClient, + pub pm_filter_graph_bundle: + Arc>>, + pub analytics_client: Arc, } #[allow(clippy::expect_used)] @@ -69,13 +108,66 @@ impl TenantAppState { .await .expect("Failed to create Redis connection Pool"); + // Initialize analytics client + let analytics_client = AnalyticsClient::new(global_config.analytics.clone()) + .map_err(|e| { + logger::warn!("Failed to initialize analytics client: {:?}", e); + e + }) + .unwrap_or_else(|_| { + // Fallback to disabled analytics client + let disabled_config = crate::analytics::AnalyticsConfig::default(); + AnalyticsClient::new(disabled_config).unwrap() + }); + Ok(Self { db, - redis_conn: Arc::new(RedisConnectionWrapper::new(redis_conn)), + redis_conn: Arc::new(RedisConnectionWrapper::new( + redis_conn, + global_config.compression_filepath.clone(), + )), api_client, config: tenant_config, + pm_filter_graph_bundle: Arc::new(TokioOnceCell::new()), + analytics_client: Arc::new(analytics_client), }) } + + pub async fn get_pm_filter_graph_bundle( + &self, + ) -> Option> { + self.pm_filter_graph_bundle + .get_or_try_init(|| async { + let bundle = crate::euclid::pm_filter_graph::build_pm_filter_graph_bundle( + &self.config.pm_filters, + self.config.routing_config.as_ref(), + ) + .map_err(|err| { + logger::error!( + tenant_id = %self.config.tenant_id, + error = %err, + "Failed to build pm_filters constraint graph; failing open" + ); + err + })?; + + logger::info!( + tenant_id = %self.config.tenant_id, + explicit_connector_count = bundle.explicit_connectors.len(), + has_default_rules = bundle.has_default_rules, + graph_node_count = bundle.node_count, + graph_edge_count = bundle.edge_count, + "pm_filters constraint graph built successfully" + ); + + Ok::, String>(Arc::new( + bundle, + )) + }) + .await + .ok() + .cloned() + } } /// @@ -172,42 +264,64 @@ where .route( "/merchant-account/:merchant-id", delete(routes::merchant_account_config::delete_merchant_config), + ) + .route( + "/config-sr-dimension", + axum::routing::post(crate::euclid::handlers::routing_rules::config_sr_dimensions), + ) + .route( + "/config/routing-keys", + axum::routing::get(crate::euclid::handlers::routing_rules::get_routing_config), ); let router = router.route("/update-score", post(routes::update_score::update_score)); let router = router.route( "/decide-gateway", post(routes::decide_gateway::decide_gateway), ); + let router = router.route( + "/routing/hybrid", + post(routes::hybrid_routing::hybrid_routing_evaluate), + ); let router = router.route( "/update-gateway-score", post(routes::update_gateway_score::update_gateway_score), ); + let router = router.nest("/analytics", routes::analytics::serve()); - let router = router.layer( - tower_trace::TraceLayer::new_for_http() - .make_span_with(|request: &Request<_>| utils::record_fields_from_header(request)) - .on_request(tower_trace::DefaultOnRequest::new().level(tracing::Level::INFO)) - .on_response( - tower_trace::DefaultOnResponse::new() - .level(tracing::Level::INFO) - .latency_unit(tower_http::LatencyUnit::Micros), - ) - .on_failure( - tower_trace::DefaultOnFailure::new() - .latency_unit(tower_http::LatencyUnit::Micros) - .level(tracing::Level::ERROR), - ), - ); + let middleware = ServiceBuilder::new() + .layer(middleware::from_fn(ensure_request_id)) + .layer(axum::middleware::from_fn_with_state( + global_app_state.clone(), + analytics_middleware, + )) + .layer( + tower_trace::TraceLayer::new_for_http() + .make_span_with(|request: &Request<_>| utils::record_fields_from_header(request)) + .on_request(tower_trace::DefaultOnRequest::new().level(tracing::Level::INFO)) + .on_response( + tower_trace::DefaultOnResponse::new() + .level(tracing::Level::INFO) + .latency_unit(tower_http::LatencyUnit::Micros), + ) + .on_failure( + tower_trace::DefaultOnFailure::new() + .latency_unit(tower_http::LatencyUnit::Micros) + .level(tracing::Level::ERROR), + ), + ); let router = router .nest("/health", routes::health::serve()) + .layer(middleware) .with_state(global_app_state.clone()); logger::info!( category = "SERVER", - "OpenRouter started [{:?}] [{:?}]", - global_app_state.global_config.server, - global_app_state.global_config.log + action = "main_server_startup", + bind_address = %socket_addr, + tls_enabled = global_app_state.global_config.tls.is_some(), + request_id_header = storage::consts::X_REQUEST_ID, + "Main HTTP server listening" ); if let Some(tls_config) = &global_app_state.global_config.tls { diff --git a/src/bin/open_router.rs b/src/bin/open_router.rs index 2b184fe7..b2b29298 100644 --- a/src/bin/open_router.rs +++ b/src/bin/open_router.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unwrap_in_result)] + use open_router::{logger, tenant::GlobalAppState}; #[allow(clippy::expect_used)] @@ -9,7 +11,7 @@ async fn main() -> Result<(), Box> { let _guard = logger::setup( &global_config.log, open_router::service_name!(), - [open_router::service_name!(), "tower_http"], + ["tower_http"], ); #[allow(clippy::expect_used)] @@ -21,6 +23,8 @@ async fn main() -> Result<(), Box> { .await .expect("Failed to fetch raw application secrets"); + log_startup_configuration(&global_config); + let global_app_state = GlobalAppState::new(global_config.clone()).await; // Run both servers concurrently using tokio::spawn @@ -41,3 +45,7 @@ async fn main() -> Result<(), Box> { Ok(()) } + +fn log_startup_configuration(global_config: &open_router::config::GlobalConfig) { + logger::info!("Decision engine started [{:?}]", global_config); +} diff --git a/src/config.rs b/src/config.rs index 61f72496..7237a358 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::decider::network_decider; use crate::{ + analytics::AnalyticsConfig, api_client::ApiClientConfig, crypto::secrets_manager::{ secrets_interface::SecretManager, secrets_management::SecretsManagementConfig, @@ -10,10 +11,12 @@ use crate::{ logger::config::Log, }; use error_stack::ResultExt; +#[cfg(all(feature = "kms-hashicorp-vault", test))] use masking::ExposeInterface; use redis_interface::RedisSettings; +use serde::Deserialize; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ops::{Deref, DerefMut}, path::PathBuf, }; @@ -32,6 +35,7 @@ pub struct GlobalConfig { #[cfg(feature = "limit")] pub limit: Limit, pub redis: RedisSettings, + pub cache_config: CacheConfig, pub tenant_secrets: TenantsSecrets, pub tls: Option, #[serde(default)] @@ -39,7 +43,12 @@ pub struct GlobalConfig { #[serde(default)] pub routing_config: Option, #[serde(default)] + pub pm_filters: ConnectorFilters, + #[serde(default)] pub debit_routing_config: network_decider::types::DebitRoutingConfig, + pub compression_filepath: Option, + #[serde(default)] + pub analytics: AnalyticsConfig, } #[derive(Clone, Debug)] @@ -47,7 +56,9 @@ pub struct TenantConfig { pub tenant_id: String, pub tenant_secrets: TenantSecrets, pub routing_config: Option, + pub pm_filters: ConnectorFilters, pub debit_routing_config: network_decider::types::DebitRoutingConfig, + pub cache_config: CacheConfig, } impl TenantConfig { @@ -60,6 +71,7 @@ impl TenantConfig { Self { tenant_id: tenant_id.clone(), routing_config: global_config.routing_config.clone(), + pm_filters: global_config.pm_filters.clone(), #[allow(clippy::unwrap_used)] tenant_secrets: global_config .tenant_secrets @@ -67,10 +79,51 @@ impl TenantConfig { .cloned() .unwrap(), debit_routing_config: global_config.debit_routing_config.clone(), + cache_config: global_config.cache_config.clone(), } } } +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(transparent)] +pub struct ConnectorFilters(pub HashMap); + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(transparent)] +pub struct PaymentMethodFilters(pub HashMap); + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(default)] +pub struct CurrencyCountryFlowFilter { + #[serde(default, deserialize_with = "deserialize_optional_hashset")] + pub country: Option>, + #[serde(default, deserialize_with = "deserialize_optional_hashset")] + pub currency: Option>, + pub not_available_flows: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(default)] +pub struct NotAvailableFlows { + pub capture_method: Option, +} + +fn deserialize_optional_hashset<'a, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'a>, +{ + let raw = Option::::deserialize(deserializer)?; + Ok(raw.map(|value| { + value + .trim() + .split(',') + .map(str::trim) + .filter(|part| !part.is_empty()) + .map(ToOwned::to_owned) + .collect() + })) +} + #[cfg(feature = "limit")] #[derive(Clone, serde::Deserialize, Debug)] pub struct Limit { @@ -105,21 +158,27 @@ pub struct PgDatabase { pub pg_pool_size: Option, } -#[derive(serde::Deserialize, Debug, Clone)] -pub struct TenantSecrets { - /// schema name for the tenant (defaults to tenant_id) - pub schema: String, +#[derive(Clone, serde::Deserialize, Debug)] +pub struct CacheConfig { + pub service_config_redis_prefix: String, + pub service_config_ttl: i64, } -fn deserialize_hex<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let deserialized_str: String = serde::Deserialize::deserialize(deserializer)?; +impl CacheConfig { + pub fn add_prefix(&self, key: &str) -> String { + format!("{}{}", self.service_config_redis_prefix, key) + } +} - let deserialized_str = deserialized_str.into_bytes(); +#[derive(Clone, serde::Deserialize, Debug)] +pub struct CompressionFilepath { + pub zstd_compression_filepath: String, +} - Ok(deserialized_str) +#[derive(serde::Deserialize, Debug, Clone)] +pub struct TenantSecrets { + /// schema name for the tenant (defaults to tenant_id) + pub schema: String, } #[derive(serde::Deserialize, Debug, Clone)] @@ -371,9 +430,3 @@ pub struct KeysConfig { #[serde(flatten)] pub keys: HashMap, } - -/// Structure for the [default] section in the TOML -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct DefaultConfig { - pub output: Vec, -} diff --git a/src/crypto/secrets_manager/secrets_management.rs b/src/crypto/secrets_manager/secrets_management.rs index 2b69a1c0..fe397482 100644 --- a/src/crypto/secrets_manager/secrets_management.rs +++ b/src/crypto/secrets_manager/secrets_management.rs @@ -45,7 +45,7 @@ enum SecretsManagerClient { #[cfg(feature = "kms-aws")] AwsKms(AwsKmsClient), #[cfg(feature = "kms-hashicorp-vault")] - HashiCorp(HashiCorpVault), + HashiCorp(Box), NoEncryption(NoEncryption), } @@ -89,7 +89,7 @@ impl SecretsManagementConfig { #[cfg(feature = "kms-hashicorp-vault")] Self::HashiCorpVault { hashi_corp_vault } => HashiCorpVault::new(hashi_corp_vault) .change_context(SecretsManagementError::ClientCreationFailed) - .map(SecretsManagerClient::HashiCorp), + .map(|v| SecretsManagerClient::HashiCorp(Box::new(v))), Self::NoEncryption => Ok(SecretsManagerClient::NoEncryption(NoEncryption)), } } diff --git a/src/decider/configs/env_vars.rs b/src/decider/configs/env_vars.rs index ccc3b6c5..92225b57 100644 --- a/src/decider/configs/env_vars.rs +++ b/src/decider/configs/env_vars.rs @@ -25,7 +25,7 @@ where // } // } -trait FromStrExt { +pub trait FromStrExt { type Error; fn from_str(s: &str) -> Result where diff --git a/src/decider/gatewaydecider/Untitled-1 b/src/decider/gatewaydecider/Untitled-1 index e86f2aca..e3557aa5 100644 --- a/src/decider/gatewaydecider/Untitled-1 +++ b/src/decider/gatewaydecider/Untitled-1 @@ -89,7 +89,7 @@ Example :- fgws } -Use m_validation_type.map(|vt| vt == ValidationType::CARD_MANDATE).unwrap_or(false) this kind of syntaxes for equality on Options +Use m_validation_type.map(|vt| vt == ValidationType::CardMandate).unwrap_or(false) this kind of syntaxes for equality on Options Please generate it for the below function diff --git a/src/decider/gatewaydecider/constants.rs b/src/decider/gatewaydecider/constants.rs index 1a5ded74..cb1a1be9 100644 --- a/src/decider/gatewaydecider/constants.rs +++ b/src/decider/gatewaydecider/constants.rs @@ -2,125 +2,125 @@ use masking::{PeekInterface, Secret}; use crate::redis::types as SC; -pub struct ENABLE_OPTIMIZATION_DURING_DOWNTIME; -impl SC::ServiceConfigKey for ENABLE_OPTIMIZATION_DURING_DOWNTIME { +pub struct EnableOptimizationDuringDowntime; +impl SC::ServiceConfigKey for EnableOptimizationDuringDowntime { fn get_key(&self) -> String { "enable_optimization_during_downtime".to_string() } } -pub const enableOptimizationDuringDowntime: ENABLE_OPTIMIZATION_DURING_DOWNTIME = - ENABLE_OPTIMIZATION_DURING_DOWNTIME; +pub const ENABLE_OPTIMIZATION_DURING_DOWNTIME: EnableOptimizationDuringDowntime = + EnableOptimizationDuringDowntime; -pub struct DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT; -impl SC::ServiceConfigKey for DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT { +pub struct DefaultSrBasedGatewayEliminationInput; +impl SC::ServiceConfigKey for DefaultSrBasedGatewayEliminationInput { fn get_key(&self) -> String { "DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT".to_string() } } -pub const defaultSRBasedGatewayEliminationInput: DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT = - DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT; +pub const DEFAULT_SRBASED_GATEWAY_ELIMINATION_INPUT: DefaultSrBasedGatewayEliminationInput = + DefaultSrBasedGatewayEliminationInput; //TODO : This is duplicate and is same key is already there in decider constants.rs -pub struct SR_V3_INPUT_CONFIG(String); -impl SC::ServiceConfigKey for SR_V3_INPUT_CONFIG { +pub struct SrV3InputConfig(String); +impl SC::ServiceConfigKey for SrV3InputConfig { fn get_key(&self) -> String { format!("SR_V3_INPUT_CONFIG_{}", self.0) } } -pub fn srV3InputConfig(mid: String) -> SR_V3_INPUT_CONFIG { - SR_V3_INPUT_CONFIG(mid) +pub fn srV3InputConfig(mid: String) -> SrV3InputConfig { + SrV3InputConfig(mid) } //TODO : This is duplicate and is same key is already there in decider constants.rs -pub struct SR_V3_INPUT_CONFIG_DEFAULT; -impl SC::ServiceConfigKey for SR_V3_INPUT_CONFIG_DEFAULT { +pub struct SrV3InputConfigDefault; +impl SC::ServiceConfigKey for SrV3InputConfigDefault { fn get_key(&self) -> String { "SR_V3_INPUT_CONFIG_DEFAULT".to_string() } } -pub const srV3DefaultInputConfig: SR_V3_INPUT_CONFIG_DEFAULT = SR_V3_INPUT_CONFIG_DEFAULT; +pub const SR_V3_DEFAULT_INPUT_CONFIG: SrV3InputConfigDefault = SrV3InputConfigDefault; pub const DEFAULT_SR_V3_BASED_BUCKET_SIZE: i32 = 125; -pub const defaultSrV3BasedUpperResetFactor: f64 = 3.0; -pub const defaultSrV3BasedLowerResetFactor: f64 = 3.0; -pub const defaultSrV3BasedHedgingPercent: f64 = 5.0; +pub const DEFAULT_SR_V3_BASED_UPPER_RESET_FACTOR: f64 = 3.0; +pub const DEFAULT_SR_V3_BASED_LOWER_RESET_FACTOR: f64 = 3.0; +pub const DEFAULT_SR_V3_BASED_HEDGING_PERCENT: f64 = 5.0; pub const DEFAULT_SR_V3_BASED_GATEWAY_SIGMA_FACTOR: f64 = 0.0; -pub struct ALT_ID_ENABLED_GATEWAY_FOR_EMIBANK; -impl SC::ServiceConfigKey for ALT_ID_ENABLED_GATEWAY_FOR_EMIBANK { +pub struct AltIdEnabledGatewayForEmibank; +impl SC::ServiceConfigKey for AltIdEnabledGatewayForEmibank { fn get_key(&self) -> String { "ALT_ID_ENABLED_GATEWAY_FOR_EMIBANK".to_string() } } -pub const altIdEnabledGatewayEmiBank: ALT_ID_ENABLED_GATEWAY_FOR_EMIBANK = - ALT_ID_ENABLED_GATEWAY_FOR_EMIBANK; +pub const ALT_ID_ENABLED_GATEWAY_EMI_BANK: AltIdEnabledGatewayForEmibank = + AltIdEnabledGatewayForEmibank; -pub struct SR_BASED_TRANSACTION_RESET_COUNT; -impl SC::ServiceConfigKey for SR_BASED_TRANSACTION_RESET_COUNT { +pub struct SrBasedTransactionResetCount; +impl SC::ServiceConfigKey for SrBasedTransactionResetCount { fn get_key(&self) -> String { "SR_BASED_TRANSACTION_RESET_COUNT".to_string() } } -pub const srBasedTxnResetCount: SR_BASED_TRANSACTION_RESET_COUNT = SR_BASED_TRANSACTION_RESET_COUNT; +pub const SR_BASED_TXN_RESET_COUNT: SrBasedTransactionResetCount = SrBasedTransactionResetCount; -pub struct SCHEDULED_OUTAGE_VALIDATION_DURATION; -impl SC::ServiceConfigKey for SCHEDULED_OUTAGE_VALIDATION_DURATION { +pub struct ScheduledOutageValidationDuration; +impl SC::ServiceConfigKey for ScheduledOutageValidationDuration { fn get_key(&self) -> String { "SCHEDULED_OUTAGE_VALIDATION_DURATION".to_string() } } -pub struct ENABLE_ELIMINATION_V2; -impl SC::ServiceConfigKey for ENABLE_ELIMINATION_V2 { +pub struct EnableEliminationV2; +impl SC::ServiceConfigKey for EnableEliminationV2 { fn get_key(&self) -> String { "ENABLE_ELIMINATION_V2".to_string() } } -pub const enableEliminationV2: ENABLE_ELIMINATION_V2 = ENABLE_ELIMINATION_V2; +pub const ENABLE_ELIMINATION_V2: EnableEliminationV2 = EnableEliminationV2; -pub struct ENABLE_OUTAGE_V2; -impl SC::ServiceConfigKey for ENABLE_OUTAGE_V2 { +pub struct EnableOutageV2; +impl SC::ServiceConfigKey for EnableOutageV2 { fn get_key(&self) -> String { "ENABLE_OUTAGE_V2".to_string() } } -pub const enableEliminationV2ForOutage: ENABLE_OUTAGE_V2 = ENABLE_OUTAGE_V2; +pub const ENABLE_ELIMINATION_V2_FOR_OUTAGE: EnableOutageV2 = EnableOutageV2; -pub const thresholdWeightSr1: &str = "THRESHOLD_WEIGHT_SR1"; -pub const thresholdWeightSr2: &str = "THRESHOLD_WEIGHT_SR2"; +pub const THRESHOLD_WEIGHT_SR1: &str = "THRESHOLD_WEIGHT_SR1"; +pub const THRESHOLD_WEIGHT_SR2: &str = "THRESHOLD_WEIGHT_SR2"; -pub struct DEFAULT_SR1(String); -impl SC::ServiceConfigKey for DEFAULT_SR1 { +pub struct DefaultSr1(String); +impl SC::ServiceConfigKey for DefaultSr1 { fn get_key(&self) -> String { format!("DEFAULT_SR1_{}", self.0) } } -pub fn defaultSr1SConfigPrefix(val: String) -> DEFAULT_SR1 { - DEFAULT_SR1(val) +pub fn defaultSr1SConfigPrefix(val: String) -> DefaultSr1 { + DefaultSr1(val) } -pub struct DEFAULT_N(String); -impl SC::ServiceConfigKey for DEFAULT_N { +pub struct DefaultN(String); +impl SC::ServiceConfigKey for DefaultN { fn get_key(&self) -> String { format!("DEFAULT_N_{}", self.0) } } -pub fn defaultNSConfigPrefix(val: String) -> DEFAULT_N { - DEFAULT_N(val) +pub fn defaultNSConfigPrefix(val: String) -> DefaultN { + DefaultN(val) } -pub struct INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND_N(String); -impl SC::ServiceConfigKey for INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND_N { +pub struct InternalDefaultEliminationV2SuccessRate1AndN(String); +impl SC::ServiceConfigKey for InternalDefaultEliminationV2SuccessRate1AndN { fn get_key(&self) -> String { format!( "INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND_N_{}", @@ -131,48 +131,48 @@ impl SC::ServiceConfigKey for INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND pub fn internalDefaultEliminationV2SuccessRate1AndNPrefix( val: String, -) -> INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND_N { - INTERNAL_DEFAULT_ELIMINATION_V2_SUCCESS_RATE_1_AND_N(val) +) -> InternalDefaultEliminationV2SuccessRate1AndN { + InternalDefaultEliminationV2SuccessRate1AndN(val) } -pub const defaultFieldNameForSR1AndN: &str = "default"; -pub const sr1KeyPrefix: &str = "sr1_"; -pub const nKeyPrefix: &str = "n_"; +pub const DEFAULT_FIELD_NAME_FOR_SR1_AND_N: &str = "default"; +pub const SR1_KEY_PREFIX: &str = "sr1_"; +pub const N_KEY_PREFIX: &str = "n_"; +pub const AGGREGATE_KEY_PREFIX: &str = "aggregate_data_"; +pub const GW_DEFAULT_TXN_SOFT_RESET_COUNT: i64 = 10; +pub const DEFAULT_GLOBAL_SELECTION_VOLUME_THRESHOLD: i64 = 20; -pub const gwDefaultTxnSoftResetCount: i64 = 10; -pub const defaultGlobalSelectionVolumeThreshold: i64 = 20; - -pub struct GATEWAY_RESET_SCORE_ENABLED; -impl SC::ServiceConfigKey for GATEWAY_RESET_SCORE_ENABLED { +pub struct GatewayResetScoreEnabled; +impl SC::ServiceConfigKey for GatewayResetScoreEnabled { fn get_key(&self) -> String { "gateway_reset_score_enabled".to_string() } } -pub const gwResetScoreEnabled: GATEWAY_RESET_SCORE_ENABLED = GATEWAY_RESET_SCORE_ENABLED; +pub const GW_RESET_SCORE_ENABLED: GatewayResetScoreEnabled = GatewayResetScoreEnabled; -pub const defSRBasedGwLevelEliminationThreshold: f64 = 0.02; -pub const defaultGlobalSelectionMaxCountThreshold: i64 = 5; +pub const DEF_SRBASED_GW_LEVEL_ELIMINATION_THRESHOLD: f64 = 0.02; +pub const DEFAULT_GLOBAL_SELECTION_MAX_COUNT_THRESHOLD: i64 = 5; -pub struct GATEWAY_SCORE_FIRST_DIMENSION_SOFT_TTL; -impl SC::ServiceConfigKey for GATEWAY_SCORE_FIRST_DIMENSION_SOFT_TTL { +pub struct GatewayScoreFirstDimensionSoftTtl; +impl SC::ServiceConfigKey for GatewayScoreFirstDimensionSoftTtl { fn get_key(&self) -> String { "gateway_score_first_dimension_soft_ttl".to_string() } } -pub const gwScoreFirstDimensionTtl: GATEWAY_SCORE_FIRST_DIMENSION_SOFT_TTL = - GATEWAY_SCORE_FIRST_DIMENSION_SOFT_TTL; +pub const GW_SCORE_FIRST_DIMENSION_TTL: GatewayScoreFirstDimensionSoftTtl = + GatewayScoreFirstDimensionSoftTtl; -pub struct GATEWAY_SCORE_SECOND_DIMENSION_SOFT_TTL; -impl SC::ServiceConfigKey for GATEWAY_SCORE_SECOND_DIMENSION_SOFT_TTL { +pub struct GatewayScoreSecondDimensionSoftTtl; +impl SC::ServiceConfigKey for GatewayScoreSecondDimensionSoftTtl { fn get_key(&self) -> String { "gateway_score_second_dimension_soft_ttl".to_string() } } -pub const gwScoreSecondDimensionTtl: GATEWAY_SCORE_SECOND_DIMENSION_SOFT_TTL = - GATEWAY_SCORE_SECOND_DIMENSION_SOFT_TTL; +pub const GW_SCORE_SECOND_DIMENSION_TTL: GatewayScoreSecondDimensionSoftTtl = + GatewayScoreSecondDimensionSoftTtl; pub struct ShouldConsumeResultFromRouter; impl SC::ServiceConfigKey for ShouldConsumeResultFromRouter { @@ -184,271 +184,271 @@ impl SC::ServiceConfigKey for ShouldConsumeResultFromRouter { pub const SHOULD_CONSUME_RESULT_FROM_ROUTER: ShouldConsumeResultFromRouter = ShouldConsumeResultFromRouter; -pub struct GATEWAY_SCORE_THIRD_DIMENSION_SOFT_TTL; -impl SC::ServiceConfigKey for GATEWAY_SCORE_THIRD_DIMENSION_SOFT_TTL { +pub struct GatewayScoreThirdDimensionSoftTtl; +impl SC::ServiceConfigKey for GatewayScoreThirdDimensionSoftTtl { fn get_key(&self) -> String { "gateway_score_third_dimension_soft_ttl".to_string() } } -pub const gwScoreThirdDimensionTtl: GATEWAY_SCORE_THIRD_DIMENSION_SOFT_TTL = - GATEWAY_SCORE_THIRD_DIMENSION_SOFT_TTL; +pub const GW_SCORE_THIRD_DIMENSION_TTL: GatewayScoreThirdDimensionSoftTtl = + GatewayScoreThirdDimensionSoftTtl; -pub struct GATEWAY_SCORE_FOURTH_DIMENSION_SOFT_TTL; -impl SC::ServiceConfigKey for GATEWAY_SCORE_FOURTH_DIMENSION_SOFT_TTL { +pub struct GatewayScoreFourthDimensionSoftTtl; +impl SC::ServiceConfigKey for GatewayScoreFourthDimensionSoftTtl { fn get_key(&self) -> String { "gateway_score_fourth_dimension_soft_ttl".to_string() } } -pub const gwScoreFourthDimensionTtl: GATEWAY_SCORE_FOURTH_DIMENSION_SOFT_TTL = - GATEWAY_SCORE_FOURTH_DIMENSION_SOFT_TTL; +pub const GW_SCORE_FOURTH_DIMENSION_TTL: GatewayScoreFourthDimensionSoftTtl = + GatewayScoreFourthDimensionSoftTtl; -pub const defScoreKeysTtl: f64 = 900000.0; +pub const DEF_SCORE_KEYS_TTL: f64 = 900000.0; -pub struct IS_GBESV2_ENABLED; -impl SC::ServiceConfigKey for IS_GBESV2_ENABLED { +pub struct IsGbesv2Enabled; +impl SC::ServiceConfigKey for IsGbesv2Enabled { fn get_key(&self) -> String { "IS_GBESV2_ENABLED".to_string() } } -pub const gbesV2Enabled: IS_GBESV2_ENABLED = IS_GBESV2_ENABLED; +pub const GBES_V2_ENABLED: IsGbesv2Enabled = IsGbesv2Enabled; -pub struct ENABLE_GATEWAY_LEVEL_SR_ELIMINATION; -impl SC::ServiceConfigKey for ENABLE_GATEWAY_LEVEL_SR_ELIMINATION { +pub struct EnableGatewayLevelSrElimination; +impl SC::ServiceConfigKey for EnableGatewayLevelSrElimination { fn get_key(&self) -> String { "enable_gateway_level_sr_elimination".to_string() } } -pub const enableGwLevelSrElimination: ENABLE_GATEWAY_LEVEL_SR_ELIMINATION = - ENABLE_GATEWAY_LEVEL_SR_ELIMINATION; +pub const ENABLE_GW_LEVEL_SR_ELIMINATION: EnableGatewayLevelSrElimination = + EnableGatewayLevelSrElimination; -pub struct SR_BASED_GATEWAY_ELIMINATION_THRESHOLD; -impl SC::ServiceConfigKey for SR_BASED_GATEWAY_ELIMINATION_THRESHOLD { +pub struct SrBasedGatewayEliminationThreshold; +impl SC::ServiceConfigKey for SrBasedGatewayEliminationThreshold { fn get_key(&self) -> String { "SR_BASED_GATEWAY_ELIMINATION_THRESHOLD".to_string() } } -pub const srBasedGatewayEliminationThreshold: SR_BASED_GATEWAY_ELIMINATION_THRESHOLD = - SR_BASED_GATEWAY_ELIMINATION_THRESHOLD; +pub const SR_BASED_GATEWAY_ELIMINATION_THRESHOLD: SrBasedGatewayEliminationThreshold = + SrBasedGatewayEliminationThreshold; -pub const defaultSrBasedGatewayEliminationThreshold: f64 = 0.05; +pub const DEFAULT_SR_BASED_GATEWAY_ELIMINATION_THRESHOLD: f64 = 0.05; -pub struct OTP_CARD_INFO_RESTRICTED_GATEWAYS; -impl SC::ServiceConfigKey for OTP_CARD_INFO_RESTRICTED_GATEWAYS { +pub struct OtpCardInfoRestrictedGateways; +impl SC::ServiceConfigKey for OtpCardInfoRestrictedGateways { fn get_key(&self) -> String { "OTP_CARD_INFO_RESTRICTED_GATEWAYS".to_string() } } -pub struct AUTH_TYPE_RESTRICTED_GATEWAYS; -impl SC::ServiceConfigKey for AUTH_TYPE_RESTRICTED_GATEWAYS { +pub struct AuthTypeRestrictedGateways; +impl SC::ServiceConfigKey for AuthTypeRestrictedGateways { fn get_key(&self) -> String { "AUTH_TYPE_RESTRICTED_GATEWAYS".to_string() } } -pub struct CARD_EMI_EXPLICIT_GATEWAYS; -impl SC::ServiceConfigKey for CARD_EMI_EXPLICIT_GATEWAYS { +pub struct CardEmiExplicitGateways; +impl SC::ServiceConfigKey for CardEmiExplicitGateways { fn get_key(&self) -> String { "CARD_EMI_EXPLICIT_GATEWAYS".to_string() } } -pub struct CONSUMER_FINANCE_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for CONSUMER_FINANCE_ONLY_GATEWAYS { +pub struct ConsumerFinanceOnlyGateways; +impl SC::ServiceConfigKey for ConsumerFinanceOnlyGateways { fn get_key(&self) -> String { "CONSUMER_FINANCE_ONLY_GATEWAYS".to_string() } } -pub struct CONSUMER_FINANCE_ALSO_GATEWAYS; -impl SC::ServiceConfigKey for CONSUMER_FINANCE_ALSO_GATEWAYS { +pub struct ConsumerFinanceAlsoGateways; +impl SC::ServiceConfigKey for ConsumerFinanceAlsoGateways { fn get_key(&self) -> String { "CONSUMER_FINANCE_ALSO_GATEWAYS".to_string() } } -pub struct MUTUAL_FUND_FLOW_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for MUTUAL_FUND_FLOW_SUPPORTED_GATEWAYS { +pub struct MutualFundFlowSupportedGateways; +impl SC::ServiceConfigKey for MutualFundFlowSupportedGateways { fn get_key(&self) -> String { "MUTUAL_FUND_FLOW_SUPPORTED_GATEWAYS".to_string() } } -pub struct CROSS_BORDER_FLOW_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for CROSS_BORDER_FLOW_SUPPORTED_GATEWAYS { +pub struct CrossBorderFlowSupportedGateways; +impl SC::ServiceConfigKey for CrossBorderFlowSupportedGateways { fn get_key(&self) -> String { "CROSS_BORDER_FLOW_SUPPORTED_GATEWAYS".to_string() } } -pub struct SBMD_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for SBMD_SUPPORTED_GATEWAYS { +pub struct SbmdSupportedGateways; +impl SC::ServiceConfigKey for SbmdSupportedGateways { fn get_key(&self) -> String { "SBMD_SUPPORTED_GATEWAYS".to_string() } } -pub struct SPLIT_SETTLEMENT_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for SPLIT_SETTLEMENT_SUPPORTED_GATEWAYS { +pub struct SplitSettlementSupportedGateways; +impl SC::ServiceConfigKey for SplitSettlementSupportedGateways { fn get_key(&self) -> String { "SPLIT_SETTLEMENT_SUPPORTED_GATEWAYS".to_string() } } -pub struct TPV_ONLY_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for TPV_ONLY_SUPPORTED_GATEWAYS { +pub struct TpvOnlySupportedGateways; +impl SC::ServiceConfigKey for TpvOnlySupportedGateways { fn get_key(&self) -> String { "TPV_ONLY_SUPPORTED_GATEWAYS".to_string() } } -pub struct NB_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for NB_ONLY_GATEWAYS { +pub struct NbOnlyGateways; +impl SC::ServiceConfigKey for NbOnlyGateways { fn get_key(&self) -> String { "NB_ONLY_GATEWAYS".to_string() } } -pub struct UPI_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for UPI_ONLY_GATEWAYS { +pub struct UpiOnlyGateways; +impl SC::ServiceConfigKey for UpiOnlyGateways { fn get_key(&self) -> String { "UPI_ONLY_GATEWAYS".to_string() } } -pub struct UPI_ALSO_GATEWAYS; -impl SC::ServiceConfigKey for UPI_ALSO_GATEWAYS { +pub struct UpiAlsoGateways; +impl SC::ServiceConfigKey for UpiAlsoGateways { fn get_key(&self) -> String { "UPI_ALSO_GATEWAYS".to_string() } } -pub struct WALLET_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for WALLET_ONLY_GATEWAYS { +pub struct WalletOnlyGateways; +impl SC::ServiceConfigKey for WalletOnlyGateways { fn get_key(&self) -> String { "WALLET_ONLY_GATEWAYS".to_string() } } -pub struct WALLET_ALSO_GATEWAYS; -impl SC::ServiceConfigKey for WALLET_ALSO_GATEWAYS { +pub struct WalletAlsoGateways; +impl SC::ServiceConfigKey for WalletAlsoGateways { fn get_key(&self) -> String { "WALLET_ALSO_GATEWAYS".to_string() } } -pub struct NO_OR_LOW_COST_EMI_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for NO_OR_LOW_COST_EMI_SUPPORTED_GATEWAYS { +pub struct NoOrLowCostEmiSupportedGateways; +impl SC::ServiceConfigKey for NoOrLowCostEmiSupportedGateways { fn get_key(&self) -> String { "NO_OR_LOW_COST_EMI_SUPPORTED_GATEWAYS".to_string() } } -pub struct SI_ON_EMI_CARD_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for SI_ON_EMI_CARD_SUPPORTED_GATEWAYS { +pub struct SiOnEmiCardSupportedGateways; +impl SC::ServiceConfigKey for SiOnEmiCardSupportedGateways { fn get_key(&self) -> String { "SI_ON_EMI_CARD_SUPPORTED_GATEWAYS".to_string() } } -pub struct SI_ON_EMI_DISABLED_CARD_BRAND_GATEWAY_MAPPING; -impl SC::ServiceConfigKey for SI_ON_EMI_DISABLED_CARD_BRAND_GATEWAY_MAPPING { +pub struct SiOnEmiDisabledCardBrandGatewayMapping; +impl SC::ServiceConfigKey for SiOnEmiDisabledCardBrandGatewayMapping { fn get_key(&self) -> String { "SI_ON_EMI_DISABLED_CARD_BRAND_GATEWAY_MAPPING".to_string() } } -pub struct TOKEN_PROVIDER_GATEWAY_MAPPING; -impl SC::ServiceConfigKey for TOKEN_PROVIDER_GATEWAY_MAPPING { +pub struct TokenProviderGatewayMapping; +impl SC::ServiceConfigKey for TokenProviderGatewayMapping { fn get_key(&self) -> String { "TOKEN_PROVIDER_GATEWAY_MAPPING".to_string() } } -pub struct TXN_TYPE_GATEWAY_MAPPING; -impl SC::ServiceConfigKey for TXN_TYPE_GATEWAY_MAPPING { +pub struct TxnTypeGatewayMapping; +impl SC::ServiceConfigKey for TxnTypeGatewayMapping { fn get_key(&self) -> String { "TXN_TYPE_GATEWAY_MAPPING".to_string() } } -pub struct TXN_DETAIL_TYPE_RESTRICTED_GATEWAYS; -impl SC::ServiceConfigKey for TXN_DETAIL_TYPE_RESTRICTED_GATEWAYS { +pub struct TxnDetailTypeRestrictedGateways; +impl SC::ServiceConfigKey for TxnDetailTypeRestrictedGateways { fn get_key(&self) -> String { "TXN_DETAIL_TYPE_RESTRICTED_GATEWAYS".to_string() } } -pub struct REWARD_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for REWARD_ONLY_GATEWAYS { +pub struct RewardOnlyGateways; +impl SC::ServiceConfigKey for RewardOnlyGateways { fn get_key(&self) -> String { "REWARD_ONLY_GATEWAYS".to_string() } } -pub struct REWARD_ALSO_GATEWAYS; -impl SC::ServiceConfigKey for REWARD_ALSO_GATEWAYS { +pub struct RewardAlsoGateways; +impl SC::ServiceConfigKey for RewardAlsoGateways { fn get_key(&self) -> String { "REWARD_ALSO_GATEWAYS".to_string() } } -pub struct SODEXO_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for SODEXO_ONLY_GATEWAYS { +pub struct SodexoOnlyGateways; +impl SC::ServiceConfigKey for SodexoOnlyGateways { fn get_key(&self) -> String { "SODEXO_ONLY_GATEWAYS".to_string() } } -pub struct SODEXO_ALSO_GATEWAYS; -impl SC::ServiceConfigKey for SODEXO_ALSO_GATEWAYS { +pub struct SodexoAlsoGateways; +impl SC::ServiceConfigKey for SodexoAlsoGateways { fn get_key(&self) -> String { "SODEXO_ALSO_GATEWAYS".to_string() } } -pub struct CASH_ONLY_GATEWAYS; -impl SC::ServiceConfigKey for CASH_ONLY_GATEWAYS { +pub struct CashOnlyGateways; +impl SC::ServiceConfigKey for CashOnlyGateways { fn get_key(&self) -> String { "CASH_ONLY_GATEWAYS".to_string() } } -pub struct AMEX_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for AMEX_SUPPORTED_GATEWAYS { +pub struct AmexSupportedGateways; +impl SC::ServiceConfigKey for AmexSupportedGateways { fn get_key(&self) -> String { "AMEX_SUPPORTED_GATEWAYS".to_string() } } -pub struct CARD_BRAND_TO_CVVLESS_TXN_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for CARD_BRAND_TO_CVVLESS_TXN_SUPPORTED_GATEWAYS { +pub struct CardBrandToCvvlessTxnSupportedGateways; +impl SC::ServiceConfigKey for CardBrandToCvvlessTxnSupportedGateways { fn get_key(&self) -> String { "CARD_BRAND_TO_CVVLESS_TXN_SUPPORTED_GATEWAYS".to_string() } } -pub struct CVVLESS_TXN_SUPPORTED_COMMON_GATEWAYS; -impl SC::ServiceConfigKey for CVVLESS_TXN_SUPPORTED_COMMON_GATEWAYS { +pub struct CvvlessTxnSupportedCommonGateways; +impl SC::ServiceConfigKey for CvvlessTxnSupportedCommonGateways { fn get_key(&self) -> String { "CVVLESS_TXN_SUPPORTED_COMMON_GATEWAYS".to_string() } } -pub struct MERCHANT_CONTAINER_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for MERCHANT_CONTAINER_SUPPORTED_GATEWAYS { +pub struct MerchantContainerSupportedGateways; +impl SC::ServiceConfigKey for MerchantContainerSupportedGateways { fn get_key(&self) -> String { "MERCHANT_CONTAINER_SUPPORTED_GATEWAYS".to_string() } } -pub struct TOKEN_SUPPORTED_GATEWAYS(pub String, pub Option, pub String, pub String); -impl SC::ServiceConfigKey for TOKEN_SUPPORTED_GATEWAYS { +pub struct TokenSupportedGateways(pub String, pub Option, pub String, pub String); +impl SC::ServiceConfigKey for TokenSupportedGateways { fn get_key(&self) -> String { format!( "{}_{}_{}_{}_SUPPORTED_GATEWAYS", @@ -460,51 +460,51 @@ impl SC::ServiceConfigKey for TOKEN_SUPPORTED_GATEWAYS { } } -pub struct TOKEN_REPEAT_OTP_SUPPORTED_GATEWAYS(String); -impl SC::ServiceConfigKey for TOKEN_REPEAT_OTP_SUPPORTED_GATEWAYS { +pub struct TokenRepeatOtpSupportedGateways(String); +impl SC::ServiceConfigKey for TokenRepeatOtpSupportedGateways { fn get_key(&self) -> String { format!("{}_TOKEN_REPEAT_OTP_SUPPORTED_GATEWAYS", self.0) } } -pub fn getTokenRepeatOtpGatewayKey(val: String) -> TOKEN_REPEAT_OTP_SUPPORTED_GATEWAYS { - TOKEN_REPEAT_OTP_SUPPORTED_GATEWAYS(val) +pub fn getTokenRepeatOtpGatewayKey(val: String) -> TokenRepeatOtpSupportedGateways { + TokenRepeatOtpSupportedGateways(val) } -pub struct TOKEN_REPEAT_CVVLESS_SUPPORTED_GATEWAYS(String); -impl SC::ServiceConfigKey for TOKEN_REPEAT_CVVLESS_SUPPORTED_GATEWAYS { +pub struct TokenRepeatCvvlessSupportedGateways(String); +impl SC::ServiceConfigKey for TokenRepeatCvvlessSupportedGateways { fn get_key(&self) -> String { format!("{}_TOKEN_REPEAT_CVVLESS_SUPPORTED_GATEWAYS", self.0) } } -pub fn getTokenRepeatCvvLessGatewayKey(val: String) -> TOKEN_REPEAT_CVVLESS_SUPPORTED_GATEWAYS { - TOKEN_REPEAT_CVVLESS_SUPPORTED_GATEWAYS(val) +pub fn getTokenRepeatCvvLessGatewayKey(val: String) -> TokenRepeatCvvlessSupportedGateways { + TokenRepeatCvvlessSupportedGateways(val) } -pub struct TOKEN_REPEAT_MANDATE_SUPPORTED_GATEWAYS(String); -impl SC::ServiceConfigKey for TOKEN_REPEAT_MANDATE_SUPPORTED_GATEWAYS { +pub struct TokenRepeatMandateSupportedGateways(String); +impl SC::ServiceConfigKey for TokenRepeatMandateSupportedGateways { fn get_key(&self) -> String { format!("{}_TOKEN_REPEAT_MANDATE_SUPPORTED_GATEWAYS", self.0) } } -pub fn getTokenRepeatMandateGatewayKey(val: String) -> TOKEN_REPEAT_MANDATE_SUPPORTED_GATEWAYS { - TOKEN_REPEAT_MANDATE_SUPPORTED_GATEWAYS(val) +pub fn getTokenRepeatMandateGatewayKey(val: String) -> TokenRepeatMandateSupportedGateways { + TokenRepeatMandateSupportedGateways(val) } -pub struct TOKEN_REPEAT_SUPPORTED_GATEWAYS(String); -impl SC::ServiceConfigKey for TOKEN_REPEAT_SUPPORTED_GATEWAYS { +pub struct TokenRepeatSupportedGateways(String); +impl SC::ServiceConfigKey for TokenRepeatSupportedGateways { fn get_key(&self) -> String { format!("{}_TOKEN_REPEAT_SUPPORTED_GATEWAYS", self.0) } } -pub fn getTokenRepeatGatewayKey(val: String) -> TOKEN_REPEAT_SUPPORTED_GATEWAYS { - TOKEN_REPEAT_SUPPORTED_GATEWAYS(val) +pub fn getTokenRepeatGatewayKey(val: String) -> TokenRepeatSupportedGateways { + TokenRepeatSupportedGateways(val) } -pub struct MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS(Option>); -impl SC::ServiceConfigKey for MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS { +pub struct MandateGuestCheckoutSupportedGateways(Option>); +impl SC::ServiceConfigKey for MandateGuestCheckoutSupportedGateways { fn get_key(&self) -> String { format!( "{}_MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS", @@ -517,12 +517,12 @@ impl SC::ServiceConfigKey for MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS { pub fn getmandateGuestCheckoutKey( val: Option>, -) -> MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS { - MANDATE_GUEST_CHECKOUT_SUPPORTED_GATEWAYS(val) +) -> MandateGuestCheckoutSupportedGateways { + MandateGuestCheckoutSupportedGateways(val) } -pub struct TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS(Option>); -impl SC::ServiceConfigKey for TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS { +pub struct TokenRepeatCvvlessSupportedBanks(Option>); +impl SC::ServiceConfigKey for TokenRepeatCvvlessSupportedBanks { fn get_key(&self) -> String { format!( "{}_TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS", @@ -535,125 +535,125 @@ impl SC::ServiceConfigKey for TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS { pub fn getTokenRepeatCvvLessBankCodeKey( val: Option>, -) -> TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS { - TOKEN_REPEAT_CVVLESS_SUPPORTED_BANKS(val) +) -> TokenRepeatCvvlessSupportedBanks { + TokenRepeatCvvlessSupportedBanks(val) } -pub struct EMI_BIN_VALIDATION_SUPPORTED_BANKS; -impl SC::ServiceConfigKey for EMI_BIN_VALIDATION_SUPPORTED_BANKS { +pub struct EmiBinValidationSupportedBanks; +impl SC::ServiceConfigKey for EmiBinValidationSupportedBanks { fn get_key(&self) -> String { "EMI_BIN_VALIDATION_SUPPORTED_BANKS".to_string() } } -pub const getEmiBinValidationSupportedBanksKey: EMI_BIN_VALIDATION_SUPPORTED_BANKS = - EMI_BIN_VALIDATION_SUPPORTED_BANKS; +pub const GET_EMI_BIN_VALIDATION_SUPPORTED_BANKS_KEY: EmiBinValidationSupportedBanks = + EmiBinValidationSupportedBanks; -pub struct METRIC_TRACKING_LOG; -impl SC::ServiceConfigKey for METRIC_TRACKING_LOG { +pub struct MetricTrackingLog; +impl SC::ServiceConfigKey for MetricTrackingLog { fn get_key(&self) -> String { "METRIC_TRACKING_LOG".to_string() } } -pub const metricTrackingLogDataKey: METRIC_TRACKING_LOG = METRIC_TRACKING_LOG; +pub const METRIC_TRACKING_LOG_DATA_KEY: MetricTrackingLog = MetricTrackingLog; -pub struct V2_ROUTING_HANDLE_LIST; -impl SC::ServiceConfigKey for V2_ROUTING_HANDLE_LIST { +pub struct V2RoutingHandleList; +impl SC::ServiceConfigKey for V2RoutingHandleList { fn get_key(&self) -> String { "V2_ROUTING_HANDLE_LIST".to_string() } } -pub const v2RoutingHandleList: V2_ROUTING_HANDLE_LIST = V2_ROUTING_HANDLE_LIST; +pub const V2_ROUTING_HANDLE_LIST: V2RoutingHandleList = V2RoutingHandleList; -pub struct V2_ROUTING_PSP_LIST; -impl SC::ServiceConfigKey for V2_ROUTING_PSP_LIST { +pub struct V2RoutingPspList; +impl SC::ServiceConfigKey for V2RoutingPspList { fn get_key(&self) -> String { "V2_ROUTING_PSP_LIST".to_string() } } -pub const v2RoutingPspList: V2_ROUTING_PSP_LIST = V2_ROUTING_PSP_LIST; +pub const V2_ROUTING_PSP_LIST: V2RoutingPspList = V2RoutingPspList; -pub struct V2_ROUTING_TOP_BANK_LIST; -impl SC::ServiceConfigKey for V2_ROUTING_TOP_BANK_LIST { +pub struct V2RoutingTopBankList; +impl SC::ServiceConfigKey for V2RoutingTopBankList { fn get_key(&self) -> String { "V2_ROUTING_TOP_BANK_LIST".to_string() } } -pub const v2RoutingTopBankList: V2_ROUTING_TOP_BANK_LIST = V2_ROUTING_TOP_BANK_LIST; +pub const V2_ROUTING_TOP_BANK_LIST: V2RoutingTopBankList = V2RoutingTopBankList; -pub struct V2_ROUTING_PSP_PACKAGE_LIST; -impl SC::ServiceConfigKey for V2_ROUTING_PSP_PACKAGE_LIST { +pub struct V2RoutingPspPackageList; +impl SC::ServiceConfigKey for V2RoutingPspPackageList { fn get_key(&self) -> String { "V2_ROUTING_PSP_PACKAGE_LIST".to_string() } } -pub const v2RoutingPspPackageList: V2_ROUTING_PSP_PACKAGE_LIST = V2_ROUTING_PSP_PACKAGE_LIST; +pub const V2_ROUTING_PSP_PACKAGE_LIST: V2RoutingPspPackageList = V2RoutingPspPackageList; -pub struct OPTIMIZATION_ROUTING_CONFIG(pub String); -impl SC::ServiceConfigKey for OPTIMIZATION_ROUTING_CONFIG { +pub struct OptimizationRoutingConfig(pub String); +impl SC::ServiceConfigKey for OptimizationRoutingConfig { fn get_key(&self) -> String { format!("{}_optimization_routing_config", self.0) } } -pub struct DEFAULT_OPTIMIZATION_ROUTING_CONFIG; -impl SC::ServiceConfigKey for DEFAULT_OPTIMIZATION_ROUTING_CONFIG { +pub struct DefaultOptimizationRoutingConfig; +impl SC::ServiceConfigKey for DefaultOptimizationRoutingConfig { fn get_key(&self) -> String { "default_optimization_routing_config".to_string() } } -pub struct ATM_PIN_CARD_INFO_RESTRICTED_GATEWAYS; -impl SC::ServiceConfigKey for ATM_PIN_CARD_INFO_RESTRICTED_GATEWAYS { +pub struct AtmPinCardInfoRestrictedGateways; +impl SC::ServiceConfigKey for AtmPinCardInfoRestrictedGateways { fn get_key(&self) -> String { "ATM_PIN_CARD_INFO_RESTRICTED_GATEWAYS".to_string() } } -pub struct OTP_CARD_INFO_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for OTP_CARD_INFO_SUPPORTED_GATEWAYS { +pub struct OtpCardInfoSupportedGateways; +impl SC::ServiceConfigKey for OtpCardInfoSupportedGateways { fn get_key(&self) -> String { "OTP_CARD_INFO_SUPPORTED_GATEWAYS".to_string() } } -pub struct MOTO_CARD_INFO_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for MOTO_CARD_INFO_SUPPORTED_GATEWAYS { +pub struct MotoCardInfoSupportedGateways; +impl SC::ServiceConfigKey for MotoCardInfoSupportedGateways { fn get_key(&self) -> String { "MOTO_CARD_INFO_SUPPORTED_GATEWAYS".to_string() } } -pub struct TA_OFFLINE_ENABLED_GATEWAYS; -impl SC::ServiceConfigKey for TA_OFFLINE_ENABLED_GATEWAYS { +pub struct TaOfflineEnabledGateways; +impl SC::ServiceConfigKey for TaOfflineEnabledGateways { fn get_key(&self) -> String { "TA_OFFLINE_ENABLED_GATEWAYS".to_string() } } -pub struct MERCHANT_WISE_MANDATE_BIN_ENFORCED_GATEWAYS; -impl SC::ServiceConfigKey for MERCHANT_WISE_MANDATE_BIN_ENFORCED_GATEWAYS { +pub struct MerchantWiseMandateBinEnforcedGateways; +impl SC::ServiceConfigKey for MerchantWiseMandateBinEnforcedGateways { fn get_key(&self) -> String { "MERCHANT_WISE_MANDATE_BIN_ENFORCED_GATEWAYS".to_string() } } -pub struct MERCHANT_WISE_AUTH_TYPE_BIN_ENFORCED_GATEWAYS; -impl SC::ServiceConfigKey for MERCHANT_WISE_AUTH_TYPE_BIN_ENFORCED_GATEWAYS { +pub struct MerchantWiseAuthTypeBinEnforcedGateways; +impl SC::ServiceConfigKey for MerchantWiseAuthTypeBinEnforcedGateways { fn get_key(&self) -> String { "MERCHANT_WISE_AUTH_TYPE_BIN_ENFORCED_GATEWAYS".to_string() } } -pub struct CARD_MANDATE_BIN_FILTER_EXCLUDED_GATEWAYS; -impl SC::ServiceConfigKey for CARD_MANDATE_BIN_FILTER_EXCLUDED_GATEWAYS { +pub struct CardMandateBinFilterExcludedGateways; +impl SC::ServiceConfigKey for CardMandateBinFilterExcludedGateways { fn get_key(&self) -> String { "CARD_MANDATE_BIN_FILTER_EXCLUDED_GATEWAYS".to_string() } } -pub struct ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INPUT(pub String); -impl SC::ServiceConfigKey for ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INPUT { +pub struct EnableGatewaySelectionBasedOnOptimizedSrInput(pub String); +impl SC::ServiceConfigKey for EnableGatewaySelectionBasedOnOptimizedSrInput { fn get_key(&self) -> String { format!( "ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INPUT_{}", @@ -663,73 +663,73 @@ impl SC::ServiceConfigKey for ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INP } pub fn enable_gateway_selection_based_on_optimized_sr_input( val: String, -) -> ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INPUT { - ENABLE_GATEWAY_SELECTION_BASED_ON_OPTIMIZED_SR_INPUT(val) +) -> EnableGatewaySelectionBasedOnOptimizedSrInput { + EnableGatewaySelectionBasedOnOptimizedSrInput(val) } -pub struct ENABLE_BETA_DISTRIBUTION_ON_SR_V3; -impl SC::ServiceConfigKey for ENABLE_BETA_DISTRIBUTION_ON_SR_V3 { +pub struct EnableBetaDistributionOnSrV3; +impl SC::ServiceConfigKey for EnableBetaDistributionOnSrV3 { fn get_key(&self) -> String { "ENABLE_BETA_DISTRIBUTION_ON_SR_V3".to_string() } } -pub const enable_beta_distribution_on_sr_v3: ENABLE_BETA_DISTRIBUTION_ON_SR_V3 = - ENABLE_BETA_DISTRIBUTION_ON_SR_V3; +pub const ENABLE_BETA_DISTRIBUTION_ON_SR_V3: EnableBetaDistributionOnSrV3 = + EnableBetaDistributionOnSrV3; -pub struct ENABLE_GATEWAY_SELECTION_BASED_ON_SR_V3_INPUT(pub String); -impl SC::ServiceConfigKey for ENABLE_GATEWAY_SELECTION_BASED_ON_SR_V3_INPUT { +pub struct EnableGatewaySelectionBasedOnSrV3Input(pub String); +impl SC::ServiceConfigKey for EnableGatewaySelectionBasedOnSrV3Input { fn get_key(&self) -> String { format!("ENABLE_GATEWAY_SELECTION_BASED_ON_SR_V3_INPUT_{}", self.0) } } pub fn enable_gateway_selection_based_on_sr_v3_input( val: String, -) -> ENABLE_GATEWAY_SELECTION_BASED_ON_SR_V3_INPUT { - ENABLE_GATEWAY_SELECTION_BASED_ON_SR_V3_INPUT(val) +) -> EnableGatewaySelectionBasedOnSrV3Input { + EnableGatewaySelectionBasedOnSrV3Input(val) } -pub struct ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3; -impl SC::ServiceConfigKey for ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3 { +pub struct EnableBinomialDistributionOnSrV3; +impl SC::ServiceConfigKey for EnableBinomialDistributionOnSrV3 { fn get_key(&self) -> String { "ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3".to_string() } } -pub const enable_binomial_distribution_on_sr_v3: ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3 = - ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3; +pub const ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3: EnableBinomialDistributionOnSrV3 = + EnableBinomialDistributionOnSrV3; -pub struct ENABLE_EXTRA_SCORE_ON_SR_V3; -impl SC::ServiceConfigKey for ENABLE_EXTRA_SCORE_ON_SR_V3 { +pub struct EnableExtraScoreOnSrV3; +impl SC::ServiceConfigKey for EnableExtraScoreOnSrV3 { fn get_key(&self) -> String { "ENABLE_EXTRA_SCORE_ON_SR_V3".to_string() } } -pub const enable_extra_score_on_sr_v3: ENABLE_EXTRA_SCORE_ON_SR_V3 = ENABLE_EXTRA_SCORE_ON_SR_V3; +pub const ENABLE_EXTRA_SCORE_ON_SR_V3: EnableExtraScoreOnSrV3 = EnableExtraScoreOnSrV3; -pub struct ENABLE_RESET_ON_SR_V3; -impl SC::ServiceConfigKey for ENABLE_RESET_ON_SR_V3 { +pub struct EnableResetOnSrV3; +impl SC::ServiceConfigKey for EnableResetOnSrV3 { fn get_key(&self) -> String { "ENABLE_RESET_ON_SR_V3".to_string() } } -pub const enable_reset_on_sr_v3: ENABLE_RESET_ON_SR_V3 = ENABLE_RESET_ON_SR_V3; +pub const ENABLE_RESET_ON_SR_V3: EnableResetOnSrV3 = EnableResetOnSrV3; -pub struct GATEWAY_REFERENCE_ID_ENABLED_MERCHANT; -impl SC::ServiceConfigKey for GATEWAY_REFERENCE_ID_ENABLED_MERCHANT { +pub struct GatewayReferenceIdEnabledMerchant; +impl SC::ServiceConfigKey for GatewayReferenceIdEnabledMerchant { fn get_key(&self) -> String { "gateway_reference_id_enabled_merchant".to_string() } } -pub const gatewayReferenceIdEnabledMerchant: GATEWAY_REFERENCE_ID_ENABLED_MERCHANT = - GATEWAY_REFERENCE_ID_ENABLED_MERCHANT; +pub const GATEWAY_REFERENCE_ID_ENABLED_MERCHANT: GatewayReferenceIdEnabledMerchant = + GatewayReferenceIdEnabledMerchant; -pub struct GATEWAYDECIDER_SCORINGFLOW; -impl SC::ServiceConfigKey for GATEWAYDECIDER_SCORINGFLOW { +pub struct GatewaydeciderScoringflow; +impl SC::ServiceConfigKey for GatewaydeciderScoringflow { fn get_key(&self) -> String { "GatewayDecider::scoringFlow".to_string() } } -pub const paymentFlowsRequiredForGwFiltering: [&str; 12] = [ +pub const PAYMENT_FLOWS_REQUIRED_FOR_GW_FILTERING: [&str; 13] = [ "DOTP", "CARD_MOTO", "MANDATE_REGISTER", @@ -742,222 +742,222 @@ pub const paymentFlowsRequiredForGwFiltering: [&str; 12] = [ "CROSS_BORDER_PAYMENT", "SINGLE_BLOCK_MULTIPLE_DEBIT", "ONE_TIME_MANDATE", + "PIX_AUTOMATIC_REDIRECT", ]; -pub const getCardBrandCacheExpiry: i32 = 2 * 24 * 60 * 60; -pub const gatewayScoringData: &str = "gateway_scoring_data_"; -pub const globalLevelOutageKeyPrefix: &str = "gw_score_global_outage"; -pub const merchantLevelOutageKeyPrefix: &str = "gw_score_outage"; +pub const GET_CARD_BRAND_CACHE_EXPIRY: i32 = 2 * 24 * 60 * 60; +pub const GATEWAY_SCORING_DATA: &str = "gateway_scoring_data_"; +pub const GLOBAL_LEVEL_OUTAGE_KEY_PREFIX: &str = "gw_score_global_outage"; +pub const MERCHANT_LEVEL_OUTAGE_KEY_PREFIX: &str = "gw_score_outage"; -pub struct MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION; -impl SC::ServiceConfigKey for MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION { +pub struct MerchantsEnabledForScoreKeysUnification; +impl SC::ServiceConfigKey for MerchantsEnabledForScoreKeysUnification { fn get_key(&self) -> String { "merchants_enabled_for_score_keys_unification".to_string() } } -pub const merchantsEnabledForScoreKeysUnification: MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION = - MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION; +pub const MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION: MerchantsEnabledForScoreKeysUnification = + MerchantsEnabledForScoreKeysUnification; -pub const gateway_selection_order_type_key_prefix: &str = "gw_sr_score"; -pub const gateway_selection_v3_order_type_key_prefix: &str = "{gw_sr_v3_score"; -pub const gatewayScoreKeysTTL: i64 = 1800; -pub const elimination_based_routing_key_prefix: &str = "gw_score"; -pub const elimination_based_routing_global_key_prefix: &str = "gw_score_global"; +pub const GATEWAY_SELECTION_ORDER_TYPE_KEY_PREFIX: &str = "gw_sr_score"; +pub const GATEWAY_SELECTION_V3_ORDER_TYPE_KEY_PREFIX: &str = "{gw_sr_v3_score"; +pub const GATEWAY_SCORE_KEYS_TTL: i64 = 1800; +pub const ELIMINATION_BASED_ROUTING_KEY_PREFIX: &str = "gw_score"; +pub const ELIMINATION_BASED_ROUTING_GLOBAL_KEY_PREFIX: &str = "gw_score_global"; -pub struct GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT; -impl SC::ServiceConfigKey for GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT { +pub struct GwRefIdSelectionBasedEnabledMerchant; +impl SC::ServiceConfigKey for GwRefIdSelectionBasedEnabledMerchant { fn get_key(&self) -> String { "gw_ref_id_selection_based_enabled_merchant".to_string() } } -pub const gwRefIdSelectionBasedEnabledMerchant: GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT = - GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT; +pub const GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT: GwRefIdSelectionBasedEnabledMerchant = + GwRefIdSelectionBasedEnabledMerchant; -pub struct ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION; -impl SC::ServiceConfigKey for ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION { +pub struct EnableSelectionBasedAuthTypeEvaluation; +impl SC::ServiceConfigKey for EnableSelectionBasedAuthTypeEvaluation { fn get_key(&self) -> String { "ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION".to_string() } } -pub const selectionBasedAuthTypeEnabledMerchant: ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION = - ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION; +pub const SELECTION_BASED_AUTH_TYPE_ENABLED_MERCHANT: EnableSelectionBasedAuthTypeEvaluation = + EnableSelectionBasedAuthTypeEvaluation; -pub struct ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION; -impl SC::ServiceConfigKey for ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION { +pub struct EnableSelectionBasedBankLevelEvaluation; +impl SC::ServiceConfigKey for EnableSelectionBasedBankLevelEvaluation { fn get_key(&self) -> String { "ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION".to_string() } } -pub const selectionBasedBankLevelEnabledMerchant: ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION = - ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION; +pub const SELECTION_BASED_BANK_LEVEL_ENABLED_MERCHANT: EnableSelectionBasedBankLevelEvaluation = + EnableSelectionBasedBankLevelEvaluation; -pub struct PUSH_DATA_TO_ROUTING_ETL_STREAM; -impl SC::ServiceConfigKey for PUSH_DATA_TO_ROUTING_ETL_STREAM { +pub struct PushDataToRoutingEtlStream; +impl SC::ServiceConfigKey for PushDataToRoutingEtlStream { fn get_key(&self) -> String { "push_data_to_routing_ETL_stream".to_string() } } -pub const pushDataToRoutingETLStream: PUSH_DATA_TO_ROUTING_ETL_STREAM = - PUSH_DATA_TO_ROUTING_ETL_STREAM; +pub const PUSH_DATA_TO_ROUTING_ETLSTREAM: PushDataToRoutingEtlStream = PushDataToRoutingEtlStream; -pub struct SR_VOLUME_CHECK_ENABLED_MERCHANT; -impl SC::ServiceConfigKey for SR_VOLUME_CHECK_ENABLED_MERCHANT { +pub struct SrVolumeCheckEnabledMerchant; +impl SC::ServiceConfigKey for SrVolumeCheckEnabledMerchant { fn get_key(&self) -> String { "SR_VOLUME_CHECK_ENABLED_MERCHANT".to_string() } } -pub const isMerchantEnabledForVolumeCheck: SR_VOLUME_CHECK_ENABLED_MERCHANT = - SR_VOLUME_CHECK_ENABLED_MERCHANT; +pub const IS_MERCHANT_ENABLED_FOR_VOLUME_CHECK: SrVolumeCheckEnabledMerchant = + SrVolumeCheckEnabledMerchant; -pub const defaultSelectionBucketTxnVolumeThrehold: i64 = 5; +pub const DEFAULT_SELECTION_BUCKET_TXN_VOLUME_THRESHOLD: i64 = 5; -pub struct SR_SELECTION_BUCKET_VOLUME_THRESHOLD; -impl SC::ServiceConfigKey for SR_SELECTION_BUCKET_VOLUME_THRESHOLD { +pub struct SrSelectionBucketVolumeThreshold; +impl SC::ServiceConfigKey for SrSelectionBucketVolumeThreshold { fn get_key(&self) -> String { "SR_SELECTION_BUCKET_VOLUME_THRESHOLD".to_string() } } -pub const selectionBucketTxnVolumeThreshold: SR_SELECTION_BUCKET_VOLUME_THRESHOLD = - SR_SELECTION_BUCKET_VOLUME_THRESHOLD; +pub const SELECTION_BUCKET_TXN_VOLUME_THRESHOLD: SrSelectionBucketVolumeThreshold = + SrSelectionBucketVolumeThreshold; -pub struct ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE; -impl SC::ServiceConfigKey for ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE { +pub struct EnableMerchantOnVolumeDistributionFeature; +impl SC::ServiceConfigKey for EnableMerchantOnVolumeDistributionFeature { fn get_key(&self) -> String { "ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE".to_string() } } -pub const routeRandomTrafficEnabledMerchant: ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE = - ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE; +pub const ROUTE_RANDOM_TRAFFIC_ENABLED_MERCHANT: EnableMerchantOnVolumeDistributionFeature = + EnableMerchantOnVolumeDistributionFeature; -pub struct ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE_SR_V3; -impl SC::ServiceConfigKey for ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE_SR_V3 { +pub struct EnableMerchantOnVolumeDistributionFeatureSrV3; +impl SC::ServiceConfigKey for EnableMerchantOnVolumeDistributionFeatureSrV3 { fn get_key(&self) -> String { "ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE_SR_V3".to_string() } } -pub const routeRandomTrafficSrV3EnabledMerchant: - ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE_SR_V3 = - ENABLE_MERCHANT_ON_VOLUME_DISTRIBUTION_FEATURE_SR_V3; +pub const ROUTE_RANDOM_TRAFFIC_SR_V3_ENABLED_MERCHANT: + EnableMerchantOnVolumeDistributionFeatureSrV3 = EnableMerchantOnVolumeDistributionFeatureSrV3; -pub struct ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3(pub String); -impl SC::ServiceConfigKey for ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3 { +pub struct EnableExploreAndExploitOnSrv3(pub String); +impl SC::ServiceConfigKey for EnableExploreAndExploitOnSrv3 { fn get_key(&self) -> String { format!("ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3_{}", self.0) } } -pub fn enableExploreAndExploitOnSrV3(val: String) -> ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3 { - ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3(val) +pub fn enableExploreAndExploitOnSrV3(val: String) -> EnableExploreAndExploitOnSrv3 { + EnableExploreAndExploitOnSrv3(val) } //TODO : This is duplicate and is same key is already there in decider constants.rs -pub struct ENABLE_DEBUG_MODE_ON_SR_V3; -impl SC::ServiceConfigKey for ENABLE_DEBUG_MODE_ON_SR_V3 { +pub struct EnableDebugModeOnSrV3; +impl SC::ServiceConfigKey for EnableDebugModeOnSrV3 { fn get_key(&self) -> String { "ENABLE_DEBUG_MODE_ON_SR_V3".to_string() } } -pub const enableDebugModeOnSrV3: ENABLE_DEBUG_MODE_ON_SR_V3 = ENABLE_DEBUG_MODE_ON_SR_V3; +pub const ENABLE_DEBUG_MODE_ON_SR_V3: EnableDebugModeOnSrV3 = EnableDebugModeOnSrV3; -pub const pendingTxnsKeyPrefix: &str = "PENDING_TXNS_"; +pub const PENDING_TXNS_KEY_PREFIX: &str = "PENDING_TXNS_"; -pub struct SR_ROUTING_RANDOM_DISTRIBUTION_PERCENTAGE; -impl SC::ServiceConfigKey for SR_ROUTING_RANDOM_DISTRIBUTION_PERCENTAGE { +pub struct SrRoutingRandomDistributionPercentage; +impl SC::ServiceConfigKey for SrRoutingRandomDistributionPercentage { fn get_key(&self) -> String { "SR_ROUTING_RANDOM_DISTRIBUTION_PERCENTAGE".to_string() } } -pub const srRoutingTrafficRandomDistribution: SR_ROUTING_RANDOM_DISTRIBUTION_PERCENTAGE = - SR_ROUTING_RANDOM_DISTRIBUTION_PERCENTAGE; +pub const SR_ROUTING_TRAFFIC_RANDOM_DISTRIBUTION: SrRoutingRandomDistributionPercentage = + SrRoutingRandomDistributionPercentage; -pub const defaultSrRoutingTrafficRandomDistribution: f64 = 10.0; +pub const DEFAULT_SR_ROUTING_TRAFFIC_RANDOM_DISTRIBUTION: f64 = 10.0; -pub struct WEIGHTED_BLOCK_SR_EVALUATION_ENABLED_MERCHANTS; -impl SC::ServiceConfigKey for WEIGHTED_BLOCK_SR_EVALUATION_ENABLED_MERCHANTS { +pub struct WeightedBlockSrEvaluationEnabledMerchants; +impl SC::ServiceConfigKey for WeightedBlockSrEvaluationEnabledMerchants { fn get_key(&self) -> String { "WEIGHTED_BLOCK_SR_EVALUATION_ENABLED_MERCHANTS".to_string() } } -pub const isWeightedSrEvaluationEnabledMerchant: WEIGHTED_BLOCK_SR_EVALUATION_ENABLED_MERCHANTS = - WEIGHTED_BLOCK_SR_EVALUATION_ENABLED_MERCHANTS; +pub const IS_WEIGHTED_SR_EVALUATION_ENABLED_MERCHANT: WeightedBlockSrEvaluationEnabledMerchants = + WeightedBlockSrEvaluationEnabledMerchants; -pub const defaultWeightsFactorForWeightedSrEvaluation: [(f64, i32); 4] = +pub const DEFAULT_WEIGHTS_FACTOR_FOR_WEIGHTED_SR_EVALUATION: [(f64, i32); 4] = [(1.0, 1), (0.98, 6), (0.92, 18), (0.85, 0)]; -pub struct SR_WEIGHT_FACTOR_FOR_WEIGHTED_EVALUATION; -impl SC::ServiceConfigKey for SR_WEIGHT_FACTOR_FOR_WEIGHTED_EVALUATION { +pub struct SrWeightFactorForWeightedEvaluation; +impl SC::ServiceConfigKey for SrWeightFactorForWeightedEvaluation { fn get_key(&self) -> String { "SR_WEIGHT_FACTOR_FOR_WEIGHTED_EVALUATION".to_string() } } -pub const selectionWeightsFactorForWeightedSrEvaluation: SR_WEIGHT_FACTOR_FOR_WEIGHTED_EVALUATION = - SR_WEIGHT_FACTOR_FOR_WEIGHTED_EVALUATION; +pub const SELECTION_WEIGHTS_FACTOR_FOR_WEIGHTED_SR_EVALUATION: SrWeightFactorForWeightedEvaluation = + SrWeightFactorForWeightedEvaluation; -pub struct MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT; -impl SC::ServiceConfigKey for MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT { +pub struct MerchantEnabledForRoutingExperiment; +impl SC::ServiceConfigKey for MerchantEnabledForRoutingExperiment { fn get_key(&self) -> String { "MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT".to_string() } } -pub const isPerformingExperiment: MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT = - MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT; +pub const IS_PERFORMING_EXPERIMENT: MerchantEnabledForRoutingExperiment = + MerchantEnabledForRoutingExperiment; -pub struct HANDLE_PACKAGE_BASED_ROUTING_CUTOVER; -impl SC::ServiceConfigKey for HANDLE_PACKAGE_BASED_ROUTING_CUTOVER { +pub struct HandlePackageBasedRoutingCutover; +impl SC::ServiceConfigKey for HandlePackageBasedRoutingCutover { fn get_key(&self) -> String { "HANDLE_PACKAGE_BASED_ROUTING_CUTOVER".to_string() } } -pub const handleAndPackageBasedRouting: HANDLE_PACKAGE_BASED_ROUTING_CUTOVER = - HANDLE_PACKAGE_BASED_ROUTING_CUTOVER; +pub const HANDLE_AND_PACKAGE_BASED_ROUTING: HandlePackageBasedRoutingCutover = + HandlePackageBasedRoutingCutover; -pub struct EDCC_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for EDCC_SUPPORTED_GATEWAYS { +pub struct EdccSupportedGateways; +impl SC::ServiceConfigKey for EdccSupportedGateways { fn get_key(&self) -> String { "EDCC_SUPPORTED_GATEWAYS".to_string() } } -pub struct MGA_ELIGIBLE_SEAMLESS_GATEWAYS; -impl SC::ServiceConfigKey for MGA_ELIGIBLE_SEAMLESS_GATEWAYS { +pub struct MgaEligibleSeamlessGateways; +impl SC::ServiceConfigKey for MgaEligibleSeamlessGateways { fn get_key(&self) -> String { "MGA_ELIGIBLE_SEAMLESS_GATEWAYS".to_string() } } -pub struct AMEX_NOT_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for AMEX_NOT_SUPPORTED_GATEWAYS { +pub struct AmexNotSupportedGateways; +impl SC::ServiceConfigKey for AmexNotSupportedGateways { fn get_key(&self) -> String { "AMEX_NOT_SUPPORTED_GATEWAYS".to_string() } } -pub struct V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS { +pub struct V2IntegrationNotSupportedGateways; +impl SC::ServiceConfigKey for V2IntegrationNotSupportedGateways { fn get_key(&self) -> String { "V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS".to_string() } } -pub struct UPI_INTENT_NOT_SUPPORTED_GATEWAYS; -impl SC::ServiceConfigKey for UPI_INTENT_NOT_SUPPORTED_GATEWAYS { +pub struct UpiIntentNotSupportedGateways; +impl SC::ServiceConfigKey for UpiIntentNotSupportedGateways { fn get_key(&self) -> String { "UPI_INTENT_NOT_SUPPORTED_GATEWAYS".to_string() } } -pub struct ENABLED_CVVLESS_V2_ENABLED_MERCHANTS; -impl SC::ServiceConfigKey for ENABLED_CVVLESS_V2_ENABLED_MERCHANTS { +pub struct EnabledCvvlessV2EnabledMerchants; +impl SC::ServiceConfigKey for EnabledCvvlessV2EnabledMerchants { fn get_key(&self) -> String { "ENABLED_CVVLESS_V2_ENABLED_MERCHANTS".to_string() } } -pub const cvvLessV2Flow: ENABLED_CVVLESS_V2_ENABLED_MERCHANTS = - ENABLED_CVVLESS_V2_ENABLED_MERCHANTS; +pub const CVV_LESS_V2_FLOW: EnabledCvvlessV2EnabledMerchants = EnabledCvvlessV2EnabledMerchants; + +pub const GATEWAYS_WITH_TENURE_BASED_CREDS: [&str; 3] = ["HDFC", "HDFC_CC_EMI", "ICICI"]; -pub const gatewaysWithTenureBasedCreds: [&str; 3] = ["HDFC", "HDFC_CC_EMI", "ICICI"]; +pub const PIX_PAYMENT_FLOWS: [&str; 1] = ["PIX_AUTOMATIC_REDIRECT"]; -pub struct MERCHANT_CONFIG_ENTITY_LEVEL_LOOKUP_CUTOVER; -impl SC::ServiceConfigKey for MERCHANT_CONFIG_ENTITY_LEVEL_LOOKUP_CUTOVER { +pub struct MerchantConfigEntityLevelLookupCutover; +impl SC::ServiceConfigKey for MerchantConfigEntityLevelLookupCutover { fn get_key(&self) -> String { "MERCHANT_CONFIG_ENTITY_LEVEL_LOOKUP_CUTOVER".to_string() } diff --git a/src/decider/gatewaydecider/flow_new.rs b/src/decider/gatewaydecider/flow_new.rs index 711b7616..2795293d 100644 --- a/src/decider/gatewaydecider/flow_new.rs +++ b/src/decider/gatewaydecider/flow_new.rs @@ -3,13 +3,12 @@ use super::types::RankingAlgorithm; use super::types::UnifiedError; use crate::app::get_tenant_app_state; use crate::decider::network_decider; -use axum::response::IntoResponse; -use diesel::expression::is_aggregate::No; use serde_json::json; use serde_json::Value as AValue; use std::collections::HashMap; use std::option::Option; use std::string::String; +use std::time::Instant; use std::vec::Vec; // use eulerhs::prelude::*; // use eulerhs::language as L; @@ -20,31 +19,20 @@ use super::runner::handle_fallback_logic; use super::types as T; use super::types::PriorityLogicFailure; use super::utils as Utils; -use super::utils::is_card_transaction; -use super::utils::is_emandate_transaction; -use super::utils::is_mandate_transaction; -use super::utils::is_tpv_mandate_transaction; -use super::utils::is_tpv_transaction; -use crate::decider::storage::utils::txn_card_info::is_google_pay_txn; -use crate::types::card::card_type::card_type_to_text; -use crate::types::card::card_type::CardType; -use crate::types::card::txn_card_info::AuthType; -// use crate::types::card::txn_card_info::TxnCardInfo; -use crate::types::card::vault_provider::VaultProvider; // use optics_core::{preview, review}; use crate::decider::gatewaydecider::constants as C; use crate::logger; +use crate::redis::feature::RedisDataStruct; use crate::types::card::txn_card_info::TxnCardInfo; -use crate::types::gateway_card_info::ValidationType; use crate::types::merchant as ETM; use crate::types::merchant::merchant_gateway_account::MerchantGatewayAccount; -use crate::types::txn_details::types as ETTD; -pub async fn deciderFullPayloadHSFunction( +pub async fn decider_full_payload_hs_function( dreq_: T::DomainDeciderRequestForApiCallV2, -) -> Result<(T::DecidedGateway), T::ErrorResponse> { + cpu_start: Instant, +) -> Result { let merchant_account = - ETM::merchant_account::load_merchant_by_merchant_id(dreq_.merchantId.clone()) + ETM::merchant_account::load_merchant_by_merchant_id(dreq_.merchant_id.clone()) .await .ok_or(T::ErrorResponse { status: "Invalid Request".to_string(), @@ -61,17 +49,17 @@ pub async fn deciderFullPayloadHSFunction( priority_logic_output: None, is_dynamic_mga_enabled: false, })?; - let enforced_gateway_filter = handleEnforcedGateway(dreq_.clone().eligibleGatewayList); + let enforced_gateway_filter = handle_enforced_gateway(dreq_.clone().eligible_gateway_list); // check if type formation is correct let merchant_prefs = ETM::merchant_iframe_preferences::MerchantIframePreferences { id: ETM::merchant_iframe_preferences::to_merchant_iframe_prefs_pid( - crate::types::merchant::id::merchant_pid_to_text(merchant_account.id.clone()), + crate::types::merchant::id::merchant_pid_to_text(merchant_account.id), ), merchantId: merchant_account.merchantId.clone(), dynamicSwitchingEnabled: enforced_gateway_filter .as_ref() - .map(|list| !(list.len() <= 1)) + .map(|list| list.len() > 1) .unwrap_or(false), isinRoutingEnabled: false, issuerRoutingEnabled: false, @@ -84,7 +72,7 @@ pub async fn deciderFullPayloadHSFunction( Some(card_bin) => Some(card_bin), None => match dreq.txnCardInfo.card_isin { Some(c_isin) => { - let res_bin = Utils::get_card_bin_from_token_bin(6, c_isin.as_str()).await; + let res_bin = Utils::get_card_bin_from_token_bin(6, c_isin.as_str(), None).await; Some(res_bin) } None => dreq.txnCardInfo.card_isin.clone(), @@ -118,23 +106,26 @@ pub async fn deciderFullPayloadHSFunction( dpPriorityLogicScript: dreq.priorityLogicScript, dpEDCCApplied: dreq.isEdccApplied, dpShouldConsumeResult: dreq.shouldConsumeResult, + dpRedisCompressionConfig: None, }; - if dreq_.rankingAlgorithm == Some(RankingAlgorithm::NTW_BASED_ROUTING) { + if dreq_.ranking_algorithm == Some(RankingAlgorithm::NtwBasedRouting) { logger::debug!("Performing debit routing"); network_decider::debit_routing::perform_debit_routing(dreq_).await } else { logger::debug!("Performing gateway routing"); - runDeciderFlow( + run_decider_flow( decider_params, - dreq_.clone().rankingAlgorithm, - dreq_.clone().eliminationEnabled, + dreq_.clone().ranking_algorithm, + dreq_.clone().elimination_enabled, + false, + cpu_start, ) .await } } -fn handleEnforcedGateway(gateway_list: Option>) -> Option> { +fn handle_enforced_gateway(gateway_list: Option>) -> Option> { match gateway_list { None => None, Some(list) if list.is_empty() => None, @@ -142,11 +133,13 @@ fn handleEnforcedGateway(gateway_list: Option>) -> Option, eliminationEnabled: Option, -) -> Result<(T::DecidedGateway), T::ErrorResponse> { + is_legacy_decider_flow: bool, + cpu_start: Instant, +) -> Result { let txnCreationTime = deciderParams .dpTxnDetail .dateCreated @@ -164,9 +157,10 @@ pub async fn runDeciderFlow( deciderParams.dpTxnDetail.clone(), deciderParams.dpTxnCardInfo.clone(), deciderParams.dpMerchantAccount.clone(), + is_legacy_decider_flow, ) .await; - let (functionalGateways) = deciderParams + let functionalGateways = deciderParams .dpEnforceGatewayList .clone() .unwrap_or_default(); @@ -190,10 +184,7 @@ pub async fn runDeciderFlow( let dResult = match ( preferredGateway.clone(), - deciderParams - .dpMerchantPrefs - .dynamicSwitchingEnabled - .clone(), + deciderParams.dpMerchantPrefs.dynamicSwitchingEnabled, ) { (Some(pgw), false) => { if functionalGateways.contains(&pgw) { @@ -202,21 +193,22 @@ pub async fn runDeciderFlow( Some(pgw.clone()), None, Vec::new(), - T::GatewayDeciderApproach::MERCHANT_PREFERENCE, + T::GatewayDeciderApproach::MerchantPreference, None, functionalGateways, None, ) .await; + let cpu_time = cpu_start.elapsed().as_millis() as u64; Ok(T::DecidedGateway { decided_gateway: pgw.clone(), gateway_priority_map: Some(json!(HashMap::from([(pgw.to_string(), 1.0)]))), filter_wise_gateways: None, priority_logic_tag: None, - routing_approach: T::GatewayDeciderApproach::MERCHANT_PREFERENCE, + routing_approach: T::GatewayDeciderApproach::MerchantPreference, gateway_before_evaluation: Some(pgw.clone()), priority_logic_output: None, - reset_approach: T::ResetApproach::NO_RESET, + reset_approach: T::ResetApproach::NoReset, routing_dimension: None, routing_dimension_level: None, is_scheduled_outage: false, @@ -224,6 +216,7 @@ pub async fn runDeciderFlow( gateway_mga_id_map: None, debit_routing_output: None, is_rust_based_decider: true, + latency: Some(cpu_time), }) } else { decider_flow @@ -246,7 +239,7 @@ pub async fn runDeciderFlow( None, None, Vec::new(), - T::GatewayDeciderApproach::NONE, + T::GatewayDeciderApproach::None, None, functionalGateways, None, @@ -256,14 +249,14 @@ pub async fn runDeciderFlow( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), None, - T::GatewayDeciderApproach::NONE, + T::GatewayDeciderApproach::None, None, decider_flow.writer.is_dynamic_mga_enabled, )) } } _ => { - let gwPLogic = if rankingAlgorithm != Some(RankingAlgorithm::SR_BASED_ROUTING) { + let gwPLogic = if rankingAlgorithm != Some(RankingAlgorithm::SrBasedRouting) { match deciderParams.dpPriorityLogicOutput { Some(ref plOp) => plOp.clone(), None => { @@ -282,16 +275,18 @@ pub async fn runDeciderFlow( } else { T::GatewayPriorityLogicOutput { gws: functionalGateways.clone(), - isEnforcement: false, - priorityLogicTag: None, - primaryLogic: None, - gatewayReferenceIds: HashMap::new(), - fallbackLogic: None, + is_enforcement: false, + priority_logic_tag: None, + primary_logic: None, + gateway_reference_ids: HashMap::new(), + fallback_logic: None, } }; - let gatewayPriorityList = - addPreferredGatewaysToPriorityList(gwPLogic.gws.clone(), preferredGateway.clone()); + let gatewayPriorityList = add_preferred_gateways_to_priority_list( + gwPLogic.gws.clone(), + preferredGateway.clone(), + ); logger::info!( tag = "gatewayPriorityList", action = "gatewayPriorityList", @@ -300,14 +295,14 @@ pub async fn runDeciderFlow( gatewayPriorityList ); - let (mut functionalGateways, updatedPriorityLogicOutput) = if gwPLogic.isEnforcement { + let (mut functionalGateways, updatedPriorityLogicOutput) = if gwPLogic.is_enforcement { logger::info!( tag = "gatewayPriorityList", action = "Enforcing Priority Logic", "Enforcing Priority Logic for {:?}", deciderParams.dpTxnDetail.txnId ); - let (res, priorityLogicOutput) = filterFunctionalGatewaysWithEnforcment( + let (res, priorityLogicOutput) = filter_functional_gateways_with_enforcement( &mut decider_flow, &functionalGateways, &gatewayPriorityList, @@ -395,14 +390,14 @@ pub async fn runDeciderFlow( [] => Err(( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), - updatedPriorityLogicOutput.priorityLogicTag.clone(), - T::GatewayDeciderApproach::NONE, + updatedPriorityLogicOutput.priority_logic_tag.clone(), + T::GatewayDeciderApproach::None, Some(updatedPriorityLogicOutput), decider_flow.writer.is_dynamic_mga_enabled, )), - gs => { + _gs => { let maxScore = Utils::get_max_score_gateway(¤tGatewayScoreMap) - .map(|(gw, score)| score); + .map(|(_gw, score)| score); let decidedGateway = Utils::random_gateway_selection_for_same_score( ¤tGatewayScoreMap, maxScore, @@ -428,8 +423,8 @@ pub async fn runDeciderFlow( let ( srEliminationInfo, - isOptimizedBasedOnSRMetricEnabled, - isSrV3MetricEnabled, + _isOptimizedBasedOnSRMetricEnabled, + _isSrV3MetricEnabled, topGatewayBeforeSRDowntimeEvaluation, isPrimaryGateway, experimentTag, @@ -439,6 +434,24 @@ pub async fn runDeciderFlow( ¤tGatewayScoreMap, decider_flow.writer.gwDeciderApproach.clone(), ); + if let Some(rule_name) = updatedPriorityLogicOutput.priority_logic_tag.clone() { + crate::analytics::record_rule_hit_event( + Some(crate::types::merchant::id::merchant_id_to_text( + deciderParams.dpMerchantAccount.merchantId.clone(), + )), + rule_name, + decidedGateway.clone(), + Some(format!("{:?}", finalDeciderApproach.clone())), + serde_json::to_string(&serde_json::json!({ + "functional_gateways": uniqueFunctionalGateways.clone(), + "experiment_tag": experimentTag.clone(), + })) + .ok(), + Some(deciderParams.dpTxnDetail.txnUuid.clone()), + decider_flow.logger.get("x-request-id").cloned(), + Some("rule_applied".to_string()), + ); + } Utils::log_gateway_decider_approach( &mut decider_flow, decidedGateway.clone(), @@ -468,38 +481,46 @@ pub async fn runDeciderFlow( // ¤tGatewayScoreMap // ).await?; - logger::info!( - action = "GATEWAY_PRIORITY_MAP", - tag = "GATEWAY_PRIORITY_MAP", - "{:?}", - gatewayPriorityMap - ); + if let Some(ref priority_map) = gatewayPriorityMap { + logger::debug!( + action = "GATEWAY_PRIORITY_MAP", + tag = "GATEWAY_PRIORITY_MAP", + gateway_priority_map = %priority_map + ); + } match decidedGateway { - Some(decideGatewayOutput) => Ok(T::DecidedGateway { - decided_gateway: decideGatewayOutput, - gateway_priority_map: gatewayPriorityMap, - filter_wise_gateways: None, - priority_logic_tag: updatedPriorityLogicOutput.priorityLogicTag.clone(), - routing_approach: finalDeciderApproach.clone(), - gateway_before_evaluation: topGatewayBeforeSRDowntimeEvaluation.clone(), - priority_logic_output: Some(updatedPriorityLogicOutput), - reset_approach: decider_flow.writer.reset_approach.clone(), - routing_dimension: decider_flow.writer.routing_dimension.clone(), - routing_dimension_level: decider_flow - .writer - .routing_dimension_level - .clone(), - is_scheduled_outage: decider_flow.writer.isScheduledOutage, - is_dynamic_mga_enabled: decider_flow.writer.is_dynamic_mga_enabled, - gateway_mga_id_map: None, - debit_routing_output: None, - is_rust_based_decider: true, - }), + Some(decideGatewayOutput) => { + let cpu_time = cpu_start.elapsed().as_millis() as u64; + Ok(T::DecidedGateway { + decided_gateway: decideGatewayOutput, + gateway_priority_map: gatewayPriorityMap, + filter_wise_gateways: None, + priority_logic_tag: updatedPriorityLogicOutput + .priority_logic_tag + .clone(), + routing_approach: finalDeciderApproach.clone(), + gateway_before_evaluation: topGatewayBeforeSRDowntimeEvaluation + .clone(), + priority_logic_output: Some(updatedPriorityLogicOutput), + reset_approach: decider_flow.writer.reset_approach.clone(), + routing_dimension: decider_flow.writer.routing_dimension.clone(), + routing_dimension_level: decider_flow + .writer + .routing_dimension_level + .clone(), + is_scheduled_outage: decider_flow.writer.isScheduledOutage, + is_dynamic_mga_enabled: decider_flow.writer.is_dynamic_mga_enabled, + gateway_mga_id_map: None, + debit_routing_output: None, + is_rust_based_decider: true, + latency: Some(cpu_time), + }) + } None => Err(( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), - updatedPriorityLogicOutput.priorityLogicTag.clone(), + updatedPriorityLogicOutput.priority_logic_tag.clone(), finalDeciderApproach.clone(), Some(updatedPriorityLogicOutput), decider_flow.writer.is_dynamic_mga_enabled, @@ -510,13 +531,15 @@ pub async fn runDeciderFlow( } }; let key = [ - C::gatewayScoringData, + C::GATEWAY_SCORING_DATA, &deciderParams.dpTxnDetail.txnUuid.clone(), ] .concat(); let updated_gateway_scoring_data = T::GatewayScoringData { routingApproach: Some(decider_flow.writer.gwDeciderApproach.clone().to_string()), eliminationEnabled: eliminationEnabled.unwrap_or_default(), + is_legacy_decider_flow, + udfs: Some(deciderParams.dpOrder.udfs.clone()), ..decider_flow.writer.gateway_scoring_data.clone() }; let app_state = get_tenant_app_state().await; @@ -527,15 +550,17 @@ pub async fn runDeciderFlow( serde_json::to_string(&updated_gateway_scoring_data.clone()) .unwrap_or_default() .as_str(), - C::gatewayScoreKeysTTL, + C::GATEWAY_SCORE_KEYS_TTL, + None, + RedisDataStruct::STRING, ) .await .unwrap_or_default(); - updated_gateway_scoring_data; + drop(updated_gateway_scoring_data); match dResult { - Ok(result) => Ok((result)), + Ok(result) => Ok(result), Err(( - debugFilterList, + _debugFilterList, _, priorityLogicTag, finalDeciderApproach, @@ -560,7 +585,8 @@ pub async fn runDeciderFlow( } } -fn getGatewayToMGAIdMapF(allMgas: &Vec, gateways: &Vec) -> AValue { +#[allow(dead_code)] +fn get_gateway_to_mga_id_map_f(allMgas: &[MerchantGatewayAccount], gateways: &[String]) -> AValue { json!(gateways .iter() .map(|x| { @@ -569,13 +595,13 @@ fn getGatewayToMGAIdMapF(allMgas: &Vec, gateways: &Vec>()) } -fn addPreferredGatewaysToPriorityList( +fn add_preferred_gateways_to_priority_list( gwPriority: Vec, preferredGateway: Option, ) -> Vec { @@ -590,7 +616,7 @@ fn addPreferredGatewaysToPriorityList( } } -async fn filterFunctionalGatewaysWithEnforcment( +async fn filter_functional_gateways_with_enforcement( decider_flow: &mut T::DeciderFlow<'_>, fGws: &[String], priorityGws: &[String], @@ -614,12 +640,12 @@ async fn filterFunctionalGatewaysWithEnforcment( decider_flow.writer.internalMetaData.clone(), decider_flow.get().dpOrderMetadata.metadata.clone(), plOp.clone(), - PriorityLogicFailure::NULL_AFTER_ENFORCE, + PriorityLogicFailure::NullAfterEnforce, ) .await; let fallBackGwPriority = - addPreferredGatewaysToPriorityList(updatedPlOp.gws.clone(), preferredGw); - if updatedPlOp.isEnforcement { + add_preferred_gateways_to_priority_list(updatedPlOp.gws.clone(), preferredGw); + if updatedPlOp.is_enforcement { let updatedEnforcedGateways = fGws .iter() .filter(|&gw| fallBackGwPriority.contains(gw)) @@ -635,7 +661,7 @@ async fn filterFunctionalGatewaysWithEnforcment( decider_flow.writer.internalMetaData.clone(), decider_flow.get().dpOrderMetadata.metadata.clone(), updatedPlOp, - PriorityLogicFailure::NULL_AFTER_ENFORCE, + PriorityLogicFailure::NullAfterEnforce, ) .await; (updatedEnforcedGateways, updatedPlOp) @@ -658,38 +684,6 @@ async fn filterFunctionalGatewaysWithEnforcment( // } // } -fn defaultDecidedGateway( - gw: String, - gpm: Option, - priorityLogicTag: Option, - finalDeciderApproach: T::GatewayDeciderApproach, - topGatewayBeforeSRDowntimeEvaluation: Option, - priorityLogicOutput: Option, - resetApproach: T::ResetApproach, - routingDimension: Option, - routingDimensionLevel: Option, - isScheduledOutage: bool, - isDynamicMGAEnabled: bool, -) -> T::DecidedGateway { - T::DecidedGateway { - decided_gateway: gw, - gateway_priority_map: gpm, - filter_wise_gateways: None, - priority_logic_tag: priorityLogicTag, - routing_approach: finalDeciderApproach, - gateway_before_evaluation: topGatewayBeforeSRDowntimeEvaluation, - priority_logic_output: priorityLogicOutput, - debit_routing_output: None, - reset_approach: resetApproach, - routing_dimension: routingDimension, - routing_dimension_level: routingDimensionLevel, - is_scheduled_outage: isScheduledOutage, - is_dynamic_mga_enabled: isDynamicMGAEnabled, - gateway_mga_id_map: None, - is_rust_based_decider: true, - } -} - // async fn addMetricsToStream( // decidedGateway: Option<&Gateway>, // finalDeciderApproach: T::RoutingApproach, diff --git a/src/decider/gatewaydecider/flows.rs b/src/decider/gatewaydecider/flows.rs index 26660fbe..49123a13 100644 --- a/src/decider/gatewaydecider/flows.rs +++ b/src/decider/gatewaydecider/flows.rs @@ -1,5 +1,6 @@ use super::runner::get_gateway_priority; use super::types::UnifiedError; +use crate::redis::feature::{RedisCompressionConfigCombined, RedisDataStruct}; use axum::response::IntoResponse; use serde_json::json; use serde_json::Value as AValue; @@ -23,7 +24,6 @@ use super::utils::is_mandate_transaction; use super::utils::is_tpv_mandate_transaction; use super::utils::is_tpv_transaction; use crate::decider::storage::utils::txn_card_info::is_google_pay_txn; -use crate::redis::types::ServiceConfigKey; use crate::types::card::card_type::card_type_to_text; use crate::types::card::card_type::CardType; use crate::types::card::txn_card_info::AuthType; @@ -33,7 +33,6 @@ use crate::types::card::vault_provider::VaultProvider; use crate::app::get_tenant_app_state; use crate::decider::gatewaydecider::constants as C; use crate::logger; -use crate::redis::feature::isFeatureEnabled; use crate::types::card::txn_card_info::TxnCardInfo; use crate::types::gateway_card_info::ValidationType; use crate::types::merchant as ETM; @@ -111,7 +110,7 @@ use crate::types::txn_details::types as ETTD; // ); // let request = T::transformRequest(req, merchant_acc, resolve_bin); // logger::debug!( -// tag = "enforeced gateway list", +// tag = "enforced gateway list", // "{}", // request.enforceGatewayList.to_string() // ); @@ -127,8 +126,9 @@ pub trait ResponseDecider { type ErrorResponse: IntoResponse; } -pub async fn deciderFullPayloadHSFunction( +pub async fn decider_full_payload_hs_function( dreq: T::DomainDeciderRequest, + redis_compression_config: Option, ) -> Result<(T::DecidedGateway, Vec<(String, Vec)>), T::ErrorResponse> { let merchant_prefs = match ETM::merchant_iframe_preferences::getMerchantIPrefsByMId( dreq.txnDetail.merchantId.0.clone(), @@ -162,12 +162,17 @@ pub async fn deciderFullPayloadHSFunction( })? } }; - let enforced_gateway_filter = handleEnforcedGateway(dreq.enforceGatewayList); + let enforced_gateway_filter = handle_enforced_gateway(dreq.enforceGatewayList); let resolve_bin = match Utils::fetch_extended_card_bin(&dreq.txnCardInfo.clone()) { Some(card_bin) => Some(card_bin), None => match dreq.txnCardInfo.card_isin { Some(c_isin) => { - let res_bin = Utils::get_card_bin_from_token_bin(6, c_isin.as_str()).await; + let res_bin = Utils::get_card_bin_from_token_bin( + 6, + c_isin.as_str(), + redis_compression_config.clone(), + ) + .await; Some(res_bin) } None => dreq.txnCardInfo.card_isin.clone(), @@ -201,11 +206,12 @@ pub async fn deciderFullPayloadHSFunction( dpPriorityLogicScript: dreq.priorityLogicScript, dpEDCCApplied: dreq.isEdccApplied, dpShouldConsumeResult: dreq.shouldConsumeResult, + dpRedisCompressionConfig: redis_compression_config, }; - runDeciderFlow(decider_params).await + run_decider_flow(decider_params, true).await } -fn handleEnforcedGateway(gateway_list: Option>) -> Option> { +fn handle_enforced_gateway(gateway_list: Option>) -> Option> { match gateway_list { None => None, Some(list) if list.is_empty() => None, @@ -312,8 +318,9 @@ fn handleEnforcedGateway(gateway_list: Option>) -> Option Result<(T::DecidedGateway, Vec<(String, Vec)>), T::ErrorResponse> { let txnCreationTime = deciderParams .dpTxnDetail @@ -332,6 +339,7 @@ pub async fn runDeciderFlow( deciderParams.dpTxnDetail.clone(), deciderParams.dpTxnCardInfo.clone(), deciderParams.dpMerchantAccount.clone(), + is_legacy_decider_flow, ) .await; let (functionalGateways, allMgas) = GF::newGwFilters(&mut decider_flow).await?; @@ -348,9 +356,9 @@ pub async fn runDeciderFlow( .gateway .clone() .or(deciderParams.dpOrder.preferredGateway.clone()); - let gatewayMgaIdMap = getGatewayToMGAIdMapF(&allMgas, &functionalGateways); + let gatewayMgaIdMap = get_gateway_to_mga_id_map_f(&allMgas, &functionalGateways); - logger::warn!( + logger::debug!( action = "PreferredGateway", tag = "PreferredGateway", "Preferred gateway provided by merchant for {:?} = {:?}", @@ -362,10 +370,7 @@ pub async fn runDeciderFlow( let dResult = match ( preferredGateway.clone(), - deciderParams - .dpMerchantPrefs - .dynamicSwitchingEnabled - .clone(), + deciderParams.dpMerchantPrefs.dynamicSwitchingEnabled, ) { (Some(pgw), false) => { if functionalGateways.contains(&pgw) { @@ -374,7 +379,7 @@ pub async fn runDeciderFlow( Some(pgw.clone()), None, Vec::new(), - T::GatewayDeciderApproach::MERCHANT_PREFERENCE, + T::GatewayDeciderApproach::MerchantPreference, None, functionalGateways, None, @@ -385,10 +390,10 @@ pub async fn runDeciderFlow( gateway_priority_map: Some(json!(HashMap::from([(pgw.to_string(), 1.0)]))), filter_wise_gateways: None, priority_logic_tag: None, - routing_approach: T::GatewayDeciderApproach::MERCHANT_PREFERENCE, + routing_approach: T::GatewayDeciderApproach::MerchantPreference, gateway_before_evaluation: Some(pgw.clone()), priority_logic_output: None, - reset_approach: T::ResetApproach::NO_RESET, + reset_approach: T::ResetApproach::NoReset, routing_dimension: None, routing_dimension_level: None, is_scheduled_outage: false, @@ -396,6 +401,7 @@ pub async fn runDeciderFlow( gateway_mga_id_map: Some(gatewayMgaIdMap), debit_routing_output: None, is_rust_based_decider: deciderParams.dpShouldConsumeResult.unwrap_or(false), + latency: None, }) } else { decider_flow @@ -406,7 +412,7 @@ pub async fn runDeciderFlow( gateways: vec![], }); - logger::info!( + logger::debug!( action = "PreferredGateway", tag = "PreferredGateway", "Preferred gateway {:?} functional/valid for merchant {:?} in txn {:?}", @@ -419,7 +425,7 @@ pub async fn runDeciderFlow( None, None, Vec::new(), - T::GatewayDeciderApproach::NONE, + T::GatewayDeciderApproach::None, None, functionalGateways, None, @@ -429,7 +435,7 @@ pub async fn runDeciderFlow( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), None, - T::GatewayDeciderApproach::NONE, + T::GatewayDeciderApproach::None, None, decider_flow.writer.is_dynamic_mga_enabled, )) @@ -452,24 +458,21 @@ pub async fn runDeciderFlow( } }; - let gatewayPriorityList = - addPreferredGatewaysToPriorityList(gwPLogic.gws.clone(), preferredGateway.clone()); + let gatewayPriorityList = add_preferred_gateways_to_priority_list( + gwPLogic.gws.clone(), + preferredGateway.clone(), + ); logger::info!( tag = "gatewayPriorityList", action = "gatewayPriorityList", - "Gateway priority for merchant for {:?} = {:?}", + "Gateway priority for merchant for {:?} = {:?}, gwPLogic : {:?}", &deciderParams.dpTxnDetail.txnId, - gatewayPriorityList + gatewayPriorityList, + gwPLogic ); - let (mut functionalGateways, updatedPriorityLogicOutput) = if gwPLogic.isEnforcement { - logger::info!( - tag = "gatewayPriorityList", - action = "Enforcing Priority Logic", - "Enforcing Priority Logic for {:?}", - deciderParams.dpTxnDetail.txnId - ); - let (res, priorityLogicOutput) = filterFunctionalGatewaysWithEnforcment( + let (mut functionalGateways, updatedPriorityLogicOutput) = if gwPLogic.is_enforcement { + let (res, priorityLogicOutput) = filter_functional_gateways_with_enforcement( &mut decider_flow, &functionalGateways, &gatewayPriorityList, @@ -557,14 +560,14 @@ pub async fn runDeciderFlow( [] => Err(( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), - updatedPriorityLogicOutput.priorityLogicTag.clone(), - T::GatewayDeciderApproach::NONE, + updatedPriorityLogicOutput.priority_logic_tag.clone(), + T::GatewayDeciderApproach::None, Some(updatedPriorityLogicOutput), decider_flow.writer.is_dynamic_mga_enabled, )), - gs => { + _gs => { let maxScore = Utils::get_max_score_gateway(¤tGatewayScoreMap) - .map(|(gw, score)| score); + .map(|(_gw, score)| score); let decidedGateway = Utils::random_gateway_selection_for_same_score( ¤tGatewayScoreMap, maxScore, @@ -591,8 +594,8 @@ pub async fn runDeciderFlow( let ( srEliminationInfo, - isOptimizedBasedOnSRMetricEnabled, - isSrV3MetricEnabled, + _isOptimizedBasedOnSRMetricEnabled, + _isSrV3MetricEnabled, topGatewayBeforeSRDowntimeEvaluation, isPrimaryGateway, experimentTag, @@ -602,6 +605,24 @@ pub async fn runDeciderFlow( ¤tGatewayScoreMap, decider_flow.writer.gwDeciderApproach.clone(), ); + if let Some(rule_name) = updatedPriorityLogicOutput.priority_logic_tag.clone() { + crate::analytics::record_rule_hit_event( + Some(crate::types::merchant::id::merchant_id_to_text( + deciderParams.dpMerchantAccount.merchantId.clone(), + )), + rule_name, + decidedGateway.clone(), + Some(format!("{:?}", finalDeciderApproach.clone())), + serde_json::to_string(&serde_json::json!({ + "functional_gateways": uniqueFunctionalGateways.clone(), + "experiment_tag": experimentTag.clone(), + })) + .ok(), + Some(deciderParams.dpTxnDetail.txnUuid.clone()), + decider_flow.logger.get("x-request-id").cloned(), + Some("rule_applied".to_string()), + ); + } Utils::log_gateway_decider_approach( &mut decider_flow, decidedGateway.clone(), @@ -614,7 +635,7 @@ pub async fn runDeciderFlow( ) .await; - logger::info!( + logger::debug!( action = "Decided Gateway", tag = "Decided Gateway", "Gateway decided for {:?} = {:?}", @@ -631,19 +652,22 @@ pub async fn runDeciderFlow( // ¤tGatewayScoreMap // ).await?; - logger::info!( - action = "GATEWAY_PRIORITY_MAP", - tag = "GATEWAY_PRIORITY_MAP", - "{:?}", - gatewayPriorityMap - ); + if let Some(ref priority_map) = gatewayPriorityMap { + logger::debug!( + action = "GATEWAY_PRIORITY_MAP", + tag = "GATEWAY_PRIORITY_MAP", + gateway_priority_map = %priority_map + ); + } match decidedGateway { Some(decideGatewayOutput) => Ok(T::DecidedGateway { decided_gateway: decideGatewayOutput, gateway_priority_map: gatewayPriorityMap, filter_wise_gateways: None, - priority_logic_tag: updatedPriorityLogicOutput.priorityLogicTag.clone(), + priority_logic_tag: updatedPriorityLogicOutput + .priority_logic_tag + .clone(), routing_approach: finalDeciderApproach.clone(), gateway_before_evaluation: topGatewayBeforeSRDowntimeEvaluation.clone(), priority_logic_output: Some(updatedPriorityLogicOutput), @@ -657,12 +681,15 @@ pub async fn runDeciderFlow( is_scheduled_outage: decider_flow.writer.isScheduledOutage, is_dynamic_mga_enabled: decider_flow.writer.is_dynamic_mga_enabled, gateway_mga_id_map: Some(gatewayMgaIdMap), - is_rust_based_decider: deciderParams.dpShouldConsumeResult.unwrap_or(false), + is_rust_based_decider: deciderParams + .dpShouldConsumeResult + .unwrap_or(false), + latency: None, }), None => Err(( decider_flow.writer.debugFilterList.clone(), decider_flow.writer.debugScoringList.clone(), - updatedPriorityLogicOutput.priorityLogicTag.clone(), + updatedPriorityLogicOutput.priority_logic_tag.clone(), finalDeciderApproach.clone(), Some(updatedPriorityLogicOutput), decider_flow.writer.is_dynamic_mga_enabled, @@ -674,12 +701,14 @@ pub async fn runDeciderFlow( }; let key = [ - C::gatewayScoringData, + C::GATEWAY_SCORING_DATA, &deciderParams.dpTxnDetail.txnUuid.clone(), ] .concat(); let updated_gateway_scoring_data = T::GatewayScoringData { routingApproach: Some(decider_flow.writer.gwDeciderApproach.clone().to_string()), + is_legacy_decider_flow, + udfs: Some(deciderParams.dpOrder.udfs.clone()), ..decider_flow.writer.gateway_scoring_data.clone() }; let app_state = get_tenant_app_state().await; @@ -691,11 +720,13 @@ pub async fn runDeciderFlow( serde_json::to_string(&updated_gateway_scoring_data.clone()) .unwrap_or_default() .as_str(), - C::gatewayScoreKeysTTL, + C::GATEWAY_SCORE_KEYS_TTL, + deciderParams.dpRedisCompressionConfig.clone(), + RedisDataStruct::STRING, ) .await .unwrap_or_default(); - updated_gateway_scoring_data; + drop(updated_gateway_scoring_data); } match dResult { Ok(result) => Ok(( @@ -738,7 +769,10 @@ pub async fn runDeciderFlow( } } -fn getGatewayToMGAIdMapF(allMgas: &Vec, gateways: &Vec) -> AValue { +fn get_gateway_to_mga_id_map_f( + allMgas: &Vec, + gateways: &Vec, +) -> AValue { json!(gateways .iter() .map(|x| { @@ -747,13 +781,13 @@ fn getGatewayToMGAIdMapF(allMgas: &Vec, gateways: &Vec>()) } -fn addPreferredGatewaysToPriorityList( +fn add_preferred_gateways_to_priority_list( gwPriority: Vec, preferredGatewayM: Option, ) -> Vec { @@ -768,7 +802,7 @@ fn addPreferredGatewaysToPriorityList( } } -async fn filterFunctionalGatewaysWithEnforcment( +async fn filter_functional_gateways_with_enforcement( decider_flow: &mut T::DeciderFlow<'_>, fGws: &[String], priorityGws: &[String], @@ -792,12 +826,12 @@ async fn filterFunctionalGatewaysWithEnforcment( decider_flow.writer.internalMetaData.clone(), decider_flow.get().dpOrderMetadata.metadata.clone(), plOp.clone(), - PriorityLogicFailure::NULL_AFTER_ENFORCE, + PriorityLogicFailure::NullAfterEnforce, ) .await; let fallBackGwPriority = - addPreferredGatewaysToPriorityList(updatedPlOp.gws.clone(), preferredGw); - if updatedPlOp.isEnforcement { + add_preferred_gateways_to_priority_list(updatedPlOp.gws.clone(), preferredGw); + if updatedPlOp.is_enforcement { let updatedEnforcedGateways = fGws .iter() .filter(|&gw| fallBackGwPriority.contains(gw)) @@ -813,7 +847,7 @@ async fn filterFunctionalGatewaysWithEnforcment( decider_flow.writer.internalMetaData.clone(), decider_flow.get().dpOrderMetadata.metadata.clone(), updatedPlOp, - PriorityLogicFailure::NULL_AFTER_ENFORCE, + PriorityLogicFailure::NullAfterEnforce, ) .await; (updatedEnforcedGateways, updatedPlOp) @@ -836,39 +870,6 @@ async fn filterFunctionalGatewaysWithEnforcment( // } // } -fn defaultDecidedGateway( - gw: String, - gpm: Option, - priorityLogicTag: Option, - finalDeciderApproach: T::GatewayDeciderApproach, - topGatewayBeforeSRDowntimeEvaluation: Option, - priorityLogicOutput: Option, - resetApproach: T::ResetApproach, - routingDimension: Option, - routingDimensionLevel: Option, - isScheduledOutage: bool, - isDynamicMGAEnabled: bool, - gatewayMgaIdMap: Option, -) -> T::DecidedGateway { - T::DecidedGateway { - decided_gateway: gw, - gateway_priority_map: gpm, - filter_wise_gateways: None, - priority_logic_tag: priorityLogicTag, - routing_approach: finalDeciderApproach, - gateway_before_evaluation: topGatewayBeforeSRDowntimeEvaluation, - priority_logic_output: priorityLogicOutput, - debit_routing_output: None, - reset_approach: resetApproach, - routing_dimension: routingDimension, - routing_dimension_level: routingDimensionLevel, - is_scheduled_outage: isScheduledOutage, - is_dynamic_mga_enabled: isDynamicMGAEnabled, - gateway_mga_id_map: gatewayMgaIdMap, - is_rust_based_decider: true, - } -} - // async fn addMetricsToStream( // decidedGateway: Option<&Gateway>, // finalDeciderApproach: T::RoutingApproach, @@ -974,7 +975,7 @@ pub async fn getFailureReasonWithFilter( ) -> String { let txn_detail = &decider_flow.get().dpTxnDetail; let txn_card_info = &decider_flow.get().dpTxnCardInfo; - let macc = &decider_flow.get().dpMerchantAccount; + let _macc = &decider_flow.get().dpMerchantAccount; let order_reference = &decider_flow.get().dpOrder; let m_internal_meta = &decider_flow.writer.internalMetaData; let m_card_brand = &decider_flow.writer.cardBrand; @@ -1012,7 +1013,7 @@ pub async fn getFailureReasonWithFilter( let reference_ids = Utils::get_all_ref_ids( decider_flow.writer.metadata.clone().unwrap_or_default(), priority_logic_output - .map(|logic| logic.gatewayReferenceIds.clone()) + .map(|logic| logic.gateway_reference_ids.clone()) .unwrap_or_default(), ) .await; @@ -1059,7 +1060,7 @@ pub async fn getFailureReasonWithFilter( .unwrap_or_else(|| "emi".to_string()) ); let emi_bank = format!("{} ", txn_detail.emiBank.clone().unwrap_or_default()); - if Utils::is_card_transaction(txn_card_info) && !txn_detail.isEmi { + if Utils::is_card_transaction(txn_card_info) && txn_detail.isEmi != Some(true) { "Gateways configured supports only emi transaction.".to_string() } else if Utils::is_card_transaction(txn_card_info) { let is_bin_eligible = Utils::check_if_bin_is_eligible_for_emi( @@ -1138,7 +1139,7 @@ pub async fn getFailureReasonWithFilter( }, "filterFunctionalGatewaysForTxnDetailType" => { format!( - "No functional gateways supporting {}transaction.", + "No functional gateways supporting {:?}transaction.", txn_detail.txnType ) } @@ -1170,7 +1171,7 @@ pub async fn getFailureReasonWithFilter( .as_ref() .and_then(|meta| meta.isCvvLessTxn) .unwrap_or(false) - && txn_card_info.authType == Some(AuthType::MOTO) + && txn_card_info.authType == Some(AuthType::Moto) { format!( "No functional gateways supporting cvv less {}repeat moto transaction.", @@ -1227,7 +1228,7 @@ pub async fn getFailureReasonWithFilter( "Conflicting configurations found or no functional gateways supporting this transaction" .to_string() } - "filterGatewaysForEMITenureSpecficGatewayCreds" => { + "filterGatewaysForEMITenureSpecificGatewayCreds" => { "No functional gateways supporting for emi.".to_string() } "FilterFunctionalGatewaysForReversePennyDrop" => { diff --git a/src/decider/gatewaydecider/gw_filter.rs b/src/decider/gatewaydecider/gw_filter.rs index 5095ff78..1587c144 100644 --- a/src/decider/gatewaydecider/gw_filter.rs +++ b/src/decider/gatewaydecider/gw_filter.rs @@ -1,16 +1,13 @@ -use crate::decider::gatewaydecider::constants::CASH_ONLY_GATEWAYS; use crate::decider::gatewaydecider::types::*; use crate::decider::gatewaydecider::utils as Utils; use crate::decider::storage::utils::gateway_card_info as ETGCIS; use crate::merchant_config_util::isPaymentFlowEnabledWithHierarchyCheck; -use crate::redis::feature::{isFeatureEnabled, isFeatureEnabledByDimension}; +use crate::redis::feature::{is_feature_enabled, is_feature_enabled_by_dimension}; use crate::redis::types::ServiceConfigKey; use crate::types::bank_code::find_bank_code; use crate::types::card::card_type as ETCA; -use crate::types::card::txn_card_info::{self as ETTCa, auth_type_to_text}; +use crate::types::card::txn_card_info::auth_type_to_text; use crate::types::card::vault_provider::VaultProvider; -use crate::types::gateway as ETG; -use crate::types::gateway as GT; use crate::types::gateway_bank_emi_support::GatewayBankEmiSupport; use crate::types::gateway_bank_emi_support_v2::GatewayBankEmiSupportV2; use crate::types::gateway_card_info as ETGCI; @@ -18,7 +15,6 @@ use crate::types::gateway_card_info::GatewayCardInfo; use crate::types::merchant as ETM; use crate::types::merchant::merchant_account::*; use crate::types::merchant::merchant_gateway_account as ETMA; -use crate::types::merchant_config::merchant_config as MerchantConfig; use crate::types::merchant_gateway_card_info as ETMGCI; use crate::types::payment_flow::PaymentFlow as PF; use crate::types::tenant::tenant_config::ModuleName as TC; @@ -37,7 +33,7 @@ use crate::types::merchant_gateway_account_sub_info::{self as ETMGASI, SubIdType use crate::types::merchant_gateway_payment_method_flow as MGPMF; use crate::types::order::Order; use crate::types::payment::payment_method::{self as ETP}; -use crate::types::payment_flow::PaymentFlow; +use crate::types::payment_flow::{payment_flows_to_text, text_to_payment_flows, PaymentFlow}; use std::collections::{HashMap, HashSet}; // use crate::types::metadata::Meta; // use crate::types::pl_ref_id_map::PLRefIdMap; @@ -66,7 +62,7 @@ where v_mut } -pub fn getGws(this: &mut DeciderFlow) -> Vec { +pub fn getGws(this: &mut DeciderFlow<'_>) -> Vec { this.writer.functionalGateways.clone() } @@ -80,7 +76,7 @@ fn makeFirstLetterSmall(s: String) -> String { } pub fn returnGwListWithLog( - this: &mut DeciderFlow, + this: &mut DeciderFlow<'_>, fName: DeciderFilterName, doOrNot: bool, ) -> Vec { @@ -117,17 +113,14 @@ pub fn catMaybes(options: &[Option]) -> Vec { options.iter().filter_map(|opt| opt.clone()).collect() } -pub fn intersect(a: &[T], b: &[T]) -> Vec -where - T: Clone, -{ +pub fn intersect(a: &[T], b: &[T]) -> Vec { let set_a: HashSet<_> = a.iter().collect(); let set_b: HashSet<_> = b.iter().collect(); set_a.intersection(&set_b).cloned().cloned().collect() } pub fn setGwsAndMgas( - this: &mut DeciderFlow, + this: &mut DeciderFlow<'_>, filteredMgas: Vec, ) { Utils::set_mgas(this, filteredMgas.clone()); @@ -136,7 +129,7 @@ pub fn setGwsAndMgas( } /// Sets the functional gateways in the DeciderFlow and updates related merchant gateway accounts -pub fn setGws(this: &mut DeciderFlow, gws: Vec) { +pub fn setGws(this: &mut DeciderFlow<'_>, gws: Vec) { // Get the merchant gateway accounts let m_mgas = Utils::get_mgas(this); @@ -191,7 +184,7 @@ pub async fn newGwFilters( if gws.is_empty() { let txnId = this.get().dpTxnDetail.txnId.clone(); let merchantId = this.get().dpTxnDetail.merchantId.clone(); - logger::warn!( + logger::debug!( tag = "GW_Filtering", action = "GW_Filtering", "There are no functional gateways for {:?} for merchant: {:?}", @@ -203,7 +196,7 @@ pub async fn newGwFilters( None, None, vec![], - GatewayDeciderApproach::NONE, + GatewayDeciderApproach::None, None, vec![], None, @@ -229,6 +222,7 @@ pub async fn newGwFilters( let _ = filterFunctionalGatewaysForSplitSettlement(this).await; let _ = filterFunctionalGatewaysForMerchantRequiredFlow(this).await; let _ = filterFunctionalGatewaysForOTMFlow(this).await; + let _ = filterFunctionalGatewaysForPixFlows(this).await; let _ = filterGatewaysForMGASelectionIntegrity(this).await; let funcGateways = returnGwListWithLog(this, DeciderFilterName::FinalFunctionalGateways, false); @@ -250,7 +244,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { let is_edcc_applied = this.get().dpEDCCApplied; let enforce_gateway_list = this.get().dpEnforceGatewayList.clone(); - logger::info!( + logger::debug!( tag = "enableGatewayReferenceIdBasedRouting", action = "enableGatewayReferenceIdBasedRouting", "enableGatewayReferenceIdBasedRouting is enable or not for txn_id : {:?}, enableGatewayReferenceIdBasedRouting: {:?}", @@ -287,7 +281,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { Utils::set_payment_flow_list(this, payment_flow_list); let mgas_ = match ( - txn_detail.isEmi || Utils::is_reccuring_payment_transaction(&txn_detail), + txn_detail.isEmi.unwrap_or(false) || Utils::is_recurring_payment_transaction(&txn_detail), &enforce_gateway_list, ) { (false, _) => enabled_gateway_accounts.clone(), @@ -322,7 +316,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { let edcc_mgas = if txn_detail.currency != oref.currency && is_edcc_applied == Some(true) { let edcc_supported_gateways: Vec = - findByNameFromRedis(C::EDCC_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::EdccSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new); @@ -376,7 +370,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { rpd_filter_mgas } else { let mga_eligible_seamless_gateways = - findByNameFromRedis(C::MGA_ELIGIBLE_SEAMLESS_GATEWAYS.get_key()) + findByNameFromRedis(C::MgaEligibleSeamlessGateways.get_key()) .await .unwrap_or_else(std::vec::Vec::new); rpd_filter_mgas @@ -385,7 +379,10 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { isMgaEligible( mga, &txn_card_info, - txn_detail.txnObjectType.clone(), + txn_detail + .txnObjectType + .clone() + .unwrap_or(TxnObjectType::Unknown), &mga_eligible_seamless_gateways, &txn_detail, ) @@ -403,7 +400,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { } fn validateAndSetDynamicMGAFlag( - this: &mut DeciderFlow, + this: &mut DeciderFlow<'_>, proceed_with_all_mgas: bool, mgas: &Vec, ) { @@ -421,7 +418,7 @@ fn validateAndSetDynamicMGAFlag( } pub fn filterMGAsByEnforcedPaymentFlows( - this: &mut DeciderFlow, + this: &mut DeciderFlow<'_>, initial_mgas: Vec, ) -> Vec { // Extract unique gateways from the merchant gateway accounts @@ -434,7 +431,7 @@ pub fn filterMGAsByEnforcedPaymentFlows( .collect(); // Get context from DeciderFlow - let txn_card_info = this.get().dpTxnCardInfo.clone(); + let _txn_card_info = this.get().dpTxnCardInfo.clone(); let oref = this.get().dpOrder.clone(); let macc = this.get().dpMerchantAccount.clone(); let txn_detail = this.get().dpTxnDetail.clone(); @@ -577,14 +574,17 @@ pub fn isMgaEligible( mgaEligibleSeamlessGateways: &[String], txn_detail: &TxnDetail, ) -> bool { - let payment_flow_list = Utils::get_payment_flow_list_from_txn_detail(&txn_detail); + let payment_flow_list = Utils::get_payment_flow_list_from_txn_detail(txn_detail); let is_otm_flow = payment_flow_list.contains(&"ONE_TIME_MANDATE".to_string()); + let is_pix_automatic_redirect_flow = + payment_flow_list.contains(&"PIX_AUTOMATIC_REDIRECT".to_string()); validateMga( mga, txnCI, mTxnObjType, mgaEligibleSeamlessGateways, is_otm_flow, + is_pix_automatic_redirect_flow, ) } @@ -594,12 +594,13 @@ fn validateMga( mTxnObjType: TxnObjectType, mgaEligibleSeamlessGateways: &[String], is_otm_flow: bool, + is_pix_automatic_redirect_flow: bool, ) -> bool { if mgaEligibleSeamlessGateways.contains(&mga.gateway) && isCardOrNbTxn(txnCI) { Utils::is_seamless(mga) } else if isMandateRegister(mTxnObjType.clone()) { Utils::is_subscription(mga) - } else if (isEmandateRegister(mTxnObjType) && !is_otm_flow) { + } else if isEmandateRegister(mTxnObjType) && !is_otm_flow && !is_pix_automatic_redirect_flow { Utils::is_emandate_enabled(mga) } else { !Utils::is_only_subscription(mga) @@ -628,18 +629,18 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList let mInternalMeta: Option = txnDetail .internalMetadata .as_ref() - .and_then(|meta| serde_json::from_str(meta).ok()); + .and_then(|meta| serde_json::from_str(meta.peek()).ok()); Utils::set_internal_meta_data(this, mInternalMeta.clone()); // CVV Less Gateway Validations if Utils::is_card_transaction(&txnCardInfo) { if let Some(true) = mInternalMeta.as_ref().and_then(|meta| meta.isCvvLessTxn) { - if txnCardInfo.authType == Some(AuthType::MOTO) { + if txnCardInfo.authType == Some(AuthType::Moto) { let st = getGws(this); let authTypeRestrictedGateways = findByNameFromRedis::>>( - C::AUTH_TYPE_RESTRICTED_GATEWAYS.get_key(), + C::AuthTypeRestrictedGateways.get_key(), ) .await .unwrap_or_else(HashMap::new); @@ -653,7 +654,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList .into_iter() .filter(|gw| motoSupportedGateways.contains(gw)) .collect(); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for MOTO cvvLessTxns support for txn_id: {:?}", @@ -666,18 +667,18 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList .as_ref() .map(|provider| provider.peek().to_string()) .unwrap_or_else(|| "DEFAULT".to_string()); - let isMerchantEnabledForCvvLessV2Flow = isFeatureEnabled( - C::cvvLessV2Flow.get_key(), + let isMerchantEnabledForCvvLessV2Flow = is_feature_enabled( + C::CVV_LESS_V2_FLOW.get_key(), mAcc.merchantId.0, "kv_redis".to_string(), ) .await; if isMerchantEnabledForCvvLessV2Flow { let configResp = isPaymentFlowEnabledWithHierarchyCheck( - mAcc.id.clone(), + mAcc.id, mAcc.tenantAccountId, - TC::MERCHANT_CONFIG, - PF::CVVLESS, + TC::MerchantConfig, + PF::Cvvless, crate::types::country::country_iso::text_db_to_country_iso( mAcc.country.as_deref().unwrap_or_default(), ) @@ -705,7 +706,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList GPMF::find_all_gpmf_by_gateway_payment_flow_payment_method( uniqueGwLs.clone(), cardPaymentMethod.id, - PaymentFlow::CVVLESS, + PaymentFlow::Cvvless, ) .await; let gmpfGws: Vec = allGPMfEntries @@ -745,7 +746,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList Vec::new() } }; - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for token repeat cvvLessTxns support for txn_id: {:?}", @@ -781,7 +782,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } else { Vec::new() }; - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for token repeat cvvLessTxns support for txn_id: {:?}", @@ -791,11 +792,11 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } } else { let cardBrandToCvvLessTxnSupportedGateways: HashMap> = - findByNameFromRedis(C::CARD_BRAND_TO_CVVLESS_TXN_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::CardBrandToCvvlessTxnSupportedGateways.get_key()) .await .unwrap_or_default(); let cvvLessTxnSupportedCommonGateways: Vec = - findByNameFromRedis(C::CVVLESS_TXN_SUPPORTED_COMMON_GATEWAYS.get_key()) + findByNameFromRedis(C::CvvlessTxnSupportedCommonGateways.get_key()) .await .unwrap_or_default(); let cvvLessTxnSupportedGateways = cvvLessTxnSupportedCommonGateways @@ -815,7 +816,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList .into_iter() .filter(|gw| cvvLessTxnSupportedGateways.contains(gw)) .collect(); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for cvvLessTxns for txn_id: {:?}", @@ -831,7 +832,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList if Utils::is_card_transaction(&txnCardInfo) && Utils::is_token_repeat_txn(mInternalMeta.clone()) { if let Some(secAuthType) = txnCardInfo.authType.clone() { - if secAuthType == AuthType::OTP { + if secAuthType == AuthType::Otp { let mTokenRepeatOtpSupportedGateways = Utils::get_token_supported_gateways( txnDetail.clone(), txnCardInfo.clone(), @@ -869,12 +870,12 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } // Amex BTA Card based gateway filter - if Utils::is_card_transaction(&txnCardInfo) && txnCardInfo.authType == Some(AuthType::MOTO) { + if Utils::is_card_transaction(&txnCardInfo) && txnCardInfo.authType == Some(AuthType::Moto) { let paymentFlowList = Utils::get_payment_flow_list_from_txn_detail(&txnDetail); let st = getGws(this); if paymentFlowList.contains(&"TA_FILE".to_string()) { let taOfflineEnabledGateways: Vec = - findByNameFromRedis::>(C::TA_OFFLINE_ENABLED_GATEWAYS.get_key()) + findByNameFromRedis::>(C::TaOfflineEnabledGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -888,14 +889,14 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } let st = getGws(this); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways before filtering for MerchantContainer for txn_id: {:?}", txnDetail.txnId ); let merchantContainerSupportedGateways: Vec = - findByNameFromRedis(C::MERCHANT_CONTAINER_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::MerchantContainerSupportedGateways.get_key()) .await .unwrap_or_default(); let filtered_gateways: Vec = if txnCardInfo.paymentMethodType == MERCHANT_CONTAINER { @@ -930,7 +931,7 @@ async fn check_cvv_less_support_rupay(txnCardInfo: &TxnCardInfo) -> bool { bCode, mCardType.unwrap_or_default().to_uppercase() ); - isFeatureEnabledByDimension(feature_key, dimension).await + is_feature_enabled_by_dimension(feature_key, dimension).await } else { false } @@ -965,33 +966,33 @@ pub async fn filterGatewaysForBrand(this: &mut DeciderFlow<'_>) -> Vec { /// - Others: filter out AMEX-not-supported and SODEXO-only gateways /// - Unknown brand: filter out AMEX-not-supported gateways pub async fn filterByCardBrand( - this: &mut DeciderFlow<'_>, + _this: &mut DeciderFlow<'_>, st: &[String], card_brand: Option<&str>, ) -> Vec { let amex_supported_gateways: HashSet = - findByNameFromRedis(C::AMEX_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::AmexSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); let amex_not_supported_gateways: HashSet = - findByNameFromRedis(C::AMEX_NOT_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::AmexNotSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); let sodexo_only_gateways: HashSet = - findByNameFromRedis(C::SODEXO_ONLY_GATEWAYS.get_key()) + findByNameFromRedis(C::SodexoOnlyGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); let sodexo_also_gateways: HashSet = - findByNameFromRedis(C::SODEXO_ALSO_GATEWAYS.get_key()) + findByNameFromRedis(C::SodexoAlsoGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() @@ -1035,14 +1036,14 @@ pub async fn filterGatewaysForAuthType( let txn_detail = this.get().dpTxnDetail.clone(); let txn_card_info = this.get().dpTxnCardInfo.clone(); let macc = this.get().dpMerchantAccount.clone(); - let dynamic_mga_enabled = Utils::get_is_merchant_enabled_for_dynamic_mga_selection(this).await; + let _dynamic_mga_enabled = Utils::get_is_merchant_enabled_for_dynamic_mga_selection(this).await; // Only proceed with filtering if card ISIN is available if let Some(ref card_isin) = txn_card_info.card_isin { // Filter for OTP authentication type if txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::OTP) + .map(|at| *at == AuthType::Otp) .unwrap_or(false) { setGwsAndMgas( @@ -1062,7 +1063,7 @@ pub async fn filterGatewaysForAuthType( if txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::MOTO) + .map(|at| *at == AuthType::Moto) .unwrap_or(false) { setGwsAndMgas( @@ -1078,11 +1079,11 @@ pub async fn filterGatewaysForAuthType( ); } - // Filter for NO_THREE_DS authentication type + // Filter for NoThreeDs authentication type if txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::NO_THREE_DS) + .map(|at| *at == AuthType::NoThreeDs) .unwrap_or(false) { setGwsAndMgas( @@ -1102,7 +1103,7 @@ pub async fn filterGatewaysForAuthType( if txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::VIES) + .map(|at| *at == AuthType::Vies) .unwrap_or(false) { setGwsAndMgas( @@ -1123,7 +1124,7 @@ pub async fn filterGatewaysForAuthType( ) .await; - logger::debug!( + logger::info!( action = "filterFunctionalGatewaysForAuthType", "BIN eligibility check feature flag: {:?}", mb_feature @@ -1135,35 +1136,35 @@ pub async fn filterGatewaysForAuthType( // Get gateway restrictions and capabilities from Redis let atm_pin_card_info_restricted_gateways = - findByNameFromRedis(C::ATM_PIN_CARD_INFO_RESTRICTED_GATEWAYS.get_key()) + findByNameFromRedis(C::AtmPinCardInfoRestrictedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); let otp_card_info_restricted_gateways = - findByNameFromRedis(C::OTP_CARD_INFO_RESTRICTED_GATEWAYS.get_key()) + findByNameFromRedis(C::OtpCardInfoRestrictedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); let otp_card_info_supported_gateways = - findByNameFromRedis(C::OTP_CARD_INFO_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::OtpCardInfoSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); let moto_card_info_supported_gateways = - findByNameFromRedis(C::MOTO_CARD_INFO_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::MotoCardInfoSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); let auth_type_restricted_gateways = - findByNameFromRedis(C::AUTH_TYPE_RESTRICTED_GATEWAYS.get_key()) + findByNameFromRedis(C::AuthTypeRestrictedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() @@ -1207,7 +1208,7 @@ pub async fn filterGatewaysForAuthType( ) .await?; - logger::debug!( + logger::info!( tag = "filterFunctionalGatewaysForAuthType", action = "filterFunctionalGatewaysForAuthType", "Functional gateways after filtering after DISABLE_DECIDER_BIN_ELIGIBILITY_CHECK check: {:?}: {:?}", @@ -1250,7 +1251,7 @@ fn isGatewayCardInfoCheckNeeded( txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::ATMPIN) + .map(|at| *at == AuthType::Atmpin) .unwrap_or(false) && atm_pin_card_info_restricted_gateways.contains(gateway) || @@ -1258,7 +1259,7 @@ fn isGatewayCardInfoCheckNeeded( txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::OTP) + .map(|at| *at == AuthType::Otp) .unwrap_or(false) && otp_card_info_supported_gateways.contains(gateway) || @@ -1266,7 +1267,7 @@ fn isGatewayCardInfoCheckNeeded( txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::MOTO) + .map(|at| *at == AuthType::Moto) .unwrap_or(false) && moto_card_info_supported_gateways.contains(gateway) } @@ -1289,13 +1290,13 @@ fn isAuthTypeSupportedGateway( (txn_card_info .authType .as_ref() - .map(|at| *at == AuthType::VIES) + .map(|at| *at == AuthType::Vies) .unwrap_or(false)) || !(txn_card_info .authType .as_ref() .map(|auth_type| { - *auth_type != AuthType::ATMPIN + *auth_type != AuthType::Atmpin && atm_pin_card_info_restricted_gateways.contains(gateway) }) .unwrap_or(false)) @@ -1303,7 +1304,7 @@ fn isAuthTypeSupportedGateway( .authType .as_ref() .map(|auth_type| { - *auth_type != AuthType::OTP + *auth_type != AuthType::Otp && otp_card_info_restricted_gateways.contains(gateway) }) .unwrap_or(false)) @@ -1345,12 +1346,12 @@ pub async fn filterFunctionalGatewaysForOTMFlow(this: &mut DeciderFlow<'_>) -> V let txn_card_info = this.get().dpTxnCardInfo.clone(); // Get merchant gateway accounts - let m_mgas = Utils::get_mgas(this); + let _m_mgas = Utils::get_mgas(this); // Check if this is a One-Time Mandate flow let payment_flow_list = Utils::get_payment_flow_list_from_txn_detail(&txn_detail); let is_otm_flow = payment_flow_list.contains(&"ONE_TIME_MANDATE".to_string()); - let internal_tracking_info = txn_detail.internalTrackingInfo.clone(); + let _internal_tracking_info = txn_detail.internalTrackingInfo.clone(); if is_otm_flow { // Get order metadata and ref IDs @@ -1393,7 +1394,7 @@ pub async fn filterFunctionalGatewaysForOTMFlow(this: &mut DeciderFlow<'_>) -> V let all_gpmf_entries = GPMF::find_all_gpmf_by_country_code_gw_pf_id_pmt_jbcid_db( crate::types::country::country_iso::CountryISO::IND, gw_list, - PaymentFlow::ONE_TIME_MANDATE, + PaymentFlow::OneTimeMandate, txn_card_info.paymentMethodType, jbc.id, ) @@ -1453,6 +1454,153 @@ pub async fn filterFunctionalGatewaysForOTMFlow(this: &mut DeciderFlow<'_>) -> V ) } +/// Filters gateways for PIX payment flows (e.g., PIX_AUTOMATIC_REDIRECT) +/// Identifies eligible gateways that support PIX flows based on bank codes and payment method compatibility +pub async fn filterFunctionalGatewaysForPixFlows(this: &mut DeciderFlow<'_>) -> Vec { + /// Helper function to get the account details flag to be checked for PIX flows + /// extend this for other pix flows. + fn get_acc_details_flag_to_be_checked(_pf: &PaymentFlow) -> &str { + "enablePixAutomaticRedirect" + } + + // Get current functional gateways + let st = getGws(this); + + // Get transaction data from context + let txn_detail = this.get().dpTxnDetail.clone(); + let macc = this.get().dpMerchantAccount.clone(); + let order_reference = this.get().dpOrder.clone(); + let txn_card_info = this.get().dpTxnCardInfo.clone(); + + // Check if this is a PIX payment flow + let payment_flow_list = Utils::get_payment_flow_list_from_txn_detail(&txn_detail); + let m_pix_flow_text = C::PIX_PAYMENT_FLOWS + .iter() + .find(|&flow| payment_flow_list.contains(&flow.to_string())); + + // Try to parse the PIX flow text into a PaymentFlow enum + let m_pix_payment_flow: Option = + m_pix_flow_text.and_then(|flow_text| text_to_payment_flows(flow_text.to_string()).ok()); + + match m_pix_payment_flow { + Some(pix_payment_flow) => { + // Get order metadata and ref IDs + let (metadata, pl_ref_id_map) = Utils::get_order_metadata_and_pl_ref_id_map( + this, + macc.enableGatewayReferenceIdBasedRouting, + &order_reference, + ); + + // Get all possible merchant reference IDs + let possible_ref_ids_of_merchant = + Utils::get_all_possible_ref_ids(metadata, order_reference.clone(), pl_ref_id_map); + + // Get merchant gateway accounts + let enabled_mgas = SETMA::get_enabled_mgas_by_merchant_id_and_ref_id( + this, + macc.merchantId, + possible_ref_ids_of_merchant, + ) + .await; + + // Get the account details flag to check for this PIX flow + let acc_details_flag_to_be_checked = + get_acc_details_flag_to_be_checked(&pix_payment_flow); + + // Filter MGAs that support PIX flows + let mgas: Vec<_> = enabled_mgas + .into_iter() + .filter(|mga| { + Utils::is_pix_flows_enabled( + mga, + payment_flows_to_text(&pix_payment_flow), + acc_details_flag_to_be_checked.to_string(), + ) + }) + .collect(); + + // Filter MGAs to only include those with gateways in our allowed list + let eligible_mga_post_filtering: Vec<_> = mgas + .into_iter() + .filter(|mga| st.contains(&mga.gateway)) + .collect(); + + // Extract just the gateway list from filtered MGAs + let gw_list: Vec = eligible_mga_post_filtering + .iter() + .map(|x| x.gateway.clone()) + .collect(); + + // If we have a valid bank code, do additional filtering based on payment method flow + if let Some(jbc) = find_bank_code(txn_card_info.paymentMethod.clone()).await { + // Find all gateway payment method flows for PIX with this bank code + // Note: Using Brazil country code (BRA) for PIX flows + let all_gpmf_entries = GPMF::find_all_gpmf_by_country_code_gw_pf_id_pmt_jbcid_db( + crate::types::country::country_iso::CountryISO::BRA, + gw_list, + pix_payment_flow.clone(), + txn_card_info.paymentMethodType.clone(), + jbc.id, + ) + .await + .unwrap_or_default(); + + // Extract MGA IDs and GPMF IDs for further filtering + let mga_ids: Vec = eligible_mga_post_filtering + .iter() + .map(|mga| mga.id.merchantGwAccId) + .collect(); + + let gpmf_ids: Vec = all_gpmf_entries + .iter() + .map(|entry| to_gateway_payment_method_flow_id(entry.id.clone())) + .collect(); + + // Get merchant gateway payment method flows that match both MGA and GPMF + let mgpmf_entries = + MGPMF::get_all_mgpmf_by_mga_id_and_gpmf_ids(mga_ids, gpmf_ids).await; + + // Extract merchant gateway account IDs that have matching payment flows + let mgpmf_mga_id_entries: Vec = mgpmf_entries + .iter() + .map(|entry| entry.merchantGatewayAccountId) + .collect(); + + // Final filtering of MGAs to only those with matching payment flows + let eligible_mga_post_filtering_pix_flows: Vec<_> = eligible_mga_post_filtering + .into_iter() + .filter(|mga| mgpmf_mga_id_entries.contains(&mga.id.merchantGwAccId)) + .collect(); + + // Extract final gateway list from filtered MGAs + let gw_list_post_pix_flows_filtering: Vec = + eligible_mga_post_filtering_pix_flows + .iter() + .map(|x| x.gateway.clone()) + .collect(); + + // Update state with final MGA and gateway lists + Utils::set_mgas(this, eligible_mga_post_filtering_pix_flows); + setGws(this, gw_list_post_pix_flows_filtering); + } else { + // If no bank code found, keep original gateway list + setGws(this, st); + } + } + // If not a PIX flow, keep original gateway list + None => { + setGws(this, st); + } + } + + // Return gateway list with logging + returnGwListWithLog( + this, + DeciderFilterName::FilterFunctionalGatewaysForPixFlows, + true, + ) +} + /// Filters gateways based on transaction validation type (Card Mandate, TPV, E-Mandate) pub async fn filterGatewaysForValidationType( this: &mut DeciderFlow<'_>, @@ -1479,61 +1627,13 @@ pub async fn filterGatewaysForValidationType( // Handle Card Mandate transactions if Utils::is_mandate_transaction(&txn_detail) && Utils::is_card_transaction(&txn_card_info) { // Get excluded gateways from Redis - let uniqueGwLs: Vec = st.clone().into_iter().collect(); - let brand = txn_card_info - .cardSwitchProvider - .as_ref() - .map(|provider| provider.peek().to_string()) - .unwrap_or_else(|| "DEFAULT".to_string()); - let mPmEntryDB = ETP::get_by_name(brand).await; - - let updatedSt = if let Some(cardPaymentMethod) = mPmEntryDB { - let uniqueGwLs: Vec = st.into_iter().collect(); - let allGPMfEntries = GPMF::find_all_gpmf_by_gateway_payment_flow_payment_method( - uniqueGwLs.clone(), - cardPaymentMethod.id, - PaymentFlow::CVVLESS, - ) - .await; - let mgaList = Utils::get_mgas(this).unwrap_or_default(); - let gmpfGws: Vec = allGPMfEntries - .iter() - .map(|gpmf| gpmf.gateway.clone()) - .collect(); - let filteredMga: Vec = mgaList - .into_iter() - .filter(|mga| gmpfGws.contains(&mga.gateway)) - .collect(); - let mgaIds: Vec = filteredMga - .iter() - .map(|mga| mga.id.merchantGwAccId) - .collect(); - let gpmfIds: Vec = - allGPMfEntries.iter().map(|gpmf| gpmf.id.clone()).collect(); - let mgpmfEntries = MGPMF::get_all_mgpmf_by_mga_id_and_gpmf_ids(mgaIds, gpmfIds).await; - let filteredMgaList: Vec = mgpmfEntries - .iter() - .map(|mgpmf| mgpmf.merchantGatewayAccountId) - .collect(); - let finalFilteredMga: Vec = filteredMga - .into_iter() - .filter(|mga| filteredMgaList.contains(&mga.id.merchantGwAccId)) - .collect(); - Utils::set_mgas(this, finalFilteredMga.clone()); - finalFilteredMga - .into_iter() - .map(|mga| mga.gateway) - .collect() - } else { - Vec::new() - }; let card_mandate_bin_filter_excluded_gateways = - findByNameFromRedis(C::CARD_MANDATE_BIN_FILTER_EXCLUDED_GATEWAYS.get_key()) + findByNameFromRedis(C::CardMandateBinFilterExcludedGateways.get_key()) .await .unwrap_or_else(Vec::new); let bin_wise_filter_excluded_gateways = - intersect(&card_mandate_bin_filter_excluded_gateways, &updatedSt); + intersect(&card_mandate_bin_filter_excluded_gateways, &st); let bin_list = Utils::get_bin_list(txn_card_info.card_isin.clone()); // Filter gateways based on card info @@ -1541,8 +1641,8 @@ pub async fn filterGatewaysForValidationType( this, macc.clone(), bin_list, - updatedSt, - txn_card_info.authType.clone(), + st, + None, Some(ETGCI::ValidationType::CardMandate), ) .await?; @@ -1552,7 +1652,7 @@ pub async fn filterGatewaysForValidationType( .map(|g| g.gateway) .collect::>(); - logger::debug!( + logger::info!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering after filterGatewaysCardInfo for txn_id {:?}: {:?}", @@ -1586,7 +1686,7 @@ pub async fn filterGatewaysForValidationType( let final_gws = if gws.is_empty() { new_gws.clone() } else { gws }; - logger::debug!( + logger::info!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for token repeat Mandate support for txn_id {:?}: {:?}", @@ -1597,7 +1697,9 @@ pub async fn filterGatewaysForValidationType( } // Handle non-express checkout, non-token repeat transactions - if !txn_detail.expressCheckout && !Utils::is_token_repeat_txn(m_internal_meta) { + if !txn_detail.expressCheckout.unwrap_or(false) + && !Utils::is_token_repeat_txn(m_internal_meta) + { let m_mandate_guest_checkout_supported_gateways: Option> = findByNameFromRedis( C::getmandateGuestCheckoutKey(txn_card_info.cardSwitchProvider).get_key(), @@ -1615,7 +1717,7 @@ pub async fn filterGatewaysForValidationType( let final_gws = if gws.is_empty() { new_gws.clone() } else { gws }; - logger::debug!( + logger::info!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for Mandate Guest Checkout support for txn_id {:?}: {:?}", @@ -1633,8 +1735,10 @@ pub async fn filterGatewaysForValidationType( // Check if we should skip processing for one-time mandate flow let payment_flow_list = Utils::get_payment_flow_list_from_txn_detail(&txn_detail); let is_otm_flow = payment_flow_list.contains(&"ONE_TIME_MANDATE".to_string()); + let is_pix_automatic_redirect_flow = + payment_flow_list.contains(&"PIX_AUTOMATIC_REDIRECT".to_string()); - if is_otm_flow { + if is_otm_flow || is_pix_automatic_redirect_flow { logger::info!( "Skipping processing for OTM flow for txn_id {:?}", txn_detail.txnId.clone() @@ -1731,7 +1835,7 @@ pub async fn filterGatewaysForValidationType( .map(|g| g.gateway) .collect::>(); - logger::debug!( + logger::info!( "nst for filterGatewaysForValidationType for txn_id {:?}: {:?}", txn_detail.txnId.clone(), nst @@ -1759,7 +1863,7 @@ pub async fn filterGatewaysForValidationType( // Handle other transaction types else { let tpv_only_supported_gateways = - findByNameFromRedis(C::TPV_ONLY_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::TpvOnlySupportedGateways.get_key()) .await .unwrap_or_else(Vec::new); @@ -1836,7 +1940,7 @@ where /// Determines if a merchant gateway account matches the provided gateway reference ID /// Used for gateway reference ID based routing pub fn predicate( - this: &mut DeciderFlow, + _this: &mut DeciderFlow<'_>, mga: ETM::merchant_gateway_account::MerchantGatewayAccount, gw: String, metadata: HashMap, @@ -1880,7 +1984,7 @@ pub async fn filterGatewaysCardInfo( .collect(); let merchant_wise_eligible_gateway_card_info = if !enabled_gateways.is_empty() { - let supported_gci = ETGCIS::getSupportedGatewayCardInfoForBins( + let supported_gci = ETGCIS::get_supported_gateway_card_info_for_bins( &appState, merchant_account, card_bins.clone(), @@ -1898,7 +2002,7 @@ pub async fn filterGatewaysCardInfo( .clone() .unwrap_or_else(|| "THREE_DS".to_string()) == auth_type_to_text( - &m_auth_type.clone().unwrap_or(AuthType::THREE_DS), + &m_auth_type.clone().unwrap_or(AuthType::ThreeDs), ) }) .collect::>() @@ -1917,14 +2021,14 @@ pub async fn filterGatewaysCardInfo( .await .into_iter() .filter(|ci| { - (ci.validationType == Some(ETGCI::ValidationType::CardMandate) + ci.validationType == Some(ETGCI::ValidationType::CardMandate) && ci .authType .clone() .unwrap_or_else(|| "THREE_DS".to_string()) == auth_type_to_text( - &m_auth_type.clone().unwrap_or(AuthType::THREE_DS), - )) + &m_auth_type.clone().unwrap_or(AuthType::ThreeDs), + ) }) .collect::>(); @@ -2019,7 +2123,7 @@ pub async fn filterGatewaysCardInfo( .collect::>(), }; - logger::info!( + logger::debug!( tag = "filterGatewaysCardInfo", action = "filterGatewaysCardInfo", "merchant_validation_required_gws - {:?}, gci_validation_gws - {:?}, gcis - {:?}, gcis_without_merchant_validation - {:?}, gcis_with_merchant_validation - {:?}, gci_ids - {:?}, mgcis_enabled_gcis - {:?},mgcis_enabled_gci_ids - {:?}, gcis_after_merchant_validation - {:?}, eligible_gateway_card_infos - {:?}", @@ -2097,7 +2201,7 @@ pub async fn filterGatewaysForTxnOfferDetails(this: &mut DeciderFlow<'_>) -> Vec /// Filters gateway list based on gateway routing rules defined in transaction offer details /// If force_routing is enabled, only keeps gateways that appear in both the input list and the offer routing rules pub async fn filterByGatewayRule( - this: &mut DeciderFlow<'_>, + _this: &mut DeciderFlow<'_>, txn_detail: &TxnDetail, gw_list_acc: Vec, txn_offer_detail: &ETOD::TxnOfferDetail, @@ -2169,10 +2273,10 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { txn_detail.isEmi ); - if txn_detail.isEmi { + if txn_detail.isEmi.unwrap_or(false) { let is_mandate_txn = Utils::is_mandate_transaction(&txn_detail); let si_on_emi_card_supported_gateways: HashSet = - findByNameFromRedis::>(C::SI_ON_EMI_CARD_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis::>(C::SiOnEmiCardSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2191,7 +2295,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let card_brand = Utils::get_card_brand(this).await; let si_on_emi_disabled_card_brand_gateway_mapping: HashMap> = findByNameFromRedis::>>( - C::SI_ON_EMI_DISABLED_CARD_BRAND_GATEWAY_MAPPING.get_key(), + C::SiOnEmiDisabledCardBrandGatewayMapping.get_key(), ) .await .unwrap_or_default(); @@ -2219,7 +2323,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let gws = if Utils::check_no_or_low_cost_emi(&txn_card_info) { let no_or_low_cost_emi_supported_gateways: HashSet = findByNameFromRedis::>( - C::NO_OR_LOW_COST_EMI_SUPPORTED_GATEWAYS.get_key(), + C::NoOrLowCostEmiSupportedGateways.get_key(), ) .await .unwrap_or_default() @@ -2285,14 +2389,14 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { gws.clone() ); - let gbes_v2_flag = isFeatureEnabled( - C::gbesV2Enabled.get_key(), + let gbes_v2_flag = is_feature_enabled( + C::GBES_V2_ENABLED.get_key(), merchant_acc.merchantId.0, "kv_redis".to_string(), ) .await; if gbes_v2_flag { - let gbes_v2_list_ = SGBES::getGatewayBankEmiSupportV2( + let gbes_v2_list_ = SGBES::get_gateway_bank_emi_support_v2( txn_detail.emiBank.clone(), gws.clone(), scope_.to_string(), @@ -2305,8 +2409,8 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let mut gbesV2List = Vec::new(); for gbes in gbes_v2_list_.clone() { let is_enabled = if let Some(emi_bank) = emi_bank.clone() { - isFeatureEnabledByDimension( - C::altIdEnabledGatewayEmiBank.get_key(), + is_feature_enabled_by_dimension( + C::ALT_ID_ENABLED_GATEWAY_EMI_BANK.get_key(), format!("{}::{}", gbes.gateway, emi_bank), ) .await @@ -2323,7 +2427,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { }; if gbes_v2_list.is_empty() { - logger::info!( + logger::debug!( tag = "GBESV2 Entry Not Found", action = "GBESV2 Entry Not Found", "GBESV2 Entry Not Found For emiBank - {:?}, gateways - {:?}, scope_ - {:?}, tenure - {:?}", @@ -2352,7 +2456,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { extractGatewaysV2(gbes_v2_filtered) } else { - let gbes_list_ = SGBES::getGatewayBankEmiSupport( + let gbes_list_ = SGBES::get_gateway_bank_emi_support( txn_detail.emiBank.clone(), gws.clone(), scope_.to_string(), @@ -2364,8 +2468,8 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let mut gbesList = Vec::new(); for gbes in gbes_list_.clone() { let is_enabled = if let Some(emi_bank) = emi_bank.clone() { - isFeatureEnabledByDimension( - C::altIdEnabledGatewayEmiBank.get_key(), + is_feature_enabled_by_dimension( + C::ALT_ID_ENABLED_GATEWAY_EMI_BANK.get_key(), format!("{}::{}", gbes.gateway, emi_bank), ) .await @@ -2389,7 +2493,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { setGws(this, gws); } else if Utils::is_card_transaction(&txn_card_info) { let card_emi_explicit_gateways: HashSet = - findByNameFromRedis::>(C::CARD_EMI_EXPLICIT_GATEWAYS.get_key()) + findByNameFromRedis::>(C::CardEmiExplicitGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2502,7 +2606,7 @@ pub async fn filterGatewaysForPaymentMethod(this: &mut DeciderFlow<'_>) -> Vec = - findByNameFromRedis::>(C::V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis::>(C::V2IntegrationNotSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2511,7 +2615,7 @@ pub async fn filterGatewaysForPaymentMethod(this: &mut DeciderFlow<'_>) -> Vec = v2_integration_not_supported_gateways.iter().cloned().collect(); let upi_intent_not_supported_gateways: Vec = - findByNameFromRedis::>(C::UPI_INTENT_NOT_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis::>(C::UpiIntentNotSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2563,13 +2667,13 @@ fn is_disjoint(gateways1: &Vec, gateways2: &Vec) -> bool { } async fn getGatewaysAcceptingPaymentMethod( - oref: &Order, - merchant_acc: &MerchantAccount, + _oref: &Order, + _merchant_acc: &MerchantAccount, eligible_mgas: &[MerchantGatewayAccount], gateways: &GatewayList, payment_method: &str, - proceed_with_all_mgas: bool, - is_dynamic_mga_enabled: bool, + _proceed_with_all_mgas: bool, + _is_dynamic_mga_enabled: bool, ) -> (GatewayList, Vec) { let filtered_mgas: Vec<_> = eligible_mgas .iter() @@ -2642,7 +2746,7 @@ pub async fn filterGatewaysForTokenProvider(this: &mut DeciderFlow<'_>) -> Gatew Some(v) => { let token_provider_gateway_mapping = findByNameFromRedis::>( - C::TOKEN_PROVIDER_GATEWAY_MAPPING.get_key(), + C::TokenProviderGatewayMapping.get_key(), ) .await .unwrap_or_default(); @@ -2668,20 +2772,20 @@ pub async fn filterGatewaysForWallet(this: &mut DeciderFlow<'_>) -> Vec let st = getGws(this); let txn_card_info = this.get().dpTxnCardInfo.clone(); let upi_only_gateways: HashSet = - findByNameFromRedis::>(C::UPI_ONLY_GATEWAYS.get_key()) + findByNameFromRedis::>(C::UpiOnlyGateways.get_key()) .await .unwrap_or_default() .into_iter() .collect(); let wallet_only_gateways: HashSet = - findByNameFromRedis::>(C::WALLET_ONLY_GATEWAYS.get_key()) + findByNameFromRedis::>(C::WalletOnlyGateways.get_key()) .await .unwrap_or_default() .into_iter() .collect(); let wallet_also_gateways: HashSet = - findByNameFromRedis::>(C::WALLET_ALSO_GATEWAYS.get_key()) + findByNameFromRedis::>(C::WalletAlsoGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2716,7 +2820,7 @@ pub async fn filterGatewaysForNbOnly(this: &mut DeciderFlow<'_>) -> Vec let txn_card_info = this.get().dpTxnCardInfo.clone(); if txn_card_info.card_type != Some(ETCA::CardType::Nb) { let nb_only_gateways: Vec = - findByNameFromRedis::>(C::NB_ONLY_GATEWAYS.get_key()) + findByNameFromRedis::>(C::NbOnlyGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2750,19 +2854,19 @@ pub async fn filterFunctionalGatewaysForMerchantRequiredFlow( let mf_filtered_gw = filter_gateways_for_flow( is_mf_order, - C::MUTUAL_FUND_FLOW_SUPPORTED_GATEWAYS.get_key(), + C::MutualFundFlowSupportedGateways.get_key(), st, ) .await; let mf_and_cb_filtered_gw = filter_gateways_for_flow( is_cb_order, - C::CROSS_BORDER_FLOW_SUPPORTED_GATEWAYS.get_key(), + C::CrossBorderFlowSupportedGateways.get_key(), mf_filtered_gw, ) .await; let filtered_gw = filter_gateways_for_flow( is_sbmd, - C::SBMD_SUPPORTED_GATEWAYS.get_key(), + C::SbmdSupportedGateways.get_key(), mf_and_cb_filtered_gw, ) .await; @@ -2852,12 +2956,12 @@ pub fn validate_only_one_mga( /// Filters gateways for EMI tenure-specific merchant gateway accounts /// Keeps only gateways that support the specific EMI tenure requested in the transaction -pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow) -> Vec { +pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow<'_>) -> Vec { // Get transaction details from context let txn_detail = this.get().dpTxnDetail.clone(); // Only filter if transaction is EMI - if txn_detail.isEmi { + if txn_detail.isEmi.unwrap_or(false) { // Get current functional gateways let st = getGws(this); @@ -2871,7 +2975,7 @@ pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow) -> Vec { // First check if gateway is in our functional list if st.contains(&gw_account.gateway) { // Check if gateway needs tenure-specific credentials - if C::gatewaysWithTenureBasedCreds + if C::GATEWAYS_WITH_TENURE_BASED_CREDS .map(|str| str.to_string()) .contains(&gw_account.gateway.to_string()) { @@ -2880,7 +2984,7 @@ pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow) -> Vec { match serde_json::from_str::(acc_details) { Ok(emi_details) => { // Check if EMI details match transaction EMI requirements - get_emi(emi_details.isEmi) == txn_detail.isEmi + get_emi(emi_details.isEmi) == txn_detail.isEmi.unwrap_or(false) && get_tenure(emi_details.emiTenure) == txn_detail.emiTenure.unwrap_or(0) } @@ -2902,7 +3006,7 @@ pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow) -> Vec { // Return the gateway list with logging returnGwListWithLog( this, - DeciderFilterName::FilterGatewaysForEMITenureSpecficGatewayCreds, + DeciderFilterName::FilterGatewaysForEMITenureSpecificGatewayCreds, true, ) } @@ -2928,7 +3032,7 @@ pub async fn filterGatewaysForConsumerFinance(this: &mut DeciderFlow<'_>) -> Vec let st = getGws(this); let txn_card_info = this.get().dpTxnCardInfo.clone(); let consumer_finance_only_gateways: Vec = - findByNameFromRedis::>(C::CONSUMER_FINANCE_ONLY_GATEWAYS.get_key()) + findByNameFromRedis::>(C::ConsumerFinanceOnlyGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2940,7 +3044,7 @@ pub async fn filterGatewaysForConsumerFinance(this: &mut DeciderFlow<'_>) -> Vec if txn_card_info.paymentMethodType == CONSUMER_FINANCE { let consumer_finance_also_gateways: Vec = - findByNameFromRedis::>(C::CONSUMER_FINANCE_ALSO_GATEWAYS.get_key()) + findByNameFromRedis::>(C::ConsumerFinanceAlsoGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2977,9 +3081,9 @@ pub async fn filterGatewaysForConsumerFinance(this: &mut DeciderFlow<'_>) -> Vec pub async fn filterGatewaysForUpi(this: &mut DeciderFlow<'_>) -> Vec { let st = getGws(this); let txn_card_info = this.get().dpTxnCardInfo.clone(); - let txn_detail = this.get().dpTxnDetail.clone(); + let _txn_detail = this.get().dpTxnDetail.clone(); let upi_only_gateways: Vec = - findByNameFromRedis::>(C::UPI_ONLY_GATEWAYS.get_key()) + findByNameFromRedis::>(C::UpiOnlyGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2990,7 +3094,7 @@ pub async fn filterGatewaysForUpi(this: &mut DeciderFlow<'_>) -> Vec { if txn_card_info.paymentMethodType == UPI { let upi_also_gateway: Vec = - findByNameFromRedis::>(C::UPI_ALSO_GATEWAYS.get_key()) + findByNameFromRedis::>(C::UpiAlsoGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -3061,11 +3165,11 @@ pub async fn filterGatewaysForTxnType(this: &mut DeciderFlow<'_>) -> Vec }; let v2_integration_not_supported_gateways: Vec = - findByNameFromRedis(C::V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::V2IntegrationNotSupportedGateways.get_key()) .await .unwrap_or_default(); let upi_intent_not_supported_gateways: Vec = - findByNameFromRedis(C::UPI_INTENT_NOT_SUPPORTED_GATEWAYS.get_key()) + findByNameFromRedis(C::UpiIntentNotSupportedGateways.get_key()) .await .unwrap_or_default(); let (_, filtered_mgas) = if ["UPI_PAY", "UPI_QR"].contains(&txn_type.as_str()) @@ -3087,7 +3191,7 @@ pub async fn filterGatewaysForTxnType(this: &mut DeciderFlow<'_>) -> Vec }; let txn_type_gateway_mapping = findByNameFromRedis::>>( - C::TXN_TYPE_GATEWAY_MAPPING.get_key(), + C::TxnTypeGatewayMapping.get_key(), ) .await .unwrap_or_default(); @@ -3123,10 +3227,10 @@ fn getTxnTypeSupportedGateways( } /// Filters gateways and merchant gateway accounts based on UPI payment flow support -/// Checks both V2 integration and UPI intent capabilities +/// Checks both V2 integration and UPI Intent capabilities pub fn filterGatewaysForUpiPayBasedOnSupportedFlow( - this: &mut DeciderFlow, - gws: Vec, + _this: &mut DeciderFlow<'_>, + _gws: Vec, mgas: Vec, v2_integration_not_supported_gateways: Vec, upi_intent_not_supported_gateways: Vec, @@ -3181,9 +3285,9 @@ pub fn filterGatewaysForUpiPayBasedOnSupportedFlow( pub async fn filterGatewaysForTxnDetailType(this: &mut DeciderFlow<'_>) -> Vec { let st = getGws(this); let m_txn_type = this.get().dpTxnDetail.txnType.clone(); - let txn_type: &str = m_txn_type.as_str(); + let txn_type: &str = m_txn_type.as_deref().unwrap_or(""); let txn_detail_type_restricted_gateways = - findByNameFromRedis(C::TXN_DETAIL_TYPE_RESTRICTED_GATEWAYS.get_key()) + findByNameFromRedis(C::TxnDetailTypeRestrictedGateways.get_key()) .await .unwrap_or_default(); let filter_gws = if txn_type == "ZERO_AUTH" { @@ -3220,13 +3324,13 @@ pub async fn filterGatewaysForReward(this: &mut DeciderFlow<'_>) -> Vec let payment_method_type = this.get().dpTxnCardInfo.paymentMethodType.clone(); let card_type = this.get().dpTxnCardInfo.card_type.clone(); let reward_also_gateways: HashSet = - findByNameFromRedis(C::REWARD_ALSO_GATEWAYS.get_key()) + findByNameFromRedis(C::RewardAlsoGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); let reward_only_gateways: HashSet = - findByNameFromRedis(C::REWARD_ONLY_GATEWAYS.get_key()) + findByNameFromRedis(C::RewardOnlyGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() @@ -3253,7 +3357,7 @@ pub async fn filterGatewaysForCash(this: &mut DeciderFlow<'_>) -> Vec { let st = getGws(this); let payment_method_type = this.get().dpTxnCardInfo.paymentMethodType.clone(); if payment_method_type != CASH { - let cash_only_gateways: Vec = findByNameFromRedis(C::CASH_ONLY_GATEWAYS.get_key()) + let cash_only_gateways: Vec = findByNameFromRedis(C::CashOnlyGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() @@ -3354,7 +3458,7 @@ pub async fn filterFunctionalGatewaysForSplitSettlement(this: &mut DeciderFlow<' .filter(|mgasi| { mgasi.merchantGatewayAccountId == mga.id && mgasi.subIdType == SubIdType::VENDOR - && mgasi.subInfoType == SubInfoType::SPLIT_SETTLEMENT + && mgasi.subInfoType == SubInfoType::SplitSettlement && !mgasi.disabled }) .map(|mgasi| mgasi.juspaySubAccountId.clone()) @@ -3392,7 +3496,7 @@ pub async fn filterFunctionalGatewaysForSplitSettlement(this: &mut DeciderFlow<' ); let st = getGws(this); let split_settlement_supported_gateways: Option> = - findByNameFromRedis(C::SPLIT_SETTLEMENT_SUPPORTED_GATEWAYS.get_key()).await; + findByNameFromRedis(C::SplitSettlementSupportedGateways.get_key()).await; if !intersect( &split_settlement_supported_gateways.unwrap_or_default(), &st, diff --git a/src/decider/gatewaydecider/gw_filter_new.rs b/src/decider/gatewaydecider/gw_filter_new.rs index 2737dd84..292539c9 100644 --- a/src/decider/gatewaydecider/gw_filter_new.rs +++ b/src/decider/gatewaydecider/gw_filter_new.rs @@ -65,14 +65,14 @@ pub async fn newGwFilters(this: &mut DeciderFlow<'_>) -> (GatewayList, Vec) -> GatewayList { let is_edcc_applied = this.get().dpEDCCApplied.clone(); let enforce_gateway_list = this.get().dpEnforceGatewayList.clone(); - logger::info!( + logger::debug!( tag = "enableGatewayReferenceIdBasedRouting", action = "enableGatewayReferenceIdBasedRouting", "enableGatewayReferenceIdBasedRouting is enable or not for txn_id : {:?}, enableGatewayReferenceIdBasedRouting: {:?}", @@ -151,7 +151,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { .collect(), }; - let edcc_supported_gateways = findByNameFromRedis(C::EDCC_SUPPORTED_GATEWAYS.get_key()).await.unwrap_or_else(|| vec![]); + let edcc_supported_gateways = findByNameFromRedis(C::EdccSupportedGateways.get_key()).await.unwrap_or_else(|| vec![]); if txn_detail.currency != oref.currency && is_edcc_applied == Some(true) { mgas.into_iter() @@ -208,7 +208,7 @@ pub async fn getFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList { let filtered_mgas = if proceed_with_all_mgas { rpd_filter_mgas } else { - let mga_eligible_seamless_gateways = findByNameFromRedis(C::MGA_ELIGIBLE_SEAMLESS_GATEWAYS.get_key()) + let mga_eligible_seamless_gateways = findByNameFromRedis(C::MgaEligibleSeamlessGateways.get_key()) .await.unwrap_or_else(|| vec![]); rpd_filter_mgas .into_iter() @@ -247,9 +247,9 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList // CVV Less Gateway Validations if Utils::is_card_transaction(&txnCardInfo) { if let Some(true) = mInternalMeta.as_ref().and_then(|meta| meta.isCvvLessTxn) { - if txnCardInfo.authType == Some(AuthType::MOTO) { + if txnCardInfo.authType == Some(AuthType::Moto) { let st = getGws(this); - let authTypeRestrictedGateways = findByNameFromRedis::>>(C::AUTH_TYPE_RESTRICTED_GATEWAYS.get_key()).await.unwrap_or_else(|| HashMap::new()); + let authTypeRestrictedGateways = findByNameFromRedis::>>(C::AuthTypeRestrictedGateways.get_key()).await.unwrap_or_else(|| HashMap::new()); let motoSupportedGateways: Vec = txnCardInfo.authType.as_ref() .and_then(|auth_type| authTypeRestrictedGateways.get(auth_type)) .cloned() @@ -257,7 +257,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList let filtered_gateways: Vec = st.into_iter() .filter(|gw| motoSupportedGateways.contains(gw)) .collect(); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for MOTO cvvLessTxns support for txn_id: {:?}", @@ -274,7 +274,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList // let configResp = MerchantConfig::is_payment_flow_enabled_with_hierarchy_check( // mAcc.id, // mAcc.tenantAccountId, - // TC::MERCHANT_CONFIG, + // TC::MerchantConfig, // PF::CVVLESS, // None, // ).await?; @@ -326,7 +326,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList Vec::new() } }; - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for token repeat cvvLessTxns support for txn_id: {:?}", @@ -352,7 +352,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } else { Vec::new() }; - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for token repeat cvvLessTxns support for txn_id: {:?}", @@ -361,8 +361,8 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList setGws(this, functionalGateways) } } else { - let cardBrandToCvvLessTxnSupportedGateways: HashMap> = findByNameFromRedis(C::CARD_BRAND_TO_CVVLESS_TXN_SUPPORTED_GATEWAYS.get_key()).await.unwrap_or_default(); - let cvvLessTxnSupportedCommonGateways: Vec = findByNameFromRedis(C::CVVLESS_TXN_SUPPORTED_COMMON_GATEWAYS.get_key()).await.unwrap_or_default(); + let cardBrandToCvvLessTxnSupportedGateways: HashMap> = findByNameFromRedis(C::CardBrandToCvvlessTxnSupportedGateways.get_key()).await.unwrap_or_default(); + let cvvLessTxnSupportedCommonGateways: Vec = findByNameFromRedis(C::CvvlessTxnSupportedCommonGateways.get_key()).await.unwrap_or_default(); let cvvLessTxnSupportedGateways = cvvLessTxnSupportedCommonGateways.into_iter() .chain(cardBrandToCvvLessTxnSupportedGateways.get(&txnCardInfo.paymentMethod).cloned().unwrap_or_default()) .collect::>() @@ -373,7 +373,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList let filtered_gateways: Vec = st.into_iter() .filter(|gw| cvvLessTxnSupportedGateways.contains(gw)) .collect(); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways after filtering for cvvLessTxns for txn_id: {:?}", @@ -389,7 +389,7 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList // Card token based repeat transaction gateway filter if Utils::is_card_transaction(&txnCardInfo) && Utils::is_token_repeat_txn(mInternalMeta.clone()) { if let Some(secAuthType) = txnCardInfo.authType.clone() { - if secAuthType == AuthType::OTP { + if secAuthType == AuthType::Otp { let mTokenRepeatOtpSupportedGateways = Utils::get_token_supported_gateways(txnDetail.clone(), txnCardInfo.clone(), "OTP".to_string(), mInternalMeta.clone()).await; let st = getGws(this); let tokenRepeatOtpSupportedGateways = mTokenRepeatOtpSupportedGateways.unwrap_or_default(); @@ -413,11 +413,11 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } // Amex BTA Card based gateway filter - if Utils::is_card_transaction(&txnCardInfo) && txnCardInfo.authType == Some(AuthType::MOTO) { + if Utils::is_card_transaction(&txnCardInfo) && txnCardInfo.authType == Some(AuthType::Moto) { let paymentFlowList = Utils::get_payment_flow_list_from_txn_detail(&txnDetail); let st = getGws(this); if paymentFlowList.contains(&"TA_FILE".to_string()) { - let taOfflineEnabledGateways: Vec = findByNameFromRedis::>(C::TA_OFFLINE_ENABLED_GATEWAYS.get_key()).await.unwrap_or_default().into_iter().collect(); + let taOfflineEnabledGateways: Vec = findByNameFromRedis::>(C::TaOfflineEnabledGateways.get_key()).await.unwrap_or_default().into_iter().collect(); let filtered_gateways: Vec = st.into_iter() .filter(|gw| taOfflineEnabledGateways.contains(gw)) .collect(); @@ -426,13 +426,13 @@ pub async fn filterFunctionalGateways(this: &mut DeciderFlow<'_>) -> GatewayList } let st = getGws(this); - logger::info!( + logger::debug!( tag = "filterFunctionalGateways", action = "filterFunctionalGateways", "Functional gateways before filtering for MerchantContainer for txn_id: {:?}", txnDetail.txnId ); - let merchantContainerSupportedGateways: Vec = findByNameFromRedis(C::MERCHANT_CONTAINER_SUPPORTED_GATEWAYS.get_key()).await.unwrap_or_default(); + let merchantContainerSupportedGateways: Vec = findByNameFromRedis(C::MerchantContainerSupportedGateways.get_key()).await.unwrap_or_default(); let filtered_gateways: Vec = if txnCardInfo.paymentMethodType == PaymentMethodType::MerchantContainer { st.into_iter() .filter(|gw| merchantContainerSupportedGateways.contains(gw)) @@ -482,7 +482,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { if txn_detail.isEmi { let is_mandate_txn = Utils::is_mandate_transaction(&txn_detail); - let si_on_emi_card_supported_gateways: HashSet = findByNameFromRedis::>(C::SI_ON_EMI_CARD_SUPPORTED_GATEWAYS.get_key()) + let si_on_emi_card_supported_gateways: HashSet = findByNameFromRedis::>(C::SiOnEmiCardSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -500,7 +500,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let st_filtered = if is_mandate_txn { let card_brand = Utils::get_card_brand().await; let si_on_emi_disabled_card_brand_gateway_mapping: HashMap> = - findByNameFromRedis::>>(C::SI_ON_EMI_DISABLED_CARD_BRAND_GATEWAY_MAPPING.get_key()) + findByNameFromRedis::>>(C::SiOnEmiDisabledCardBrandGatewayMapping.get_key()) .await .unwrap_or_default(); @@ -525,7 +525,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { }; let gws = if Utils::check_no_or_low_cost_emi(&txn_card_info) { - let no_or_low_cost_emi_supported_gateways: HashSet = findByNameFromRedis::>(C::NO_OR_LOW_COST_EMI_SUPPORTED_GATEWAYS.get_key()) + let no_or_low_cost_emi_supported_gateways: HashSet = findByNameFromRedis::>(C::NoOrLowCostEmiSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -576,14 +576,14 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { let gbes_v2_flag = isFeatureEnabled(C::gbesV2Enabled.get_key(), merchant_acc.merchantId.merchantId, "kv_redis".to_string()).await; if gbes_v2_flag { - let gbes_v2s = SGBES::getGatewayBankEmiSupportV2( + let gbes_v2s = SGBES::get_gateway_bank_emi_support_v2( txn_detail.emiBank, gws, scope.to_string(), txn_detail.emiTenure.map(|tenure| tenure as i32) ).await; if gbes_v2s.is_empty() { - logger::info!( + logger::debug!( tag = "GBESV2 Entry Not Found", action = "GBESV2 Entry Not Found", "GBESV2 Entry Not Found For emiBank - {:?}, gateways - {:?}, scope - {:?}, tenure - {:?}", @@ -595,7 +595,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { } extractGatewaysV2(gbes_v2s) } else { - extractGateways(SGBES::getGatewayBankEmiSupport( + extractGateways(SGBES::get_gateway_bank_emi_support( txn_detail.emiBank, gws, scope.to_string() @@ -607,7 +607,7 @@ pub async fn filterGatewaysForEmi(this: &mut DeciderFlow<'_>) -> GatewayList { setGws(this, gws); } else if Utils::is_card_transaction(&txn_card_info) { - let card_emi_explicit_gateways: HashSet = findByNameFromRedis::>(C::CARD_EMI_EXPLICIT_GATEWAYS.get_key()) + let card_emi_explicit_gateways: HashSet = findByNameFromRedis::>(C::CardEmiExplicitGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -647,7 +647,7 @@ pub async fn filterGatewaysForTokenProvider(this: &mut DeciderFlow<'_>) -> Gatew None => returnGwListWithLog(this, DeciderFilterName::FilterFunctionalGatewaysForTokenProvider, false), Some(VaultProvider::Juspay) => returnGwListWithLog(this, DeciderFilterName::FilterFunctionalGatewaysForTokenProvider, true), Some(v) => { - let token_provider_gateway_mapping = findByNameFromRedis::>(C::TOKEN_PROVIDER_GATEWAY_MAPPING.get_key()) + let token_provider_gateway_mapping = findByNameFromRedis::>(C::TokenProviderGatewayMapping.get_key()) .await.unwrap_or_default(); let new_st = st .into_iter() @@ -672,10 +672,10 @@ pub async fn filterFunctionalGatewaysForMerchantRequiredFlow(this: &mut DeciderF let is_cb_order = payment_flow_list.contains(&"CROSS_BORDER_PAYMENT".to_string()); let is_sbmd = payment_flow_list.contains(&"SINGLE_BLOCK_MULTIPLE_DEBIT".to_string()); - let mf_filtered_gw = filter_gateways_for_flow(is_mf_order, C::MUTUAL_FUND_FLOW_SUPPORTED_GATEWAYS.get_key(), st).await; + let mf_filtered_gw = filter_gateways_for_flow(is_mf_order, C::MutualFundFlowSupportedGateways.get_key(), st).await; let mf_and_cb_filtered_gw = - filter_gateways_for_flow(is_cb_order, C::CROSS_BORDER_FLOW_SUPPORTED_GATEWAYS.get_key(), mf_filtered_gw).await; - let filtered_gw = filter_gateways_for_flow(is_sbmd, C::SBMD_SUPPORTED_GATEWAYS.get_key(), mf_and_cb_filtered_gw).await; + filter_gateways_for_flow(is_cb_order, C::CrossBorderFlowSupportedGateways.get_key(), mf_filtered_gw).await; + let filtered_gw = filter_gateways_for_flow(is_sbmd, C::SbmdSupportedGateways.get_key(), mf_and_cb_filtered_gw).await; setGws(this, filtered_gw); returnGwListWithLog(this, DeciderFilterName::FilterFunctionalGatewaysForMerchantRequiredFlow, true) @@ -811,7 +811,7 @@ pub async fn filterFunctionalGatewaysForOTMFlow(this: &mut DeciderFlow<'_>) -> V let all_gpmf_entries = GPMF::find_all_gpmf_by_country_code_gw_pf_id_pmt_jbcid_db( crate::types::country::country_iso::CountryISO::IND, gw_list, - PaymentFlow::ONE_TIME_MANDATE, + PaymentFlow::OneTimeMandate, txn_card_info.paymentMethodType, jbc.id, ) @@ -968,10 +968,19 @@ pub async fn filterGatewaysForValidationType(this: &mut DeciderFlow<'_>) -> () { // Handle Card Mandate transactions if Utils::is_mandate_transaction(&txn_detail) && Utils::is_card_transaction(&txn_card_info) { // Get excluded gateways from Redis - let card_mandate_bin_filter_excluded_gateways = findByNameFromRedis(C::CARD_MANDATE_BIN_FILTER_EXCLUDED_GATEWAYS.get_key()) + let mut card_mandate_bin_filter_excluded_gateways = findByNameFromRedis(C::CardMandateBinFilterExcludedGateways.get_key()) .await .unwrap_or_else(Vec::new); - + + // Get card brand + let card_brand = Utils::get_card_brand(this).await; + // If card brand is RUPAY remove HYPERPG from config list + if card_brand.as_deref() == Some("RUPAY") { + card_mandate_bin_filter_excluded_gateways + .retain(|gw| gw != &ETG::Gateway::HYPERPG); + } + + let bin_wise_filter_excluded_gateways = intersect(&card_mandate_bin_filter_excluded_gateways, &st); let bin_list = Utils::get_bin_list(txn_card_info.card_isin.clone()); @@ -1160,7 +1169,7 @@ pub async fn filterGatewaysForValidationType(this: &mut DeciderFlow<'_>) -> () { } // Handle other transaction types else { - let tpv_only_supported_gateways = findByNameFromRedis(C::TPV_ONLY_SUPPORTED_GATEWAYS.get_key()) + let tpv_only_supported_gateways = findByNameFromRedis(C::TpvOnlySupportedGateways.get_key()) .await .unwrap_or_else(Vec::new); @@ -1236,15 +1245,15 @@ fn isGatewayCardInfoCheckNeeded( gateway: &Gateway, ) -> bool { // Check for ATM PIN auth type and if the gateway is in the restricted list - txn_card_info.authType.as_ref().map(|at| *at == AuthType::ATMPIN).unwrap_or(false) + txn_card_info.authType.as_ref().map(|at| *at == AuthType::Atmpin).unwrap_or(false) && atm_pin_card_info_restricted_gateways.contains(gateway) || // Check for OTP auth type and if the gateway supports it - txn_card_info.authType.as_ref().map(|at| *at == AuthType::OTP).unwrap_or(false) + txn_card_info.authType.as_ref().map(|at| *at == AuthType::Otp).unwrap_or(false) && otp_card_info_supported_gateways.contains(gateway) || // Check for MOTO auth type and if the gateway supports it - txn_card_info.authType.as_ref().map(|at| *at == AuthType::MOTO).unwrap_or(false) + txn_card_info.authType.as_ref().map(|at| *at == AuthType::Moto).unwrap_or(false) && moto_card_info_supported_gateways.contains(gateway) } @@ -1260,7 +1269,7 @@ pub async fn filterGatewaysForAuthType(this: &mut DeciderFlow<'_>) -> Vec) -> Vec) -> Vec) -> Vec) -> Vec>(); - let otp_card_info_restricted_gateways = findByNameFromRedis(C::OTP_CARD_INFO_RESTRICTED_GATEWAYS.get_key()).await + let otp_card_info_restricted_gateways = findByNameFromRedis(C::OtpCardInfoRestrictedGateways.get_key()).await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); - let otp_card_info_supported_gateways = findByNameFromRedis(C::OTP_CARD_INFO_SUPPORTED_GATEWAYS.get_key()).await + let otp_card_info_supported_gateways = findByNameFromRedis(C::OtpCardInfoSupportedGateways.get_key()).await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); - let moto_card_info_supported_gateways = findByNameFromRedis(C::MOTO_CARD_INFO_SUPPORTED_GATEWAYS.get_key()).await + let moto_card_info_supported_gateways = findByNameFromRedis(C::MotoCardInfoSupportedGateways.get_key()).await .unwrap_or_else(Vec::new) .into_iter() .collect::>(); - let auth_type_restricted_gateways = findByNameFromRedis(C::AUTH_TYPE_RESTRICTED_GATEWAYS.get_key()).await + let auth_type_restricted_gateways = findByNameFromRedis(C::AuthTypeRestrictedGateways.get_key()).await .unwrap_or_else(Vec::new) .into_iter() .collect::>>(); @@ -1418,25 +1427,25 @@ pub async fn filterGatewaysForAuthType(this: &mut DeciderFlow<'_>) -> Vec, st: &[Gateway], card_brand: Option<&str>) -> Vec { - let amex_supported_gateways: HashSet = findByNameFromRedis(C::AMEX_SUPPORTED_GATEWAYS.get_key()) + let amex_supported_gateways: HashSet = findByNameFromRedis(C::AmexSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); - let amex_not_supported_gateways: HashSet = findByNameFromRedis(C::AMEX_NOT_SUPPORTED_GATEWAYS.get_key()) + let amex_not_supported_gateways: HashSet = findByNameFromRedis(C::AmexNotSupportedGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); - let sodexo_only_gateways: HashSet = findByNameFromRedis(C::SODEXO_ONLY_GATEWAYS.get_key()) + let sodexo_only_gateways: HashSet = findByNameFromRedis(C::SodexoOnlyGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() .collect(); - let sodexo_also_gateways: HashSet = findByNameFromRedis(C::SODEXO_ALSO_GATEWAYS.get_key()) + let sodexo_also_gateways: HashSet = findByNameFromRedis(C::SodexoAlsoGateways.get_key()) .await .unwrap_or_else(Vec::new) .into_iter() @@ -1529,7 +1538,7 @@ pub fn filterForEMITenureSpecificMGAs(this: &mut DeciderFlow) -> Vec>(); @@ -1980,7 +1989,7 @@ pub async fn filterGatewaysCardInfo( let gcis_with_merchant_validation = gcis .iter() .filter(|gci| { - merchant_validation_required_gws.contains(&gci.gateway.clone().unwrap_or(ETG::Gateway::NONE)) + merchant_validation_required_gws.contains(&gci.gateway.clone().unwrap_or(ETG::Gateway::None)) }) .cloned() .collect::>(); @@ -2130,17 +2139,17 @@ pub fn setGwsAndMgas(this: &mut DeciderFlow, filteredMgas: Vec) -> Vec { let st = getGws(this); let txn_card_info = this.get().dpTxnCardInfo.clone(); - let upi_only_gateways = findByNameFromRedis::>(C::UPI_ONLY_GATEWAYS.get_key()).await + let upi_only_gateways = findByNameFromRedis::>(C::UpiOnlyGateways.get_key()).await .unwrap_or_default() .into_iter() .collect::>(); - let wallet_only_gateways:Vec = findByNameFromRedis::>(C::WALLET_ONLY_GATEWAYS.get_key()).await + let wallet_only_gateways:Vec = findByNameFromRedis::>(C::WalletOnlyGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); let wallet_only_gateways_hashset: HashSet = wallet_only_gateways.into_iter().collect::>(); - let wallet_also_gateways:Vec = findByNameFromRedis::>(C::WALLET_ALSO_GATEWAYS.get_key()).await + let wallet_also_gateways:Vec = findByNameFromRedis::>(C::WalletAlsoGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); @@ -2170,7 +2179,7 @@ pub async fn filterGatewaysForNbOnly(this: &mut DeciderFlow<'_>) -> Vec = findByNameFromRedis::>(C::NB_ONLY_GATEWAYS.get_key()).await + let nb_only_gateways:Vec = findByNameFromRedis::>(C::NbOnlyGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); @@ -2189,7 +2198,7 @@ pub async fn filterGatewaysForNbOnly(this: &mut DeciderFlow<'_>) -> Vec) -> Vec { let st = getGws(this); let txn_card_info = this.get().dpTxnCardInfo.clone(); - let consumer_finance_only_gateways:Vec = findByNameFromRedis::>(C::CONSUMER_FINANCE_ONLY_GATEWAYS.get_key()).await + let consumer_finance_only_gateways:Vec = findByNameFromRedis::>(C::ConsumerFinanceOnlyGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); @@ -2198,7 +2207,7 @@ pub async fn filterGatewaysForConsumerFinance(this: &mut DeciderFlow<'_>) -> Vec if txn_card_info.paymentMethodType == ETP::PaymentMethodType::ConsumerFinance{ - let consumer_finance_also_gateways:Vec = findByNameFromRedis::>(C::CONSUMER_FINANCE_ALSO_GATEWAYS.get_key()) + let consumer_finance_also_gateways:Vec = findByNameFromRedis::>(C::ConsumerFinanceAlsoGateways.get_key()) .await.unwrap_or_default() .into_iter() .collect(); @@ -2217,7 +2226,7 @@ pub async fn filterGatewaysForUpi(this: &mut DeciderFlow<'_>) -> Vec = findByNameFromRedis::>(C::UPI_ONLY_GATEWAYS.get_key()).await + let upi_only_gateways: Vec = findByNameFromRedis::>(C::UpiOnlyGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); @@ -2227,7 +2236,7 @@ pub async fn filterGatewaysForUpi(this: &mut DeciderFlow<'_>) -> Vec = findByNameFromRedis::>(C::UPI_ALSO_GATEWAYS.get_key()).await + let upi_also_gateway: Vec = findByNameFromRedis::>(C::UpiAlsoGateways.get_key()).await .unwrap_or_default() .into_iter() .collect(); @@ -2279,8 +2288,8 @@ pub async fn filterGatewaysForTxnType(this: &mut DeciderFlow<'_>) -> Vec = findByNameFromRedis(C::V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS.get_key()).await.unwrap_or_default(); - let upi_intent_not_supported_gateways: Vec = findByNameFromRedis(C::UPI_INTENT_NOT_SUPPORTED_GATEWAYS.get_key()).await.unwrap_or_default(); + let v2_integration_not_supported_gateways: Vec = findByNameFromRedis(C::V2IntegrationNotSupportedGateways.get_key()).await.unwrap_or_default(); + let upi_intent_not_supported_gateways: Vec = findByNameFromRedis(C::UpiIntentNotSupportedGateways.get_key()).await.unwrap_or_default(); let (_, filtered_mgas) = if ["UPI_PAY", "UPI_QR"].contains(&txn_type.as_str()) // && intersect(&st, &(v2_integration_not_supported_gateways.clone() + upi_intent_not_supported_gateways.clone())).is_empty() && { @@ -2300,7 +2309,7 @@ pub async fn filterGatewaysForTxnType(this: &mut DeciderFlow<'_>) -> Vec,) -> Vec< let st = getGws(this); let m_txn_type = this.get().dpTxnDetail.txnType.clone(); let txn_type: &str = m_txn_type.as_str(); - let txn_detail_type_restricted_gateways = findByNameFromRedis(C::TXN_DETAIL_TYPE_RESTRICTED_GATEWAYS.get_key()) + let txn_detail_type_restricted_gateways = findByNameFromRedis(C::TxnDetailTypeRestrictedGateways.get_key()) .await.unwrap_or_default(); let filter_gws = if txn_type == "ZERO_AUTH" { st.iter() @@ -2345,11 +2354,11 @@ pub async fn filterGatewaysForReward(this: &mut DeciderFlow<'_>,) -> Vec = findByNameFromRedis(C::REWARD_ALSO_GATEWAYS.get_key()) + let reward_also_gateways: HashSet = findByNameFromRedis(C::RewardAlsoGateways.get_key()) .await.unwrap_or_else(Vec::new) .into_iter() .collect(); - let reward_only_gateways: HashSet = findByNameFromRedis(C::REWARD_ONLY_GATEWAYS.get_key()) + let reward_only_gateways: HashSet = findByNameFromRedis(C::RewardOnlyGateways.get_key()) .await.unwrap_or_else(Vec::new) .into_iter() .collect(); @@ -2372,7 +2381,7 @@ pub async fn filterGatewaysForCash(this: &mut DeciderFlow<'_>,) -> Vec = findByNameFromRedis(C::CASH_ONLY_GATEWAYS.get_key()) + let cash_only_gateways: Vec = findByNameFromRedis(C::CashOnlyGateways.get_key()) .await.unwrap_or_else(Vec::new) .into_iter() .collect(); @@ -2445,7 +2454,7 @@ pub async fn filterFunctionalGatewaysForSplitSettlement(this: &mut DeciderFlow<' .filter(|mgasi| { mgasi.merchantGatewayAccountId == mga.id && mgasi.subIdType == SubIdType::VENDOR - && mgasi.subInfoType == SubInfoType::SPLIT_SETTLEMENT + && mgasi.subInfoType == SubInfoType::SplitSettlement && !mgasi.disabled }) .map(|mgasi| mgasi.juspaySubAccountId.clone()) @@ -2476,7 +2485,7 @@ pub async fn filterFunctionalGatewaysForSplitSettlement(this: &mut DeciderFlow<' msg ); let st = getGws(this); - let split_settlement_supported_gateways: Option> = findByNameFromRedis(C::SPLIT_SETTLEMENT_SUPPORTED_GATEWAYS.get_key()).await; + let split_settlement_supported_gateways: Option> = findByNameFromRedis(C::SplitSettlementSupportedGateways.get_key()).await; if !intersect(&split_settlement_supported_gateways.unwrap_or_else(Vec::new), &st).is_empty() { let mgas = SETMA::get_split_settlement_only_gateway_accounts( this, @@ -2564,7 +2573,7 @@ pub async fn filterGatewaysForPaymentMethod (this: &mut DeciderFlow<'_>,) -> Vec ); let pm = getPaymentMethodForNonCardTransaction(&txn_card_info); - let v2_integration_not_supported_gateways:Vec = findByNameFromRedis::>(C::V2_INTEGRATION_NOT_SUPPORTED_GATEWAYS.get_key()) + let v2_integration_not_supported_gateways:Vec = findByNameFromRedis::>(C::V2IntegrationNotSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() @@ -2572,7 +2581,7 @@ pub async fn filterGatewaysForPaymentMethod (this: &mut DeciderFlow<'_>,) -> Vec // let v2_integration_not_supported_gateways_hashset: HashSet = v2_integration_not_supported_gateways.iter().cloned().collect(); - let upi_intent_not_supported_gateways: Vec = findByNameFromRedis::>(C::UPI_INTENT_NOT_SUPPORTED_GATEWAYS.get_key()) + let upi_intent_not_supported_gateways: Vec = findByNameFromRedis::>(C::UpiIntentNotSupportedGateways.get_key()) .await .unwrap_or_default() .into_iter() diff --git a/src/decider/gatewaydecider/gw_scoring.rs b/src/decider/gatewaydecider/gw_scoring.rs index 51ec6db2..bc8bff90 100644 --- a/src/decider/gatewaydecider/gw_scoring.rs +++ b/src/decider/gatewaydecider/gw_scoring.rs @@ -3,39 +3,33 @@ use crate::app::get_tenant_app_state; use crate::decider::gatewaydecider::gw_filter::{getGws, setGws}; use crate::decider::gatewaydecider::types::{ - toListOfGatewayScore, DeciderFlow, DeciderScoringName, GatewayDeciderApproach, GatewayScoreMap, - SRMetricLogData, + toListOfGatewayScore, ConfigSource, DeciderFlow, DeciderScoringName, FilterLevel, + GatewayDeciderApproach, GatewayScoreMap, SRMetricLogData, SrRoutingDimensions, }; +use crate::feedback::gateway_scoring_service::MetricEntry; use crate::logger; use crate::merchant_config_util::{ isMerchantEnabledForPaymentFlows, isPaymentFlowEnabledWithHierarchyCheck, }; use crate::redis::types::ServiceConfigKey; -#[cfg(feature = "mysql")] -use crate::storage::schema::txn_detail; -#[cfg(feature = "postgres")] -use crate::storage::schema_pg::txn_detail; use crate::types::gateway_routing_input::{ EliminationLevel, EliminationSuccessRateInput, GatewayScore, GatewaySuccessRateBasedRoutingInput, GatewayWiseSuccessRateBasedRoutingInput, - GlobalGatewayScore, GlobalScore, GlobalScoreLog, SelectionLevel, + GlobalGatewayScore, GlobalScore, GlobalScoreLog, }; use crate::types::payment_flow::PaymentFlow; use crate::types::tenant::tenant_config::ModuleName; use crate::types::transaction::id::TransactionId; use crate::utils::{generate_random_number, get_current_date_in_millis}; -use diesel::dsl::update; -use rand::prelude::*; +use masking::PeekInterface; use rand_distr::{Beta, Binomial, Distribution}; use serde::{Deserialize, Serialize}; use time::{OffsetDateTime, PrimitiveDateTime}; // use crate::types::card_brand_routes as ETCBR; -use crate::redis::feature::{self as M, isFeatureEnabled}; -use crate::types::gateway as ETG; +use crate::redis::feature::{self as M, is_feature_enabled, RedisCompressionConfigCombined}; use crate::types::gateway_routing_input as ETGRI; // use crate::types::gateway_health as ETGH; use crate::types::card as ETCT; -use crate::types::payment as ETP; // use crate::types::issuer_routes as ETIssuerR; use crate::types::merchant as ETM; use crate::types::txn_details::types as ETTD; @@ -64,18 +58,16 @@ use crate::types::gateway_outage::{self as ETGO, GatewayOutage}; // use system_random::stateful::{init_std_gen, new_io_gen_m, IOGenM}; // use system_random::internal::StdGen; use super::types::{ - transform_gateway_wise_success_rate_based_routing, ConfigSource, DebugScoringEntry, - DeciderGatewayWiseSuccessRateBasedRoutingInput, Dimension, DownTime, FilterLevel, Gateway, - GatewayRedisKeyMap, GatewayScoringData, GlobalSREvaluationScoreLog, LogCurrScore, - RankingAlgorithm, RedisKey, ResetApproach, ResetGatewayInput, ScoreKeyType, SrV3InputConfig, - SuccessRate1AndNConfig, + transform_gateway_wise_success_rate_based_routing, DebugScoringEntry, + DeciderGatewayWiseSuccessRateBasedRoutingInput, Dimension, DownTime, GatewayRedisKeyMap, + GatewayScoringData, GlobalSREvaluationScoreLog, LogCurrScore, RankingAlgorithm, RedisKey, + ResetApproach, ResetGatewayInput, ScoreKeyType, SrV3InputConfig, SuccessRate1AndNConfig, }; use crate::feedback::gateway_elimination_scoring::flow::getTTLForKey; use crate::types::payment::payment_method_type_const::*; use std::collections::HashMap as MP; use std::iter::Iterator; use std::option::Option; -use std::primitive; use std::string::String as T; use std::time::{SystemTime, UNIX_EPOCH}; use std::vec::Vec; @@ -188,7 +180,7 @@ pub async fn scoring_flow( if functional_gateways.len() == 1 { set_gwsm(decider_flow, create_score_map(functional_gateways.clone())); - set_decider_approach(decider_flow, GatewayDeciderApproach::DEFAULT); + set_decider_approach(decider_flow, GatewayDeciderApproach::Default); Utils::set_top_gateway_before_sr_downtime_evaluation( decider_flow, functional_gateways.first().cloned(), @@ -221,24 +213,21 @@ pub async fn scoring_flow( maybe_source_object.unwrap_or_default(), ); - let is_merchant_enabled_for_sr_based_routing = isMerchantEnabledForPaymentFlows( - merchant.id.clone(), - vec![PaymentFlow::SR_BASED_ROUTING], - ) - .await - || ranking_algorithm == Some(RankingAlgorithm::SR_BASED_ROUTING); + let is_merchant_enabled_for_sr_based_routing = + isMerchantEnabledForPaymentFlows(merchant.id, vec![PaymentFlow::SrBasedRouting]).await + || ranking_algorithm == Some(RankingAlgorithm::SrBasedRouting); let is_sr_v3_metric_enabled = if is_merchant_enabled_for_sr_based_routing { - let is_sr_v3_metric_enabled = isFeatureEnabled( + let is_sr_v3_metric_enabled = is_feature_enabled( C::enable_gateway_selection_based_on_sr_v3_input(pmt_str.clone()).get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await - || ranking_algorithm == Some(RankingAlgorithm::SR_BASED_ROUTING); + || ranking_algorithm == Some(RankingAlgorithm::SrBasedRouting); if is_sr_v3_metric_enabled { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Deciding Gateway based on SR V3 Routing for merchant {:?} and for txn Id {:?}", @@ -251,39 +240,52 @@ pub async fn scoring_flow( ) .await; let default_sr_v3_input_config = - findByNameFromRedis(C::srV3DefaultInputConfig.get_key()).await; + findByNameFromRedis(C::SR_V3_DEFAULT_INPUT_CONFIG.get_key()).await; - logger::info!( + logger::debug!( tag = "scoringFlow_Sr_V3_Input_Config", action = "scoringFlow_Sr_V3_Input_Config", "Sr V3 Input Config {:?}", merchant_sr_v3_input_config ); - logger::info!( + logger::debug!( tag = "scoringFlow_Sr_V3_Default_Input_Config", action = "scoringFlow_Sr_V3_Default_Input_Config", "Sr V3 Default Input Config {:?}", default_sr_v3_input_config ); + let sr_routing_dimensions = SrRoutingDimensions { + card_network: txn_card_info + .cardSwitchProvider + .as_ref() + .map(|s| s.peek().to_string()), + card_isin: txn_card_info.card_isin.clone(), + currency: Some(decider_flow.get().dpOrder.currency.to_string()), + country: txn_detail.country.as_ref().map(|a| a.to_string()), + auth_type: txn_card_info.authType.as_ref().map(|a| a.to_string()), + }; + let hedging_percent = Utils::get_sr_v3_hedging_percent( merchant_sr_v3_input_config.clone(), &pmt_str, pm.clone().as_str(), + &sr_routing_dimensions, ) .or_else(|| { Utils::get_sr_v3_hedging_percent( default_sr_v3_input_config.clone(), &pmt_str, pm.clone().as_str(), + &sr_routing_dimensions, ) }) - .unwrap_or(C::defaultSrV3BasedHedgingPercent); + .unwrap_or(C::DEFAULT_SR_V3_BASED_HEDGING_PERCENT); Utils::set_sr_v3_hedging_percent(decider_flow, hedging_percent); - let is_explore_and_exploit_enabled = isFeatureEnabled( + let is_explore_and_exploit_enabled = is_feature_enabled( C::enableExploreAndExploitOnSrV3(pmt_str).get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), @@ -315,7 +317,7 @@ pub async fn scoring_flow( let initial_sr_gw_scores_list = toListOfGatewayScore(initial_sr_gw_scores.clone()); - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Gateway Scores based on SR V3 Routing for txn id : {:?} is {:?}", @@ -326,7 +328,7 @@ pub async fn scoring_flow( if !initial_sr_gw_scores.is_empty() { Utils::set_sr_gateway_scores(decider_flow, initial_sr_gw_scores_list); - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Considering Gateway Scores based on SR V3 for txn id : {:?}", @@ -334,16 +336,16 @@ pub async fn scoring_flow( ); if should_explore { - set_decider_approach(decider_flow, GatewayDeciderApproach::SR_V3_HEDGING); + set_decider_approach(decider_flow, GatewayDeciderApproach::SrV3Hedging); } else { set_decider_approach( decider_flow, - GatewayDeciderApproach::SR_SELECTION_V3_ROUTING, + GatewayDeciderApproach::SrSelectionV3Routing, ); } - let is_route_random_traffic_enabled = isFeatureEnabled( - C::routeRandomTrafficSrV3EnabledMerchant.get_key(), + let is_route_random_traffic_enabled = is_feature_enabled( + C::ROUTE_RANDOM_TRAFFIC_SR_V3_ENABLED_MERCHANT.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) @@ -371,8 +373,8 @@ pub async fn scoring_flow( if sr_gw_score.len() > 1 && (!is_explore_and_exploit_enabled || should_explore) { - let is_debug_mode_enabled = isFeatureEnabled( - C::enableDebugModeOnSrV3.get_key(), + let _is_debug_mode_enabled = is_feature_enabled( + C::ENABLE_DEBUG_MODE_ON_SR_V3.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ); @@ -386,7 +388,7 @@ pub async fn scoring_flow( true } else { - logger::info!( + logger::debug!( tag="scoringFlow", action = "scoringFlow", "Gateway Scores based on SR V3 for txn id : {:?} and for merchant : {:?} is null, So falling back to priorityLogic", @@ -411,19 +413,19 @@ pub async fn scoring_flow( Utils::set_is_optimized_based_on_sr_metric_enabled(decider_flow, false); if !is_sr_v3_metric_enabled { - logger::info!( + logger::debug!( tag="scoringFlow", action = "scoringFlow", "Ordering gateways available based on PRIORITY for merchant {:?} and for txn Id {:?}", Utils::get_m_id(merchant.merchantId.clone()), txn_detail.txnId.clone() ); - set_decider_approach(decider_flow, GatewayDeciderApproach::PRIORITY_LOGIC); + set_decider_approach(decider_flow, GatewayDeciderApproach::PriorityLogic); let gateway_score = get_score_with_priority(functional_gateways.clone(), gateway_priority_list.clone()); set_gwsm(decider_flow, gateway_score.clone()); return_sm_with_log(decider_flow, DeciderScoringName::GetScoreWithPriority, true); - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Gateway scores after considering priority for {:?} : {:?}", @@ -434,7 +436,7 @@ pub async fn scoring_flow( // update_score_for_isin(decider_flow); // update_score_for_card_brand(decider_flow); } else { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "skipped priority for merchant {:?} and for txn Id {:?}", @@ -488,18 +490,46 @@ pub async fn get_cached_scores_based_on_srv3( let sr_gateway_redis_key_map: GatewayRedisKeyMap = Utils::get_consumer_key( decider_flow, gateway_scoring_data, - super::types::ScoreKeyType::SR_V3_KEY, + super::types::ScoreKeyType::SrV3Key, false, functional_gateways.clone(), ) .await; - let merchant_bucket_size = - Utils::get_sr_v3_bucket_size(merchant_srv3_input_config.clone(), &pmt_str, &pm) - .or_else(|| { - Utils::get_sr_v3_bucket_size(default_srv3_input_config.clone(), &pmt_str, &pm) - }) - .unwrap_or(C::DEFAULT_SR_V3_BASED_BUCKET_SIZE); + // Extract the new parameters from txn_card_info + let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); + + let sr_routing_dimensions = SrRoutingDimensions { + card_network: txn_card_info + .cardSwitchProvider + .as_ref() + .map(|s| s.peek().to_string()), + card_isin: txn_card_info.card_isin, + currency: Some(decider_flow.get().dpOrder.currency.to_string()), + country: decider_flow + .get() + .dpTxnDetail + .country + .as_ref() + .map(|a| a.to_string()), + auth_type: txn_card_info.authType.as_ref().map(|a| a.to_string()), + }; + + let merchant_bucket_size = Utils::get_sr_v3_bucket_size( + merchant_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + .or_else(|| { + Utils::get_sr_v3_bucket_size( + default_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + }) + .unwrap_or(C::DEFAULT_SR_V3_BASED_BUCKET_SIZE); logger::debug!( tag = "Sr_V3_Bucket_Size", @@ -537,33 +567,43 @@ pub async fn get_cached_scores_based_on_srv3( ) .await; - let is_srv3_reset_enabled = M::isFeatureEnabled( - C::ENABLE_RESET_ON_SR_V3.get_key(), + let is_srv3_reset_enabled = M::is_feature_enabled( + C::EnableResetOnSrV3.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; let updated_score_map_after_reset = if is_srv3_reset_enabled { - let upper_reset_factor = - Utils::get_sr_v3_upper_reset_factor(merchant_srv3_input_config.clone(), &pmt_str, &pm) - .or_else(|| { - Utils::get_sr_v3_upper_reset_factor( - default_srv3_input_config.clone(), - &pmt_str, - &pm, - ) - }) - .unwrap_or(C::defaultSrV3BasedUpperResetFactor); - let lower_reset_factor = - Utils::get_sr_v3_lower_reset_factor(merchant_srv3_input_config.clone(), &pmt_str, &pm) - .or_else(|| { - Utils::get_sr_v3_lower_reset_factor( - default_srv3_input_config.clone(), - &pmt_str, - &pm, - ) - }) - .unwrap_or(C::defaultSrV3BasedLowerResetFactor); + let upper_reset_factor = Utils::get_sr_v3_upper_reset_factor( + merchant_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + .or_else(|| { + Utils::get_sr_v3_upper_reset_factor( + default_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + }) + .unwrap_or(C::DEFAULT_SR_V3_BASED_UPPER_RESET_FACTOR); + let lower_reset_factor = Utils::get_sr_v3_lower_reset_factor( + merchant_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + .or_else(|| { + Utils::get_sr_v3_lower_reset_factor( + default_srv3_input_config.clone(), + &pmt_str, + &pm, + &sr_routing_dimensions, + ) + }) + .unwrap_or(C::DEFAULT_SR_V3_BASED_LOWER_RESET_FACTOR); logger::debug!( tag = "Sr_V3_Upper_Reset_Factor", action = "Sr_V3_Upper_Reset_Factor", @@ -597,15 +637,15 @@ pub async fn get_cached_scores_based_on_srv3( "SR_SELECTION_V3_EVALUATION_AFTER_RESET".to_string(), ) .await; - Utils::set_reset_approach(decider_flow, ResetApproach::SRV3_RESET); + Utils::set_reset_approach(decider_flow, ResetApproach::Srv3Reset); } updated_score_map_after_reset } else { score_map }; - let is_srv3_extra_score_enabled = M::isFeatureEnabled( - C::enable_extra_score_on_sr_v3.get_key(), + let is_srv3_extra_score_enabled = M::is_feature_enabled( + C::ENABLE_EXTRA_SCORE_ON_SR_V3.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) @@ -621,6 +661,7 @@ pub async fn get_cached_scores_based_on_srv3( pmt_str.to_string(), pm.clone(), gw.clone(), + sr_routing_dimensions.clone(), ); final_score_map.insert(gw, extra_score); } @@ -641,14 +682,14 @@ pub async fn get_cached_scores_based_on_srv3( updated_score_map_after_reset }; - let is_srv3_binomial_distribution_enabled = M::isFeatureEnabled( - C::enable_binomial_distribution_on_sr_v3.get_key(), + let is_srv3_binomial_distribution_enabled = M::is_feature_enabled( + C::ENABLE_BINOMIAL_DISTRIBUTION_ON_SR_V3.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; - let is_srv3_beta_distribution_enabled = M::isFeatureEnabled( - C::enable_beta_distribution_on_sr_v3.get_key(), + let is_srv3_beta_distribution_enabled = M::is_feature_enabled( + C::ENABLE_BETA_DISTRIBUTION_ON_SR_V3.get_key(), Utils::get_m_id(merchant.merchantId.clone()), "kv_redis".to_string(), ) @@ -749,13 +790,25 @@ pub fn add_extra_score( pmt: String, pm: String, gw: String, + sr_routing_dimensions: SrRoutingDimensions, ) -> f64 { - let gateway_sigma_factor = - Utils::get_sr_v3_gateway_sigma_factor(merchant_sr_v3_input_config, &pmt, &pm, &gw) - .or_else(|| { - Utils::get_sr_v3_gateway_sigma_factor(default_sr_v3_input_config, &pmt, &pm, &gw) - }) - .unwrap_or(C::DEFAULT_SR_V3_BASED_GATEWAY_SIGMA_FACTOR); + let gateway_sigma_factor = Utils::get_sr_v3_gateway_sigma_factor( + merchant_sr_v3_input_config, + &pmt, + &pm, + &gw, + &sr_routing_dimensions, + ) + .or_else(|| { + Utils::get_sr_v3_gateway_sigma_factor( + default_sr_v3_input_config, + &pmt, + &pm, + &gw, + &sr_routing_dimensions, + ) + }) + .unwrap_or(C::DEFAULT_SR_V3_BASED_GATEWAY_SIGMA_FACTOR); logger::debug!( tag = "add_extra_score", action = "add_extra_score", @@ -807,7 +860,7 @@ pub async fn reset_sr_v3_score( .iter() .filter_map(|(gw, score)| { sr_gateway_redis_key_map - .get(&format!("{}", gw)) + .get(&gw.to_string()) .map(|key| (key.clone(), *score)) }) .collect(); @@ -853,13 +906,50 @@ pub async fn reset_gateway_for_sr_v3( } pub async fn get_score_from_redis(bucket_size: i32, redis_key: &RedisKey) -> f64 { + let queue_key = format!("{}{}", redis_key, "_}queue"); let score_key = format!("{}{}", redis_key, "_}score"); let app_state = get_tenant_app_state().await; + let bucket_size = bucket_size.max(1); + let queue_window = app_state + .redis_conn + .get_list_range(&queue_key, 0, i64::from(bucket_size - 1)) + .await + .ok() + .filter(|values| !values.is_empty()); + + if let Some(values) = queue_window { + let parsed_scores: Vec = values + .iter() + .filter_map(|value| value.parse::().ok()) + .collect(); + + if !parsed_scores.is_empty() { + let success_count: f64 = parsed_scores.iter().sum(); + let score = (success_count / parsed_scores.len() as f64).clamp(0.0, 1.0); + logger::info!( + tag = "get_score_from_redis", + action = "get_score_from_redis", + "Derived sr_v3 score {:?} from queue {:?} using {:?} samples", + score, + queue_key, + parsed_scores.len() + ); + return score; + } + } + let success_count = app_state .redis_conn .get_key::(&score_key, "sr_v3_score_key") .await .unwrap_or(bucket_size); + logger::info!( + tag = "get_score_from_redis", + action = "get_score_from_redis", + "Fetched success count {:?} for redis key {:?}", + success_count, + score_key + ); (success_count as f64 / bucket_size as f64).clamp(0.0, 1.0) } @@ -873,7 +963,7 @@ pub fn prepare_log_curr_score( score: f64, ) -> &Vec { acc.push(LogCurrScore { - gateway: format!("{}", gw), + gateway: gw.to_string(), current_score: score, }); acc @@ -896,6 +986,7 @@ pub async fn reset_and_log_metrics( .unwrap_or_default(), ); Utils::metric_tracker_log( + decider_flow.get().dpShouldConsumeResult, metric_title.clone().as_str(), "GW_SCORING", Utils::get_metric_log_format(decider_flow, metric_title.as_str()), @@ -942,10 +1033,11 @@ pub async fn update_score_for_outage(decider_flow: &mut DeciderFlow<'_>) -> Gate let txn_detail = decider_flow.get().dpTxnDetail.clone(); let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); let merchant = decider_flow.get().dpMerchantAccount.clone(); - let scheduled_outage_validation_duration = - RService::findByNameFromRedis(C::SCHEDULED_OUTAGE_VALIDATION_DURATION.get_key()) - .await - .unwrap_or(86400); + let scheduled_outage_validation_duration = RService::findByNameFromRedisWithDefault( + C::ScheduledOutageValidationDuration.get_key(), + 86400, + ) + .await; let potential_outages = get_scheduled_outage(scheduled_outage_validation_duration).await; logger::debug!("updated score for outage {:?}", potential_outages); @@ -957,7 +1049,7 @@ pub async fn update_score_for_outage(decider_flow: &mut DeciderFlow<'_>) -> Gate let out_gws: Vec<_> = potential_outages .into_iter() .filter(|outage| { - check_scheduled_outtage( + check_scheduled_outage( &txn_detail, &txn_card_info, &merchant.merchantId, @@ -1017,7 +1109,7 @@ where } } -fn check_scheduled_outtage( +fn check_scheduled_outage( txn_detail: &ETTD::TxnDetail, txn_card_info: &ETCT::txn_card_info::TxnCardInfo, merchant_id: &ETM::id::MerchantId, @@ -1052,7 +1144,7 @@ fn check_scheduled_outtage( false } }, - Some(juspay_bank_code.clone()), + Some(juspay_bank_code), scheduled_outage.bank.clone(), ) && check_scheduled_outage_metadata( txn_detail, @@ -1099,7 +1191,7 @@ fn check_scheduled_outage_metadata( Some(scheduled_outage_metadata) => { schedule_equal_to( |x, y| x == y, - Some(txn_detail.txnObjectType.clone()), + txn_detail.txnObjectType.clone(), scheduled_outage_metadata.txnObjectType.clone(), ) && schedule_equal_to( |x, y| x == y, @@ -1118,16 +1210,16 @@ fn check_scheduled_outage_metadata( UPI => txn_card_info .paymentSource .as_ref() - .map_or(false, |payment_source| { - if payment_source.contains('@') { + .is_some_and(|payment_source| { + if payment_source.peek().contains('@') { false } else { schedule_equal_to( - |x, y| x == y, + |x, y| x.peek() == &y, Some(payment_source.clone()), scheduled_outage_metadata.app.clone(), ) && schedule_equal_to( - |x, y| x == y, + |x, y| x.peek() == &y, Some(payment_source.clone()), scheduled_outage_metadata.handle.clone(), ) @@ -1145,10 +1237,10 @@ async fn get_scheduled_outage(scheduled_outage_validation_duration: i64) -> Vec< let scheduled_outages = ETGO::getPotentialGwOutages(primitive_time).await; let validated_outages: Vec = scheduled_outages .iter() - .cloned() - .filter(|outage| { + .filter(|&outage| { validate_scheduled_outage(scheduled_outage_validation_duration, outage.clone()) }) + .cloned() .collect(); logger::debug!("scheduled Outages length: {:?}", scheduled_outages.len()); // if validated_outages.len() != scheduled_outages.len() { @@ -1200,28 +1292,61 @@ pub async fn get_global_gateway_score( ) -> Option<(Vec, f64)> { if let (Some(max_count), Some(score_threshold)) = (max_count, score_threshold) { let app_state = get_tenant_app_state().await; + // let m_value: Option = app_state + // .redis_conn + // .get_key(&redis_key, "global_gateway_score_key") + // .await + // .inspect_err(|err| logger::error!("get_global_gateway_score get_key_error: {:?}", err)) + // .unwrap_or(None); let m_value: Option = app_state .redis_conn - .get_key(&redis_key, "global_gateway_score_key") + .get_key::(&redis_key, "global_gateway_score_key") .await - .unwrap_or(None); + .inspect_err(|err| logger::error!("get_global_gateway_score get_key_error: {:?}", err)) + .ok(); + logger::info!( + tag = "getGlobalGatewayScore", + action = "getGlobalGatewayScore", + "Fetched GlobalGatewayScore for key {:?}: {:?}", + redis_key, + m_value.clone() + ); match m_value { - None => None, + None => { + logger::info!( + tag = "getGlobalGatewayScore", + action = "getGlobalGatewayScore", + "No GlobalGatewayScore found for key {:?}", + redis_key + ); + None + } Some(global_gateway_score) => { let sorted_filtered_merchants: Vec = global_gateway_score .merchants .iter() - .cloned() .take(max_count as usize) + .cloned() .collect::>(); let should_penalize = sorted_filtered_merchants.len() >= max_count as usize && sorted_filtered_merchants .iter() .all(|x| x.score < score_threshold); let filtered_merchants: Vec = sorted_filtered_merchants + .clone() .into_iter() .map(|gs| mk_gsl(gs, score_threshold, max_count)) .collect(); + logger::info!( + tag = "getGlobalGatewayScore", + action = "getGlobalGatewayScore", + "Filtered merchants for key {:?}: {:?} : {:?} : {:?} : {:?}", + redis_key, + global_gateway_score, + sorted_filtered_merchants, + should_penalize.clone(), + filtered_merchants.clone() + ); Some(( filtered_merchants, if should_penalize { @@ -1233,7 +1358,7 @@ pub async fn get_global_gateway_score( } } } else { - logger::warn!( + logger::info!( tag = "getGlobalGatewayScore", action = "getGlobalGatewayScore", "max_count is {:?}, score_threshold is {:?}", @@ -1302,7 +1427,7 @@ pub fn get_gateway_wise_routing_inputs_for_global_sr( .as_ref() .and_then(|input| input.defaultGlobalEliminationLevel.clone()) }) - .or(Some(ETGRI::EliminationLevel::PAYMENT_METHOD)); + .or(Some(ETGRI::EliminationLevel::PaymentMethod)); gri.eliminationMaxCountThreshold = gri .eliminationMaxCountThreshold .or(global_routing_defaults.defaultGlobalEliminationMaxCountThreshold); @@ -1322,11 +1447,18 @@ pub async fn get_global_elimination_gateway_score( gateway_key_map: GatewayRedisKeyMap, gsri: GatewayWiseSuccessRateBasedRoutingInput, ) -> Option<(Vec, f64)> { - if gsri.eliminationLevel != Some(ETGRI::EliminationLevel::NONE) { + if gsri.eliminationLevel != Some(ETGRI::EliminationLevel::None) { let redis_key = gateway_key_map .get(&gsri.gateway.to_string()) .cloned() .unwrap_or_default(); + logger::info!( + tag = "get_global_elimination_gateway_score", + action = "get_global_elimination_gateway_score", + "Redis Key for Gateway {:?} : {:?}", + gsri, + redis_key + ); get_global_gateway_score( redis_key, gsri.eliminationMaxCountThreshold, @@ -1334,6 +1466,12 @@ pub async fn get_global_elimination_gateway_score( ) .await } else { + logger::error!( + tag = "get_global_elimination_gateway_score", + action = "get_global_elimination_gateway_score", + "Elimination Level is None for Gateway {:?}", + gsri + ); None } } @@ -1360,8 +1498,8 @@ pub async fn update_gateway_score_based_on_global_success_rate( Ok(global_routing_defaults) => { let gateway_success_rate_inputs = gateway_score .clone() - .iter() - .map(|(k, _)| { + .keys() + .map(|k| { get_gateway_wise_routing_inputs_for_global_sr( k.clone(), merchant_wise_global_routing_input.clone(), @@ -1371,16 +1509,32 @@ pub async fn update_gateway_score_based_on_global_success_rate( }) .collect::>(); + logger::info!( + tag = "scoringFlow", + action = "scoringFlow", + "Gateway Success Rate Inputs for Global SR based elimination for {:?} : {:?}", + txn_detail.txnId, + gateway_success_rate_inputs + ); + let gateway_list = Utils::get_gateway_list(gateway_score.clone()); let gateway_redis_key_map = Utils::get_consumer_key( decider_flow, gateway_scoring_data, - ScoreKeyType::ELIMINATION_GLOBAL_KEY, + ScoreKeyType::EliminationGlobalKey, false, gateway_list, ) .await; + logger::info!( + tag = "scoringFlow", + action = "scoringFlow", + "Gateway Redis Key Map for Global SR based elimination for {:?} : {:?}", + txn_detail.txnId, + gateway_redis_key_map + ); + let mut upd_gateway_success_rate_inputs = Vec::new(); let mut global_gateway_scores = Vec::new(); for gsri in gateway_success_rate_inputs { @@ -1389,22 +1543,54 @@ pub async fn update_gateway_score_based_on_global_success_rate( gsri.clone(), ) .await; - match global_elimination_gateway_score { - Some((global_gateway_score, s)) => { - let new_gsri = GatewayWiseSuccessRateBasedRoutingInput { - currentScore: Some(s), - ..gsri.clone() - }; - upd_gateway_success_rate_inputs.push(new_gsri); - global_gateway_scores.extend(update_global_score_log( - gsri.gateway.clone(), - global_gateway_score, - )); - } - None => {} + logger::info!( + tag = "scoringFlow", + action = "scoringFlow", + "Global Elimination Gateway Score for {:?} : {:?}", + txn_detail.txnId, + global_elimination_gateway_score + ); + if let Some((global_gateway_score, s)) = global_elimination_gateway_score { + logger::info!(action = "global_gateway_score", "s-value : {:?}", s); + logger::info!( + action = "global_gateway_score", + "global_gateway_score{:?}", + global_gateway_score + ); + let new_gsri = GatewayWiseSuccessRateBasedRoutingInput { + currentScore: Some(s), + ..gsri.clone() + }; + logger::info!( + action = "global_gateway_score", + "Global Elimination Gateway Score for {:?} : {:?}", + txn_detail.txnId, + new_gsri + ); + upd_gateway_success_rate_inputs.push(new_gsri); + logger::info!( + action = "global_gateway_score", + "upd_gateway_success_rate_inputs{:?}", + upd_gateway_success_rate_inputs + ); + global_gateway_scores.extend(update_global_score_log( + gsri.gateway.clone(), + global_gateway_score, + )); + logger::info!( + action = "update_global_score_log", + "global_gateway_scores{:?}", + global_gateway_scores + ); } } + logger::info!( + action = "update_gateway_score_based_on_global_success_rate", + "upd_gateway_success_rate_inputs{:?}", + upd_gateway_success_rate_inputs + ); + let filtered_gateway_success_rate_inputs: Vec< GatewayWiseSuccessRateBasedRoutingInput, > = upd_gateway_success_rate_inputs @@ -1418,6 +1604,12 @@ pub async fn update_gateway_score_based_on_global_success_rate( }) .collect(); + logger::info!( + action = "filtered_gateway_success_rate_inputs", + "filtered_gateway_success_rate_inputs{:?}", + filtered_gateway_success_rate_inputs + ); + reset_metric_log_data(decider_flow); let init_metric_log_data = decider_flow.writer.srMetricLogData.clone(); let before_gwsm = get_gwsm(decider_flow); @@ -1471,7 +1663,7 @@ pub async fn update_gateway_score_based_on_global_success_rate( } let old_sr_metric_log_data = decider_flow.writer.srMetricLogData.clone(); - logger::debug!( + logger::info!( tag = "MetricData-GLOBAL-ELIMINATION", action = "MetricData-GLOBAL-ELIMINATION", "{:?}", @@ -1492,6 +1684,7 @@ pub async fn update_gateway_score_based_on_global_success_rate( }, ); Utils::metric_tracker_log( + decider_flow.get().dpShouldConsumeResult, "GLOBAL_SR_EVALUATION", "GW_SCORING", Utils::get_metric_log_format(decider_flow, "GLOBAL_SR_EVALUATION"), @@ -1521,13 +1714,13 @@ pub async fn update_gateway_score_based_on_global_success_rate( ) } Err(reason) => { - logger::debug!( + logger::error!( tag = "Global SR routing", action = "Global SR routing", "{:?}", reason ); - logger::info!( + logger::error!( tag = "scoringFlow", action = "scoringFlow", "Global SR routing not enabled for merchant {:?} txn {:?}", @@ -1661,14 +1854,15 @@ pub fn check_sr_global_routing_defaults( } pub fn is_forced_pm(v: &GatewaySuccessRateBasedRoutingInput) -> bool { - v.defaultGlobalEliminationLevel == Some(EliminationLevel::FORCED_PAYMENT_METHOD) + v.defaultGlobalEliminationLevel == Some(EliminationLevel::ForcedPaymentMethod) } pub fn global_elim_lvl_not_none(v: &GatewaySuccessRateBasedRoutingInput) -> bool { - v.defaultGlobalEliminationLevel != Some(EliminationLevel::NONE) + v.defaultGlobalEliminationLevel != Some(EliminationLevel::None) } pub async fn get_gateway_wise_routing_inputs_for_merchant_sr( + gatewayScoringData: GatewayScoringData, merchant_acc: ETM::merchant_account::MerchantAccount, txn_detail: ETTD::TxnDetail, txn_card_info: ETCT::txn_card_info::TxnCardInfo, @@ -1676,21 +1870,23 @@ pub async fn get_gateway_wise_routing_inputs_for_merchant_sr( gateway_success_rate_merchant_input: Option, default_success_rate_based_routing_input: Option, ) -> GatewayWiseSuccessRateBasedRoutingInput { - let m_option = - RService::findByNameFromRedis(C::SR_BASED_GATEWAY_ELIMINATION_THRESHOLD.get_key()).await; - let default_soft_txn_reset_count = - RService::findByNameFromRedis(C::srBasedTxnResetCount.get_key()) - .await - .unwrap_or(C::gwDefaultTxnSoftResetCount); - let is_elimination_v2_enabled = isFeatureEnabled( - C::ENABLE_ELIMINATION_V2.get_key(), + let default_elimination_threshold = RService::findByNameFromRedisWithDefault( + C::SrBasedGatewayEliminationThreshold.get_key(), + C::DEFAULT_SR_BASED_GATEWAY_ELIMINATION_THRESHOLD, + ) + .await; + let default_soft_txn_reset_count = RService::findByNameFromRedisWithDefault( + C::SR_BASED_TXN_RESET_COUNT.get_key(), + C::GW_DEFAULT_TXN_SOFT_RESET_COUNT, + ) + .await; + let is_elimination_v2_enabled = is_feature_enabled( + C::EnableEliminationV2.get_key(), merchant_acc.merchantId.0.clone(), "kv_redis".to_string(), ) .await; - let default_elimination_threshold = - m_option.unwrap_or(C::defaultSrBasedGatewayEliminationThreshold); let merchant_given_default_threshold = gateway_success_rate_merchant_input .clone() .map(|input| input.defaultEliminationThreshold); @@ -1724,9 +1920,14 @@ pub async fn get_gateway_wise_routing_inputs_for_merchant_sr( .unwrap_or(default_merchant_elimination_threshold.unwrap_or(default_elimination_threshold)); let elimination_threshold_updated = if is_elimination_v2_enabled { - get_elimination_v2_threshold(&merchant_acc, &txn_card_info, &txn_detail) - .await - .unwrap_or(elimination_threshold) + get_elimination_v2_threshold( + &merchant_acc, + &txn_card_info, + &txn_detail, + gatewayScoringData, + ) + .await + .unwrap_or(elimination_threshold) } else { elimination_threshold }; @@ -1739,7 +1940,7 @@ pub async fn get_gateway_wise_routing_inputs_for_merchant_sr( .eliminationLevel .clone() .or(merchant_given_default_elimination_level.clone()) - .or(Some(EliminationLevel::GATEWAY)), + .or(Some(EliminationLevel::Gateway)), ..e.clone() }) .unwrap_or(GatewayWiseSuccessRateBasedRoutingInput { @@ -1753,11 +1954,11 @@ pub async fn get_gateway_wise_routing_inputs_for_merchant_sr( gatewayLevelEliminationThreshold: merchant_given_default_gateway_sr_threshold .unwrap_or( default_gateway_level_sr_elimination_threshold - .unwrap_or(Some(C::defSRBasedGwLevelEliminationThreshold)), + .unwrap_or(Some(C::DEF_SRBASED_GW_LEVEL_ELIMINATION_THRESHOLD)), ), eliminationLevel: merchant_given_default_elimination_level .or(default_merchant_elimination_level) - .or(Some(EliminationLevel::PAYMENT_METHOD)), + .or(Some(EliminationLevel::PaymentMethod)), currentScore: None, lastResetTimeStamp: None, }) @@ -1767,6 +1968,7 @@ async fn get_elimination_v2_threshold( merchant_acc: &ETM::merchant_account::MerchantAccount, txn_card_info: &ETCT::txn_card_info::TxnCardInfo, txn_detail: &ETTD::TxnDetail, + gateway_Scoring_Data: GatewayScoringData, ) -> Option { let m_gateway_success_rate_merchant_input = Utils::decode_and_log_error( "Gateway Decider Input Decode Error", @@ -1791,21 +1993,25 @@ async fn get_elimination_v2_threshold( // }; // let sr2_th_weight = Env::lookup_env(sr2_th_weight_env).await; - if let Some((sr1, sr2, n, m_pmt, m_pm, m_txn_object_type, source)) = get_sr1_and_sr2_and_n( - m_gateway_success_rate_merchant_input, - merchant_acc.merchantId.0.clone(), - txn_card_info.clone(), - txn_detail.clone(), - ) - .await + if let Some((sr1, sr2, n, n_m_, m_pmt, m_pm, m_txn_object_type, _source)) = + get_sr1_and_sr2_and_n( + m_gateway_success_rate_merchant_input, + merchant_acc.merchantId.0.clone(), + txn_card_info.clone(), + txn_detail.clone(), + gateway_Scoring_Data.isGriEnabledForElimination, + gateway_Scoring_Data.gatewayReferenceId.clone(), + ) + .await { logger::info!( tag="scoringFlow", action = "scoringFlow", - "Calculating Threshold: SR1: {:?} SR2: {:?} N: {:?} PMT: {:?} PM: {:?} TxnObjectType: {:?} SourceObject: {:?}", + "Calculating Threshold: SR1: {:?} SR2: {:?} N: {:?} N_M_: {:?} PMT: {:?} PM: {:?} TxnObjectType: {:?} SourceObject: {:?}", sr1, sr2, n, + n_m_.unwrap_or(0.0), m_pmt.unwrap_or_else(|| "Nothing".to_string()), m_pm.unwrap_or_else(|| "Nothing".to_string()), m_txn_object_type.unwrap_or_else(|| "Nothing".to_string()), @@ -1843,84 +2049,182 @@ pub async fn get_sr1_and_sr2_and_n( merchant_id: String, txn_card_info: ETCT::txn_card_info::TxnCardInfo, txn_detail: ETTD::TxnDetail, + is_gri_enabled_for_elimination: bool, + gateway_reference_id: Option, ) -> Option<( f64, f64, f64, + Option, Option, Option, Option, ConfigSource, )> { - if let Some(gateway_success_rate_merchant_input) = m_gateway_success_rate_merchant_input { - if let Some(inputs) = gateway_success_rate_merchant_input.eliminationV2SuccessRateInputs { - let pmt = &txn_card_info.paymentMethodType; - let source_obj = if txn_card_info.paymentMethod == UPI { - txn_detail.sourceObject.clone() - } else { - Some(txn_card_info.paymentMethod.clone()) - }; - let pm = if txn_card_info.paymentMethodType == UPI { - source_obj.clone() - } else { - Some(txn_card_info.paymentMethod.clone()) - }; - let txn_obj_type = txn_detail.txnObjectType.to_string(); + let pmt = &txn_card_info.paymentMethodType; + let pm = if txn_card_info.paymentMethodType == UPI { + txn_detail.sourceObject.clone() + } else { + Some(txn_card_info.paymentMethod.clone()) + }; + let txn_obj_type = match txn_detail.txnObjectType { + Some(obj_type) => obj_type.to_text().to_string(), + None => return None, + }; + let _card_type = txn_card_info.card_type.clone(); + + match m_gateway_success_rate_merchant_input { + None => { + filter_using_redis( + merchant_id, + pmt.to_string(), + pm, + txn_obj_type, + None, + is_gri_enabled_for_elimination, + gateway_reference_id, + ) + .await + } + Some(ref gateway_success_rate_merchant_input) => { + let inputs = gateway_success_rate_merchant_input + .eliminationV2SuccessRateInputs + .as_ref(); + + // Try Redis first + let redis_result = filter_using_redis( + merchant_id.clone(), + pmt.to_string(), + pm.clone(), + txn_obj_type.clone(), + inputs.cloned(), + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), + ) + .await; - filter_using_service_config(merchant_id, pmt.to_string(), pm, txn_obj_type, inputs) - .await - } else { - None - // fetch_default_sr1_and_sr2_and_n(&gateway_success_rate_merchant_input).await + if redis_result.is_some() { + return redis_result; + } + + // Try Service Config + let service_config_result = filter_using_service_config( + merchant_id.clone(), + pmt.to_string(), + pm.clone(), + txn_obj_type.clone(), + inputs.cloned(), + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), + ) + .await; + + if service_config_result.is_some() { + return service_config_result; + } + + // Try default SR1 and SR2 and N + fetch_default_sr1_and_sr2_and_n(gateway_success_rate_merchant_input, &merchant_id).await } + } +} + +async fn fetch_default_sr1_and_sr2_and_n( + gateway_success_rate_merchant_input: &GatewaySuccessRateBasedRoutingInput, + merchant_id: &str, +) -> Option<( + f64, + f64, + f64, + Option, + Option, + Option, + Option, + ConfigSource, +)> { + if let Some(sr2) = gateway_success_rate_merchant_input.defaultEliminationV2SuccessRate { + fetch_default_sr1_and_n_and_mk_result(sr2, merchant_id).await } else { None } } -// async fn fetch_default_sr1_and_sr2_and_n( -// gateway_success_rate_merchant_input: &GatewayWiseSuccessRateBasedRoutingInput, -// ) -> Option<(f64, f64, f64, Option, Option, Option, ConfigSource)> { -// if let Some(sr2) = gateway_success_rate_merchant_input.default_elimination_v2_success_rate { -// fetch_default_sr1_and_n_and_mk_result(sr2).await -// } else { -// None -// } -// } +async fn fetch_default_sr1_and_n_and_mk_result( + sr2: f64, + merchant_id: &str, +) -> Option<( + f64, + f64, + f64, + Option, + Option, + Option, + Option, + ConfigSource, +)> { + let app_state = get_tenant_app_state().await; + let redis_conn = &app_state.redis_conn; -// async fn fetch_default_sr1_and_n_and_mk_result(sr2: f64) -> Option<(f64, f64, f64, Option, Option, Option, ConfigSource)> { -// let m_default_sr1 = RC::r_hget(Config::EC_REDIS, construct_sr1_key(merchant_id), C::DEFAULT_FIELD_NAME_FOR_SR1_AND_N).await; -// let m_default_n = RC::r_hget(Config::EC_REDIS, construct_n_key(merchant_id), C::DEFAULT_FIELD_NAME_FOR_SR1_AND_N).await; - -// if let (Some(sr1), Some(n)) = (m_default_sr1, m_default_n) { -// Some((sr1, sr2, n, None, None, None, ConfigSource::MERCHANT_DEFAULT)) -// } else { -// let m_s_config_sr1 = RService::find_by_name_from_redis(C::DEFAULT_SR1_S_CONFIG_PREFIX(merchant_id)).await; -// let m_s_config_n = RService::find_by_name_from_redis(C::DEFAULT_N_S_CONFIG_PREFIX(merchant_id)).await; - -// if let (Some(sr1), Some(n)) = (m_s_config_sr1, m_s_config_n) { -// Some((sr1, sr2, n, None, None, None, ConfigSource::GLOBAL_DEFAULT)) -// } else { -// None -// } -// } -// } + let sr1_key = format!("{}{}", C::SR1_KEY_PREFIX, merchant_id); + let n_key = format!("{}{}", C::N_KEY_PREFIX, merchant_id); + + let m_default_sr1 = redis_conn.get_key::(&sr1_key, "f64").await.ok(); + let m_default_n = redis_conn.get_key::(&n_key, "f64").await.ok(); + + if let (Some(sr1), Some(n)) = (m_default_sr1, m_default_n) { + Some(( + sr1, + sr2, + n, + None, + None, + None, + None, + ConfigSource::MerchantDefault, + )) + } else { + let sr1_config_key = C::defaultSr1SConfigPrefix(merchant_id.to_string()).get_key(); + let n_config_key = C::defaultNSConfigPrefix(merchant_id.to_string()).get_key(); + + let m_s_config_sr1 = RService::findByNameFromRedis::(sr1_config_key).await; + let m_s_config_n = RService::findByNameFromRedis::(n_config_key).await; + + if let (Some(sr1), Some(n)) = (m_s_config_sr1, m_s_config_n) { + Some(( + sr1, + sr2, + n, + None, + None, + None, + None, + ConfigSource::GlobalDefault, + )) + } else { + None + } + } +} async fn filter_using_service_config( merchant_id: String, pmt: String, pm: Option, txn_obj_type: String, - inputs: Vec, + inputs: Option>, + _is_gri_enabled_for_elimination: bool, + _gateway_reference_id: Option, ) -> Option<( f64, f64, f64, + Option, Option, Option, Option, ConfigSource, )> { + let inputs_vec = inputs.unwrap_or_default(); let m_configs = RService::findByNameFromRedis( C::internalDefaultEliminationV2SuccessRate1AndNPrefix(merchant_id.clone()).get_key(), ) @@ -1928,33 +2232,33 @@ async fn filter_using_service_config( let configs = m_configs.unwrap_or_else(Vec::new); fetch_sr1_and_n_from_service_config_upto( - FilterLevel::TXN_OBJECT_TYPE, + FilterLevel::TxnObjectType, merchant_id.clone(), pmt.clone(), pm.clone(), txn_obj_type.clone(), - inputs.clone(), + inputs_vec.clone(), configs.clone(), ) .or_else(|| { fetch_sr1_and_n_from_service_config_upto( - FilterLevel::PAYMENT_METHOD, + FilterLevel::PaymentMethod, merchant_id.clone(), pmt.clone(), pm.clone(), txn_obj_type.clone(), - inputs.clone(), + inputs_vec.clone(), configs.clone(), ) }) .or_else(|| { fetch_sr1_and_n_from_service_config_upto( - FilterLevel::PAYMENT_METHOD_TYPE, + FilterLevel::PaymentMethodType, merchant_id, pmt, pm, txn_obj_type, - inputs, + inputs_vec, configs, ) }) @@ -1968,95 +2272,32 @@ pub fn filter_inputs_upto( inputs: Vec, ) -> Option { match level { - FilterLevel::TXN_OBJECT_TYPE => { + FilterLevel::TxnObjectType => { filter_inputs_upto_txn_object_type(pmt, pm, txn_obj_type, inputs) } - FilterLevel::PAYMENT_METHOD => filter_inputs_upto_payment_method(pmt, pm, inputs), - FilterLevel::PAYMENT_METHOD_TYPE => filter_inputs_upto_payment_method_type(pmt, inputs), + FilterLevel::PaymentMethod => filter_inputs_upto_payment_method(pmt, pm, inputs), + FilterLevel::PaymentMethodType => filter_inputs_upto_payment_method_type(pmt, inputs), } } -// pub async fn filter_using_redis_upto( -// level: FilterLevel, -// merchant_id: T, -// pmt: T, -// pm: Option, -// txn_obj_type: T, -// inputs: Vec, -// ) -> Option<(f64, f64, f64, Option, Option, Option, ConfigSource)> { -// let m_input = filter_inputs_upto(level, pmt.clone(), pm.clone(), txn_obj_type.clone(), inputs); -// let m_sr1_and_n = get_sr1_and_n_from_redis_upto(level, merchant_id.clone(), pmt.clone(), pm.clone(), txn_obj_type.clone()).await; -// match (m_input, m_sr1_and_n) { -// (Some(input), Some((sr1, n))) => Some(( -// sr1, -// input.success_rate, -// n, -// Some(input.payment_method_type), -// input.payment_method.clone(), -// input.txn_object_type.clone(), -// ConfigSource::Redis, -// )), -// _ => None, -// } -// } - -// pub async fn get_sr1_and_n_from_redis_upto( -// level: FilterLevel, -// merchant_id: T, -// pmt: T, -// m_pm: Option, -// txn_obj_type: T, -// ) -> Option<(f64, f64)> { -// let sr1_key = construct_sr1_key(&merchant_id); -// let n_key = construct_n_key(&merchant_id); -// let dim_key = construct_dimension_key(level, &pmt, m_pm.as_ref(), &txn_obj_type); - -// let redis_sr1 = fetch_from_redis(&sr1_key, &dim_key).await; -// let redis_n = fetch_from_redis(&n_key, &dim_key).await; - -// match (redis_sr1, redis_n) { -// (Some(sr1), Some(n)) => Some((sr1, n)), -// _ => None, -// } -// } - -// fn construct_sr1_key(merchant_id: &T) -> T { -// format!("{}{}", C::SR1_KEY_PREFIX, merchant_id) -// } - -// fn construct_n_key(merchant_id: &T) -> T { -// format!("{}{}", C::N_KEY_PREFIX, merchant_id) -// } - -// fn construct_dimension_key( -// level: FilterLevel, -// pmt: &T, -// pm: Option<&T>, -// txn_obj_type: &T, -// ) -> Option { -// match level { -// FilterLevel::TxnObjectType => pm.map(|pm| format!("{}|{}|{}", pmt, pm, txn_obj_type)), -// FilterLevel::PaymentMethod => pm.map(|pm| format!("{}|{}", pmt, pm)), -// FilterLevel::PaymentMethodType => Some(pmt.clone()), -// } -// } - -// async fn fetch_from_redis(key: &T, dim_key: &Option) -> Option { -// match dim_key { -// None => None, -// Some(dkey) => RC::r_hget(Config::EC_REDIS, key, dkey).await, -// } -// } - pub fn fetch_sr1_and_n_from_service_config_upto( level: FilterLevel, - merchant_id: String, + _merchant_id: String, pmt: String, pm: Option, txn_object_type: String, inputs: Vec, configs: Vec, -) -> Option<(f64, f64, f64, Option, Option, Option, ConfigSource)> { +) -> Option<( + f64, + f64, + f64, + Option, + Option, + Option, + Option, + ConfigSource, +)> { let m_input = filter_inputs_upto( level.clone(), pmt.clone(), @@ -2065,13 +2306,13 @@ pub fn fetch_sr1_and_n_from_service_config_upto( inputs, ); let m_config = match level { - FilterLevel::TXN_OBJECT_TYPE => { + FilterLevel::TxnObjectType => { filter_configs_upto_txn_object_type(&pmt, pm.as_ref(), &txn_object_type, &configs) } - FilterLevel::PAYMENT_METHOD => { + FilterLevel::PaymentMethod => { filter_configs_upto_payment_method(&pmt, pm.as_ref(), &configs) } - FilterLevel::PAYMENT_METHOD_TYPE => filter_configs_upto_payment_method_type(&pmt, &configs), + FilterLevel::PaymentMethodType => filter_configs_upto_payment_method_type(&pmt, &configs), }; match (m_input, m_config) { @@ -2079,10 +2320,11 @@ pub fn fetch_sr1_and_n_from_service_config_upto( config.successRate, input.successRate, config.nValue, + None, // Added missing Option element Some(input.paymentMethodType), input.paymentMethod.clone(), input.txnObjectType.clone(), - ConfigSource::SERVICE_CONFIG, + ConfigSource::ServiceConfig, )), _ => None, } @@ -2179,8 +2421,7 @@ pub async fn get_success_rate_routing_inputs( Option, Option, ) { - let redis_input = - findByNameFromRedis(C::DEFAULT_SR_BASED_GATEWAY_ELIMINATION_INPUT.get_key()).await; + let redis_input = findByNameFromRedis(C::DefaultSrBasedGatewayEliminationInput.get_key()).await; let decoded_input = Utils::decode_and_log_error( "Gateway Decider Input Decode Error", &merchant_acc.gatewaySuccessRateBasedDeciderInput, @@ -2219,10 +2460,10 @@ pub async fn update_gateway_score_based_on_success_rate( let txn_detail = decider_flow.get().dpTxnDetail.clone(); let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); let enable_success_rate_based_gateway_elimination = isPaymentFlowEnabledWithHierarchyCheck( - merchant_acc.id.clone(), + merchant_acc.id, merchant_acc.tenantAccountId.clone(), - ModuleName::MERCHANT_CONFIG, - PaymentFlow::ELIMINATION_BASED_ROUTING, + ModuleName::MerchantConfig, + PaymentFlow::EliminationBasedRouting, crate::types::country::country_iso::text_db_to_country_iso( merchant_acc.country.as_deref().unwrap_or_default(), ) @@ -2243,13 +2484,30 @@ pub async fn update_gateway_score_based_on_success_rate( let (default_success_rate_based_routing_input, gateway_success_rate_merchant_input) = get_success_rate_routing_inputs(merchant_acc.clone()).await; - let is_reset_score_enabled_for_merchant = isFeatureEnabled( - C::GATEWAY_RESET_SCORE_ENABLED.get_key(), + logger::debug!( + action = "update_gateway_score_based_on_success_rate", + "Default SR based routing input: {:?}", + default_success_rate_based_routing_input + ); + logger::debug!( + action = "update_gateway_score_based_on_success_rate", + "Merchant SR based routing input: {:?}", + gateway_success_rate_merchant_input + ); + + let is_reset_score_enabled_for_merchant = is_feature_enabled( + C::GatewayResetScoreEnabled.get_key(), Utils::get_m_id(txn_detail.merchantId.clone()), "kv_redis".to_string(), ) .await; + logger::debug!( + action = "update_gateway_score_based_on_success_rate", + "Is reset score enabled for merchant {:?}", + is_reset_score_enabled_for_merchant + ); + let payment_method_type = if Utils::is_card_transaction(&txn_card_info) { CARD } else { @@ -2261,10 +2519,12 @@ pub async fn update_gateway_score_based_on_success_rate( .map(|input| input.enabledPaymentMethodTypes.clone()) .unwrap_or_default(); + logger::debug!(action = "update_gateway_score_based_on_success_rate","Enabled payment method types for merchant {:?} and Payment method type for transaction {:?}", enabled_payment_method_types, payment_method_type); + if !enabled_payment_method_types.is_empty() && !enabled_payment_method_types.contains(&payment_method_type.to_string()) { - logger::info!( + logger::debug!( tag="scoringFlow", action = "scoringFlow", "Transaction {:?} with payment method types {:?} not enabled by {:?} for SR based routing", @@ -2285,7 +2545,7 @@ pub async fn update_gateway_score_based_on_success_rate( ) .await; - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Gateway scores input for merchant wise SR based evaluation for {:?} : {:?}", @@ -2303,6 +2563,7 @@ pub async fn update_gateway_score_based_on_success_rate( for (gw, _) in gateway_score_global_sr.clone() { gateway_success_rate_inputs.push( get_gateway_wise_routing_inputs_for_merchant_sr( + gateway_scoring_data.clone(), merchant_acc.clone(), txn_detail.clone(), txn_card_info.clone(), @@ -2319,7 +2580,7 @@ pub async fn update_gateway_score_based_on_success_rate( let gateway_redis_key_map = Utils::get_consumer_key( decider_flow, gateway_scoring_data.clone(), - ScoreKeyType::ELIMINATION_MERCHANT_KEY, + ScoreKeyType::EliminationMerchantKey, false, gateway_list.clone(), ) @@ -2407,7 +2668,7 @@ pub async fn update_gateway_score_based_on_success_rate( }, ); - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "No gateways are eligible for penalties & fallback : {:?}", @@ -2446,6 +2707,7 @@ pub async fn update_gateway_score_based_on_success_rate( ); Utils::metric_tracker_log( + decider_flow.get().dpShouldConsumeResult, "SR_EVALUATION", "GW_SCORING", Utils::get_metric_log_format(decider_flow, "SR_EVALUATION"), @@ -2494,7 +2756,7 @@ pub async fn update_gateway_score_based_on_success_rate( // }; let reset_gw_list = decider_flow.writer.resetGatewayList.clone(); - if (!reset_gw_list.is_empty()) { + if !reset_gw_list.is_empty() { trigger_reset_gateway_score( decider_flow, gateway_success_rate_inputs, @@ -2502,6 +2764,7 @@ pub async fn update_gateway_score_based_on_success_rate( reset_gw_list, is_reset_score_enabled_for_merchant, gateway_redis_key_map.clone(), + gateway_scoring_data.clone(), ) .await; } @@ -2511,8 +2774,8 @@ pub async fn update_gateway_score_based_on_success_rate( if filtered_gateway_success_rate_inputs.len() > 1 && new_gateway_score.len() == filtered_gateway_success_rate_inputs.len() { - let optimization_during_downtime_enabled = isFeatureEnabled( - C::ENABLE_OPTIMIZATION_DURING_DOWNTIME.get_key(), + let optimization_during_downtime_enabled = is_feature_enabled( + C::EnableOptimizationDuringDowntime.get_key(), Utils::get_m_id(txn_detail.merchantId.clone()), "kv_redis".to_string(), ) @@ -2520,7 +2783,7 @@ pub async fn update_gateway_score_based_on_success_rate( if optimization_during_downtime_enabled { if is_sr_metric_enabled { - logger::info!( + logger::debug!( tag="scoringFlow", action = "scoringFlow", "Overriding priority with SR Scores during downtime for {:?} : {:?}", @@ -2528,18 +2791,18 @@ pub async fn update_gateway_score_based_on_success_rate( new_gateway_score, ); - (new_gateway_score.clone(), DownTime::ALL_DOWNTIME, vec![]) + (new_gateway_score.clone(), DownTime::AllDowntime, vec![]) } else { - logger::info!( + logger::debug!( "Overriding priority with PL during downtime for {:?} : {:?}", txn_detail.txnId, initial_gw_scores, ); - (initial_gw_scores.clone(), DownTime::ALL_DOWNTIME, vec![]) + (initial_gw_scores.clone(), DownTime::AllDowntime, vec![]) } } else { - logger::info!( + logger::debug!( tag="scoringFlow", action = "scoringFlow", "Overriding priority with SR Scores during downtime is not enabled for {:?} : {:?}", @@ -2549,7 +2812,7 @@ pub async fn update_gateway_score_based_on_success_rate( ( new_gateway_score.clone(), - DownTime::ALL_DOWNTIME, + DownTime::AllDowntime, sr_based_elimination_approach_info, ) } @@ -2559,19 +2822,19 @@ pub async fn update_gateway_score_based_on_success_rate( { ( new_gateway_score.clone(), - DownTime::GLOBAL_DOWNTIME, + DownTime::GlobalDowntime, sr_based_elimination_approach_info, ) } else if !filtered_gateway_success_rate_inputs.is_empty() { ( new_gateway_score.clone(), - DownTime::DOWNTIME, + DownTime::Downtime, sr_based_elimination_approach_info, ) } else { ( new_gateway_score.clone(), - DownTime::NO_DOWNTIME, + DownTime::NoDowntime, sr_based_elimination_approach_info, ) }; @@ -2589,14 +2852,14 @@ pub async fn update_gateway_score_based_on_success_rate( sr_based_elimination_approach_info_res, ); - logger::info!("routing_approach: {:?}", gateway_decider_approach); + logger::debug!("routing_approach: {:?}", gateway_decider_approach); } } } let gateway_score_sr_based = get_gwsm(decider_flow); - logger::info!( + logger::debug!( tag = "GW_Scoring", action = "GW_Scoring", "Gateway scores after considering SR based elimination for {:?} : {:?}", @@ -2618,19 +2881,19 @@ pub fn update_score_with_log( ) -> GatewayScoreMap { let new_m = m .iter() - .filter_map(|(gw, score)| { + .map(|(gw, score)| { if *gw == v.gateway { let new_score = *score / 5_f64; - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Penalizing gateway {:?} for {:?}", v.gateway, txn_id ); - Some((gw.clone(), new_score)) + (gw.clone(), new_score) } else { - Some((gw.clone(), *score)) + (gw.clone(), *score) } }) .collect(); @@ -2652,13 +2915,13 @@ pub async fn update_current_score( i: ETGRI::GatewayWiseSuccessRateBasedRoutingInput, ) -> ETGRI::GatewayWiseSuccessRateBasedRoutingInput { let redis_key = gateway_redis_key_map - .get(&format!("{}", i.gateway)) + .get(&i.gateway.to_string()) .unwrap_or(&String::new()) .to_string(); let txn_detail = decider_flow.get().dpTxnDetail.clone(); let m_score = get_merchant_elimination_gateway_score(redis_key).await; - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Current score for {:?} {:?} : {:?} with elimination level {:?} threshold {:?}", @@ -2684,8 +2947,8 @@ pub fn merchantGatewayScoreDimension( routingInput: GatewayWiseSuccessRateBasedRoutingInput, ) -> Dimension { match routingInput.eliminationLevel { - Some(EliminationLevel::PAYMENT_METHOD_TYPE) => Dimension::SECOND, - Some(EliminationLevel::PAYMENT_METHOD) => Dimension::THIRD, + Some(EliminationLevel::PaymentMethodType) => Dimension::SECOND, + Some(EliminationLevel::PaymentMethod) => Dimension::THIRD, _ => Dimension::FIRST, } } @@ -2693,20 +2956,20 @@ pub fn merchantGatewayScoreDimension( pub async fn getKeyTTLFromMerchantDimension(dimension: Dimension) -> f64 { let mTtl: Option = match dimension { Dimension::FIRST => { - RService::findByNameFromRedis(C::gwScoreFirstDimensionTtl.get_key()).await + RService::findByNameFromRedis(C::GW_SCORE_FIRST_DIMENSION_TTL.get_key()).await } Dimension::SECOND => { - RService::findByNameFromRedis(C::gwScoreSecondDimensionTtl.get_key()).await + RService::findByNameFromRedis(C::GW_SCORE_SECOND_DIMENSION_TTL.get_key()).await } Dimension::THIRD => { - RService::findByNameFromRedis(C::gwScoreThirdDimensionTtl.get_key()).await + RService::findByNameFromRedis(C::GW_SCORE_THIRD_DIMENSION_TTL.get_key()).await } Dimension::FOURTH => { - RService::findByNameFromRedis(C::gwScoreFourthDimensionTtl.get_key()).await + RService::findByNameFromRedis(C::GW_SCORE_FOURTH_DIMENSION_TTL.get_key()).await } }; - mTtl.unwrap_or(C::defScoreKeysTtl) + mTtl.unwrap_or(C::DEF_SCORE_KEYS_TTL) } pub async fn evaluate_reset_gateway_score( @@ -2731,7 +2994,7 @@ pub async fn evaluate_reset_gateway_score( let key_ttl = getKeyTTLFromMerchantDimension(merchantGatewayScoreDimension(it.clone())).await; if let Some(last_reset_time) = it.lastResetTimeStamp { - if (current_time * 1000 - last_reset_time as i64) > key_ttl.round() as i64 { + if (current_time * 1000 - last_reset_time) > key_ttl.round() as i64 { logger::debug!( tag = "evaluateResetGatewayScore", action = "evaluateResetGatewayScore", @@ -2755,15 +3018,16 @@ pub async fn trigger_reset_gateway_score( reset_gateway_list: Vec, is_reset_score_enabled_for_merchant: bool, gateway_redis_key_map: GatewayRedisKeyMap, + gateway_scoring_data: GatewayScoringData, ) { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Triggering Reset for Gateways for {:?}", reset_gateway_list ); if is_reset_score_enabled_for_merchant { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Reset Gateway Scores is enabled for {:?} and merchantId {:?}", @@ -2772,7 +3036,7 @@ pub async fn trigger_reset_gateway_score( ); let mut reset_gateway_sr_list = Vec::new(); for it in &reset_gateway_list { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Adding gateway {:?} to resetAPI Request for {:?}", @@ -2791,19 +3055,22 @@ pub async fn trigger_reset_gateway_score( if let Some(sr_input) = m_sr_input { let gw_ref_id = Utils::get_gateway_reference_id(meta, it, oref, pl_ref_id_map); - let hard_ttl = getTTLForKey(ScoreKeyType::ELIMINATION_MERCHANT_KEY).await; + let hard_ttl = getTTLForKey(ScoreKeyType::EliminationMerchantKey).await; let soft_ttl = getKeyTTLFromMerchantDimension(merchantGatewayScoreDimension(sr_input.clone())) .await; let reset_gateway_input = ResetGatewayInput { gateway: it.clone(), eliminationThreshold: sr_input.eliminationThreshold, - eliminationMaxCount: sr_input.softTxnResetCount.map(|v| v as i64), + eliminationMaxCount: sr_input.softTxnResetCount.map(|v| v), gatewayEliminationThreshold: sr_input.gatewayLevelEliminationThreshold, gatewayReferenceId: gw_ref_id.map(|id| id.mga_reference_id), key: gateway_redis_key_map.get(it).cloned(), hardTtl: hard_ttl, softTtl: soft_ttl, + gatewayReferenceIdEnabled: Some( + gateway_scoring_data.isGriEnabledForElimination, + ), }; // Now these await calls are in an async context @@ -2811,11 +3078,13 @@ pub async fn trigger_reset_gateway_score( decider_flow, txn_detail.clone(), reset_gateway_input.clone(), + gateway_scoring_data.clone(), + decider_flow.get().dpRedisCompressionConfig.clone(), ) .await; reset_gateway_sr_list.push(reset_gateway_input.clone()); } else { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "No SR Input for {:?} and {:?}", @@ -2827,21 +3096,21 @@ pub async fn trigger_reset_gateway_score( let reset_approach = Utils::get_reset_approach(decider_flow); match reset_approach { - ResetApproach::SRV2_RESET => { - Utils::set_reset_approach(decider_flow, ResetApproach::SRV2_ELIMINATION_RESET) + ResetApproach::Srv2Reset => { + Utils::set_reset_approach(decider_flow, ResetApproach::Srv2EliminationReset) } - ResetApproach::SRV3_RESET => { - Utils::set_reset_approach(decider_flow, ResetApproach::SRV3_ELIMINATION_RESET) + ResetApproach::Srv3Reset => { + Utils::set_reset_approach(decider_flow, ResetApproach::Srv3EliminationReset) } - _ => Utils::set_reset_approach(decider_flow, ResetApproach::ELIMINATION_RESET), + _ => Utils::set_reset_approach(decider_flow, ResetApproach::EliminationReset), } - logger::info!( + logger::debug!( tag = "RESET_APPROACH", action = "RESET_APPROACH", "{:?}", reset_approach ); - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Reset Gateway List for {:?} is {:?}", @@ -2849,7 +3118,7 @@ pub async fn trigger_reset_gateway_score( reset_gateway_sr_list ); } else { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Reset Gateway Scores is not enabled for {:?} and merchantId {:?}", @@ -2873,6 +3142,8 @@ pub async fn reset_gateway_score( decider_flow: &mut DeciderFlow<'_>, txn_detail: ETTD::TxnDetail, reset_gateway_input: ResetGatewayInput, + gateway_scoring_data: GatewayScoringData, + redis_compression_config: Option, ) { let current_timestamp = get_current_date_in_millis(); match ( @@ -2881,17 +3152,26 @@ pub async fn reset_gateway_score( reset_gateway_input.clone().eliminationMaxCount, ) { (Some(key), Some(threshold), Some(max_count)) => { - let penality_factor = Utils::get_penality_factor_(decider_flow).await; + let penality_factor = + Utils::get_penality_factor_(decider_flow, &gateway_scoring_data).await; let score = get_merchant_elimination_gateway_score(key.clone()).await; + logger::debug!( + tag = "scoringFlow", + action = "scoringFlow", + "Current Gateway Score for {:?} : key {:?} before reset attempt: {:?}", + txn_detail.txnId, + key, + score + ); let (is_eligible_for_reset, reset_cached_gateway_score) = match score { Some(score) => { - let current_score = score.score; + let _current_score = score.score; let transaction_count = score.transactionCount; - let last_reset_time = score.lastResetTimestamp; - let is_eligible_for_reset_ = Utils::is_reset_eligibile( + let _last_reset_time = score.lastResetTimestamp; + let is_eligible_for_reset_ = Utils::is_reset_eligible( Some(reset_gateway_input.clone().softTtl), - current_timestamp.clone(), - threshold.clone(), + current_timestamp, + threshold, score.clone(), ); if is_eligible_for_reset_ { @@ -2900,12 +3180,12 @@ pub async fn reset_gateway_score( let reset_cached_gateway_score_ = GatewayScore { score: reset_score, transactionCount: transaction_count, - lastResetTimestamp: current_timestamp.clone() as i64, + lastResetTimestamp: current_timestamp as i64, timestamp: score.clone().timestamp, }; (true, reset_cached_gateway_score_) } else { - logger::info!( + logger::debug!( tag = "scoringFresetKeyScorelow", action = "resetKeyScore", "Key {:?} is not eligible for reset", @@ -2918,10 +3198,10 @@ pub async fn reset_gateway_score( let default_gw_score = GatewayScore { score: 1.0, transactionCount: 0, - lastResetTimestamp: current_timestamp.clone() as i64, - timestamp: current_timestamp.clone() as i64, + lastResetTimestamp: current_timestamp as i64, + timestamp: current_timestamp as i64, }; - logger::info!( + logger::debug!( tag = "hard Reset", action = "hard Reset", "Score for key {:?} is being hard reset", @@ -2932,9 +3212,8 @@ pub async fn reset_gateway_score( }; let elapsed_time = current_timestamp.saturating_sub(reset_cached_gateway_score.timestamp as u128); - let remaining_ttl = (reset_gateway_input.hardTtl as u128).saturating_sub(elapsed_time); - #[allow(clippy::absurd_extreme_comparisons)] - let safe_remaining_ttl = if remaining_ttl < 0 { + let remaining_ttl = reset_gateway_input.hardTtl.saturating_sub(elapsed_time); + let safe_remaining_ttl = if elapsed_time > reset_gateway_input.hardTtl { reset_gateway_input.hardTtl as i64 } else { remaining_ttl as i64 @@ -2943,12 +3222,13 @@ pub async fn reset_gateway_score( key.clone(), reset_cached_gateway_score.clone(), safe_remaining_ttl, + redis_compression_config, ) .await; match result { Ok(_) => { if is_eligible_for_reset { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Resetting Gateway Score for {:?} with new score {:?}", @@ -2956,7 +3236,7 @@ pub async fn reset_gateway_score( reset_cached_gateway_score ); } else { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Gateway Score is not eligible for reset for {:?}", @@ -2965,7 +3245,7 @@ pub async fn reset_gateway_score( } } Err(e) => { - logger::error!( + logger::info!( tag = "scoringFlow", action = "scoringFlow", "Failed to reset Gateway Score for {:?} with error: {:?}", @@ -2976,7 +3256,7 @@ pub async fn reset_gateway_score( } } _ => { - logger::info!( + logger::debug!( tag = "scoringFlow", action = "scoringFlow", "Reset Gateway Score is not enabled for {:?} and merchantId {:?}", @@ -2995,7 +3275,7 @@ pub fn route_random_traffic( decider_flow: &mut DeciderFlow<'_>, gws: GatewayScoreMap, hedging_percent: f64, - is_sr_v3_metric_enabled: bool, + _is_sr_v3_metric_enabled: bool, tag: String, ) -> GatewayScoreMap { let num = generate_random_number( @@ -3014,11 +3294,11 @@ pub fn route_random_traffic( if num < hedging_percent * (remaining_gateways.len() as f64) { let remaining_gateways: Vec<_> = remaining_gateways .iter() - .map(|(gw, _)| (gw.clone(), 1.0)) + .map(|(gw, _)| ((*gw).clone(), 1.0)) .collect(); let head_gateways: Vec<_> = head_gateway .iter() - .map(|(gw, _)| (gw.clone(), 0.5)) + .map(|(gw, _)| ((*gw).clone(), 0.5)) .collect(); logger::debug!( "Gateway Scores After Route Random Traffic Feature: {:?}", @@ -3027,11 +3307,9 @@ pub fn route_random_traffic( .chain(head_gateways.iter()) .collect::>() ); - if is_sr_v3_metric_enabled { - set_decider_approach(decider_flow, GatewayDeciderApproach::SR_V3_HEDGING); - } else { - set_decider_approach(decider_flow, GatewayDeciderApproach::SR_V3_HEDGING); - } + + set_decider_approach(decider_flow, GatewayDeciderApproach::SrV3Hedging); + remaining_gateways .into_iter() .map(|(gw, score)| (gw.clone(), score)) @@ -3049,3 +3327,199 @@ pub fn route_random_traffic( .collect() } } + +pub async fn filter_using_redis( + merchant_id: String, + pmt: String, + pm: Option, + txn_obj_type: String, + inputs: Option>, + is_gri_enabled_for_elimination: bool, + gateway_reference_id: Option, +) -> Option<( + f64, + f64, + f64, + Option, + Option, + Option, + Option, + ConfigSource, +)> { + let inputs_vec = inputs.unwrap_or_default(); + filter_using_redis_upto( + None, + FilterLevel::TxnObjectType, + merchant_id.clone(), + pmt.clone(), + pm.clone(), + txn_obj_type.clone(), + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), + inputs_vec.clone(), + None, + ) + .await + .or(filter_using_redis_upto( + None, + FilterLevel::PaymentMethod, + merchant_id.clone(), + pmt.clone(), + pm.clone(), + txn_obj_type.clone(), + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), + inputs_vec.clone(), + None, + ) + .await) + .or(filter_using_redis_upto( + None, + FilterLevel::PaymentMethodType, + merchant_id, + pmt, + pm, + txn_obj_type, + is_gri_enabled_for_elimination, + gateway_reference_id, + inputs_vec, + None, + ) + .await) +} + +async fn filter_using_redis_upto( + m_metric_entry: Option, + level: FilterLevel, + merchant_id: String, + pmt: String, + pm: Option, + txn_obj_type: String, + is_gri_enabled_for_elimination: bool, + gateway_reference_id: Option, + inputs: Vec, + card_type: Option, +) -> Option<( + f64, + f64, + f64, + Option, + Option, + Option, + Option, + ConfigSource, +)> { + let m_input = filter_inputs_upto( + level.clone(), + pmt.clone(), + pm.clone(), + txn_obj_type.clone(), + inputs, + ); + + let m_metric_entry_final = match m_metric_entry { + Some(entry) => Some(entry), + None => { + get_metric_entry_data( + merchant_id.clone(), + pmt.clone(), + pm.clone(), + txn_obj_type.clone(), + card_type, + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), + ) + .await + } + }; + + match (m_input, m_metric_entry_final) { + (Some(input), Some(metric_entry)) => Some(( + metric_entry.success_rate.into(), + input.successRate, + metric_entry.sigma_factor.into(), + Some(metric_entry.n_value.into()), + Some(input.paymentMethodType), + input.paymentMethod, + input.txnObjectType, + ConfigSource::REDIS, + )), + (None, Some(metric_entry)) => Some(( + metric_entry.success_rate.into(), + metric_entry.default_success_threshold.into(), + metric_entry.sigma_factor.into(), + Some(metric_entry.n_value.into()), + Some(pmt), + pm, + Some(txn_obj_type), + ConfigSource::REDIS, + )), + _ => None, + } +} + +pub async fn get_metric_entry_data( + merchant_id: String, + pmt: String, + m_pm: Option, + txn_obj_type: String, + card_type: Option, + isGriEnabledForElimination: bool, + gatewayReferenceId: Option, +) -> Option { + let aggregate_key = construct_aggregate_key(&merchant_id); + let dim_key = construct_dimension_key(&pmt, &m_pm, &txn_obj_type, &card_type); + + if isGriEnabledForElimination && gatewayReferenceId.is_some() { + let gri_dim_key = format!( + "{}_{}", + dim_key.clone().unwrap_or_default(), + gatewayReferenceId.unwrap_or_default() + ); + fetch_from_redis(&aggregate_key, &Some(gri_dim_key)).await + } else { + fetch_from_redis(&aggregate_key, &dim_key).await + } +} + +fn construct_dimension_key( + pmt: &str, + m_pm: &Option, + txn_obj_type: &str, + card_type: &Option, +) -> Option { + if pmt == CARD { + Some(format!( + "{}_{}_{}_{}", + txn_obj_type, + pmt, + m_pm.as_ref().unwrap_or(&String::new()), + card_type.as_ref().unwrap_or(&String::new()) + )) + } else { + Some(format!( + "{}_{}_{}", + txn_obj_type, + pmt, + m_pm.as_ref().unwrap_or(&String::new()) + )) + } +} + +async fn fetch_from_redis(key: &str, dim_key: &Option) -> Option { + match dim_key { + None => None, + Some(dkey) => { + let app_state = get_tenant_app_state().await; + app_state + .redis_conn + .hget::(key, dkey, "metric_entry") + .await + .ok() + .flatten() + } + } +} +fn construct_aggregate_key(merchant_id: &str) -> String { + format!("{}{}", C::AGGREGATE_KEY_PREFIX, merchant_id) +} diff --git a/src/decider/gatewaydecider/runner.rs b/src/decider/gatewaydecider/runner.rs index ccf7dc2c..8113fc3a 100644 --- a/src/decider/gatewaydecider/runner.rs +++ b/src/decider/gatewaydecider/runner.rs @@ -35,14 +35,16 @@ use crate::{ }, utils::call_api, }; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use serde_json::{from_str, Value}; use crate::decider::gatewaydecider::types as DeciderTypes; use super::utils; +use crate::decider::gatewaydecider::utils::mask_secret_option; use crate::types::payment::payment_method_type_const::*; + // use serde_json::Value as AValue; // use eulerhs::prelude::*; // use data::aeson::{Object, either_decode, (.:)}; @@ -156,14 +158,14 @@ pub struct FilteredTxnInfo { pub fn filter_txn(detail: TxnDetail) -> FilteredTxnInfo { FilteredTxnInfo { - isEmi: detail.isEmi, + isEmi: detail.isEmi.unwrap_or(false), emiBank: detail.emiBank, emiTenure: detail.emiTenure, txnId: detail.txnId, - addToLocker: detail.addToLocker, - expressCheckout: detail.expressCheckout, + addToLocker: detail.addToLocker.unwrap_or(false), + expressCheckout: detail.expressCheckout.unwrap_or(false), sourceObject: detail.sourceObject, - txnObjectType: detail.txnObjectType, + txnObjectType: detail.txnObjectType.unwrap_or(TxnObjectType::Unknown), } } @@ -171,10 +173,10 @@ pub fn fetch_emi_type(txnCardInfo: TxnCardInfo) -> Result> match txnCardInfo.paymentSource { None => Err(vec![]), Some(ps) => { - if ps.contains("emi_type") { + if ps.peek().contains("emi_type") { Err(vec![]) } else { - match from_str::(&ps) { + match from_str::(ps.peek()) { Ok(value) => match value.get("emi_type") { Some(emi_type) => match emi_type.as_str() { Some(emi_type_str) => Ok(emi_type_str.to_string()), @@ -206,7 +208,8 @@ pub struct FilteredPaymentInfo { pub paymentMethod: Option, pub paymentMethodType: Option, pub authType: Option, - pub paymentSource: Option, + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, pub emiType: Option, pub cardSubType: Option, pub storedCardProvider: Option, @@ -235,12 +238,12 @@ pub fn make_payment_info( paymentMethodType: txnCardInfo .card_type .as_ref() - .map(|ct| card_type_to_text(&ct)) - .or_else(|| Some(txnCardInfo.paymentMethodType)), + .map(card_type_to_text) + .or(Some(txnCardInfo.paymentMethodType)), paymentMethod: txnCardInfo .cardIssuerBankName .clone() - .or_else(|| Some(txnCardInfo.paymentMethod)), + .or(Some(txnCardInfo.paymentMethod)), paymentSource: txnCardInfo.paymentSource, cardIssuer: txnCardInfo.cardIssuerBankName, cardType: txnCardInfo.card_type.map(|c| card_type_to_text(&c)), @@ -479,10 +482,10 @@ pub fn parse_log_entry(log: Vec) -> LogEntry { pub fn pl_execution_retry_failure_reasons() -> Vec { vec![ - DeciderTypes::PriorityLogicFailure::CONNECTION_FAILED, - DeciderTypes::PriorityLogicFailure::RESPONSE_CONTENT_TYPE_NOT_SUPPORTED, - DeciderTypes::PriorityLogicFailure::RESPONSE_DECODE_FAILURE, - DeciderTypes::PriorityLogicFailure::RESPONSE_PARSE_ERROR, + DeciderTypes::PriorityLogicFailure::ConnectionFailed, + DeciderTypes::PriorityLogicFailure::ResponseContentTypeNotSupported, + DeciderTypes::PriorityLogicFailure::ResponseDecodeFailure, + DeciderTypes::PriorityLogicFailure::ResponseParseError, ] } @@ -493,7 +496,7 @@ pub async fn execute_priority_logic( .txnDetail .internalMetadata .as_ref() - .and_then(|im| serde_json::from_str(im).ok()); + .and_then(|im| serde_json::from_str(im.peek()).ok()); let order_metadata = req.orderMetadata.metadata.clone(); // resolveBin <- case Utils.fetchExtendedCardBin req.txnCardInfo of // Just cardBin -> pure (Just cardBin) @@ -506,7 +509,9 @@ pub async fn execute_priority_logic( let resolve_bin = match utils::fetch_extended_card_bin(&req.txnCardInfo) { Some(card_bin) => Some(card_bin), None => match req.txnCardInfo.card_isin { - Some(c_isin) => Some(utils::get_card_bin_from_token_bin(6, c_isin.as_str()).await), + Some(c_isin) => { + Some(utils::get_card_bin_from_token_bin(6, c_isin.as_str(), None).await) + } None => None, }, }; @@ -563,9 +568,16 @@ pub async fn get_gateway_priority( let (script, priority_logic_tag) = get_script(macc.clone(), priority_logic_script_m).await; let result = evaluate_script(script.clone(), priority_logic_tag.clone()).await; + logger::info!( + tag = "PRIORITY_LOGIC_EXECUTION_RESULT", + "MerchantId: {:?} , Result: {:?}", + macc.merchantId.clone(), + result + ); + match result { - EvaluationResult::PLResponse(gws, pl_data, logs, status) => { - logger::info!( + EvaluationResult::PLResponse(gws, pl_data, logs, _status) => { + logger::debug!( tag = "PRIORITY_LOGIC_EXECUTION_", "MerchantId: {:?} , Gateways: {:?}, Logs: {:?}", macc.merchantId.clone(), @@ -573,16 +585,16 @@ pub async fn get_gateway_priority( logs ); DeciderTypes::GatewayPriorityLogicOutput { - isEnforcement: gws.isEnforcement, + is_enforcement: gws.is_enforcement, gws: gws.gws, - priorityLogicTag: gws.priorityLogicTag, - gatewayReferenceIds: gws.gatewayReferenceIds, - primaryLogic: Some(pl_data), - fallbackLogic: gws.fallbackLogic, + priority_logic_tag: gws.priority_logic_tag, + gateway_reference_ids: gws.gateway_reference_ids, + primary_logic: Some(pl_data), + fallback_logic: gws.fallback_logic, } } EvaluationResult::EvaluationError(priority_logic_data, err) => { - logger::info!( + logger::debug!( tag = "PRIORITY_LOGIC_EXECUTION_FAILURE", "MerchantId: {:?}, Error: {:?}", macc.merchantId, @@ -595,7 +607,7 @@ pub async fn get_gateway_priority( match retry_result { EvaluationResult::PLResponse(retry_gws, retry_pl_data, logs, status) => { let tag = format!("PRIORITY_LOGIC_EXECUTION_RETRY_{}", status); - logger::info!( + logger::debug!( tag = %tag, "MerchantId: {:?} , Gateways: {:?}, Logs: {:?}", macc.merchantId, @@ -603,16 +615,16 @@ pub async fn get_gateway_priority( logs ); DeciderTypes::GatewayPriorityLogicOutput { - isEnforcement: retry_gws.isEnforcement, + is_enforcement: retry_gws.is_enforcement, gws: retry_gws.gws, - priorityLogicTag: retry_gws.priorityLogicTag, - gatewayReferenceIds: retry_gws.gatewayReferenceIds, - primaryLogic: Some(retry_pl_data), - fallbackLogic: retry_gws.fallbackLogic, + priority_logic_tag: retry_gws.priority_logic_tag, + gateway_reference_ids: retry_gws.gateway_reference_ids, + primary_logic: Some(retry_pl_data), + fallback_logic: retry_gws.fallback_logic, } } EvaluationResult::EvaluationError(retry_pl_data, err) => { - logger::info!( + logger::debug!( tag = "PRIORITY_LOGIC_EXECUTION_RETRY_FAILURE", "MerchantId: {:?} , Error: {:?}", macc.merchantId, @@ -627,8 +639,8 @@ pub async fn get_gateway_priority( m_internal_meta, order_meta_data, default_gateway_priority_logic_output() - .setPriorityLogicTag(priority_logic_tag) - .setPrimaryLogic(Some(retry_pl_data)) + .set_priority_logic_tag(priority_logic_tag) + .set_primary_logic(Some(retry_pl_data)) .build(), priority_logic_data.failure_reason.clone(), ) @@ -646,8 +658,8 @@ pub async fn get_gateway_priority( m_internal_meta, order_meta_data, default_gateway_priority_logic_output() - .setPriorityLogicTag(priority_logic_tag) - .setPrimaryLogic(Some(priority_logic_data.clone())) + .set_priority_logic_tag(priority_logic_tag) + .set_primary_logic(Some(priority_logic_data.clone())) .build(), priority_logic_data.failure_reason.clone(), ) @@ -661,7 +673,7 @@ pub async fn get_gateway_priority( None => default_gateway_priority_logic_output(), Some(t) => { if t.is_empty() { - logger::info!( + logger::debug!( tag = "gatewayPriority", "gatewayPriority for merchant: {:?} is empty.", macc.merchantId @@ -673,7 +685,7 @@ pub async fn get_gateway_priority( .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect::>(); - logger::info!( + logger::debug!( tag = "gatewayPriority", "gatewayPriority for merchant: {:?} listOfGateway: {:?}", macc.merchantId, @@ -681,7 +693,7 @@ pub async fn get_gateway_priority( ); match list_of_gateway.as_slice() { [] => { - logger::info!( + logger::debug!( tag = "gatewayPriority emptyList", "Can't get gatewayPriority for merchant: {:?} . Input: {:?}", macc.merchantId, @@ -690,14 +702,14 @@ pub async fn get_gateway_priority( default_gateway_priority_logic_output() } res => { - logger::info!( + logger::debug!( tag = "gatewayPriority decoding", "Decoded successfully. Input: {:?} output: {:?}", t, res ); default_gateway_priority_logic_output() - .setGws(res.to_vec()) + .set_gws(res.to_vec()) .build() } } @@ -719,12 +731,12 @@ async fn get_script( fn default_gateway_priority_logic_output() -> DeciderTypes::GatewayPriorityLogicOutput { DeciderTypes::GatewayPriorityLogicOutput { - isEnforcement: false, + is_enforcement: false, gws: vec![], - priorityLogicTag: None, - gatewayReferenceIds: std::collections::HashMap::new(), - primaryLogic: None, - fallbackLogic: None, + priority_logic_tag: None, + gateway_reference_ids: std::collections::HashMap::new(), + primary_logic: None, + fallback_logic: None, } } @@ -815,7 +827,7 @@ async fn get_priority_logic_script_from_tenant_config( Some(ref tenant_account_id) => { match get_tenant_config_by_tenant_id_and_module_name_and_module_key_and_type( tenant_account_id.to_string(), - ModuleName::PRIORITY_LOGIC, + ModuleName::PriorityLogic, "priority_logic".to_string(), ConfigType::FALLBACK, ) @@ -824,7 +836,7 @@ async fn get_priority_logic_script_from_tenant_config( Some(tenant_config) => { match (tenant_config.filterDimension, tenant_config.filterGroupId) { (Some(filter_dimension), Some(filter_group_id)) => { - logger::info!( + logger::debug!( tag = "getPriorityLogicScriptFromTenantConfig", "Filter dimension found: {:?}", filter_dimension @@ -838,7 +850,7 @@ async fn get_priority_logic_script_from_tenant_config( .await } _ => { - logger::info!( + logger::debug!( tag = "getPriorityLogicScriptFromTenantConfig", "Filter dimension and filter groupId are not present. Proceeding with default tenant config value." ); @@ -848,7 +860,7 @@ async fn get_priority_logic_script_from_tenant_config( } None => { let tenant_account_id = macc.tenantAccountId.clone().unwrap_or_default(); - logger::info!( + logger::debug!( tag = "getPriorityLogicScriptFromTenantConfig", "Tenant Config not found for tenant account id {}", tenant_account_id @@ -872,7 +884,7 @@ async fn get_pl_by_filter_dimension( get_pl_by_merchant_category_code(macc, filter_group_id, config_value).await } _ => { - logger::info!( + logger::debug!( tag = "getPLByFilterDimension", "Filter dimension is not supported. Proceeding with default tenant config value." ); @@ -895,14 +907,14 @@ async fn get_pl_by_merchant_category_code( .await { Some(tenant_config_filter) => { - logger::info!( + logger::debug!( tag = "getPLByMerchantCategoryCode", "Proceeding with tenant config filter priority logic." ); decode_tenant_pl_config(tenant_config_filter.configValue, macc).await } None => { - logger::info!( + logger::debug!( tag = "getPLByMerchantCategoryCode", "Unable to find tenant config filter for groupId {:?} and dimension value {}", filter_group_id, @@ -913,7 +925,7 @@ async fn get_pl_by_merchant_category_code( } } None => { - logger::info!( + logger::debug!( tag = "getPLByMerchantCategoryCode", "Merchant category code is not present for merchantId {:?}", macc.merchantId @@ -929,7 +941,7 @@ async fn decode_tenant_pl_config( ) -> (String, Option) { match utils::either_decode_t::(&config_value) { Ok(tenant_pl_config) => { - logger::info!( + logger::debug!( tag = "decodeTenantPLConfig", "Tenant Priority Logic Config decoded successfully with name: {}", tenant_pl_config.name @@ -992,7 +1004,8 @@ pub async fn handle_fallback_logic( primary_logic_output: DeciderTypes::GatewayPriorityLogicOutput, pl_failure_reason: DeciderTypes::PriorityLogicFailure, ) -> DeciderTypes::GatewayPriorityLogicOutput { - if primary_logic_output.fallbackLogic.is_none() && primary_logic_output.primaryLogic.is_some() { + if primary_logic_output.fallback_logic.is_none() && primary_logic_output.primary_logic.is_some() + { let (fallback_logic, fallback_pl_tag) = get_fallback_priority_logic_script(&macc).await; match fallback_logic { Some(fallback_script) => { @@ -1009,8 +1022,8 @@ pub async fn handle_fallback_logic( ) .await; match fallback_result { - EvaluationResult::PLResponse(gws, pl_data, logs, status) => { - logger::info!( + EvaluationResult::PLResponse(gws, pl_data, logs, _status) => { + logger::debug!( tag = "FALLBACK_PRIORITY_LOGIC_EXECUTION_", "MerchantId: {:?} , Gateways: {:?}, Logs: {:?}", macc.merchantId.clone(), @@ -1018,37 +1031,37 @@ pub async fn handle_fallback_logic( logs ); DeciderTypes::GatewayPriorityLogicOutput { - fallbackLogic: Some(pl_data), - priorityLogicTag: fallback_pl_tag, - primaryLogic: check_and_update_pl_failure_reason( - primary_logic_output.primaryLogic, + fallback_logic: Some(pl_data), + priority_logic_tag: fallback_pl_tag, + primary_logic: check_and_update_pl_failure_reason( + primary_logic_output.primary_logic, pl_failure_reason, ), ..primary_logic_output } } EvaluationResult::EvaluationError(priority_logic_data, err) => { - logger::info!( + logger::debug!( tag = "FALLBACK_PRIORITY_LOGIC_EXECUTION_FAILURE", "MerchantId: {:?} , Error: {:?}", macc.merchantId, err ); DeciderTypes::GatewayPriorityLogicOutput { - primaryLogic: check_and_update_pl_failure_reason( - primary_logic_output.primaryLogic, + primary_logic: check_and_update_pl_failure_reason( + primary_logic_output.primary_logic, pl_failure_reason, ), - fallbackLogic: Some(priority_logic_data), - priorityLogicTag: fallback_pl_tag, + fallback_logic: Some(priority_logic_data), + priority_logic_tag: fallback_pl_tag, ..primary_logic_output } } } } None => DeciderTypes::GatewayPriorityLogicOutput { - primaryLogic: check_and_update_pl_failure_reason( - primary_logic_output.primaryLogic, + primary_logic: check_and_update_pl_failure_reason( + primary_logic_output.primary_logic, pl_failure_reason, ), ..primary_logic_output @@ -1056,8 +1069,8 @@ pub async fn handle_fallback_logic( } } else { DeciderTypes::GatewayPriorityLogicOutput { - fallbackLogic: check_and_update_pl_failure_reason( - primary_logic_output.fallbackLogic, + fallback_logic: check_and_update_pl_failure_reason( + primary_logic_output.fallback_logic, pl_failure_reason, ), ..primary_logic_output @@ -1073,7 +1086,7 @@ fn check_and_update_pl_failure_reason( None => None, Some(mut data) => { if data.failure_reason != pl_failure_reason { - data.status = DeciderTypes::Status::FAILURE; + data.status = DeciderTypes::Status::Failure; data.failure_reason = pl_failure_reason; } Some(data) @@ -1135,7 +1148,7 @@ async fn handle_response( .collect(); let pl_data = DeciderTypes::PriorityLogicData { name: priority_logic_tag, - status: DeciderTypes::Status::FAILURE, + status: DeciderTypes::Status::Failure, failure_reason: pl_resp.errorMessage, }; EvaluationResult::EvaluationError(pl_data, log_entries) @@ -1163,8 +1176,8 @@ async fn handle_failure_response( ) -> EvaluationResult { let pl_data = DeciderTypes::PriorityLogicData { name: priority_logic_tag, - status: DeciderTypes::Status::FAILURE, - failure_reason: DeciderTypes::PriorityLogicFailure::PL_EVALUATION_FAILED, + status: DeciderTypes::Status::Failure, + failure_reason: DeciderTypes::PriorityLogicFailure::PlEvaluationFailed, }; EvaluationResult::EvaluationError(pl_data, log_entries) } @@ -1175,19 +1188,19 @@ async fn handle_success_response( log_entries: Vec, ) -> EvaluationResult { let gws = response_body.result.gatewayPriority.unwrap_or_default(); - let status = DeciderTypes::Status::SUCCESS; + let status = DeciderTypes::Status::Success; let pl_data = DeciderTypes::PriorityLogicData { name: priority_logic_tag.clone(), status: status.clone(), - failure_reason: DeciderTypes::PriorityLogicFailure::NO_ERROR, + failure_reason: DeciderTypes::PriorityLogicFailure::NoError, }; let pl_output = DeciderTypes::GatewayPriorityLogicOutput { - isEnforcement: response_body.result.isEnforcement.unwrap_or(false), + is_enforcement: response_body.result.isEnforcement.unwrap_or(false), gws, - priorityLogicTag: priority_logic_tag.clone(), - gatewayReferenceIds: response_body.result.gatewayReferenceIds.unwrap_or_default(), - primaryLogic: None, - fallbackLogic: None, + priority_logic_tag: priority_logic_tag.clone(), + gateway_reference_ids: response_body.result.gatewayReferenceIds.unwrap_or_default(), + primary_logic: None, + fallback_logic: None, }; EvaluationResult::PLResponse(pl_output, pl_data, log_entries, status) } @@ -1196,7 +1209,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { match client_error { ApiClientError::BadRequest(bytes) => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::COMPILATION_ERROR, + errorMessage: DeciderTypes::PriorityLogicFailure::CompilationError, userMessage: "Bad request sent to the server.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1206,7 +1219,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { }, ApiClientError::Unauthorized(bytes) => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::CONNECTION_FAILED, + errorMessage: DeciderTypes::PriorityLogicFailure::ConnectionFailed, userMessage: "Unauthorized access.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1216,7 +1229,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { }, ApiClientError::InternalServerError(bytes) => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::UNHANDLED_EXCEPTION, + errorMessage: DeciderTypes::PriorityLogicFailure::UnhandledException, userMessage: "Internal server error occurred.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1226,7 +1239,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { }, ApiClientError::ResponseDecodingFailed => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::RESPONSE_DECODE_FAILURE, + errorMessage: DeciderTypes::PriorityLogicFailure::ResponseDecodeFailure, userMessage: "Failed to decode the response.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1236,7 +1249,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { }, ApiClientError::RequestNotSent => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::CONNECTION_FAILED, + errorMessage: DeciderTypes::PriorityLogicFailure::ConnectionFailed, userMessage: "The request was not sent.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1249,7 +1262,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { message, } => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::UNHANDLED_EXCEPTION, + errorMessage: DeciderTypes::PriorityLogicFailure::UnhandledException, userMessage: "An unexpected error occurred.".to_string(), log: Some(vec![vec![ "Error".to_string(), @@ -1259,7 +1272,7 @@ fn handle_client_error(client_error: ApiClientError) -> PLExecutorError { }, _ => PLExecutorError { error: true, - errorMessage: DeciderTypes::PriorityLogicFailure::UNHANDLED_EXCEPTION, + errorMessage: DeciderTypes::PriorityLogicFailure::UnhandledException, userMessage: "An unknown error occurred.".to_string(), log: Some(vec![vec![ "Error".to_string(), diff --git a/src/decider/gatewaydecider/types.rs b/src/decider/gatewaydecider/types.rs index 235353c8..1c484b83 100644 --- a/src/decider/gatewaydecider/types.rs +++ b/src/decider/gatewaydecider/types.rs @@ -1,17 +1,18 @@ use crate::app::{get_tenant_app_state, TenantAppState}; use crate::decider::network_decider; +use crate::redis::feature::RedisCompressionConfigCombined; +use crate::types::country::country_iso::CountryISO2; use crate::types::currency::Currency; use crate::types::money::internal as ETMo; use crate::types::order::udfs::UDFs; use crate::types::transaction::id as ETId; use crate::types::txn_details::types::TxnObjectType; -use masking::Secret; +use masking::{PeekInterface, Secret}; use serde::ser::SerializeStruct; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as AValue; use std::collections::HashMap as HMap; use std::collections::HashMap; -use std::i64; use std::option::Option; use std::string::String; use std::vec::Vec; @@ -22,8 +23,8 @@ use time::{OffsetDateTime, PrimitiveDateTime}; // use data::reflection::Given; // use data::time::{UTCTime, LocalTime}; // use unsafe_coerce::unsafeCoerce; +use crate::decider::gatewaydecider::utils::mask_secret_option; use crate::types::card as ETCa; -use crate::types::gateway as ETG; use crate::types::merchant as ETM; use crate::types::merchant::id::MerchantId; use crate::types::order as ETO; @@ -36,7 +37,6 @@ use crate::types::gateway_routing_input as ETGRI; // use eulerhs::language as L; // use juspay::extra::parsing as Parsing; use crate::types::customer as ETCu; -use crate::types::payment as ETP; use diesel::sql_types; use std::fmt; @@ -73,9 +73,10 @@ pub enum DeciderFilterName { GatewayPriorityList, FilterFunctionalGatewaysForMerchantRequiredFlow, FilterGatewaysForMGASelectionIntegrity, - FilterGatewaysForEMITenureSpecficGatewayCreds, + FilterGatewaysForEMITenureSpecificGatewayCreds, FilterFunctionalGatewaysForReversePennyDrop, FilterFunctionalGatewaysForOTM, + FilterFunctionalGatewaysForPixFlows, } impl fmt::Display for DeciderFilterName { @@ -149,8 +150,8 @@ impl fmt::Display for DeciderFilterName { Self::FilterGatewaysForMGASelectionIntegrity => { write!(f, "FilterGatewaysForMGASelectionIntegrity") } - Self::FilterGatewaysForEMITenureSpecficGatewayCreds => { - write!(f, "FilterGatewaysForEMITenureSpecficGatewayCreds") + Self::FilterGatewaysForEMITenureSpecificGatewayCreds => { + write!(f, "FilterGatewaysForEMITenureSpecificGatewayCreds") } Self::FilterFunctionalGatewaysForReversePennyDrop => { write!(f, "FilterFunctionalGatewaysForReversePennyDrop") @@ -158,6 +159,9 @@ impl fmt::Display for DeciderFilterName { Self::FilterFunctionalGatewaysForOTM => { write!(f, "FilterFunctionalGatewaysForOTM") } + Self::FilterFunctionalGatewaysForPixFlows => { + write!(f, "FilterFunctionalGatewaysForPixFlows") + } } } } @@ -179,27 +183,30 @@ pub enum DeciderScoringName { } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum DetailedGatewayScoringType { - ELIMINATION_PENALISE, - ELIMINATION_REWARD, - SRV2_PENALISE, - SRV2_REWARD, - SRV3_PENALISE, - SRV3_REWARD, + EliminationPenalise, + EliminationReward, + Srv2Penalise, + Srv2Reward, + Srv3Penalise, + Srv3Reward, } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RoutingFlowType { - ELIMINATION_FLOW, - SRV2_FLOW, - SRV3_FLOW, + EliminationFlow, + Srv2Flow, + Srv3Flow, } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ScoreUpdateStatus { - PENALISED, - REWARDED, - NOT_INITIATED, + Penalised, + Rewarded, + NotInitiated, } pub type GatewayScoreMap = HMap; @@ -263,7 +270,7 @@ pub struct GatewayScoringTypeLogData { #[derive(Debug)] pub struct GatewayScoringTypeLog { - pub log_data: AValue, + pub data: AValue, } impl Serialize for GatewayScoringTypeLog { @@ -272,7 +279,7 @@ impl Serialize for GatewayScoringTypeLog { S: serde::Serializer, { let mut state = serializer.serialize_struct("GatewayScoringTypeLog", 1)?; - state.serialize_field("data", &self.log_data)?; + state.serialize_field("data", &self.data)?; state.end() } } @@ -287,7 +294,7 @@ impl<'de> Deserialize<'de> for GatewayScoringTypeLog { &["data"], GatewayScoringTypeLogVisitor, )?; - Ok(Self { log_data: data }) + Ok(Self { data }) } } @@ -296,7 +303,7 @@ struct GatewayScoringTypeLogVisitor; impl<'de> serde::de::Visitor<'de> for GatewayScoringTypeLogVisitor { type Value = AValue; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("struct GatewayScoringTypeLog") } @@ -365,20 +372,15 @@ pub fn transform_gateway_wise_success_rate_based_routing( ) -> DeciderGatewayWiseSuccessRateBasedRoutingInput { DeciderGatewayWiseSuccessRateBasedRoutingInput { gateway: gateway_wise_success_rate_input.gateway.clone(), - eliminationThreshold: gateway_wise_success_rate_input.eliminationThreshold.clone(), - eliminationMaxCountThreshold: gateway_wise_success_rate_input - .eliminationMaxCountThreshold - .clone(), - selectionMaxCountThreshold: gateway_wise_success_rate_input - .selectionMaxCountThreshold - .clone(), - softTxnResetCount: gateway_wise_success_rate_input.softTxnResetCount.clone(), + eliminationThreshold: gateway_wise_success_rate_input.eliminationThreshold, + eliminationMaxCountThreshold: gateway_wise_success_rate_input.eliminationMaxCountThreshold, + selectionMaxCountThreshold: gateway_wise_success_rate_input.selectionMaxCountThreshold, + softTxnResetCount: gateway_wise_success_rate_input.softTxnResetCount, gatewayLevelEliminationThreshold: gateway_wise_success_rate_input - .gatewayLevelEliminationThreshold - .clone(), + .gatewayLevelEliminationThreshold, eliminationLevel: gateway_wise_success_rate_input.eliminationLevel.clone(), - currentScore: gateway_wise_success_rate_input.currentScore.clone(), - lastResetTimeStamp: gateway_wise_success_rate_input.lastResetTimeStamp.clone(), + currentScore: gateway_wise_success_rate_input.currentScore, + lastResetTimeStamp: gateway_wise_success_rate_input.lastResetTimeStamp, } } @@ -400,7 +402,8 @@ pub struct MessageFormat { pub log_type: String, pub payment_method: String, pub payment_method_type: String, - pub payment_source: Option, + #[serde(serialize_with = "mask_secret_option")] + pub payment_source: Option>, pub source_object: Option, pub txn_detail_id: ETTD::TxnDetailId, pub stage: String, @@ -413,6 +416,7 @@ pub struct MessageFormat { pub x_request_id: Option, #[serde(rename = "data")] pub log_data: AValue, + pub udf_consumed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -490,7 +494,7 @@ pub fn initial_decider_state(date_created: String) -> DeciderState { dateCreated: date_created, gatewayBeforeDowntimeEvaluation: None, }, - gwDeciderApproach: GatewayDeciderApproach::NONE, + gwDeciderApproach: GatewayDeciderApproach::None, srElminiationApproachInfo: vec![], allMgas: None, paymentFlowList: vec![], @@ -500,7 +504,7 @@ pub fn initial_decider_state(date_created: String) -> DeciderState { isSrV3MetricEnabled: false, isPrimaryGateway: Some(true), experiment_tag: None, - reset_approach: ResetApproach::NO_RESET, + reset_approach: ResetApproach::NoReset, routing_dimension: None, routing_dimension_level: None, isScheduledOutage: false, @@ -529,6 +533,14 @@ pub fn initial_decider_state(date_created: String) -> DeciderState { routingApproach: None, dateCreated: OffsetDateTime::now_utc(), eliminationEnabled: false, + cardIsIn: None, + cardSwitchProvider: None, + currency: None, + country: None, + is_legacy_decider_flow: true, + udfs: None, + udfs_consumed_for_routing: None, + gatewayReferenceId: None, }, } } @@ -542,7 +554,8 @@ pub struct GatewayScoringData { pub cardType: Option, pub bankCode: Option, pub authType: Option, - pub paymentSource: Option, + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, pub isPaymentSourceEnabledForSrRouting: bool, pub isAuthLevelEnabledForSrRouting: bool, pub isBankLevelEnabledForSrRouting: bool, @@ -551,12 +564,31 @@ pub struct GatewayScoringData { pub routingApproach: Option, pub dateCreated: OffsetDateTime, pub eliminationEnabled: bool, + pub cardIsIn: Option, + pub cardSwitchProvider: Option>, + pub currency: Option, + pub country: Option, + pub is_legacy_decider_flow: bool, + pub gatewayReferenceId: Option, + pub udfs: Option, + pub udfs_consumed_for_routing: Option, +} + +impl GatewayScoringData { + pub fn get_payment_source(&self) -> String { + self.paymentSource + .as_ref() + .map(|s| s.peek().to_string()) + .unwrap_or_default() + } } #[derive(Debug)] +#[allow(dead_code)] pub struct MetricsStreamKeyShard(String, i32); #[derive(Debug)] +#[allow(dead_code)] pub struct MetricsStreamKey(String); // # TODO - Implement RedisKey for MetricsStreamKeyShard @@ -576,67 +608,72 @@ pub struct MetricsStreamKey(String); // } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ScoreKeyType { - ELIMINATION_GLOBAL_KEY, - ELIMINATION_MERCHANT_KEY, - OUTAGE_GLOBAL_KEY, - OUTAGE_MERCHANT_KEY, - SR_V2_KEY, - SR_V3_KEY, + EliminationGlobalKey, + EliminationMerchantKey, + OutageGlobalKey, + OutageMerchantKey, + SrV2Key, + SrV3Key, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum GatewayDeciderApproach { - SR_SELECTION, - SR_SELECTION_V2_ROUTING, - SR_SELECTION_V3_ROUTING, - PRIORITY_LOGIC, - DEFAULT, - NONE, - MERCHANT_PREFERENCE, - PL_ALL_DOWNTIME_ROUTING, - PL_DOWNTIME_ROUTING, - PL_GLOBAL_DOWNTIME_ROUTING, - SR_V2_ALL_DOWNTIME_ROUTING, - SR_V2_DOWNTIME_ROUTING, - SR_V2_GLOBAL_DOWNTIME_ROUTING, - SR_V2_HEDGING, - SR_V2_ALL_DOWNTIME_HEDGING, - SR_V2_DOWNTIME_HEDGING, - SR_V2_GLOBAL_DOWNTIME_HEDGING, - SR_V3_ALL_DOWNTIME_ROUTING, - SR_V3_DOWNTIME_ROUTING, - SR_V3_GLOBAL_DOWNTIME_ROUTING, - SR_V3_HEDGING, - SR_V3_ALL_DOWNTIME_HEDGING, - SR_V3_DOWNTIME_HEDGING, - SR_V3_GLOBAL_DOWNTIME_HEDGING, - NTW_BASED_ROUTING, + SrSelection, + SrSelectionV2Routing, + SrSelectionV3Routing, + PriorityLogic, + Default, + None, + MerchantPreference, + PlAllDowntimeRouting, + PlDowntimeRouting, + PlGlobalDowntimeRouting, + SrV2AllDowntimeRouting, + SrV2DowntimeRouting, + SrV2GlobalDowntimeRouting, + SrV2Hedging, + SrV2AllDowntimeHedging, + SrV2DowntimeHedging, + SrV2GlobalDowntimeHedging, + SrV3AllDowntimeRouting, + SrV3DowntimeRouting, + SrV3GlobalDowntimeRouting, + SrV3Hedging, + SrV3AllDowntimeHedging, + SrV3DowntimeHedging, + SrV3GlobalDowntimeHedging, + NtwBasedRouting, } #[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum DownTime { - ALL_DOWNTIME, - GLOBAL_DOWNTIME, - DOWNTIME, - NO_DOWNTIME, + AllDowntime, + GlobalDowntime, + Downtime, + NoDowntime, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ResetApproach { - ELIMINATION_RESET, - SRV2_RESET, - SRV3_RESET, - NO_RESET, - SRV2_ELIMINATION_RESET, - SRV3_ELIMINATION_RESET, + EliminationReset, + Srv2Reset, + Srv3Reset, + NoReset, + Srv2EliminationReset, + Srv3EliminationReset, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RankingAlgorithm { - SR_BASED_ROUTING, - PL_BASED_ROUTING, - NTW_BASED_ROUTING, + SrBasedRouting, + PlBasedRouting, + NtwBasedRouting, } // pub type DeciderFlow = for<'a> fn(&'a mut (dyn MonadFlow + 'a)) -> ReaderT, R>; @@ -754,6 +791,7 @@ pub struct ApiTxnDetail { pub sourceObject: Option, pub sourceObjectId: Option, pub currency: Option, + pub country: Option, pub netAmount: Option, pub surchargeAmount: Option, pub taxAmount: Option, @@ -850,7 +888,8 @@ pub struct ApiTxnCardInfo { pub paymentMethodType: Option, pub paymentMethod: Option, pub cardGlobalFingerprint: Option, - pub paymentSource: Option, + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, pub authType: Option, pub partitionKey: Option, } @@ -889,102 +928,143 @@ pub struct DomainDeciderRequest { // impl Given for DomainDeciderRequest {} #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct DomainDeciderRequestForApiCallV2 { - pub paymentInfo: PaymentInfo, - pub merchantId: String, - pub eligibleGatewayList: Option>, - pub rankingAlgorithm: Option, - pub eliminationEnabled: Option, + pub payment_info: PaymentInfo, + pub merchant_id: String, + pub eligible_gateway_list: Option>, + pub ranking_algorithm: Option, + pub elimination_enabled: Option, +} + +pub fn deserialize_optional_udfs_to_hashmap<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + // First try to deserialize as Option>> + let opt_raw_vec: Option>> = Option::deserialize(deserializer)?; + + match opt_raw_vec { + None => Ok(None), + Some(raw_vec) => { + // Convert the Vec> to a HashMap + let hashmap: HashMap = raw_vec + .into_iter() + .enumerate() + .filter_map(|(index, value)| value.map(|v| (index as i32, v))) + .collect(); + + Ok(Some(UDFs(hashmap))) + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PaymentInfo { - paymentId: String, + payment_id: String, pub amount: f64, currency: Currency, - customerId: Option, + country: Option, + customer_id: Option, + #[serde(default, deserialize_with = "deserialize_optional_udfs_to_hashmap")] udfs: Option, - preferredGateway: Option, - paymentType: TxnObjectType, + preferred_gateway: Option, + payment_type: TxnObjectType, pub metadata: Option, - internalMetadata: Option, - isEmi: Option, - emiBank: Option, - emiTenure: Option, - paymentMethodType: String, - paymentMethod: String, - paymentSource: Option, - authType: Option, - cardIssuerBankName: Option, - pub cardIsin: Option, - cardType: Option, - cardSwitchProvider: Option>, + internal_metadata: Option, + is_emi: Option, + emi_bank: Option, + emi_tenure: Option, + payment_method_type: String, + payment_method: String, + payment_source: Option, + auth_type: Option, + card_issuer_bank_name: Option, + pub card_isin: Option, + card_type: Option, + card_switch_provider: Option>, } // write a function to transfer DomainDeciderRequestForApiCallV2 to DomainDeciderRequest impl DomainDeciderRequestForApiCallV2 { + pub fn payment_id(&self) -> &str { + &self.payment_info.payment_id + } + + pub fn payment_method_type(&self) -> &str { + &self.payment_info.payment_method_type + } + + pub fn payment_method(&self) -> &str { + &self.payment_info.payment_method + } + pub async fn to_domain_decider_request(&self) -> DomainDeciderRequest { DomainDeciderRequest { orderReference: ETO::Order { id: ETO::id::to_order_prim_id(1), - amount: ETMo::Money::from_double(self.paymentInfo.amount), - currency: self.paymentInfo.currency.clone(), + amount: ETMo::Money::from_double(self.payment_info.amount), + currency: self.payment_info.currency.clone(), dateCreated: OffsetDateTime::now_utc(), - merchantId: ETM::id::to_merchant_id(self.merchantId.clone()), - orderId: ETO::id::to_order_id(self.paymentInfo.paymentId.clone()), + merchantId: ETM::id::to_merchant_id(self.merchant_id.clone()), + orderId: ETO::id::to_order_id(self.payment_info.payment_id.clone()), status: ETO::OrderStatus::Created, description: None, - customerId: self.paymentInfo.customerId.clone(), + customerId: self.payment_info.customer_id.clone(), udfs: self - .paymentInfo + .payment_info .udfs .clone() .unwrap_or(UDFs(HashMap::new())), - preferredGateway: self.paymentInfo.preferredGateway.clone(), + preferredGateway: self.payment_info.preferred_gateway.clone(), productId: None, orderType: ETO::OrderType::from_txn_object_type( - self.paymentInfo.paymentType.clone(), + self.payment_info.payment_type.clone(), ), - metadata: self.paymentInfo.metadata.clone(), - internalMetadata: self.paymentInfo.internalMetadata.clone(), + metadata: self.payment_info.metadata.clone(), + internalMetadata: self.payment_info.internal_metadata.clone(), }, shouldConsumeResult: None, orderMetadata: ETOMV2::OrderMetadataV2 { id: ETOMV2::to_order_metadata_v2_pid(1), date_created: OffsetDateTime::now_utc(), last_updated: OffsetDateTime::now_utc(), - metadata: self.paymentInfo.metadata.clone(), + metadata: self.payment_info.metadata.clone(), order_reference_id: 1, ip_address: None, partition_key: None, }, txnDetail: ETTD::TxnDetail { id: ETTD::to_txn_detail_id(1), - orderId: ETO::id::to_order_id(self.paymentInfo.paymentId.clone()), + orderId: ETO::id::to_order_id(self.payment_info.payment_id.clone()), status: ETTD::TxnStatus::Started, - txnId: ETId::to_transaction_id(self.paymentInfo.paymentId.clone()), - txnType: "NOT_DEFINED".to_string(), + txnId: ETId::to_transaction_id(self.payment_info.payment_id.clone()), + txnType: Some("NOT_DEFINED".to_string()), dateCreated: OffsetDateTime::now_utc(), - addToLocker: false, - merchantId: ETM::id::to_merchant_id(self.merchantId.clone()), + addToLocker: Some(false), + merchantId: ETM::id::to_merchant_id(self.merchant_id.clone()), gateway: None, - expressCheckout: false, - isEmi: self.paymentInfo.isEmi.clone().unwrap_or(false), - emiBank: self.paymentInfo.emiBank.clone(), - emiTenure: self.paymentInfo.emiTenure.clone(), - txnUuid: self.paymentInfo.paymentId.clone(), + expressCheckout: Some(false), + isEmi: Some(self.payment_info.is_emi.unwrap_or(false)), + emiBank: self.payment_info.emi_bank.clone(), + emiTenure: self.payment_info.emi_tenure, + txnUuid: self.payment_info.payment_id.clone(), merchantGatewayAccountId: None, - txnAmount: ETMo::Money::from_double(self.paymentInfo.amount), - txnObjectType: self.paymentInfo.paymentType.clone(), - sourceObject: Some(self.paymentInfo.paymentMethod.clone()), + txnAmount: Some(ETMo::Money::from_double(self.payment_info.amount)), + txnObjectType: Some(self.payment_info.payment_type.clone()), + sourceObject: Some(self.payment_info.payment_method.clone()), sourceObjectId: None, - currency: self.paymentInfo.currency.clone(), - netAmount: ETMo::Money::from_double(self.paymentInfo.amount), + currency: self.payment_info.currency.clone(), + country: self.payment_info.country, + netAmount: Some(ETMo::Money::from_double(self.payment_info.amount)), surchargeAmount: None, taxAmount: None, - internalMetadata: self.paymentInfo.internalMetadata.clone(), - metadata: self.paymentInfo.metadata.clone(), + internalMetadata: self.payment_info.internal_metadata.clone().map(Secret::new), + metadata: self.payment_info.metadata.clone().map(Secret::new), offerDeductionAmount: None, internalTrackingInfo: None, partitionKey: None, @@ -993,20 +1073,20 @@ impl DomainDeciderRequestForApiCallV2 { txnOfferDetails: None, txnCardInfo: ETCa::txn_card_info::TxnCardInfo { id: ETCa::txn_card_info::to_txn_card_info_pid(1), - card_isin: self.paymentInfo.cardIsin.clone(), - cardIssuerBankName: self.paymentInfo.cardIssuerBankName.clone(), - cardSwitchProvider: self.paymentInfo.cardSwitchProvider.clone(), - card_type: self.paymentInfo.cardType.clone(), + card_isin: self.payment_info.card_isin.clone(), + cardIssuerBankName: self.payment_info.card_issuer_bank_name.clone(), + cardSwitchProvider: self.payment_info.card_switch_provider.clone(), + card_type: self.payment_info.card_type.clone(), nameOnCard: None, dateCreated: OffsetDateTime::now_utc(), - paymentMethodType: self.paymentInfo.paymentMethodType.to_string(), - paymentMethod: self.paymentInfo.paymentMethod.clone(), - paymentSource: self.paymentInfo.paymentSource.clone(), - authType: self.paymentInfo.authType.clone(), + paymentMethodType: self.payment_info.payment_method_type.to_string(), + paymentMethod: self.payment_info.payment_method.clone(), + paymentSource: self.payment_info.payment_source.clone().map(Secret::new), + authType: self.payment_info.auth_type.clone(), partitionKey: None, }, merchantAccount: ETM::merchant_account::load_merchant_by_merchant_id( - self.merchantId.clone(), + self.merchant_id.clone(), ) .await .expect("Merchant account not found"), @@ -1113,6 +1193,11 @@ pub struct SrV3InputConfig { pub struct SrV3SubLevelInputConfig { pub paymentMethodType: Option, pub paymentMethod: Option, + pub cardNetwork: Option, + pub cardIsIn: Option, + pub currency: Option, + pub country: Option, + pub authType: Option, pub latencyThreshold: Option, pub bucketSize: Option, pub hedgingPercent: Option, @@ -1127,6 +1212,12 @@ pub struct GatewayWiseExtraScore { pub gatewaySigmaFactor: f64, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TransactionLatencyThreshold { + /// To have a hard threshold for latency, which is used to filter out gateways that exceed this threshold. + pub gatewayLatency: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct UnifiedError { pub code: String, @@ -1195,6 +1286,7 @@ pub struct DecidedGateway { pub is_dynamic_mga_enabled: bool, pub gateway_mga_id_map: Option, pub is_rust_based_decider: bool, + pub latency: Option, } #[derive(Debug, Serialize, Clone, Deserialize)] @@ -1214,6 +1306,7 @@ pub struct DeciderParams { pub dpPriorityLogicScript: Option, pub dpEDCCApplied: Option, pub dpShouldConsumeResult: Option, + pub dpRedisCompressionConfig: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -1265,19 +1358,21 @@ pub struct Offer { } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum EmiType { - NO_COST_EMI, - LOW_COST_EMI, + NoCostEmi, + LowCostEmi, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ValidationType { - CARD_MANDATE, - EMANDATE, - TPV, - TPV_MANDATE, - REWARD, - TPV_EMANDATE, + CardMandate, + Emandate, + Tpv, + TpvMandate, + Reward, + TpvEmandate, } impl fmt::Display for DeciderScoringName { @@ -1312,12 +1407,12 @@ impl fmt::Display for DeciderScoringName { impl fmt::Display for DetailedGatewayScoringType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ELIMINATION_PENALISE => write!(f, "ELIMINATION_PENALISE"), - Self::ELIMINATION_REWARD => write!(f, "ELIMINATION_REWARD"), - Self::SRV2_PENALISE => write!(f, "SRV2_PENALISE"), - Self::SRV2_REWARD => write!(f, "SRV2_REWARD"), - Self::SRV3_PENALISE => write!(f, "SRV3_PENALISE"), - Self::SRV3_REWARD => write!(f, "SRV3_REWARD"), + Self::EliminationPenalise => write!(f, "ELIMINATION_PENALISE"), + Self::EliminationReward => write!(f, "ELIMINATION_REWARD"), + Self::Srv2Penalise => write!(f, "SRV2_PENALISE"), + Self::Srv2Reward => write!(f, "SRV2_REWARD"), + Self::Srv3Penalise => write!(f, "SRV3_PENALISE"), + Self::Srv3Reward => write!(f, "SRV3_REWARD"), } } } @@ -1325,9 +1420,9 @@ impl fmt::Display for DetailedGatewayScoringType { impl fmt::Display for RoutingFlowType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ELIMINATION_FLOW => write!(f, "ELIMINATION_FLOW"), - Self::SRV2_FLOW => write!(f, "SRV2_FLOW"), - Self::SRV3_FLOW => write!(f, "SRV3_FLOW"), + Self::EliminationFlow => write!(f, "ELIMINATION_FLOW"), + Self::Srv2Flow => write!(f, "SRV2_FLOW"), + Self::Srv3Flow => write!(f, "SRV3_FLOW"), } } } @@ -1335,9 +1430,9 @@ impl fmt::Display for RoutingFlowType { impl fmt::Display for ScoreUpdateStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::PENALISED => write!(f, "PENALISED"), - Self::REWARDED => write!(f, "REWARDED"), - Self::NOT_INITIATED => write!(f, "NOT_INITIATED"), + Self::Penalised => write!(f, "PENALISED"), + Self::Rewarded => write!(f, "REWARDED"), + Self::NotInitiated => write!(f, "NOT_INITIATED"), } } } @@ -1345,12 +1440,12 @@ impl fmt::Display for ScoreUpdateStatus { impl fmt::Display for ScoreKeyType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ELIMINATION_GLOBAL_KEY => write!(f, "ELIMINATION_GLOBAL_KEY"), - Self::ELIMINATION_MERCHANT_KEY => write!(f, "ELIMINATION_MERCHANT_KEY"), - Self::OUTAGE_GLOBAL_KEY => write!(f, "OUTAGE_GLOBAL_KEY"), - Self::OUTAGE_MERCHANT_KEY => write!(f, "OUTAGE_MERCHANT_KEY"), - Self::SR_V2_KEY => write!(f, "SR_V2_KEY"), - Self::SR_V3_KEY => write!(f, "SR_V3_KEY"), + Self::EliminationGlobalKey => write!(f, "ELIMINATION_GLOBAL_KEY"), + Self::EliminationMerchantKey => write!(f, "ELIMINATION_MERCHANT_KEY"), + Self::OutageGlobalKey => write!(f, "OUTAGE_GLOBAL_KEY"), + Self::OutageMerchantKey => write!(f, "OUTAGE_MERCHANT_KEY"), + Self::SrV2Key => write!(f, "SR_V2_KEY"), + Self::SrV3Key => write!(f, "SR_V3_KEY"), } } } @@ -1358,49 +1453,49 @@ impl fmt::Display for ScoreKeyType { impl fmt::Display for GatewayDeciderApproach { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::SR_SELECTION => write!(f, "SR_SELECTION"), - Self::SR_SELECTION_V2_ROUTING => write!(f, "SR_SELECTION_V2_ROUTING"), - Self::SR_SELECTION_V3_ROUTING => write!(f, "SR_SELECTION_V3_ROUTING"), - Self::PRIORITY_LOGIC => write!(f, "PRIORITY_LOGIC"), - Self::DEFAULT => write!(f, "DEFAULT"), - Self::NONE => write!(f, "NONE"), - Self::MERCHANT_PREFERENCE => write!(f, "MERCHANT_PREFERENCE"), - Self::PL_ALL_DOWNTIME_ROUTING => write!(f, "PL_ALL_DOWNTIME_ROUTING"), - Self::PL_DOWNTIME_ROUTING => write!(f, "PL_DOWNTIME_ROUTING"), - Self::PL_GLOBAL_DOWNTIME_ROUTING => { + Self::SrSelection => write!(f, "SR_SELECTION"), + Self::SrSelectionV2Routing => write!(f, "SR_SELECTION_V2_ROUTING"), + Self::SrSelectionV3Routing => write!(f, "SR_SELECTION_V3_ROUTING"), + Self::PriorityLogic => write!(f, "PRIORITY_LOGIC"), + Self::Default => write!(f, "DEFAULT"), + Self::None => write!(f, "NONE"), + Self::MerchantPreference => write!(f, "MERCHANT_PREFERENCE"), + Self::PlAllDowntimeRouting => write!(f, "PL_ALL_DOWNTIME_ROUTING"), + Self::PlDowntimeRouting => write!(f, "PL_DOWNTIME_ROUTING"), + Self::PlGlobalDowntimeRouting => { write!(f, "PL_GLOBAL_DOWNTIME_ROUTING") } - Self::SR_V2_ALL_DOWNTIME_ROUTING => { + Self::SrV2AllDowntimeRouting => { write!(f, "SR_V2_ALL_DOWNTIME_ROUTING") } - Self::SR_V2_DOWNTIME_ROUTING => write!(f, "SR_V2_DOWNTIME_ROUTING"), - Self::SR_V2_GLOBAL_DOWNTIME_ROUTING => { + Self::SrV2DowntimeRouting => write!(f, "SR_V2_DOWNTIME_ROUTING"), + Self::SrV2GlobalDowntimeRouting => { write!(f, "SR_V2_GLOBAL_DOWNTIME_ROUTING") } - Self::SR_V2_HEDGING => write!(f, "SR_V2_HEDGING"), - Self::SR_V2_ALL_DOWNTIME_HEDGING => { + Self::SrV2Hedging => write!(f, "SR_V2_HEDGING"), + Self::SrV2AllDowntimeHedging => { write!(f, "SR_V2_ALL_DOWNTIME_HEDGING") } - Self::SR_V2_DOWNTIME_HEDGING => write!(f, "SR_V2_DOWNTIME_HEDGING"), - Self::SR_V2_GLOBAL_DOWNTIME_HEDGING => { + Self::SrV2DowntimeHedging => write!(f, "SR_V2_DOWNTIME_HEDGING"), + Self::SrV2GlobalDowntimeHedging => { write!(f, "SR_V2_GLOBAL_DOWNTIME_HEDGING") } - Self::SR_V3_ALL_DOWNTIME_ROUTING => { + Self::SrV3AllDowntimeRouting => { write!(f, "SR_V3_ALL_DOWNTIME_ROUTING") } - Self::SR_V3_DOWNTIME_ROUTING => write!(f, "SR_V3_DOWNTIME_ROUTING"), - Self::SR_V3_GLOBAL_DOWNTIME_ROUTING => { + Self::SrV3DowntimeRouting => write!(f, "SR_V3_DOWNTIME_ROUTING"), + Self::SrV3GlobalDowntimeRouting => { write!(f, "SR_V3_GLOBAL_DOWNTIME_ROUTING") } - Self::SR_V3_HEDGING => write!(f, "SR_V3_HEDGING"), - Self::SR_V3_ALL_DOWNTIME_HEDGING => { + Self::SrV3Hedging => write!(f, "SR_V3_HEDGING"), + Self::SrV3AllDowntimeHedging => { write!(f, "SR_V3_ALL_DOWNTIME_HEDGING") } - Self::SR_V3_DOWNTIME_HEDGING => write!(f, "SR_V3_DOWNTIME_HEDGING"), - Self::SR_V3_GLOBAL_DOWNTIME_HEDGING => { + Self::SrV3DowntimeHedging => write!(f, "SR_V3_DOWNTIME_HEDGING"), + Self::SrV3GlobalDowntimeHedging => { write!(f, "SR_V3_GLOBAL_DOWNTIME_HEDGING") } - Self::NTW_BASED_ROUTING => { + Self::NtwBasedRouting => { write!(f, "NTW_BASED_ROUTING") } } @@ -1410,10 +1505,10 @@ impl fmt::Display for GatewayDeciderApproach { impl fmt::Display for DownTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ALL_DOWNTIME => write!(f, "ALL_DOWNTIME"), - Self::GLOBAL_DOWNTIME => write!(f, "GLOBAL_DOWNTIME"), - Self::DOWNTIME => write!(f, "DOWNTIME"), - Self::NO_DOWNTIME => write!(f, "NO_DOWNTIME"), + Self::AllDowntime => write!(f, "ALL_DOWNTIME"), + Self::GlobalDowntime => write!(f, "GLOBAL_DOWNTIME"), + Self::Downtime => write!(f, "DOWNTIME"), + Self::NoDowntime => write!(f, "NO_DOWNTIME"), } } } @@ -1421,12 +1516,12 @@ impl fmt::Display for DownTime { impl fmt::Display for ResetApproach { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ELIMINATION_RESET => write!(f, "ELIMINATION_RESET"), - Self::SRV2_RESET => write!(f, "SRV2_RESET"), - Self::SRV3_RESET => write!(f, "SRV3_RESET"), - Self::NO_RESET => write!(f, "NO_RESET"), - Self::SRV2_ELIMINATION_RESET => write!(f, "SRV2_ELIMINATION_RESET"), - Self::SRV3_ELIMINATION_RESET => write!(f, "SRV3_ELIMINATION_RESET"), + Self::EliminationReset => write!(f, "ELIMINATION_RESET"), + Self::Srv2Reset => write!(f, "SRV2_RESET"), + Self::Srv3Reset => write!(f, "SRV3_RESET"), + Self::NoReset => write!(f, "NO_RESET"), + Self::Srv2EliminationReset => write!(f, "SRV2_ELIMINATION_RESET"), + Self::Srv3EliminationReset => write!(f, "SRV3_ELIMINATION_RESET"), } } } @@ -1434,12 +1529,12 @@ impl fmt::Display for ResetApproach { impl fmt::Display for ValidationType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::CARD_MANDATE => write!(f, "CARD_MANDATE"), - Self::EMANDATE => write!(f, "EMANDATE"), - Self::TPV => write!(f, "TPV"), - Self::TPV_MANDATE => write!(f, "TPV_MANDATE"), - Self::REWARD => write!(f, "REWARD"), - Self::TPV_EMANDATE => write!(f, "TPV_EMANDATE"), + Self::CardMandate => write!(f, "CARD_MANDATE"), + Self::Emandate => write!(f, "EMANDATE"), + Self::Tpv => write!(f, "TPV"), + Self::TpvMandate => write!(f, "TPV_MANDATE"), + Self::Reward => write!(f, "REWARD"), + Self::TpvEmandate => write!(f, "TPV_EMANDATE"), } } } @@ -1447,8 +1542,8 @@ impl fmt::Display for ValidationType { impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::SUCCESS => write!(f, "SUCCESS"), - Self::FAILURE => write!(f, "FAILURE"), + Self::Success => write!(f, "SUCCESS"), + Self::Failure => write!(f, "FAILURE"), } } } @@ -1456,22 +1551,22 @@ impl fmt::Display for Status { impl fmt::Display for PriorityLogicFailure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NO_ERROR => write!(f, "NO_ERROR"), - Self::CONNECTION_FAILED => write!(f, "CONNECTION_FAILED"), - Self::COMPILATION_ERROR => write!(f, "COMPILATION_ERROR"), - Self::MEMORY_EXCEEDED => write!(f, "MEMORY_EXCEEDED"), - Self::GATEWAY_NAME_PARSE_FAILURE => { + Self::NoError => write!(f, "NO_ERROR"), + Self::ConnectionFailed => write!(f, "CONNECTION_FAILED"), + Self::CompilationError => write!(f, "COMPILATION_ERROR"), + Self::MemoryExceeded => write!(f, "MEMORY_EXCEEDED"), + Self::GatewayNameParseFailure => { write!(f, "GATEWAY_NAME_PARSE_FAILURE") } - Self::RESPONSE_CONTENT_TYPE_NOT_SUPPORTED => { + Self::ResponseContentTypeNotSupported => { write!(f, "RESPONSE_CONTENT_TYPE_NOT_SUPPORTED") } - Self::RESPONSE_DECODE_FAILURE => write!(f, "RESPONSE_DECODE_FAILURE"), - Self::RESPONSE_PARSE_ERROR => write!(f, "RESPONSE_PARSE_ERROR"), - Self::PL_EVALUATION_FAILED => write!(f, "PL_EVALUATION_FAILED"), - Self::NULL_AFTER_ENFORCE => write!(f, "NULL_AFTER_ENFORCE"), - Self::UNHANDLED_EXCEPTION => write!(f, "UNHANDLED_EXCEPTION"), - Self::CODE_TOO_LARGE => write!(f, "CODE_TOO_LARGE"), + Self::ResponseDecodeFailure => write!(f, "RESPONSE_DECODE_FAILURE"), + Self::ResponseParseError => write!(f, "RESPONSE_PARSE_ERROR"), + Self::PlEvaluationFailed => write!(f, "PL_EVALUATION_FAILED"), + Self::NullAfterEnforce => write!(f, "NULL_AFTER_ENFORCE"), + Self::UnhandledException => write!(f, "UNHANDLED_EXCEPTION"), + Self::CodeTooLarge => write!(f, "CODE_TOO_LARGE"), } } } @@ -1490,8 +1585,8 @@ impl fmt::Display for Dimension { impl fmt::Display for EmiType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NO_COST_EMI => write!(f, "NO_COST_EMI"), - Self::LOW_COST_EMI => write!(f, "LOW_COST_EMI"), + Self::NoCostEmi => write!(f, "NO_COST_EMI"), + Self::LowCostEmi => write!(f, "LOW_COST_EMI"), } } } @@ -1526,57 +1621,58 @@ pub struct ApiDeciderFullRequest { } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct GatewayPriorityLogicOutput { - pub isEnforcement: bool, + pub is_enforcement: bool, pub gws: Vec, - pub priorityLogicTag: Option, - pub gatewayReferenceIds: HMap, - pub primaryLogic: Option, - pub fallbackLogic: Option, + pub priority_logic_tag: Option, + pub gateway_reference_ids: HMap, + pub primary_logic: Option, + pub fallback_logic: Option, } impl GatewayPriorityLogicOutput { pub fn new( - isEnforcement: bool, + is_enforcement: bool, gws: Vec, - priorityLogicTag: Option, - gatewayReferenceIds: HMap, - primaryLogic: Option, - fallbackLogic: Option, + priority_logic_tag: Option, + gateway_reference_ids: HMap, + primary_logic: Option, + fallback_logic: Option, ) -> Self { Self { - isEnforcement, + is_enforcement, gws, - priorityLogicTag, - gatewayReferenceIds, - primaryLogic, - fallbackLogic, + priority_logic_tag, + gateway_reference_ids, + primary_logic, + fallback_logic, } } - pub fn setIsEnforcement(&mut self, isEnforcement: bool) -> &mut Self { - self.isEnforcement = isEnforcement; + pub fn set_is_enforcement(&mut self, is_enforcement: bool) -> &mut Self { + self.is_enforcement = is_enforcement; self } - pub fn setPriorityLogicTag(&mut self, priorityLogicTag: Option) -> &mut Self { - self.priorityLogicTag = priorityLogicTag; + pub fn set_priority_logic_tag(&mut self, priorityLogicTag: Option) -> &mut Self { + self.priority_logic_tag = priorityLogicTag; self } - pub fn setPrimaryLogic(&mut self, primaryLogic: Option) -> &mut Self { - self.primaryLogic = primaryLogic; + pub fn set_primary_logic(&mut self, primaryLogic: Option) -> &mut Self { + self.primary_logic = primaryLogic; self } - pub fn setGws(&mut self, setGws: Vec) -> &mut Self { + pub fn set_gws(&mut self, setGws: Vec) -> &mut Self { self.gws = setGws; self } pub fn build(&self) -> Self { Self { - isEnforcement: self.isEnforcement, + is_enforcement: self.is_enforcement, gws: self.gws.clone(), - priorityLogicTag: self.priorityLogicTag.clone(), - gatewayReferenceIds: self.gatewayReferenceIds.clone(), - primaryLogic: self.primaryLogic.clone(), - fallbackLogic: self.fallbackLogic.clone(), + priority_logic_tag: self.priority_logic_tag.clone(), + gateway_reference_ids: self.gateway_reference_ids.clone(), + primary_logic: self.primary_logic.clone(), + fallback_logic: self.fallback_logic.clone(), } } } @@ -1589,27 +1685,27 @@ pub struct PriorityLogicData { } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] -// #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PriorityLogicFailure { - NO_ERROR, - CONNECTION_FAILED, - COMPILATION_ERROR, - MEMORY_EXCEEDED, - GATEWAY_NAME_PARSE_FAILURE, - RESPONSE_CONTENT_TYPE_NOT_SUPPORTED, - RESPONSE_DECODE_FAILURE, - RESPONSE_PARSE_ERROR, - PL_EVALUATION_FAILED, - NULL_AFTER_ENFORCE, - UNHANDLED_EXCEPTION, - CODE_TOO_LARGE, + NoError, + ConnectionFailed, + CompilationError, + MemoryExceeded, + GatewayNameParseFailure, + ResponseContentTypeNotSupported, + ResponseDecodeFailure, + ResponseParseError, + PlEvaluationFailed, + NullAfterEnforce, + UnhandledException, + CodeTooLarge, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] -// #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Status { - SUCCESS, - FAILURE, + Success, + Failure, } #[derive(Debug, Serialize, Deserialize)] @@ -1630,6 +1726,7 @@ pub struct ResetGatewayInput { pub key: Option, pub hardTtl: u128, pub softTtl: f64, + pub gatewayReferenceIdEnabled: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -1775,20 +1872,23 @@ pub struct SuccessRate1AndNConfig { pub paymentMethodType: String, pub paymentMethod: Option, pub txnObjectType: Option, + pub gatewayReferenceId: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum FilterLevel { - TXN_OBJECT_TYPE, - PAYMENT_METHOD, - PAYMENT_METHOD_TYPE, + TxnObjectType, + PaymentMethod, + PaymentMethodType, } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ConfigSource { - GLOBAL_DEFAULT, - MERCHANT_DEFAULT, - SERVICE_CONFIG, + GlobalDefault, + MerchantDefault, + ServiceConfig, REDIS, } @@ -1834,7 +1934,32 @@ pub async fn initial_decider_flow<'a>( } } -struct Reader { - reader: T, - tenant_state: TenantAppState, +pub struct Reader { + pub reader: T, + pub tenant_state: TenantAppState, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SrRoutingDimensions { + pub card_network: Option, + pub card_isin: Option, + pub currency: Option, + pub country: Option, + pub auth_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricEntry { + pub n_value: f64, + pub success_rate: f64, + pub sigma_factor: f64, + pub average_latency: f64, + pub tp99_latency: f64, + pub default_success_threshold: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SrMetrics { + pub dimension: String, + pub value: MetricEntry, } diff --git a/src/decider/gatewaydecider/utils.rs b/src/decider/gatewaydecider/utils.rs index 45c6451b..86d0c254 100644 --- a/src/decider/gatewaydecider/utils.rs +++ b/src/decider/gatewaydecider/utils.rs @@ -1,27 +1,32 @@ use crate::app::get_tenant_app_state; use crate::decider::gatewaydecider::types::{self, DeciderFlow}; +use crate::euclid::errors::EuclidErrors; +use crate::euclid::types::SrDimensionConfig; use crate::feedback::gateway_elimination_scoring::flow::{ eliminationV2RewardFactor, getPenaltyFactor, }; -use crate::redis::feature::isFeatureEnabled; +use crate::redis::feature::{is_feature_enabled, RedisCompressionConfigCombined, RedisDataStruct}; use crate::redis::types::ServiceConfigKey; use crate::types::card::card_type::card_type_to_text; -use crate::types::merchant::id::{merchant_id_to_text, MerchantId}; +use crate::types::country::country_iso::CountryISO2; +use crate::types::currency::Currency; +use crate::types::merchant::id::merchant_id_to_text; use crate::types::merchant::merchant_gateway_account::MerchantGatewayAccount; use crate::types::money::internal::Money; use crate::types::payment::payment_method_type_const::*; use crate::types::payment_flow::{payment_flows_to_text, PaymentFlow}; +use crate::types::service_configuration::find_config_by_name; use crate::types::user_eligibility_info::{ get_eligibility_info, identifier_name_to_text, IdentifierName, }; -use crate::utils::{generate_random_number, get_current_date_in_millis}; -use crate::{decider, feedback, logger}; -use diesel::Identifiable; +use crate::utils::generate_random_number; +use crate::{feedback, logger}; +use error_stack::ResultExt; use fred::prelude::{KeysInterface, ListInterface}; use masking::PeekInterface; +use masking::Secret; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_json::from_value; +use serde::{Deserialize, Serialize, Serializer}; use serde_json::{from_slice, from_str, Value}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -54,9 +59,9 @@ use crate::types::feature as ETF; // use crate::types::gateway as Gateway; // // use types::gateway_payment_method as ETGPM; use super::types::{ - ConfigurableBlock, GatewayList, GatewayRedisKeyMap, GatewayScoreMap, GatewayScoringData, - GatewayWiseExtraScore, InternalMetadata, MessageFormat, OptimizationRedisBlockData, - ScoreKeyType, SplitSettlementDetails, SrV3InputConfig, SrV3SubLevelInputConfig, + GatewayList, GatewayRedisKeyMap, GatewayScoreMap, GatewayScoringData, GatewayWiseExtraScore, + InternalMetadata, MessageFormat, ScoreKeyType, SplitSettlementDetails, SrRoutingDimensions, + SrV3InputConfig, SrV3SubLevelInputConfig, }; use crate::types::merchant as ETM; use crate::types::merchant_gateway_card_info as ETMGCI; @@ -68,7 +73,6 @@ use crate::types::order as ETO; use crate::types::txn_details::types as ETTD; use crate::types::txn_offer as ETTO; // use juspay::extra::parsing as P; -use crate::types::gateway as ETG; use crate::types::gateway_routing_input::{GatewayScore, GatewaySuccessRateBasedRoutingInput}; use crate::types::token_bin_info as ETTB; // // use utils::config::constants as Config; @@ -76,8 +80,7 @@ use crate::types::token_bin_info as ETTB; // // use safe::Safe; // // use control::category::Category; // // use juspay::extra::non_empty_text as NET; -use crate::redis::{self, cache as RService}; -use crate::types::isin_routes as ETIsinR; +use crate::redis::cache as RService; // // use utils::redis as EWRedis; // // use db::common::types::payment_flows as PF; // // use utils::redis as Redis; @@ -86,6 +89,7 @@ use crate::types::isin_routes as ETIsinR; // // use configs::env_vars as ENV; use crate::error::StorageError; use crate::types::gateway_card_info::ValidationType; +use crate::types::order::udfs::{get_udf, UDFs}; pub fn either_decode_t Deserialize<'de>>(text: &str) -> Result { from_slice(text.as_bytes()).map_err(|e| e.to_string()) @@ -127,6 +131,14 @@ pub fn is_otm_enabled(mga: &ETM::merchant_gateway_account::MerchantGatewayAccoun check_if_enabled_in_mga(mga, "ONE_TIME_MANDATE", "OTM_ENABLED") } +pub fn is_pix_flows_enabled( + mga: &ETM::merchant_gateway_account::MerchantGatewayAccount, + payment_flow: String, + acc_details_flag: String, +) -> bool { + check_if_enabled_in_mga(mga, &payment_flow, &acc_details_flag) +} + pub fn is_seamless(mga: &ETM::merchant_gateway_account::MerchantGatewayAccount) -> bool { let secret_json = Some(mga.account_details.peek()); secret_json @@ -144,21 +156,21 @@ pub fn fetch_emi_type(txn_card_info: &ETCa::txn_card_info::TxnCardInfo) -> Optio txn_card_info .paymentSource .as_ref() - .and_then(|source| get_value("emi_type", source)) + .and_then(|source| get_value("emi_type", source.peek())) } pub fn fetch_extended_card_bin(txn_card_info: &ETCa::txn_card_info::TxnCardInfo) -> Option { txn_card_info .paymentSource .as_ref() - .and_then(|source| get_value("extended_card_bin", source)) + .and_then(|source| get_value("extended_card_bin", source.peek())) } pub fn fetch_juspay_bank_code(txn_card_info: &ETCa::txn_card_info::TxnCardInfo) -> Option { txn_card_info .paymentSource .as_ref() - .and_then(|source| get_value("juspay_bank_code", source)) + .and_then(|source| get_value("juspay_bank_code", source.peek())) } pub fn get_pl_gw_ref_id_map(decider_flow: &DeciderFlow<'_>) -> HashMap { @@ -166,13 +178,13 @@ pub fn get_pl_gw_ref_id_map(decider_flow: &DeciderFlow<'_>) -> HashMap, enable_gateway_reference_id_based_routing: Option, - order: &ETO::Order, + _order: &ETO::Order, ) -> (HashMap, HashMap) { if enable_gateway_reference_id_based_routing.unwrap_or(false) { let order_metadata = get_metadata(decider_flow); @@ -195,36 +207,39 @@ pub fn is_emandate_supported_payment_method( pub fn is_emandate_transaction(txn_detail: &ETTD::TxnDetail) -> bool { matches!( txn_detail.txnObjectType, - ETTD::TxnObjectType::EmandateRegister - | ETTD::TxnObjectType::TpvEmandateRegister - | ETTD::TxnObjectType::EmandatePayment - | ETTD::TxnObjectType::TpvEmandatePayment + Some(ETTD::TxnObjectType::EmandateRegister) + | Some(ETTD::TxnObjectType::TpvEmandateRegister) + | Some(ETTD::TxnObjectType::EmandatePayment) + | Some(ETTD::TxnObjectType::TpvEmandatePayment) ) } pub fn is_emandate_payment_transaction(txn_detail: &ETTD::TxnDetail) -> bool { matches!( txn_detail.txnObjectType, - ETTD::TxnObjectType::EmandatePayment | ETTD::TxnObjectType::TpvEmandatePayment + Some(ETTD::TxnObjectType::EmandatePayment) | Some(ETTD::TxnObjectType::TpvEmandatePayment) ) } -pub fn is_reccuring_payment_transaction(txn_detail: &ETTD::TxnDetail) -> bool { +pub fn is_recurring_payment_transaction(txn_detail: &ETTD::TxnDetail) -> bool { matches!( txn_detail.txnObjectType, - ETTD::TxnObjectType::EmandatePayment - | ETTD::TxnObjectType::TpvEmandatePayment - | ETTD::TxnObjectType::MandatePayment - | ETTD::TxnObjectType::TpvMandatePayment + Some(ETTD::TxnObjectType::EmandatePayment) + | Some(ETTD::TxnObjectType::TpvEmandatePayment) + | Some(ETTD::TxnObjectType::MandatePayment) + | Some(ETTD::TxnObjectType::TpvMandatePayment) ) } pub fn is_tpv_transaction(txn_detail: &ETTD::TxnDetail) -> bool { - txn_detail.txnObjectType == ETTD::TxnObjectType::TpvPayment + matches!( + txn_detail.txnObjectType, + Some(ETTD::TxnObjectType::TpvPayment) + ) } pub fn is_tpv_mandate_transaction(txn_detail: &ETTD::TxnDetail) -> bool { - txn_detail.txnObjectType == ETTD::TxnObjectType::TpvEmandateRegister + txn_detail.txnObjectType == Some(ETTD::TxnObjectType::TpvEmandateRegister) } pub fn get_merchant_wise_si_bin_key(gw: &String) -> String { @@ -234,18 +249,18 @@ pub fn get_merchant_wise_si_bin_key(gw: &String) -> String { fn get_merchant_gateway_card_info_feature_name( auth_type: Option<&ETCa::txn_card_info::AuthType>, validation_type: Option<&ValidationType>, - gateway: &String, + gw: &String, ) -> Option { let flow = validation_type .map(|v| format!("{}", v)) .or_else(|| auth_type.map(|a| format!("{}", a)))?; - Some(format!("MERCHANT_GATEWAY_CARD_INFO_{}_{}", flow, gateway)) + Some(format!("MERCHANT_GATEWAY_CARD_INFO_{}_{}", flow, gw)) } pub fn is_mandate_transaction(txn: &ETTD::TxnDetail) -> bool { matches!( txn.txnObjectType, - ETTD::TxnObjectType::MandateRegister | ETTD::TxnObjectType::MandatePayment + Some(ETTD::TxnObjectType::MandateRegister) | Some(ETTD::TxnObjectType::MandatePayment) ) } @@ -255,26 +270,26 @@ pub async fn get_merchant_wise_mandate_bin_eligible_gateways( ) -> Vec { let merchant_wise_mandate_bin_enforced_gateways: Vec = RService::findByNameFromRedis::>( - C::MERCHANT_WISE_MANDATE_BIN_ENFORCED_GATEWAYS.get_key(), + C::MerchantWiseMandateBinEnforcedGateways.get_key(), ) .await .unwrap_or_default(); let merchant_wise_mandate_supported_gateway: Vec = merchant_wise_mandate_bin_enforced_gateways .into_iter() - .filter(|gateway| mandate_enabled_gateways.contains(gateway)) + .filter(|gw| mandate_enabled_gateways.contains(gw)) .collect(); let mut gws = Vec::new(); - for gateway in merchant_wise_mandate_supported_gateway { + for gw in merchant_wise_mandate_supported_gateway { if ETF::get_feature_enabled( - &get_merchant_wise_si_bin_key(&gateway), + &get_merchant_wise_si_bin_key(&gw), &merchant_account.merchantId, true, ) .await .is_some() { - gws.push(gateway); + gws.push(gw); } } gws @@ -284,17 +299,17 @@ pub async fn is_merchant_wise_auth_type_check_needed( merchant_account: &ETM::merchant_account::MerchantAccount, auth_type: Option<&ETCa::txn_card_info::AuthType>, validation_type: Option<&ValidationType>, - gateway: &String, + gw: &String, ) -> bool { let merchant_wise_auth_type_bin_enforced_gateways: Vec = RService::findByNameFromRedis::>( - C::MERCHANT_WISE_AUTH_TYPE_BIN_ENFORCED_GATEWAYS.get_key(), + C::MerchantWiseAuthTypeBinEnforcedGateways.get_key(), ) .await .unwrap_or_default(); - if merchant_wise_auth_type_bin_enforced_gateways.contains(gateway) { + if merchant_wise_auth_type_bin_enforced_gateways.contains(gw) { if let Some(feature_key) = - get_merchant_gateway_card_info_feature_name(auth_type, validation_type, gateway) + get_merchant_gateway_card_info_feature_name(auth_type, validation_type, gw) { return ETF::get_feature_enabled(&feature_key, &merchant_account.merchantId, true) .await @@ -462,24 +477,6 @@ pub fn get_value_from_text(key: &str, t: &Value) -> Option { } } -fn get_enabled_gateway_for_brand(brand: &str, enabled_gateways: Option<&Value>) -> Option { - enabled_gateways.and_then(|gateways| match gateways { - Value::Object(map) => map.get(brand).cloned(), - _ => None, - }) -} - -fn parse_aeson_string Deserialize<'de>>(value: &Value) -> Option { - match value { - Value::String(s) => from_str(s).ok(), - _ => None, - } -} - -fn result_to_maybe(result: Result) -> Option { - result.ok() -} - fn decode_metadata(text: &str) -> HashMap { from_str::>(text) .unwrap_or_default() @@ -496,7 +493,7 @@ fn decode_metadata(text: &str) -> HashMap { pub fn get_all_possible_ref_ids( metadata: HashMap, - oref: ETO::Order, + _oref: ETO::Order, pl_ref_id_map: HashMap, ) -> Vec { let gateway_ref_ids = is_suffix_of_gateway_ref_id(metadata.iter().collect()); @@ -540,7 +537,7 @@ pub async fn get_all_ref_ids( pub fn get_gateway_reference_id( metadata: HashMap, gw: &String, - oref: ETO::Order, + _oref: ETO::Order, pl_ref_id_map: HashMap, ) -> Option { let meta_res = pl_ref_id_map @@ -559,7 +556,7 @@ pub fn get_gateway_reference_id( pub async fn effective_amount_with_txn_amount(txn_detail: ETTD::TxnDetail) -> Money { let def_amount = Money::from_double(0.0); - let amount_txn = &txn_detail.txnAmount; + let amount_txn = txn_detail.txnAmount.as_ref().unwrap_or(&def_amount); let offers = ETTO::getOffers(&txn_detail.id).await; let discount_sum: Money = Money::from_double( offers @@ -607,7 +604,7 @@ pub fn is_emandate_amount_filter_needed( } pub fn is_emandate_register_transaction(txn_detail: &ETTD::TxnDetail) -> bool { - txn_detail.txnObjectType == ETTD::TxnObjectType::EmandateRegister + txn_detail.txnObjectType == Some(ETTD::TxnObjectType::EmandateRegister) } pub async fn get_card_brand(decider_flow: &mut DeciderFlow<'_>) -> Option { @@ -715,11 +712,16 @@ pub async fn get_split_settlement_details( } } -pub async fn metric_tracker_log(stage: &str, flowtype: &str, log_data: MessageFormat) { +pub async fn metric_tracker_log( + consume_from_router: Option, + _stage: &str, + _flowtype: &str, + log_data: MessageFormat, +) { let normalized_log_data = match serde_json::to_value(&log_data) { Ok(value) => value, Err(e) => { - crate::logger::error!( + crate::logger::info!( action = "metric_tracking_log_error", "Failed to serialize log_data: {}", e @@ -727,11 +729,27 @@ pub async fn metric_tracker_log(stage: &str, flowtype: &str, log_data: MessageFo return; } }; - crate::logger::info!( - action = "metric_tracking_log", - "{}", - normalized_log_data.to_string(), - ); + + if consume_from_router == Some(true) { + crate::logger::info!( + action = "metric_tracking_log", + "{}", + normalized_log_data.to_string(), + ); + } else { + crate::logger::info!( + action = "metric_tracking_log_diff_check_de", + "{}", + normalized_log_data.to_string(), + ); + } +} + +pub fn mask_secret_option(_: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str("FILTERED") } pub fn get_metric_log_format(decider_flow: &mut DeciderFlow<'_>, stage: &str) -> MessageFormat { @@ -741,17 +759,17 @@ pub fn get_metric_log_format(decider_flow: &mut DeciderFlow<'_>, stage: &str) -> let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); let order_reference = decider_flow.get().dpOrder.clone(); let x_req_id = decider_flow.logger.get("x-request-id"); - let payment_source_m = txn_card_info - .paymentSource - .as_ref() - .and_then(|ps| last(split("@", ps))); + let payment_source_m = txn_card_info.get_payment_source_last(); MessageFormat { - model: txn_detail.txnObjectType.to_string(), + model: txn_detail + .txnObjectType + .map(|t| t.to_string()) + .unwrap_or_default(), log_type: "APP_EVENT".to_string(), payment_method: txn_card_info.paymentMethod.clone(), payment_method_type: txn_card_info.paymentMethodType.clone(), - payment_source: payment_source_m, + payment_source: payment_source_m.map(Secret::new), source_object: txn_detail.sourceObject.clone(), txn_detail_id: txn_detail.id.clone(), stage: stage.to_string(), @@ -767,6 +785,12 @@ pub fn get_metric_log_format(decider_flow: &mut DeciderFlow<'_>, stage: &str) -> bank_code: fetch_juspay_bank_code(&txn_card_info), x_request_id: x_req_id.cloned(), log_data: serde_json::to_value(mp).unwrap(), + udf_consumed: decider_flow + .writer + .gateway_scoring_data + .udfs_consumed_for_routing + .as_ref() + .cloned(), } } @@ -787,7 +811,7 @@ pub async fn log_gateway_decider_approach( let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); let x_req_id = decider_flow.logger.get("x-request-id").cloned(); let txn_creation_time = txn_detail.dateCreated.to_string(); // Assuming dateCreated is a DateTime field - + let consume_from_router = decider_flow.get().dpShouldConsumeResult; let mp = types::DeciderApproachLogData { decided_gateway: m_decided_gateway, routing_approach: gateway_decider_approach, @@ -799,23 +823,28 @@ pub async fn log_gateway_decider_approach( dateCreated: txn_creation_time, }; - let payment_source_m = txn_card_info - .paymentSource - .as_ref() - .and_then(|ps| ps.split('@').next_back().map(String::from)); + let payment_source_m = txn_card_info.get_payment_source_last(); metric_tracker_log( + consume_from_router, "GATEWAY_DECIDER_APPROACH", "DECIDER", MessageFormat { - model: txn_detail.txnObjectType.to_string(), + model: txn_detail + .txnObjectType + .map(|t| t.to_string()) + .unwrap_or_default(), log_type: "APP_EVENT".to_string(), payment_method: txn_card_info.clone().paymentMethod, payment_method_type: txn_card_info.clone().paymentMethodType.to_string(), - payment_source: payment_source_m, + payment_source: payment_source_m.map(Secret::new), source_object: txn_detail.sourceObject, txn_detail_id: txn_detail.id, - stage: "GATEWAY_DECIDER_APPROACH".to_string(), + stage: if consume_from_router == Some(true) { + "GATEWAY_DECIDER_APPROACH".to_string() + } else { + "GATEWAY_DECIDER_APPROACH_DIFF_CHECK_DE".to_string() + }, merchant_id: merchant_id_to_text(order_reference.merchantId), txn_uuid: txn_detail.txnUuid, order_id: order_reference.orderId.0, @@ -828,6 +857,7 @@ pub async fn log_gateway_decider_approach( bank_code: fetch_juspay_bank_code(&txn_card_info), x_request_id: x_req_id, log_data: serde_json::to_value(mp).unwrap(), + udf_consumed: None, }, ) .await; @@ -846,7 +876,11 @@ pub fn get_true_string(val: Option) -> Option { } } -pub async fn get_card_bin_from_token_bin(length: usize, token_bin: &str) -> String { +pub async fn get_card_bin_from_token_bin( + length: usize, + token_bin: &str, + redis_compression_config: Option, +) -> String { let key = format!("token_bin_{}", token_bin); let app_state = get_tenant_app_state().await; // let redis = &decider_flow.state().redis_conn; @@ -854,9 +888,14 @@ pub async fn get_card_bin_from_token_bin(length: usize, token_bin: &str) -> Stri Some(bin) => bin.chars().take(length).collect(), None => match get_extended_token_bin_info(token_bin).await { Some(token_bin_info) => { - app_state + let _ = app_state .redis_conn - .set_key(&key, &token_bin_info.cardBin) + .set_key( + &key, + &token_bin_info.cardBin, + redis_compression_config.clone(), + RedisDataStruct::STRING, + ) .await; token_bin_info.cardBin.chars().take(length).collect() } @@ -932,7 +971,7 @@ pub enum EnabledGatewaysForBrand { } pub async fn get_token_supported_gateways( - txn_detail: ETTD::TxnDetail, + _txn_detail: ETTD::TxnDetail, txn_card_info: ETCa::txn_card_info::TxnCardInfo, flow: String, m_internal_meta: Option, @@ -1019,13 +1058,13 @@ async fn get_token_supported_gateways_key( ) -> Option> { if brand == token_provider { RService::findByNameFromRedis( - C::TOKEN_SUPPORTED_GATEWAYS(brand, None, provider_category, flow).get_key(), + C::TokenSupportedGateways(brand, None, provider_category, flow).get_key(), ) .await .unwrap_or_default() } else { RService::findByNameFromRedis( - C::TOKEN_SUPPORTED_GATEWAYS(brand, Some(token_provider), provider_category, flow) + C::TokenSupportedGateways(brand, Some(token_provider), provider_category, flow) .get_key(), ) .await @@ -1091,25 +1130,19 @@ pub fn get_m_id(mid: ETM::id::MerchantId) -> String { } async fn get_upi_handle_list() -> Vec { - RService::findByNameFromRedis(C::V2_ROUTING_HANDLE_LIST.get_key()) - .await - .unwrap_or_default() -} - -async fn get_upi_psp_list() -> Vec { - RService::findByNameFromRedis(C::V2_ROUTING_PSP_LIST.get_key()) + RService::findByNameFromRedis(C::V2RoutingHandleList.get_key()) .await .unwrap_or_default() } async fn get_routing_top_bank_list() -> Vec { - RService::findByNameFromRedis(C::V2_ROUTING_TOP_BANK_LIST.get_key()) + RService::findByNameFromRedis(C::V2RoutingTopBankList.get_key()) .await .unwrap_or_default() } async fn get_upi_package_list() -> Vec { - RService::findByNameFromRedis(C::V2_ROUTING_PSP_PACKAGE_LIST.get_key()) + RService::findByNameFromRedis(C::V2RoutingPspPackageList.get_key()) .await .unwrap_or_default() } @@ -1129,27 +1162,6 @@ pub fn get_bin_list(card_bin: Option) -> Vec> { } } -async fn get_isin_routes_with_extended_bins( - card_bin: Option, - merchant_id: MerchantId, -) -> Option { - match get_true_string(card_bin) { - None => None, - Some(bin) => { - let bin_list = if bin.len() > 6 { - (6..=9).map(|len| bin[..len].to_string()).collect() - } else { - vec![bin] - }; - let mut isin_route_list = - ETIsinR::find_all_by_isin_and_merchant_id(bin_list, &merchant_id).await; - isin_route_list.sort_by(|x, y| y.isin.cmp(&x.isin)); - let reverse_list: Vec<_> = isin_route_list.into_iter().collect(); - reverse_list.first().cloned() - } - } -} - pub async fn get_card_info_by_bin(card_bin: Option) -> Option { logger::debug!("getCardInfoByBin cardBin: {:?}", card_bin); match get_true_string(card_bin) { @@ -1158,7 +1170,7 @@ pub async fn get_card_info_by_bin(card_bin: Option) -> Option 6 { (6..=9) .filter(|&len| len <= bin.len()) - .map(|len| (ETCa::isin::to_isin(bin[..len].to_string()))) + .map(|len| ETCa::isin::to_isin(bin[..len].to_string())) .collect() } else { vec![(ETCa::isin::to_isin(bin))] @@ -1193,7 +1205,7 @@ pub fn get_payment_flow_list_from_txn_detail(txn_detail: &ETTD::TxnDetail) -> Ve Some(PaymentFlowInfoInInternalTrackingInfo { paymentFlowInfo }) => paymentFlowInfo .paymentFlows .into_iter() - .filter(|flow| C::paymentFlowsRequiredForGwFiltering.contains(&flow.as_str())) + .filter(|flow| C::PAYMENT_FLOWS_REQUIRED_FOR_GW_FILTERING.contains(&flow.as_str())) .collect(), None => vec![], } @@ -1201,19 +1213,6 @@ pub fn get_payment_flow_list_from_txn_detail(txn_detail: &ETTD::TxnDetail) -> Ve use crate::decider::gatewaydecider::types::PaymentFlowInfoInInternalTrackingInfo; -fn get_payment_flow_list_from_txn_detail_(txn_detail: &ETTD::TxnDetail) -> Vec { - match txn_detail - .internalTrackingInfo - .as_ref() - .and_then(|info| either_decode_t(info).ok()) - { - Some(PaymentFlowInfoInInternalTrackingInfo { paymentFlowInfo }) => { - paymentFlowInfo.paymentFlows - } - None => vec![], - } -} - pub fn set_payment_flow_list(decider_flow: &mut DeciderFlow<'_>, payment_flow_list: Vec) { decider_flow.writer.paymentFlowList = payment_flow_list; } @@ -1286,10 +1285,10 @@ pub fn get_gateway_decider_approach( if gw_set.len() > 1 { gateway_decider_approach } else { - types::GatewayDeciderApproach::DEFAULT + types::GatewayDeciderApproach::Default } } else { - types::GatewayDeciderApproach::NONE + types::GatewayDeciderApproach::None } } @@ -1298,60 +1297,52 @@ pub fn modify_gateway_decider_approach( down_time: types::DownTime, ) -> types::GatewayDeciderApproach { match gw_decider_approach { - types::GatewayDeciderApproach::SR_SELECTION_V3_ROUTING => match down_time { - types::DownTime::ALL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V3_ALL_DOWNTIME_ROUTING + types::GatewayDeciderApproach::SrSelectionV3Routing => match down_time { + types::DownTime::AllDowntime => types::GatewayDeciderApproach::SrV3AllDowntimeRouting, + types::DownTime::GlobalDowntime => { + types::GatewayDeciderApproach::SrV3GlobalDowntimeRouting } - types::DownTime::GLOBAL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V3_GLOBAL_DOWNTIME_ROUTING - } - types::DownTime::DOWNTIME => types::GatewayDeciderApproach::SR_V3_DOWNTIME_ROUTING, - types::DownTime::NO_DOWNTIME => types::GatewayDeciderApproach::SR_SELECTION_V3_ROUTING, + types::DownTime::Downtime => types::GatewayDeciderApproach::SrV3DowntimeRouting, + types::DownTime::NoDowntime => types::GatewayDeciderApproach::SrSelectionV3Routing, }, - types::GatewayDeciderApproach::SR_V3_HEDGING => match down_time { - types::DownTime::ALL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V3_ALL_DOWNTIME_HEDGING - } - types::DownTime::GLOBAL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V3_GLOBAL_DOWNTIME_HEDGING + types::GatewayDeciderApproach::SrV3Hedging => match down_time { + types::DownTime::AllDowntime => types::GatewayDeciderApproach::SrV3AllDowntimeHedging, + types::DownTime::GlobalDowntime => { + types::GatewayDeciderApproach::SrV3GlobalDowntimeHedging } - types::DownTime::DOWNTIME => types::GatewayDeciderApproach::SR_V3_DOWNTIME_HEDGING, - types::DownTime::NO_DOWNTIME => types::GatewayDeciderApproach::SR_V3_HEDGING, + types::DownTime::Downtime => types::GatewayDeciderApproach::SrV3DowntimeHedging, + types::DownTime::NoDowntime => types::GatewayDeciderApproach::SrV3Hedging, }, - types::GatewayDeciderApproach::SR_SELECTION_V2_ROUTING => match down_time { - types::DownTime::ALL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V2_ALL_DOWNTIME_ROUTING + types::GatewayDeciderApproach::SrSelectionV2Routing => match down_time { + types::DownTime::AllDowntime => types::GatewayDeciderApproach::SrV2AllDowntimeRouting, + types::DownTime::GlobalDowntime => { + types::GatewayDeciderApproach::SrV2GlobalDowntimeRouting } - types::DownTime::GLOBAL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V2_GLOBAL_DOWNTIME_ROUTING - } - types::DownTime::DOWNTIME => types::GatewayDeciderApproach::SR_V2_DOWNTIME_ROUTING, - types::DownTime::NO_DOWNTIME => types::GatewayDeciderApproach::SR_SELECTION_V2_ROUTING, + types::DownTime::Downtime => types::GatewayDeciderApproach::SrV2DowntimeRouting, + types::DownTime::NoDowntime => types::GatewayDeciderApproach::SrSelectionV2Routing, }, - types::GatewayDeciderApproach::SR_V2_HEDGING => match down_time { - types::DownTime::ALL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V2_ALL_DOWNTIME_HEDGING - } - types::DownTime::GLOBAL_DOWNTIME => { - types::GatewayDeciderApproach::SR_V2_GLOBAL_DOWNTIME_HEDGING + types::GatewayDeciderApproach::SrV2Hedging => match down_time { + types::DownTime::AllDowntime => types::GatewayDeciderApproach::SrV2AllDowntimeHedging, + types::DownTime::GlobalDowntime => { + types::GatewayDeciderApproach::SrV2GlobalDowntimeHedging } - types::DownTime::DOWNTIME => types::GatewayDeciderApproach::SR_V2_DOWNTIME_HEDGING, - types::DownTime::NO_DOWNTIME => types::GatewayDeciderApproach::SR_V2_HEDGING, + types::DownTime::Downtime => types::GatewayDeciderApproach::SrV2DowntimeHedging, + types::DownTime::NoDowntime => types::GatewayDeciderApproach::SrV2Hedging, }, _ => match down_time { - types::DownTime::ALL_DOWNTIME => types::GatewayDeciderApproach::PL_ALL_DOWNTIME_ROUTING, - types::DownTime::GLOBAL_DOWNTIME => { - types::GatewayDeciderApproach::PL_GLOBAL_DOWNTIME_ROUTING + types::DownTime::AllDowntime => types::GatewayDeciderApproach::PlAllDowntimeRouting, + types::DownTime::GlobalDowntime => { + types::GatewayDeciderApproach::PlGlobalDowntimeRouting } - types::DownTime::DOWNTIME => types::GatewayDeciderApproach::PL_DOWNTIME_ROUTING, - types::DownTime::NO_DOWNTIME => types::GatewayDeciderApproach::PRIORITY_LOGIC, + types::DownTime::Downtime => types::GatewayDeciderApproach::PlDowntimeRouting, + types::DownTime::NoDowntime => types::GatewayDeciderApproach::PriorityLogic, }, } } pub fn get_juspay_bank_code_from_internal_metadata(txn_detail: &ETTD::TxnDetail) -> Option { txn_detail.internalMetadata.as_ref().and_then(|metadata| { - from_str::(metadata).ok().and_then(|json| { + from_str::(metadata.peek()).ok().and_then(|json| { json.get("juspayBankCode") .and_then(|v| v.as_str().map(|s| s.to_string())) }) @@ -1367,6 +1358,48 @@ pub fn get_ref_id_value( } } +pub async fn get_common_gateway_ref_id( + decider_flow: &mut DeciderFlow<'_>, +) -> (bool, Option) { + let order_ref = decider_flow.get().dpOrder.clone(); + let merchant = decider_flow.get().dpMerchantAccount.clone(); + + let (meta, pl_ref_id_map) = get_order_metadata_and_pl_ref_id_map( + decider_flow, + merchant.enableGatewayReferenceIdBasedRouting, + &order_ref, + ); + + let gateway_list = decider_flow.writer.functionalGateways.clone(); + + let ref_ids: Vec = gateway_list + .iter() + .map(|gw| { + let gw_ref_id = get_gateway_reference_id( + meta.clone(), + gw, + order_ref.clone(), + pl_ref_id_map.clone(), + ); + match gw_ref_id { + None => "NULL".to_string(), + Some(ref_id) => ref_id.mga_reference_id, + } + }) + .collect(); + + if ref_ids.is_empty() { + return (false, None); + } + + let first_ref_id = &ref_ids[0]; + if ref_ids.iter().all(|ref_id| ref_id == first_ref_id) { + (true, Some(first_ref_id.clone())) + } else { + (false, None) + } +} + pub fn decider_filter_order(filter_name: &str) -> i32 { match filter_name { "getFunctionalGateways" => 1, @@ -1392,7 +1425,7 @@ pub fn decider_filter_order(filter_name: &str) -> i32 { "preferredGateway" => 21, "filterEnforcement" => 22, "filterFunctionalGatewaysForMerchantRequiredFlow" => 23, - "filterGatewaysForEMITenureSpecficGatewayCreds" => 24, + "filterGatewaysForEMITenureSpecificGatewayCreds" => 24, "filterGatewaysForMGASelectionIntegrity" => 25, "FilterFunctionalGatewaysForOTM" => 26, _ => 27, @@ -1429,13 +1462,13 @@ pub fn decider_filter_order(filter_name: &str) -> i32 { // pub async fn get_block_time_period(merchant_id: &str) -> i64 { // match RService::findByNameFromRedis::( -// C::OPTIMIZATION_ROUTING_CONFIG(merchant_id.to_string()).get_key(), +// C::OptimizationRoutingConfig(merchant_id.to_string()).get_key(), // ) // .await // { // Some(config_block) => config_block.block_timeperiod.round() as i64, // None => match RService::findByNameFromRedis::( -// C::DEFAULT_OPTIMIZATION_ROUTING_CONFIG.get_key(), +// C::DefaultOptimizationRoutingConfig.get_key(), // ) // .await // { @@ -1501,15 +1534,15 @@ pub async fn get_experiment_tag(utc_time: OffsetDateTime, dim: &str) -> Option, ) { // todo!() let app_state = get_tenant_app_state().await; - let r: Result, error_stack::Report> = + let _r: Result, error_stack::Report> = app_state .redis_conn .multi(false, |transaction| { @@ -1517,24 +1550,18 @@ pub async fn create_moving_window_and_score( transaction.del::<(), _>(queue_key.clone()).await?; transaction .lpush::<(), _, _>( - queue_key.as_bytes().clone(), + queue_key.as_bytes(), score_list.iter().map(|s| s.as_bytes()).collect::>(), ) .await?; transaction - .set::<(), _, _>( - score_key.clone(), - score.to_string().clone(), - None, - None, - false, - ) + .set::<(), _, _>(score_key.clone(), score.to_string(), None, None, false) .await?; transaction - .expire::<(), _>(queue_key.as_bytes().clone(), 10000000) + .expire::<(), _>(queue_key.as_bytes(), 10000000) .await?; transaction - .expire::<(), _>(score_key.as_bytes().clone(), 10000000) + .expire::<(), _>(score_key.as_bytes(), 10000000) .await?; Ok(()) }) @@ -1560,11 +1587,16 @@ pub fn get_sr_v3_latency_threshold( sr_v3_input_config: Option, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.latencyThreshold.is_some() - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| x.latencyThreshold.is_some(), + ) .and_then(|sub_config| sub_config.latencyThreshold) .or(config.defaultLatencyThreshold) }) @@ -1574,11 +1606,16 @@ pub fn get_sr_v3_bucket_size( sr_v3_input_config: Option, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.bucketSize.is_some() - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| x.bucketSize.is_some(), + ) .and_then(|sub_config| sub_config.bucketSize) .or(config.defaultBucketSize) .filter(|&size| size > 0) @@ -1589,11 +1626,16 @@ pub fn get_sr_v3_hedging_percent( sr_v3_input_config: Option, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.hedgingPercent.is_some() - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| x.hedgingPercent.is_some(), + ) .and_then(|sub_config| sub_config.hedgingPercent) .or(config.defaultHedgingPercent) .filter(|&percent| percent >= 0.0) @@ -1604,11 +1646,16 @@ pub fn get_sr_v3_lower_reset_factor( sr_v3_input_config: Option, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.lowerResetFactor.is_some() - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| x.lowerResetFactor.is_some(), + ) .and_then(|sub_config| sub_config.lowerResetFactor) .or(config.defaultLowerResetFactor) .filter(|&factor| factor >= 0.0) @@ -1619,11 +1666,16 @@ pub fn get_sr_v3_upper_reset_factor( sr_v3_input_config: Option, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.upperResetFactor.is_some() - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| x.upperResetFactor.is_some(), + ) .and_then(|sub_config| sub_config.upperResetFactor) .or(config.defaultUpperResetFactor) .filter(|&factor| factor >= 0.0) @@ -1635,13 +1687,20 @@ pub fn get_sr_v3_gateway_sigma_factor( pmt: &str, pm: &str, gw: &String, + sr_routing_dimensions: &SrRoutingDimensions, ) -> Option { sr_v3_input_config.and_then(|config| { - get_sr_v3_sub_level_input_config(&config.subLevelInputConfig, pmt, pm, |x| { - x.gatewayExtraScore - .as_ref() - .is_some_and(|scores| scores.iter().any(|score| score.gatewayName == *gw)) - }) + get_sr_v3_sub_level_input_config( + &config.subLevelInputConfig, + pmt, + pm, + sr_routing_dimensions, + |x| { + x.gatewayExtraScore + .as_ref() + .is_some_and(|scores| scores.iter().any(|score| score.gatewayName == *gw)) + }, + ) .and_then(|sub_config| find_gateway_sigma_factor(&sub_config.gatewayExtraScore, gw)) .or_else(|| find_gateway_sigma_factor(&config.defaultGatewayExtraScore, gw)) }) @@ -1663,6 +1722,7 @@ fn get_sr_v3_sub_level_input_config( sub_level_input_config: &Option>, pmt: &str, pm: &str, + sr_routing_dimensions: &SrRoutingDimensions, is_input_non_null: impl Fn(&SrV3SubLevelInputConfig) -> bool, ) -> Option { sub_level_input_config @@ -1671,20 +1731,55 @@ fn get_sr_v3_sub_level_input_config( configs .iter() .find(|config| { - config.paymentMethodType == Some(pmt.to_string()) - && config.paymentMethod == Some(pm.to_string()) - && is_input_non_null(config) + is_sr_v3_config_match( + config, + Some(pmt.to_string()), + Some(pm.to_string()), + sr_routing_dimensions, + ) && is_input_non_null(config) }) .or_else(|| { configs.iter().find(|config| { - config.paymentMethodType == Some(pmt.to_string()) - && is_input_non_null(config) + is_sr_v3_config_match( + config, + Some(pmt.to_string()), + None, + sr_routing_dimensions, + ) && is_input_non_null(config) }) }) }) .cloned() } +fn is_sr_v3_config_match( + config: &SrV3SubLevelInputConfig, + pmt: Option, + pm: Option, + sr_routing_dimensions: &SrRoutingDimensions, +) -> bool { + let pmt_matches = config.paymentMethodType == pmt; + let pm_matches = config.paymentMethod.is_none() || config.paymentMethod == pm; + let card_network_matches = + config.cardNetwork.is_none() || config.cardNetwork == sr_routing_dimensions.card_network; + let card_isin_matches = + config.cardIsIn.is_none() || config.cardIsIn == sr_routing_dimensions.card_isin; + let currency_matches = + config.currency.is_none() || config.currency == sr_routing_dimensions.currency; + let country_matches = + config.country.is_none() || config.country == sr_routing_dimensions.country; + let auth_type_matches = + config.authType.is_none() || config.authType == sr_routing_dimensions.auth_type; + + pmt_matches + && pm_matches + && card_network_matches + && card_isin_matches + && currency_matches + && auth_type_matches + && country_matches +} + pub fn filter_upto_pmt( sub_level_input_config: Vec, pmt: String, @@ -1733,7 +1828,7 @@ pub async fn delete_score_key_if_bucket_size_changes( .delete_key(&[sr_redis_key, "}score".to_string()].concat()) .await { - Ok(res) => (), + Ok(_res) => (), Err(err) => { logger::error!( action = "deleteScoreKeyIfBucketSizeChanges", @@ -1759,7 +1854,7 @@ pub fn intercalate>(separator: &str, strings: &[S]) -> String { // Function to check if the bucket size has changed for a specific gateway pub async fn check_if_bucket_size_changed( - decider_flow: &mut DeciderFlow<'_>, + _decider_flow: &mut DeciderFlow<'_>, merchant_bucket_size: i32, gateway_redis_key: (String, String), ) -> bool { @@ -1818,7 +1913,8 @@ pub async fn check_if_bin_is_eligible_for_emi( (card_isin, juspay_bank_code, card_type) { let bin_check_mandated_banks: Option> = - RService::findByNameFromRedis(C::getEmiBinValidationSupportedBanksKey.get_key()).await; + RService::findByNameFromRedis(C::GET_EMI_BIN_VALIDATION_SUPPORTED_BANKS_KEY.get_key()) + .await; let should_do_bin_validation = bin_check_mandated_banks .is_some_and(|banks| banks.contains(&format!("{}::{}", juspay_bank_code, card_type))); if should_do_bin_validation { @@ -1831,7 +1927,7 @@ pub async fn check_if_bin_is_eligible_for_emi( bin_list, identifier_name_to_text(IdentifierName::BIN), juspay_bank_code, - payment_flows_to_text(&PaymentFlow::PG_EMI), + payment_flows_to_text(&PaymentFlow::PgEmi), ) .await; !emi_eligible_bins.is_empty() @@ -1864,6 +1960,15 @@ pub fn get_default_gateway_scoring_data( is_gri_enabled_for_elimination: bool, is_gri_enabled_for_sr_routing: bool, date_created: OffsetDateTime, + card_isin: Option, + card_switch_provider: Option>, + currency: Option, + country: Option, + auth_type: Option, + useServiceConfigForGri: bool, + gatewayRefId: Option, + udfs: Option, + is_legacy_decider_flow: bool, ) -> GatewayScoringData { GatewayScoringData { merchantId: merchant_id, @@ -1872,16 +1977,32 @@ pub fn get_default_gateway_scoring_data( orderType: order_type, cardType: None, bankCode: None, - authType: None, + authType: auth_type, paymentSource: None, isPaymentSourceEnabledForSrRouting: false, isAuthLevelEnabledForSrRouting: false, isBankLevelEnabledForSrRouting: false, - isGriEnabledForElimination: is_gri_enabled_for_elimination, - isGriEnabledForSrRouting: is_gri_enabled_for_sr_routing, + isGriEnabledForElimination: if useServiceConfigForGri { + is_gri_enabled_for_elimination + } else { + false + }, + isGriEnabledForSrRouting: if useServiceConfigForGri { + is_gri_enabled_for_sr_routing + } else { + false + }, routingApproach: None, dateCreated: date_created, eliminationEnabled: false, + cardIsIn: card_isin, + cardSwitchProvider: card_switch_provider, + currency, + country, + is_legacy_decider_flow, + udfs, + udfs_consumed_for_routing: None, + gatewayReferenceId: gatewayRefId, } } @@ -1890,39 +2011,49 @@ pub async fn get_gateway_scoring_data( txn_detail: ETTD::TxnDetail, txn_card_info: ETCa::txn_card_info::TxnCardInfo, merchant: ETM::merchant_account::MerchantAccount, + is_legacy_decider_flow: bool, ) -> GatewayScoringData { - let merchant_enabled_for_unification = isFeatureEnabled( - C::MERCHANTS_ENABLED_FOR_SCORE_KEYS_UNIFICATION.get_key(), + let _merchant_enabled_for_unification = is_feature_enabled( + C::MerchantsEnabledForScoreKeysUnification.get_key(), merchant_id_to_text(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; let merchant_id = merchant_id_to_text(merchant.merchantId.clone()); - let order_type = txn_detail.txnObjectType.to_string(); + let order_type = txn_detail + .txnObjectType + .map(|t| t.to_string()) + .unwrap_or_default(); let payment_method_type = txn_card_info.paymentMethodType.to_uppercase(); let m_source_object = if txn_card_info.paymentMethod == UPI { txn_detail.sourceObject.clone().unwrap_or_default() } else { txn_card_info.paymentMethod.clone() }; - let is_performing_experiment = isFeatureEnabled( - C::MERCHANT_ENABLED_FOR_ROUTING_EXPERIMENT.get_key(), + let is_performing_experiment = is_feature_enabled( + C::MerchantEnabledForRoutingExperiment.get_key(), merchant_id_to_text(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; - let is_gri_enabled_for_elimination = isFeatureEnabled( - C::GATEWAY_REFERENCE_ID_ENABLED_MERCHANT.get_key(), + let is_gri_enabled_for_elimination = is_feature_enabled( + C::GatewayReferenceIdEnabledMerchant.get_key(), merchant_id_to_text(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; - let is_gri_enabled_for_sr_routing = isFeatureEnabled( - C::GW_REF_ID_SELECTION_BASED_ENABLED_MERCHANT.get_key(), + let is_gri_enabled_for_sr_routing = is_feature_enabled( + C::GwRefIdSelectionBasedEnabledMerchant.get_key(), merchant_id_to_text(merchant.merchantId.clone()), "kv_redis".to_string(), ) .await; + let (useServiceConfigForGri, gatewayRefId) = + if is_gri_enabled_for_sr_routing || is_gri_enabled_for_elimination { + get_common_gateway_ref_id(decider_flow).await + } else { + (true, None) + }; let mut default_gateway_scoring_data = get_default_gateway_scoring_data( merchant_id.clone(), order_type, @@ -1930,12 +2061,26 @@ pub async fn get_gateway_scoring_data( m_source_object, is_gri_enabled_for_elimination, is_gri_enabled_for_sr_routing, - decider_flow.get().dpTxnDetail.dateCreated.clone(), + decider_flow.get().dpTxnDetail.dateCreated, + decider_flow.get().dpTxnCardInfo.card_isin.clone(), + decider_flow.get().dpTxnCardInfo.cardSwitchProvider.clone(), + Some(decider_flow.get().dpOrder.currency.clone()), + decider_flow.get().dpTxnDetail.country, + decider_flow + .get() + .dpTxnCardInfo + .authType + .as_ref() + .map(|a| a.to_string()), + useServiceConfigForGri, + gatewayRefId, + Some(decider_flow.get().dpOrder.udfs.clone()), + is_legacy_decider_flow, ); let updated_gateway_scoring_data = match txn_card_info.paymentMethodType.as_str() { UPI => { - let handle_and_package_based_routing = isFeatureEnabled( - C::HANDLE_PACKAGE_BASED_ROUTING_CUTOVER.get_key(), + let handle_and_package_based_routing = is_feature_enabled( + C::HandlePackageBasedRoutingCutover.get_key(), merchant_id.clone(), "kv_redis".to_string(), ) @@ -1945,23 +2090,24 @@ pub async fn get_gateway_scoring_data( set_is_experiment_tag(decider_flow, experiment_tag); } - let payment_source = get_true_string(txn_card_info.paymentSource.clone()) + let payment_source = get_true_string(txn_card_info.get_payment_source()) .map(|source| source.split("@").last().unwrap_or_default().to_uppercase()) - .unwrap_or_default(); + .map(Secret::new) + .unwrap_or_else(|| Secret::new(String::new())); default_gateway_scoring_data.paymentSource = Some(payment_source); default_gateway_scoring_data.isPaymentSourceEnabledForSrRouting = handle_and_package_based_routing; default_gateway_scoring_data } CARD => { - let sr_evaluation_at_auth_level = isFeatureEnabled( - C::ENABLE_SELECTION_BASED_AUTH_TYPE_EVALUATION.get_key(), + let sr_evaluation_at_auth_level = is_feature_enabled( + C::EnableSelectionBasedAuthTypeEvaluation.get_key(), merchant_id.clone(), "kv_redis".to_string(), ) .await; - let sr_evaluation_at_bank_level = isFeatureEnabled( - C::ENABLE_SELECTION_BASED_BANK_LEVEL_EVALUATION.get_key(), + let sr_evaluation_at_bank_level = is_feature_enabled( + C::EnableSelectionBasedBankLevelEvaluation.get_key(), merchant_id.clone(), "kv_redis".to_string(), ) @@ -2018,12 +2164,13 @@ pub async fn get_gateway_scoring_data( get_experiment_tag(txn_detail.dateCreated, "GRI_BASED_SR_ROUTING").await; set_is_experiment_tag(decider_flow, experiment_tag); } - let key = [C::gatewayScoringData, &txn_detail.txnUuid.clone()].concat(); + let _key = [C::GATEWAY_SCORING_DATA, &txn_detail.txnUuid.clone()].concat(); updated_gateway_scoring_data } pub async fn get_unified_key( gateway_scoring_data: GatewayScoringData, + decider_flow: Option<&mut DeciderFlow<'_>>, score_key_type: ScoreKeyType, enforce1d: bool, gateway_ref_id_map: types::GatewayReferenceIdMap, @@ -2034,14 +2181,14 @@ pub async fn get_unified_key( let payment_method = gateway_scoring_data.paymentMethod.clone(); let gateway_redis_key_map = match score_key_type { - ScoreKeyType::ELIMINATION_GLOBAL_KEY => { - let key_prefix = C::elimination_based_routing_global_key_prefix; + ScoreKeyType::EliminationGlobalKey => { + let key_prefix = C::ELIMINATION_BASED_ROUTING_GLOBAL_KEY_PREFIX; let (prefix_key, suffix_key) = if payment_method_type == CARD { ( vec![key_prefix, &order_type.as_str()], vec![ - payment_method_type, - payment_method, + payment_method_type.to_string(), + payment_method.to_string(), gateway_scoring_data .cardType .clone() @@ -2081,9 +2228,9 @@ pub async fn get_unified_key( }); result_keys } - ScoreKeyType::ELIMINATION_MERCHANT_KEY => { + ScoreKeyType::EliminationMerchantKey => { let isgri_enabled = gateway_scoring_data.isGriEnabledForElimination; - let key_prefix = C::elimination_based_routing_key_prefix; + let key_prefix = C::ELIMINATION_BASED_ROUTING_KEY_PREFIX; let (prefix_key, suffix_key) = if payment_method_type == CARD { ( vec![key_prefix, &merchant_id, &order_type.as_str()], @@ -2145,16 +2292,17 @@ pub async fn get_unified_key( ); result_keys } - ScoreKeyType::SR_V2_KEY => { - let key = get_unified_sr_key(&gateway_scoring_data, false, enforce1d).await; + ScoreKeyType::SrV2Key => { + let key = + get_unified_sr_key(&gateway_scoring_data, false, enforce1d, decider_flow).await; let gri_sr_v2_cutover = gateway_scoring_data.isGriEnabledForSrRouting; if gri_sr_v2_cutover { gateway_ref_id_map.iter().fold( GatewayRedisKeyMap::new(), - |mut acc, (gateway, ref_id)| { + |mut acc, (gw, ref_id)| { acc.insert( - gateway.clone(), + gw.clone(), intercalate_without_empty_string( "_", &vec![key.clone(), ref_id.as_deref().unwrap_or("").to_string()], @@ -2169,61 +2317,75 @@ pub async fn get_unified_key( map } } - ScoreKeyType::SR_V3_KEY => { - let base_key = get_unified_sr_key(&gateway_scoring_data, true, enforce1d).await; + ScoreKeyType::SrV3Key => { + let base_key = + get_unified_sr_key(&gateway_scoring_data, true, enforce1d, decider_flow).await; let gri_sr_v2_cutover = gateway_scoring_data.isGriEnabledForSrRouting; if gri_sr_v2_cutover { gateway_ref_id_map.iter().fold( GatewayRedisKeyMap::new(), - |mut acc, (gateway, ref_id)| { + |mut acc, (gw, ref_id)| { let key = intercalate_without_empty_string( "_", &vec![ base_key.clone(), ref_id.as_deref().unwrap_or("").to_string(), - gateway.to_string(), + gw.to_string(), ], ); - acc.insert(gateway.clone(), key); + acc.insert(gw.clone(), key); acc }, ) } else { - gateway_ref_id_map.iter().fold( - GatewayRedisKeyMap::new(), - |mut acc, (gateway, _)| { + gateway_ref_id_map + .iter() + .fold(GatewayRedisKeyMap::new(), |mut acc, (gw, _)| { acc.insert( - gateway.clone(), + gw.clone(), intercalate_without_empty_string( "_", - &vec![base_key.clone(), gateway.to_string()], + &vec![base_key.clone(), gw.to_string()], ), ); acc - }, - ) + }) } } - ScoreKeyType::OUTAGE_GLOBAL_KEY => { - let key_prefix = C::globalLevelOutageKeyPrefix; + ScoreKeyType::OutageGlobalKey => { + let key_prefix = C::GLOBAL_LEVEL_OUTAGE_KEY_PREFIX; let base_key = if payment_method_type == CARD { vec![ - key_prefix, - &payment_method_type, - &payment_method, - gateway_scoring_data.bankCode.as_deref().unwrap_or(""), - gateway_scoring_data.cardType.as_deref().unwrap_or(""), + key_prefix.to_string(), + payment_method_type.to_string(), + payment_method.to_string(), + gateway_scoring_data + .bankCode + .as_deref() + .unwrap_or("") + .to_string(), + gateway_scoring_data + .cardType + .as_deref() + .unwrap_or("") + .to_string(), ] } else if payment_method_type == UPI { + let peek = gateway_scoring_data.get_payment_source(); + vec![ - key_prefix, - &payment_method_type, - &payment_method, - gateway_scoring_data.paymentSource.as_deref().unwrap_or(""), + key_prefix.to_string(), + payment_method_type.to_string(), + payment_method.to_string(), + peek, ] } else { - vec![key_prefix, &payment_method_type, &payment_method] + vec![ + key_prefix.to_string(), + payment_method_type.to_string(), + payment_method.to_string(), + ] }; let mut map = GatewayRedisKeyMap::new(); @@ -2239,31 +2401,41 @@ pub async fn get_unified_key( ); map } - ScoreKeyType::OUTAGE_MERCHANT_KEY => { - let key_prefix = C::merchantLevelOutageKeyPrefix; - let base_key = if payment_method_type == CARD { + ScoreKeyType::OutageMerchantKey => { + let key_prefix = C::MERCHANT_LEVEL_OUTAGE_KEY_PREFIX; + let base_key: Vec = if payment_method_type == CARD { vec![ - key_prefix, - &merchant_id, - &payment_method_type, - &payment_method, - gateway_scoring_data.bankCode.as_deref().unwrap_or(""), - gateway_scoring_data.cardType.as_deref().unwrap_or(""), + key_prefix.to_string(), + merchant_id.clone(), + payment_method_type.to_string(), + payment_method.to_string(), + gateway_scoring_data + .bankCode + .as_deref() + .unwrap_or("") + .to_string(), + gateway_scoring_data + .cardType + .as_deref() + .unwrap_or("") + .to_string(), ] } else if payment_method_type == UPI { + let peek = gateway_scoring_data.get_payment_source(); + vec![ - key_prefix, - &merchant_id, - &payment_method_type, - &payment_method, - gateway_scoring_data.paymentSource.as_deref().unwrap_or(""), + key_prefix.to_string(), + merchant_id.clone(), + payment_method_type.to_string(), + payment_method.to_string(), + peek, ] } else { vec![ - key_prefix, - &merchant_id, - &payment_method_type, - &payment_method, + key_prefix.to_string(), + merchant_id.clone(), + payment_method_type.to_string(), + payment_method.to_string(), ] }; @@ -2296,15 +2468,165 @@ pub async fn get_unified_sr_key( gateway_scoring_data: &GatewayScoringData, is_sr_v3_metric_enabled: bool, enforce1d: bool, + decider_flow: Option<&mut DeciderFlow<'_>>, ) -> String { let merchant_id = gateway_scoring_data.merchantId.clone(); + + let name = format!("SR_DIMENSION_CONFIG_{}", merchant_id); + + let service_config = find_config_by_name(name.clone()) + .await + .change_context(EuclidErrors::StorageError) + .and_then(|opt_config| { + opt_config.and_then(|config| config.value).ok_or_else(|| { + error_stack::report!(EuclidErrors::InvalidSrDimensionConfig( + "SR dimension config not found".to_string() + )) + }) + }) + .and_then(|config| { + serde_json::from_str::(&config).change_context( + EuclidErrors::InvalidSrDimensionConfig( + "Failed to parse SR dimension config".to_string(), + ), + ) + }); + + let udfs = service_config + .as_ref() + .map(|config| config.paymentInfo.udfs.clone()) + .unwrap_or_default(); + + let udf_values = + gateway_scoring_data + .udfs + .as_ref() + .zip(Some(udfs)) + .and_then(|(udf_map, udf_keys)| { + let mut values = Vec::with_capacity(udf_keys.len()); + + for udf in udf_keys { + match get_udf(udf_map, udf) { + Some(value) => values.push(value.to_string()), + None => { + return None; + } + } + } + + Some(values) + }); + + if let Some(df) = decider_flow { + if let Some(udf_values) = udf_values.as_ref() { + if udf_values.len() == 1 { + df.writer.gateway_scoring_data.udfs_consumed_for_routing = + Some(udf_values[0].clone()); + } + } + } + + let is_legacy_decider_flow = gateway_scoring_data.is_legacy_decider_flow; + + if is_legacy_decider_flow { + let sr_keys = + get_legacy_unified_sr_key(gateway_scoring_data, is_sr_v3_metric_enabled, enforce1d) + .await; + + let sr_key_with_udfs = if let Some(udf_values) = udf_values.clone() { + let mut key_components = vec![sr_keys]; + key_components.extend(udf_values); + intercalate_without_empty_string("_", &key_components) + } else { + sr_keys + }; + return sr_key_with_udfs; + } + let order_type = gateway_scoring_data.orderType.clone(); let payment_method_type = gateway_scoring_data.paymentMethodType.clone(); let payment_method = gateway_scoring_data.paymentMethod.clone(); + let card_network = gateway_scoring_data.cardSwitchProvider.clone(); + let card_isin = gateway_scoring_data.cardIsIn.clone(); + let currency = gateway_scoring_data + .currency + .as_ref() + .map(|c| c.to_string()); + let country = gateway_scoring_data.country.as_ref().map(|c| c.to_string()); + let auth_type = gateway_scoring_data.authType.clone(); let key_prefix = if is_sr_v3_metric_enabled { - C::gateway_selection_v3_order_type_key_prefix.to_string() + C::GATEWAY_SELECTION_V3_ORDER_TYPE_KEY_PREFIX.to_string() } else { - C::gateway_selection_order_type_key_prefix.to_string() + C::GATEWAY_SELECTION_ORDER_TYPE_KEY_PREFIX.to_string() + }; + + // Base key components that are always present + let mut key_components = vec![ + key_prefix, + merchant_id.clone(), + order_type, + payment_method_type, + payment_method, + ]; + + let fields = service_config + .as_ref() + .map(|config| config.paymentInfo.fields.clone()) + .unwrap_or_default(); + + for field in fields.into_iter().flatten() { + match field.as_str() { + "card_network" => { + if let Some(cn) = card_network.clone() { + key_components.push(cn.peek().to_string()); + } + } + "card_is_in" => { + if let Some(ci) = card_isin.clone() { + key_components.push(ci); + } + } + "currency" => { + if let Some(cu) = currency.clone() { + key_components.push(cu); + } + } + "country" => { + if let Some(co) = country.clone() { + key_components.push(co); + } + } + "auth_type" => { + if let Some(at) = auth_type.clone() { + key_components.push(at); + } + } + _ => { + // Unknown field + } + } + } + + if let Some(udf_values) = udf_values { + key_components.extend(udf_values); + } + + intercalate_without_empty_string("_", &key_components) +} + +async fn get_legacy_unified_sr_key( + gateway_scoring_data: &GatewayScoringData, + is_sr_v3_metric_enabled: bool, + enforce1d: bool, +) -> String { + let merchant_id = gateway_scoring_data.merchantId.clone(); + let order_type = gateway_scoring_data.orderType.clone(); + let payment_method_type = gateway_scoring_data.paymentMethodType.clone(); + let payment_method = gateway_scoring_data.paymentMethod.clone(); + let key_prefix = if is_sr_v3_metric_enabled { + C::GATEWAY_SELECTION_V3_ORDER_TYPE_KEY_PREFIX.to_string() + } else { + C::GATEWAY_SELECTION_ORDER_TYPE_KEY_PREFIX.to_string() }; let base_key = vec![ key_prefix.clone(), @@ -2328,9 +2650,9 @@ pub async fn get_unified_sr_key( match payment_method.as_str() { "UPI_COLLECT" | "COLLECT" => { let handle_list = get_upi_handle_list().await; - let upi_handle = gateway_scoring_data.paymentSource.as_deref().unwrap_or(""); - let append_handle = if handle_list.contains(&upi_handle.to_string()) { - upi_handle + let upi_handle = gateway_scoring_data.get_payment_source(); + let append_handle = if handle_list.contains(&upi_handle) { + &upi_handle } else { "" }; @@ -2341,9 +2663,9 @@ pub async fn get_unified_sr_key( } "UPI_PAY" | "PAY" => { let package_list = get_upi_package_list().await; - let upi_package = gateway_scoring_data.paymentSource.as_deref().unwrap_or(""); - let append_package = if package_list.contains(&upi_package.to_string()) { - upi_package + let upi_package = gateway_scoring_data.get_payment_source(); + let append_package = if package_list.contains(&upi_package) { + upi_package.as_str() } else { "" }; @@ -2409,7 +2731,7 @@ pub async fn get_consumer_key( gateway_list: GatewayList, ) -> GatewayRedisKeyMap { let merchant = decider_flow.get().dpMerchantAccount.clone(); - let txn_detail = decider_flow.get().dpTxnDetail.clone(); + let _txn_detail = decider_flow.get().dpTxnDetail.clone(); let gw_ref_id_map = if gateway_scoring_data.isGriEnabledForElimination || gateway_scoring_data.isGriEnabledForSrRouting { @@ -2419,11 +2741,11 @@ pub async fn get_consumer_key( merchant.enableGatewayReferenceIdBasedRouting, &order_ref, ); - let gw_ref_ids = gateway_list.iter().fold(HashMap::new(), |acc, gateway| { + let gw_ref_ids = gateway_list.iter().fold(HashMap::new(), |acc, gw| { let mut map = acc; let gwref_id = get_gateway_reference_id( meta.clone(), - gateway, + gw, order_ref.clone(), pl_ref_id_map.clone(), ); @@ -2431,22 +2753,21 @@ pub async fn get_consumer_key( None => "NULL".to_string(), Some(ref_id) => ref_id.mga_reference_id, }; - map.insert(gateway.clone(), Some(val)); + map.insert(gw.clone(), Some(val)); map }); set_gw_ref_id(decider_flow, gw_ref_ids.values().next().cloned().flatten()); logger::debug!("gwRefId {:?}", gw_ref_ids); gw_ref_ids } else { - gateway_list - .iter() - .fold(HashMap::new(), |mut acc, gateway| { - acc.insert(gateway.clone(), None); - acc - }) + gateway_list.iter().fold(HashMap::new(), |mut acc, gw| { + acc.insert(gw.clone(), None); + acc + }) }; let gateway_redis_key_map = get_unified_key( gateway_scoring_data, + Some(decider_flow), score_key_type, enforce1d, gw_ref_id_map, @@ -2475,8 +2796,9 @@ async fn set_routing_dimension_and_reference( "UPI_COLLECT" | "COLLECT" => { let handle_list = get_upi_handle_list().await; let upi_handle = gateway_scoring_data.paymentSource.unwrap_or_default(); - let append_handle = if handle_list.contains(&upi_handle) { - upi_handle + let upi_handle_peek = upi_handle.peek().to_string(); + let append_handle = if handle_list.contains(&upi_handle_peek) { + upi_handle_peek } else { "".to_string() }; @@ -2491,8 +2813,8 @@ async fn set_routing_dimension_and_reference( "UPI_PAY" | "PAY" => { let package_list = get_upi_package_list().await; let upi_package = gateway_scoring_data.paymentSource.unwrap_or_default(); - let append_package = if package_list.contains(&upi_package) { - upi_package + let append_package = if package_list.contains(upi_package.peek()) { + upi_package.peek().to_string() } else { "".to_string() }; @@ -2536,7 +2858,7 @@ async fn set_routing_dimension_and_reference( let bank_code = gateway_scoring_data .bankCode .unwrap_or("UNKNOWN".to_string()); - let append_bank_code = if top_bank_list.contains(&bank_code) { + let _append_bank_code = if top_bank_list.contains(&bank_code) { bank_code.clone() } else { "".to_string() @@ -2630,7 +2952,11 @@ pub fn set_outage_dimension( "/", &[ base_dimension, - vec![gateway_scoring_data.paymentSource.unwrap_or_default()], + vec![gateway_scoring_data + .paymentSource + .unwrap_or_default() + .peek() + .to_string()], ] .concat(), ) @@ -2653,7 +2979,7 @@ pub fn route_random_traffic_to_explore( num < explore_hedging_percent } -pub fn is_reset_eligibile( +pub fn is_reset_eligible( soft_ttl: Option, current_time_in_millis: u128, threshold: f64, @@ -2677,13 +3003,20 @@ pub async fn writeToCacheWithTTL( key: String, cached_gateway_score: GatewayScore, ttl: i64, + redis_compression_config: Option, ) -> Result { - //from CachedGatewayScore comvert encoded_score to a encoded jasson that can be used as a value for redis sextx + //from CachedGatewayScore convert encoded_score to a encoded json that can be used as a value for redis sextx let encoded_score = serde_json::to_string(&cached_gateway_score).unwrap_or_else(|_| "".to_string()); - let primary_write = - addToCacheWithExpiry("kv_redis".to_string(), key.clone(), encoded_score, ttl).await; + let primary_write = addToCacheWithExpiry( + "kv_redis".to_string(), + key.clone(), + encoded_score, + ttl, + redis_compression_config, + ) + .await; match primary_write { Ok(_) => Ok(0), @@ -2693,41 +3026,59 @@ pub async fn writeToCacheWithTTL( // Original Haskell function: addToCacheWithExpiry pub async fn addToCacheWithExpiry( - redis_name: String, + _redis_name: String, key: String, value: String, ttl: i64, + redis_compression_config: Option, ) -> Result<(), StorageError> { let app_state = get_tenant_app_state().await; - let cached_resp = app_state.redis_conn.setx(&key, &value, ttl).await; + let cached_resp = app_state + .redis_conn + .setx( + &key, + &value, + ttl, + redis_compression_config, + RedisDataStruct::STRING, + ) + .await; match cached_resp { Ok(_) => Ok(()), - Err(error) => Err(StorageError::InsertError), + Err(_error) => Err(StorageError::InsertError), } } -pub async fn get_penality_factor_(decider_flow: &mut DeciderFlow<'_>) -> f64 { +pub async fn get_penality_factor_( + decider_flow: &mut DeciderFlow<'_>, + gateway_scoring_data: &GatewayScoringData, +) -> f64 { let merchant = decider_flow.get().dpMerchantAccount.clone(); let txn_detail = decider_flow.get().dpTxnDetail.clone(); let txn_card_info = decider_flow.get().dpTxnCardInfo.clone(); let merchant_id = get_m_id(merchant.merchantId); - let is_elimination_v2_enabled = isFeatureEnabled( - C::ENABLE_ELIMINATION_V2.get_key(), + let is_elimination_v2_enabled = is_feature_enabled( + C::EnableEliminationV2.get_key(), merchant_id.clone(), feedback::constants::kvRedis(), ) .await; if is_elimination_v2_enabled { - let m_reward_factor = - eliminationV2RewardFactor(&merchant_id, &txn_card_info, &txn_detail).await; + let griEnabled = gateway_scoring_data.gatewayReferenceId.is_some(); + let m_reward_factor = eliminationV2RewardFactor( + &merchant_id, + &txn_card_info, + &txn_detail, + griEnabled, + gateway_scoring_data.gatewayReferenceId.clone(), + ) + .await; match m_reward_factor { - Some(reward_factor) => return (1.0 - reward_factor), - None => { - return getPenaltyFactor(ScoreKeyType::ELIMINATION_MERCHANT_KEY).await; - } + Some(reward_factor) => 1.0 - reward_factor, + None => getPenaltyFactor(ScoreKeyType::EliminationMerchantKey).await, } } else { - return getPenaltyFactor(ScoreKeyType::ELIMINATION_MERCHANT_KEY).await; + getPenaltyFactor(ScoreKeyType::EliminationMerchantKey).await } } diff --git a/src/decider/gatewaydecider/validators.rs b/src/decider/gatewaydecider/validators.rs index 66508a9e..902deaba 100644 --- a/src/decider/gatewaydecider/validators.rs +++ b/src/decider/gatewaydecider/validators.rs @@ -28,9 +28,9 @@ use super::types as T; use crate::error; use crate::types::card::card_type as Ca; use crate::types::card::txn_card_info as ETCa; +use crate::types::country::country_iso::CountryISO2; use crate::types::currency::Currency; use crate::types::customer as ETC; -use crate::types::gateway as ETG; use crate::types::merchant::id::to_merchant_id; use crate::types::merchant::merchant_gateway_account as ETMGA; use crate::types::money::internal::Money; @@ -39,7 +39,6 @@ use crate::types::order::id as ETOID; use crate::types::order::id::to_order_id; use crate::types::order::udfs::UDFs; use crate::types::order_metadata_v2 as ETOMV2; -use crate::types::payment::payment_method as ETP; use crate::types::source_object_id as SO; use crate::types::transaction::id as ETTID; use crate::types::txn_details::types as ETTD; @@ -52,8 +51,8 @@ use std::string::String; use std::vec::Vec; // pub fn parseFromApiOrderReference(apiType: T::ApiOrderReference) -> Option{ -pub fn parseFromApiOrderReference(apiType: T::ApiOrderReference) -> Option { - let udfs = parseUDFs(&apiType)?; +pub fn parse_from_api_order_reference(apiType: T::ApiOrderReference) -> Option { + let udfs = parse_udfs(&apiType)?; Some(ETO::Order { id: apiType @@ -116,7 +115,7 @@ impl FromIterator<(i32, String)> for UDFs { } } -fn parseUDFs(apiType: &T::ApiOrderReference) -> Option { +fn parse_udfs(apiType: &T::ApiOrderReference) -> Option { Some(UDFs::from_iter( udfLine(apiType) .into_iter() @@ -182,7 +181,7 @@ fn convert_metadata_to_string(metadata: Option>) -> Opti }) } -pub fn parseFromApiOrderMetadataV2( +pub fn parse_from_api_order_metadata_v2( apiType: T::ApiOrderMetadataV2, ) -> Option { Some(ETOMV2::OrderMetadataV2 { @@ -199,7 +198,7 @@ pub fn parseFromApiOrderMetadataV2( }) } -pub fn parseFromApiTxnDetail(apiType: T::ApiTxnDetail) -> Option { +pub fn parse_from_api_txn_detail(apiType: T::ApiTxnDetail) -> Option { Some(ETTD::TxnDetail { id: apiType .id @@ -209,23 +208,23 @@ pub fn parseFromApiTxnDetail(apiType: T::ApiTxnDetail) -> Option Option Option Option { +pub fn parse_from_api_txn_card_info(apiType: T::ApiTxnCardInfo) -> Option { Some(ETCa::TxnCardInfo { id: apiType .id @@ -276,10 +280,10 @@ pub fn parseFromApiTxnCardInfo(apiType: T::ApiTxnCardInfo) -> Option Result { - match parseApiDeciderRequestO(apiType) { + match parse_api_decider_request_o(apiType) { Some(domainDeciderRequest) => Ok(domainDeciderRequest), None => Err(error::ApiError::ParsingError( "Failed to parse ApiDeciderRequest", @@ -287,14 +291,14 @@ pub fn parseApiDeciderRequest( } } -pub fn parseApiDeciderRequestO( +pub fn parse_api_decider_request_o( apiType: T::ApiDeciderRequest, ) -> Option { Some(T::DomainDeciderRequestForApiCall { - orderReference: parseFromApiOrderReference(apiType.orderReference)?, - orderMetadata: parseFromApiOrderMetadataV2(apiType.orderMetadata)?, - txnDetail: parseFromApiTxnDetail(apiType.txnDetail)?, - txnCardInfo: parseFromApiTxnCardInfo(apiType.txnCardInfo)?, + orderReference: parse_from_api_order_reference(apiType.orderReference)?, + orderMetadata: parse_from_api_order_metadata_v2(apiType.orderMetadata)?, + txnDetail: parse_from_api_txn_detail(apiType.txnDetail)?, + txnCardInfo: parse_from_api_txn_card_info(apiType.txnCardInfo)?, card_token: apiType.card_token, txn_type: apiType.txn_type, should_create_mandate: apiType.should_create_mandate, diff --git a/src/decider/network_decider/co_badged_card_info.rs b/src/decider/network_decider/co_badged_card_info.rs index 1ce0d321..bf8bbb3f 100644 --- a/src/decider/network_decider/co_badged_card_info.rs +++ b/src/decider/network_decider/co_badged_card_info.rs @@ -2,6 +2,7 @@ use crate::app; use crate::{ decider::{gatewaydecider, network_decider::types, storage::utils::co_badged_card_info}, error, logger, + storage::types as storage_types, utils::CustomResult, }; use error_stack::ResultExt; @@ -127,75 +128,92 @@ impl CoBadgedCardInfoList { } } -pub async fn get_co_badged_cards_info( +async fn co_badged_cards_info_lookup( app_state: &app::TenantAppState, card_isin: String, -) -> CustomResult, error::ApiError> { - // Pad the card number to 19 digits to match the co-badged card bin length - let card_number_str = CoBadgedCardInfoList::pad_card_number_to_19_digit(card_isin); - - let parsed_number: i64 = card_number_str +) -> CustomResult, error::ApiError> { + let card_bin_string = CoBadgedCardInfoList::pad_card_number_to_19_digit(card_isin); + let parsed_number: i64 = card_bin_string .parse::() .change_context(error::ApiError::UnknownError) .attach_printable( "Failed to convert card number to integer in co-badged cards info flow", )?; - let co_badged_card_infos_record = - co_badged_card_info::find_co_badged_cards_info_by_card_bin(app_state, parsed_number).await; + co_badged_card_info::find_co_badged_cards_info_by_card_bin(app_state, parsed_number) + .await + .change_context(error::ApiError::UnknownError) + .attach_printable("Error while fetching co-badged card info record") +} - let filtered_co_badged_card_info_list_optional = match co_badged_card_infos_record { - Err(error) => { - logger::error!( - "Error while fetching co-badged card info record: {:?}", - error - ); - Err(error::ApiError::UnknownError) - .attach_printable("Error while fetching co-badged card info record") - } - Ok(co_badged_card_infos) => { - logger::debug!("Co-badged card info record retrieved successfully"); - - // Parse the co-badged card info records into domain data - let parsed_cards: Vec = co_badged_card_infos - .into_iter() - .filter_map(|raw_co_badged_card_info| { - match raw_co_badged_card_info.clone().try_into() { - Ok(parsed) => Some(parsed), - Err(error) => { - logger::warn!( - "Skipping co-badged card with card_network = {:?} due to error: {}", - raw_co_badged_card_info.card_network, - error - ); - None - } - } - }) - .collect(); - - let co_badged_card_infos_list = CoBadgedCardInfoList(parsed_cards); - - let filtered_list_optional = co_badged_card_infos_list - .is_valid_length() - .then(|| { - co_badged_card_infos_list - .is_only_one_global_network_present() - .then_some(co_badged_card_infos_list.filter_cards()) - }) - .flatten() - .and_then(|filtered_list| filtered_list.is_valid_length().then_some(filtered_list)); - - Ok(filtered_list_optional) +pub async fn get_co_badged_cards_info( + app_state: &app::TenantAppState, + card_isin: String, +) -> CustomResult, error::ApiError> { + let co_badged_card_infos = + match co_badged_cards_info_lookup(app_state, card_isin.clone()).await { + Ok(records) => { + if records.is_empty() && card_isin.len() == 8 { + logger::debug!( + "No co-badged card info found with 8-digit BIN. Retrying with 6-digit BIN." + ); + + co_badged_cards_info_lookup(app_state, card_isin[..6].to_string()).await + } else { + Ok(records) + } + } + Err(error) => { + logger::error!( + "Error while fetching co-badged card info record: {:?}", + error + ); + Err(error::ApiError::UnknownError) + .attach_printable("Error while fetching co-badged card info record")? + } } - }?; + .change_context(error::ApiError::UnknownError) + .attach_printable("Error while fetching co-badged card info record")?; + + logger::debug!( + "Co-badged card info records retrieved successfully records: {:?}", + co_badged_card_infos + ); - let co_badged_cards_info_response = filtered_co_badged_card_info_list_optional + // Parse the co-badged card info records into domain data + let parsed_cards: Vec = co_badged_card_infos + .into_iter() + .filter_map( + |raw_co_badged_card_info| match raw_co_badged_card_info.clone().try_into() { + Ok(parsed) => Some(parsed), + Err(error) => { + logger::warn!( + "Skipping co-badged card with card_network = {:?} due to error: {}", + raw_co_badged_card_info.card_network, + error + ); + None + } + }, + ) + .collect(); + + let co_badged_card_infos_list = CoBadgedCardInfoList(parsed_cards); + + let filtered_list_optional = co_badged_card_infos_list + .is_valid_length() + .then(|| { + co_badged_card_infos_list + .is_only_one_global_network_present() + .then_some(co_badged_card_infos_list.filter_cards()) + }) + .flatten() + .and_then(|filtered_list| filtered_list.is_valid_length().then_some(filtered_list)); + + filtered_list_optional .map(|filtered_list| filtered_list.get_co_badged_cards_info_response()) .transpose() - .attach_printable("Failed to construct co-badged card info response")?; - - Ok(co_badged_cards_info_response) + .attach_printable("Failed to construct co-badged card info response") } pub fn calculate_interchange_fee( @@ -214,7 +232,7 @@ pub fn calculate_interchange_fee( &debit_routing.interchange_fee.regulated } else { logger::debug!("Non regulated bank"); - debit_routing.get_non_regulated_interchange_fee(&merchant_category_code, network)? + debit_routing.get_non_regulated_interchange_fee(merchant_category_code, network)? }; let percentage = fee_data.percentage; @@ -286,8 +304,8 @@ pub fn calculate_total_fees_per_network( .map(|network| { let interchange_fee = calculate_interchange_fee( &network, - &co_badged_cards_info, - &merchant_category_code, + co_badged_cards_info, + merchant_category_code, amount, debit_routing_config, ) diff --git a/src/decider/network_decider/debit_routing.rs b/src/decider/network_decider/debit_routing.rs index 9f1a8139..a782c234 100644 --- a/src/decider/network_decider/debit_routing.rs +++ b/src/decider/network_decider/debit_routing.rs @@ -10,18 +10,17 @@ pub async fn perform_debit_routing( decider_request: gateway_decider_types::DomainDeciderRequestForApiCallV2, ) -> Result { let app_state = get_tenant_app_state().await; - let card_isin_optional = decider_request.paymentInfo.cardIsin; - let amount = decider_request.paymentInfo.amount; + let card_isin_optional = decider_request.payment_info.card_isin; + let amount = decider_request.payment_info.amount; let first_connector_from_request = decider_request - .eligibleGatewayList + .eligible_gateway_list .as_ref() .and_then(|connector| connector.first().cloned()); if let Some(metadata_value) = decider_request - .paymentInfo + .payment_info .metadata - .map(|metadata_string| gateway_decider_utils::parse_json_from_string(&metadata_string)) - .flatten() + .and_then(|metadata_string| gateway_decider_utils::parse_json_from_string(&metadata_string)) { logger::debug!("Parsed debit routing metadata to json"); match TryInto::::try_into(metadata_value) { @@ -32,23 +31,24 @@ pub async fn perform_debit_routing( .await { return Ok(gateway_decider_types::DecidedGateway { - // This field should not be consumed when the request is made to /decide-gateway with the rankingAlgorithm set to NTW_BASED_ROUTING. + // This field should not be consumed when the request is made to /decide-gateway with the rankingAlgorithm set to NtwBasedRouting. decided_gateway: first_connector_from_request.unwrap_or("".to_string()), gateway_priority_map: None, filter_wise_gateways: None, priority_logic_tag: None, routing_approach: - gateway_decider_types::GatewayDeciderApproach::NTW_BASED_ROUTING, + gateway_decider_types::GatewayDeciderApproach::NtwBasedRouting, gateway_before_evaluation: None, priority_logic_output: None, debit_routing_output: Some(debit_routing_output), - reset_approach: gateway_decider_types::ResetApproach::NO_RESET, + reset_approach: gateway_decider_types::ResetApproach::NoReset, routing_dimension: None, routing_dimension_level: None, is_scheduled_outage: false, is_dynamic_mga_enabled: false, gateway_mga_id_map: None, is_rust_based_decider: true, + latency: None, }); } } diff --git a/src/decider/network_decider/helpers.rs b/src/decider/network_decider/helpers.rs index 6f63dee1..0d5839f2 100644 --- a/src/decider/network_decider/helpers.rs +++ b/src/decider/network_decider/helpers.rs @@ -34,15 +34,14 @@ impl types::DebitRoutingConfig { &self, network: &gateway_decider_types::NETWORK, ) -> CustomResult<&types::NetworkProcessingData, error::ApiError> { - Ok(self - .network_fee + self.network_fee .get(network) .ok_or(error::ApiError::MissingRequiredField( "interchange fee for non regulated", )) .attach_printable( "Failed to fetch interchange fee for non regulated banks in debit routing", - )?) + ) } } @@ -199,15 +198,8 @@ impl types::CoBadgedCardRequest { impl gateway_decider_types::NETWORK { pub fn is_global_network(&self) -> bool { match self { - gateway_decider_types::NETWORK::VISA - | gateway_decider_types::NETWORK::AMEX - | gateway_decider_types::NETWORK::DINERS - | gateway_decider_types::NETWORK::RUPAY - | gateway_decider_types::NETWORK::MASTERCARD => true, - gateway_decider_types::NETWORK::STAR - | gateway_decider_types::NETWORK::PULSE - | gateway_decider_types::NETWORK::ACCEL - | gateway_decider_types::NETWORK::NYCE => false, + Self::VISA | Self::AMEX | Self::DINERS | Self::RUPAY | Self::MASTERCARD => true, + Self::STAR | Self::PULSE | Self::ACCEL | Self::NYCE => false, } } } diff --git a/src/decider/network_decider/types.rs b/src/decider/network_decider/types.rs index ade649f6..90e2bbfa 100644 --- a/src/decider/network_decider/types.rs +++ b/src/decider/network_decider/types.rs @@ -303,7 +303,7 @@ pub struct CoBadgedCardInfoResponse { impl From for CoBadgedCardInfoResponse { fn from(co_badged_card_data: DebitRoutingRequestData) -> Self { - CoBadgedCardInfoResponse { + Self { co_badged_card_networks: co_badged_card_data.co_badged_card_networks, issuer_country: co_badged_card_data.issuer_country, is_regulated: co_badged_card_data.is_regulated, diff --git a/src/decider/network_decider/utils.rs b/src/decider/network_decider/utils.rs index 50781124..8eaf5fae 100644 --- a/src/decider/network_decider/utils.rs +++ b/src/decider/network_decider/utils.rs @@ -13,7 +13,7 @@ macro_rules! impl_to_sql_from_sql_text_mysql { &'b self, out: &mut ::diesel::serialize::Output<'b, '_, ::diesel::mysql::Mysql>, ) -> ::diesel::serialize::Result { - use ::std::io::Write; + use std::io::Write; out.write_all(self.to_string().as_bytes())?; Ok(::diesel::serialize::IsNull::No) } @@ -22,8 +22,10 @@ macro_rules! impl_to_sql_from_sql_text_mysql { impl ::diesel::deserialize::FromSql<::diesel::sql_types::Text, ::diesel::mysql::Mysql> for $type { - fn from_sql(value: ::diesel::mysql::MysqlValue) -> ::diesel::deserialize::Result { - use ::core::str::FromStr; + fn from_sql( + value: ::diesel::mysql::MysqlValue<'_>, + ) -> ::diesel::deserialize::Result { + use core::str::FromStr; let s = ::core::str::from_utf8(value.as_bytes())?; <$type>::from_str(s).map_err(|_| "Unrecognized enum variant".into()) } @@ -38,15 +40,15 @@ macro_rules! impl_to_sql_from_sql_text_pg { &'b self, out: &mut ::diesel::serialize::Output<'b, '_, ::diesel::pg::Pg>, ) -> ::diesel::serialize::Result { - use ::std::io::Write; + use std::io::Write; out.write_all(self.to_string().as_bytes())?; Ok(::diesel::serialize::IsNull::No) } } impl ::diesel::deserialize::FromSql<::diesel::sql_types::Text, ::diesel::pg::Pg> for $type { - fn from_sql(value: ::diesel::pg::PgValue) -> ::diesel::deserialize::Result { - use ::core::str::FromStr; + fn from_sql(value: ::diesel::pg::PgValue<'_>) -> ::diesel::deserialize::Result { + use core::str::FromStr; let s = ::core::str::from_utf8(value.as_bytes())?; <$type>::from_str(s).map_err(|_| "Unrecognized enum variant".into()) } diff --git a/src/decider/storage/utils/gateway_bank_emi_support.rs b/src/decider/storage/utils/gateway_bank_emi_support.rs index deab8ee4..e2d2a1db 100644 --- a/src/decider/storage/utils/gateway_bank_emi_support.rs +++ b/src/decider/storage/utils/gateway_bank_emi_support.rs @@ -1,7 +1,6 @@ // use eulerhs::prelude::*; // use eulerhs::language::MonadFlow; use crate::types::emi_bank_code as EBC; -use crate::types::gateway as ETG; use crate::types::gateway_bank_emi_support as ETGBES; use crate::types::gateway_bank_emi_support_v2 as ETGBESV2; use std::option::Option; @@ -10,7 +9,7 @@ use std::vec::Vec; // use data::list::is_suffix_of; // use data::text as T; -pub async fn getGatewayBankEmiSupport( +pub async fn get_gateway_bank_emi_support( emiBank: Option, gws: Vec, scope: String, @@ -20,9 +19,9 @@ pub async fn getGatewayBankEmiSupport( Some(_) if gws.is_empty() => vec![], Some(emiBank) => { if scope == "CARDLESS" { - ETGBES::getGatewayBankEmiSupport(emiBank, gws, "CARD".to_string()).await + ETGBES::get_gateway_bank_emi_support(emiBank, gws, "CARD".to_string()).await } else { - ETGBES::getGatewayBankEmiSupport(emiBank, gws, scope).await + ETGBES::get_gateway_bank_emi_support(emiBank, gws, scope).await } } } @@ -32,7 +31,7 @@ pub fn is_suffix_of(suffix: &str, str: &str) -> bool { str.ends_with(suffix) } -pub async fn getGatewayBankEmiSupportV2( +pub async fn get_gateway_bank_emi_support_v2( emiBank: Option, gws: Vec, scope: String, @@ -40,10 +39,10 @@ pub async fn getGatewayBankEmiSupportV2( ) -> Vec { match (emiBank, tenure) { (Some(emiBank), Some(tenure)) => { - let emiBankCodeList = EBC::findEmiBankCodeByEMIBank(&trimSuffix(&emiBank)).await; + let emiBankCodeList = EBC::findEmiBankCodeByEMIBank(&trim_suffix(&emiBank)).await; match (emiBankCodeList.as_slice(), scope.as_str()) { ([emiBankCode], "CARDLESS") => { - ETGBESV2::getGatewayBankEmiSupportV2( + ETGBESV2::get_gateway_bank_emi_support_v2( emiBankCode.juspay_bank_code_id, gws.clone(), scope, @@ -58,7 +57,7 @@ pub async fn getGatewayBankEmiSupportV2( } else { "CREDIT".to_string() }; - ETGBESV2::getGatewayBankEmiSupportV2( + ETGBESV2::get_gateway_bank_emi_support_v2( emiBankCode.juspay_bank_code_id, gws.clone(), scope, @@ -74,7 +73,7 @@ pub async fn getGatewayBankEmiSupportV2( } } -fn trimSuffix(str: &str) -> String { +fn trim_suffix(str: &str) -> String { if is_suffix_of("_CLEMI", str) { str[..str.len() - "_CLEMI".len()].to_string() } else if is_suffix_of("_CC", str) { diff --git a/src/decider/storage/utils/gateway_card_info.rs b/src/decider/storage/utils/gateway_card_info.rs index 02fbefb0..3b230e90 100644 --- a/src/decider/storage/utils/gateway_card_info.rs +++ b/src/decider/storage/utils/gateway_card_info.rs @@ -33,13 +33,13 @@ use crate::{ logger, }; -pub async fn getSupportedGatewayCardInfoForBins( +pub async fn get_supported_gateway_card_info_for_bins( app_state: &crate::app::TenantAppState, input_merchant_account: MerchantAccount, card_bins: Vec>, ) -> Result, ErrorResponse> { // Step 1: Query GatewayCardInfo with diesel - let gci_records: Vec = match crate::generics::generic_find_all::< + let gci_records: Vec = crate::generics::generic_find_all::< ::Table, _, DBGatewayCardInfo, @@ -50,10 +50,7 @@ pub async fn getSupportedGatewayCardInfoForBins( .and(dsl::disabled.eq(Some(BitBool(false)))), ) .await - { - Ok(records) => records, - Err(_) => Vec::new(), - }; + .unwrap_or_default(); let gcis: Vec = gci_records.iter().map(|r| r.id).collect(); if gcis.is_empty() { @@ -61,7 +58,7 @@ pub async fn getSupportedGatewayCardInfoForBins( } // Step 2: Query MerchantGatewayCardInfo using diesel - let mgci_records: Vec = match crate::generics::generic_find_all::< + let mgci_records: Vec = crate::generics::generic_find_all::< ::Table, _, DBMerchantGatewayCardInfo, @@ -73,10 +70,7 @@ pub async fn getSupportedGatewayCardInfoForBins( .and(m_dsl::gateway_card_info_id.eq_any(gcis)), ) .await - { - Ok(records) => records, - Err(_) => Vec::new(), - }; + .unwrap_or_default(); // Step 3: Filter GatewayCardInfo records let gcis_filtered: Vec = mgci_records diff --git a/src/decider/storage/utils/merchant_gateway_account.rs b/src/decider/storage/utils/merchant_gateway_account.rs index b6b423c4..652a2c04 100644 --- a/src/decider/storage/utils/merchant_gateway_account.rs +++ b/src/decider/storage/utils/merchant_gateway_account.rs @@ -5,8 +5,6 @@ use crate::decider::gatewaydecider::types::*; // use gatewaydecider::types::*; // use juspay::extra::secret::make_secret; use crate::decider::gatewaydecider::utils::{get_mgas, is_emandate_enabled, set_mgas}; -use crate::logger; -use crate::types::gateway as ETG; use crate::types::merchant as ETM; use crate::types::merchant::id::MerchantId; use crate::types::merchant::merchant_gateway_account::MgaReferenceId; @@ -19,7 +17,7 @@ pub async fn get_enabled_mgas_by_merchant_id_and_ref_id( mid: MerchantId, ref_ids: Vec, ) -> Vec { - crate::logger::info!( + crate::logger::debug!( "MGAS: Length : {:?}", this.writer.mgas.as_ref().map_or(611, |mgas| mgas.len()) ); @@ -40,7 +38,7 @@ pub async fn get_enabled_mgas_by_merchant_id_and_ref_id( ) .await; - crate::logger::info!( + crate::logger::debug!( "length of mgas for txnId in main function: {}", d_mgas.len() ); @@ -88,7 +86,7 @@ fn filter_morpheus( mgas: Vec, ) -> Vec { mgas.into_iter() - .filter(|mga| mga.gateway != "MORPHEUS".to_string()) + .filter(|mga| mga.gateway != "MORPHEUS") .collect() } @@ -97,7 +95,7 @@ pub fn is_only_one_paylater( ) -> bool { match mgas.as_slice() { [] => false, - [mga] => mga.gateway == "PAYLATER".to_string(), + [mga] => mga.gateway == "PAYLATER", _ => false, } } diff --git a/src/decider/storage/utils/merchant_gateway_card_info.rs b/src/decider/storage/utils/merchant_gateway_card_info.rs index 198891d6..77f76874 100644 --- a/src/decider/storage/utils/merchant_gateway_card_info.rs +++ b/src/decider/storage/utils/merchant_gateway_card_info.rs @@ -103,7 +103,7 @@ pub async fn filter_gateways_for_payment_method_and_validation_type( &app_state.db, m_dsl::gateway_card_info_id .eq_any(gci_ids) - .and(m_dsl::merchant_account_id.eq(merchant_pid_to_text(merchant_account.id.clone()))) + .and(m_dsl::merchant_account_id.eq(merchant_pid_to_text(merchant_account.id))) .and(m_dsl::disabled.eq(BitBool(false))) .and(m_dsl::merchant_gateway_account_id.eq_any(enabled_gateway_accounts_ids)), ) @@ -114,8 +114,5 @@ pub async fn filter_gateways_for_payment_method_and_validation_type( }; // Step 4: Convert to Domain Types - mgci_records - .into_iter() - .filter_map(|db_record| MerchantGatewayCardInfo::try_from(db_record).ok()) - .collect() + mgci_records.into_iter().map(From::from).collect() } diff --git a/src/error/container.rs b/src/error/container.rs index bb80d7dd..ab43d521 100644 --- a/src/error/container.rs +++ b/src/error/container.rs @@ -37,8 +37,7 @@ pub trait ErrorTransform {} impl From> for ContainerError where T: error_stack::Context, - U: error_stack::Context, - for<'a> U: From<&'a T> + Sync + Send, + for<'a> U: error_stack::Context + From<&'a T> + Sync + Send, Self: ErrorTransform>, { #[track_caller] diff --git a/src/error/custom_error.rs b/src/error/custom_error.rs index f28a8e26..3eb6978e 100644 --- a/src/error/custom_error.rs +++ b/src/error/custom_error.rs @@ -109,7 +109,7 @@ pub enum RuleConfigurationError { impl axum::response::IntoResponse for RuleConfigurationError { fn into_response(self) -> axum::response::Response { match self { - RuleConfigurationError::StorageError => ( + Self::StorageError => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -118,7 +118,7 @@ impl axum::response::IntoResponse for RuleConfigurationError { )), ) .into_response(), - RuleConfigurationError::DeserializationError => ( + Self::DeserializationError => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -127,7 +127,7 @@ impl axum::response::IntoResponse for RuleConfigurationError { )), ) .into_response(), - RuleConfigurationError::InvalidRuleConfiguration => ( + Self::InvalidRuleConfiguration => ( hyper::StatusCode::BAD_REQUEST, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -136,7 +136,7 @@ impl axum::response::IntoResponse for RuleConfigurationError { )), ) .into_response(), - RuleConfigurationError::MerchantNotFound => ( + Self::MerchantNotFound => ( hyper::StatusCode::NOT_FOUND, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -145,7 +145,7 @@ impl axum::response::IntoResponse for RuleConfigurationError { )), ) .into_response(), - RuleConfigurationError::ConfigurationNotFound => ( + Self::ConfigurationNotFound => ( hyper::StatusCode::NOT_FOUND, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -154,7 +154,7 @@ impl axum::response::IntoResponse for RuleConfigurationError { )), ) .into_response(), - RuleConfigurationError::ConfigurationAlreadyExists => ( + Self::ConfigurationAlreadyExists => ( hyper::StatusCode::BAD_REQUEST, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -186,7 +186,7 @@ pub enum MerchantAccountConfigurationError { impl axum::response::IntoResponse for MerchantAccountConfigurationError { fn into_response(self) -> axum::response::Response { match self { - MerchantAccountConfigurationError::StorageError => ( + Self::StorageError => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -195,7 +195,7 @@ impl axum::response::IntoResponse for MerchantAccountConfigurationError { )), ) .into_response(), - MerchantAccountConfigurationError::InvalidConfiguration => ( + Self::InvalidConfiguration => ( hyper::StatusCode::BAD_REQUEST, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -204,7 +204,7 @@ impl axum::response::IntoResponse for MerchantAccountConfigurationError { )), ) .into_response(), - MerchantAccountConfigurationError::MerchantNotFound => ( + Self::MerchantNotFound => ( hyper::StatusCode::NOT_FOUND, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -213,7 +213,7 @@ impl axum::response::IntoResponse for MerchantAccountConfigurationError { )), ) .into_response(), - MerchantAccountConfigurationError::MerchantAlreadyExists => ( + Self::MerchantAlreadyExists => ( hyper::StatusCode::BAD_REQUEST, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -222,7 +222,7 @@ impl axum::response::IntoResponse for MerchantAccountConfigurationError { )), ) .into_response(), - MerchantAccountConfigurationError::MerchantDeletionFailed => ( + Self::MerchantDeletionFailed => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, @@ -231,7 +231,7 @@ impl axum::response::IntoResponse for MerchantAccountConfigurationError { )), ) .into_response(), - MerchantAccountConfigurationError::MerchantInsertionFailed => ( + Self::MerchantInsertionFailed => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(crate::error::ApiErrorResponse::new( crate::error::error_codes::TE_04, diff --git a/src/euclid.rs b/src/euclid.rs index 4d026675..b5d44a7a 100644 --- a/src/euclid.rs +++ b/src/euclid.rs @@ -3,5 +3,9 @@ pub mod cgraph; pub mod errors; pub mod handlers; pub mod interpreter; +pub mod pm_filter_graph; pub mod types; pub mod utils; + +#[cfg(test)] +mod test; diff --git a/src/euclid/ast.rs b/src/euclid/ast.rs index b75a9ff5..68666dba 100644 --- a/src/euclid/ast.rs +++ b/src/euclid/ast.rs @@ -40,6 +40,10 @@ pub enum ValueType { } impl ValueType { + pub fn is_metadata(&self) -> bool { + matches!(self, Self::MetadataVariant(_)) + } + pub fn get_type(&self) -> DataType { match self { Self::Number(_) => DataType::Number, @@ -113,7 +117,7 @@ pub type IfCondition = Vec; pub struct IfStatement { // #[schema(value_type=Vec)] pub condition: IfCondition, - pub nested: Option>, + pub nested: Option>, } /// Represents a rule diff --git a/src/euclid/cgraph.rs b/src/euclid/cgraph.rs index 1ba58f25..cbc72d29 100644 --- a/src/euclid/cgraph.rs +++ b/src/euclid/cgraph.rs @@ -40,11 +40,11 @@ pub enum AnalysisTrace { }, AllAggregation { - unsatisfied: Vec, + unsatisfied: Vec, }, AnyAggregation { - unsatisfied: Vec, + unsatisfied: Vec, }, } @@ -237,6 +237,12 @@ pub struct ConstraintGraphData { edges: Vec, } +impl ConstraintGraphData { + pub fn new(nodes: Vec, edges: Vec) -> Self { + Self { nodes, edges } + } +} + impl TryFrom for ConstraintGraph { type Error = String; @@ -438,12 +444,12 @@ mod tests { #[derive(Debug, Serialize, Deserialize)] struct Config { - routing_config: RoutingConfig, + graph_config: GraphConfig, } #[derive(Debug, Serialize, Deserialize)] - struct RoutingConfig { - constraint_graph: ConstraintGraphData, + struct GraphConfig { + graph: ConstraintGraphData, } #[test] @@ -504,9 +510,7 @@ mod tests { }; let config = Config { - routing_config: RoutingConfig { - constraint_graph: graph, - }, + graph_config: GraphConfig { graph }, }; let string = toml::to_string(&config).expect("failed toml conversion"); diff --git a/src/euclid/errors.rs b/src/euclid/errors.rs index 28806a76..877d6aa0 100644 --- a/src/euclid/errors.rs +++ b/src/euclid/errors.rs @@ -40,12 +40,39 @@ pub enum EuclidErrors { #[error("Storage error")] StorageError, + + #[error("Invalid Sr Dimension Configuration")] + InvalidSrDimensionConfig(String), + + #[error("Field validation failed: {0}")] + FieldValidationFailed(String), +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ValidationErrorDetails { + pub field: String, + pub error_type: String, + pub message: String, +} + +impl ValidationErrorDetails { + pub fn new( + field: impl Into, + error_type: impl Into, + message: impl Into, + ) -> Self { + Self { + field: field.into(), + error_type: error_type.into(), + message: message.into(), + } + } } impl axum::response::IntoResponse for EuclidErrors { fn into_response(self) -> axum::response::Response { match self { - EuclidErrors::InvalidRuleConfiguration => ( + Self::InvalidRuleConfiguration => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -55,7 +82,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::InvalidRequestParameter(param) => ( + Self::InvalidRequestParameter(param) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -68,7 +95,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::FailedToValidateRoutingRule => ( + Self::FailedToValidateRoutingRule => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -78,7 +105,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::InvalidRequest(msg) => ( + Self::InvalidRequest(msg) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -91,7 +118,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::GlobalRoutingConfigsUnavailable => ( + Self::GlobalRoutingConfigsUnavailable => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -101,7 +128,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::StorageError => ( + Self::StorageError => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -111,7 +138,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::FailedToParseJsonInput => ( + Self::FailedToParseJsonInput => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -121,7 +148,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::FailedToSerializeJsonToString => ( + Self::FailedToSerializeJsonToString => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -131,7 +158,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::ActiveRoutingAlgorithmNotFound(msg) => ( + Self::ActiveRoutingAlgorithmNotFound(msg) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -144,7 +171,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::RoutingAlgorithmNotFound(msg) => ( + Self::RoutingAlgorithmNotFound(msg) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -157,7 +184,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::DefaultFallbackNotFound(creator_by) => ( + Self::DefaultFallbackNotFound(creator_by) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -170,7 +197,7 @@ impl axum::response::IntoResponse for EuclidErrors { ) .into_response(), - EuclidErrors::FailedToEvaluateOutput(msg) => ( + Self::FailedToEvaluateOutput(msg) => ( hyper::StatusCode::BAD_REQUEST, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -182,7 +209,7 @@ impl axum::response::IntoResponse for EuclidErrors { )), ).into_response(), - EuclidErrors::RoutingInterpretationFailed => ( + Self::RoutingInterpretationFailed => ( hyper::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(ApiErrorResponse::new( error_codes::TE_04, @@ -191,6 +218,26 @@ impl axum::response::IntoResponse for EuclidErrors { )), ) .into_response(), + + Self::InvalidSrDimensionConfig(msg) => ( + hyper::StatusCode::BAD_REQUEST, + axum::Json(ApiErrorResponse::new( + error_codes::TE_04, + msg, + None, + )), + ) + .into_response(), + + Self::FieldValidationFailed(msg) => ( + hyper::StatusCode::BAD_REQUEST, + axum::Json(ApiErrorResponse::new( + error_codes::TE_04, + format!("Field validation failed: {}", msg), + None, + )), + ) + .into_response(), } } } diff --git a/src/euclid/handlers/routing_rules.rs b/src/euclid/handlers/routing_rules.rs index 7c1e0fa6..9f368c5b 100644 --- a/src/euclid/handlers/routing_rules.rs +++ b/src/euclid/handlers/routing_rules.rs @@ -5,17 +5,18 @@ use crate::storage::schema_pg::routing_algorithm::dsl; use crate::{ error::ApiErrorResponse, euclid::{ - ast::{self, ComparisonType, ConnectorInfo, Output, ValueType}, - cgraph, + ast::{ConnectorInfo, Output, ValueType}, interpreter::{evaluate_output, InterpreterBackend}, + pm_filter_graph, types::{ - ActivateRoutingConfigRequest, Context, JsonifiedRoutingAlgorithm, + ActivateRoutingConfigRequest, Context, JsonifiedRoutingAlgorithm, KeyDataType, RoutingAlgorithmMapperNew, RoutingDictionaryRecord, RoutingEvaluateResponse, - RoutingRequest, RoutingRule, StaticRoutingAlgorithm, + RoutingRequest, RoutingRule, SrDimensionConfig, StaticRoutingAlgorithm, + ELIGIBLE_DIMENSIONS, }, utils::{generate_random_id, is_valid_enum_value, validate_routing_rule}, }, - types::service_configuration::find_config_by_name, + types::service_configuration::{find_config_by_name, insert_config, update_config}, }; use crate::euclid::{ @@ -23,17 +24,111 @@ use crate::euclid::{ types::{RoutingAlgorithmMapper, RoutingAlgorithmMapperUpdate}, }; use crate::{euclid::types::RoutingAlgorithm, logger, metrics}; -use axum::{extract::Path, Json}; +use axum::{extract::Path, response::IntoResponse, Json}; use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; use error_stack::ResultExt; use crate::app::get_tenant_app_state; -use crate::error::{self, ContainerError}; +use crate::error::ContainerError; use crate::metrics::{API_LATENCY_HISTOGRAM, API_REQUEST_COUNTER, API_REQUEST_TOTAL_COUNTER}; use serde_json::{json, Value}; +#[allow(dead_code)] const DEFAULT_FALLBACK_IDENTIFIER: &str = "default_fallback_enabled"; +pub async fn config_sr_dimensions( + Json(payload): Json, +) -> Result, ContainerError> { + let timer = metrics::API_LATENCY_HISTOGRAM + .with_label_values(&["config_sr_dimensions"]) + .start_timer(); + metrics::API_REQUEST_TOTAL_COUNTER + .with_label_values(&["config_sr_dimensions"]) + .inc(); + logger::debug!("Received SR Dimension config: {:?}", payload); + + // Validate dimensions against ELIGIBLE_DIMENSIONS + let invalid_dimensions: Vec<&String> = payload + .paymentInfo + .fields + .as_ref() + .map(|fields| { + fields + .iter() + .filter(|field| !ELIGIBLE_DIMENSIONS.contains(&field.as_str())) + .collect() + }) + .unwrap_or_default(); + + if !invalid_dimensions.is_empty() { + metrics::API_REQUEST_COUNTER + .with_label_values(&["config_sr_dimensions", "failure"]) + .inc(); + timer.observe_duration(); + + logger::error!( + "Invalid dimensions found for merchant {}: {:?}", + payload.merchant_id, + invalid_dimensions.clone() + ); + + return Err(EuclidErrors::InvalidSrDimensionConfig(format!( + "Invalid dimensions: {:?}. Valid dimensions are: {}", + invalid_dimensions.clone(), + ELIGIBLE_DIMENSIONS.join(", ") + )) + .into()); + } + + let mid = payload.merchant_id.clone(); + let config = serde_json::to_string(&payload) + .change_context(EuclidErrors::FailedToSerializeJsonToString)?; + let name = format!("SR_DIMENSION_CONFIG_{}", mid); + + let service_config = find_config_by_name(name.clone()) + .await + .change_context(EuclidErrors::StorageError)?; + let result = match service_config { + Some(_) => { + logger::debug!( + "Updating existing SR Dimension config for merchant: {}", + mid + ); + update_config(name, Some(config)) + .await + .change_context(EuclidErrors::StorageError) + } + None => { + logger::debug!("Inserting new SR Dimension config for merchant: {}", mid); + insert_config(name, Some(config)) + .await + .change_context(EuclidErrors::StorageError) + } + }; + + if let Err(_) = result { + metrics::API_REQUEST_COUNTER + .with_label_values(&["config_sr_dimensions", "failure"]) + .inc(); + timer.observe_duration(); + logger::error!( + "Failed to insert or update SR Dimension config for merchant: {}", + mid + ); + return Err(ContainerError::from(EuclidErrors::StorageError)); + } + metrics::API_REQUEST_COUNTER + .with_label_values(&["config_sr_dimensions", "success"]) + .inc(); + timer.observe_duration(); + logger::debug!( + "SR Dimension configuration updated successfully for merchant: {}", + mid + ); + Ok(Json( + "SR Dimension configuration updated successfully".to_string(), + )) +} pub async fn routing_create( Json(payload): Json, @@ -52,34 +147,57 @@ pub async fn routing_create( logger::debug!("Received routing config: {:?}", config); - if let Err(err) = validate_routing_rule(&config, &state.config.routing_config) { - let source = err.get_inner(); + match validate_routing_rule(&config, &state.config.routing_config) { + Ok(validation_result) => { + if !validation_result.is_valid { + for error in &validation_result.errors { + logger::error!( + field = %error.field, + error_type = %error.error_type, + message = %error.message, + "Field validation error during routing rule creation" + ); + } - if let EuclidErrors::FailedToValidateRoutingRule = source { - if let Some(validation_messages) = err.downcast_ref::>() { - let detailed_error = validation_messages.join("; "); - logger::error!("Routing rule validation failed with errors: {detailed_error}"); + let error_details: Vec = validation_result + .errors + .iter() + .map(|e| { + serde_json::json!({ + "field": e.field, + "error_type": e.error_type, + "message": e.message, + }) + }) + .collect(); + + let detailed_error = validation_result.to_error_message(); metrics::API_REQUEST_COUNTER .with_label_values(&["routing_create", "failure"]) .inc(); timer.observe_duration(); + return Err(ContainerError::new_with_status_code_and_payload( - EuclidErrors::FailedToValidateRoutingRule, + EuclidErrors::FieldValidationFailed(detailed_error.clone()), axum::http::StatusCode::BAD_REQUEST, ApiErrorResponse::new( - "INVALID_REQUEST_DATA", + "FIELD_VALIDATION_FAILED", format!("Routing rule validation failed: {}", detailed_error), - None, + Some(serde_json::json!({ "validation_errors": error_details })), ), )); } + logger::debug!("Routing rule validation passed successfully"); + } + Err(err) => { + logger::error!(error = ?err, "Failed to validate routing rule configuration"); + metrics::API_REQUEST_COUNTER + .with_label_values(&["routing_create", "failure"]) + .inc(); + timer.observe_duration(); + return Err(err); } - metrics::API_REQUEST_COUNTER - .with_label_values(&["routing_create", "failure"]) - .inc(); - timer.observe_duration(); - return Err(err.into()); } let utc_date_time = time::OffsetDateTime::now_utc(); @@ -122,7 +240,7 @@ pub async fn routing_create( timestamp, timestamp, ); - logger::info!("Response: {response:?}"); + logger::debug!("Response: {response:?}"); metrics::API_REQUEST_COUNTER .with_label_values(&["routing_create", "success"]) @@ -134,9 +252,11 @@ pub async fn routing_create( pub async fn routing_evaluate( Json(payload): Json, ) -> Result, ContainerError> { - let timer = metrics::API_LATENCY_HISTOGRAM + let mut timer = Some( + metrics::API_LATENCY_HISTOGRAM .with_label_values(&["routing_evaluate"]) - .start_timer(); + .start_timer(), + ); API_REQUEST_TOTAL_COUNTER .with_label_values(&["routing_evaluate"]) @@ -144,41 +264,36 @@ pub async fn routing_evaluate( let state = get_tenant_app_state().await; logger::debug!( - "Received routing evaluation request for ID: {}", - payload.created_by + payment_id = ?payload.payment_id, + created_by = %payload.created_by, + "Received routing evaluation request" ); - - let config_identifier = format!( - "{}_{}", - DEFAULT_FALLBACK_IDENTIFIER, - payload.created_by.clone() + crate::analytics::record_request_hit_event( + "routing_evaluate", + Some(payload.created_by.clone()), + payload.payment_id.clone(), + None, ); - // Check for the default_fallback config: - let configs = find_config_by_name(config_identifier) - .await - .change_context(EuclidErrors::StorageError)?; - - // In default state it should be false, and should only be made true, if the value is present - let mut check_default_fallback_present = false; - - // Not adding parsing error, as this value can only be written by application - configs.map(|config| { - config.value.map(|value| { - if let Ok(parsed_value) = value.parse::() { - check_default_fallback_present = parsed_value; - } - }) - }); + let update_failure_metrics = || { + API_REQUEST_COUNTER + .with_label_values(&["routing_evaluate", "failure"]) + .inc(); + }; + let mut fail_preview = |err: ContainerError, stage: &'static str| { + record_routing_evaluate_preview_error(&payload, &err, stage); + update_failure_metrics(); + if let Some(timer) = timer.take() { + timer.observe_duration(); + } + Err(err) + }; - if check_default_fallback_present - && payload - .fallback_output - .clone() - .is_none_or(|fallback| fallback.is_empty()) - { - return Err(EuclidErrors::DefaultFallbackNotFound(payload.created_by.clone()).into()); - } + // Check for the fallback_output in evaluate request: + let default_output_present = payload + .fallback_output + .as_ref() + .is_some_and(|output| !output.is_empty()); // fetch the active routing_algorithm of the merchant let active_routing_algorithm_id = match crate::generics::generic_find_one::< @@ -194,13 +309,7 @@ pub async fn routing_evaluate( payload.created_by.clone(), )) { Ok(mapper) => mapper.routing_algorithm_id, - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "active_routing_lookup_failed"), }; let parameters = payload.parameters.clone(); @@ -212,48 +321,41 @@ pub async fn routing_evaluate( .ok_or(EuclidErrors::GlobalRoutingConfigsUnavailable) { Ok(config) => config, - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "routing_config_unavailable"), }; - for (key, _) in ¶meters { - if !routing_config.keys.keys.contains_key(key) { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(EuclidErrors::InvalidRequestParameter(key.clone()).into()); + for (key, value) in ¶meters { + if !routing_config.keys.keys.contains_key(key) + && value.as_ref().is_some_and(|val| !val.is_metadata()) + { + return fail_preview( + EuclidErrors::InvalidRequestParameter(key.clone()).into(), + "parameter_validation_failed", + ); } if let Some(key_config) = routing_config.keys.keys.get(key) { - if key_config.data_type == "enum" { + if key_config.data_type == KeyDataType::Enum { if let Some(Some(ValueType::EnumVariant(value))) = parameters.get(key) { if !is_valid_enum_value(routing_config, key, value) { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(EuclidErrors::InvalidRequest(format!( - "Invalid enum value '{}' for key '{}'", - value, key - )) - .into()); + return fail_preview( + EuclidErrors::InvalidRequest(format!( + "Invalid enum value '{}' for key '{}'", + value, key + )) + .into(), + "parameter_validation_failed", + ); } } else { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(EuclidErrors::InvalidRequest(format!( - "Expected enum value for key '{}'", - key - )) - .into()); + return fail_preview( + EuclidErrors::InvalidRequest(format!( + "Expected enum value for key '{}'", + key + )) + .into(), + "parameter_validation_failed", + ); } } } @@ -265,24 +367,17 @@ pub async fn routing_evaluate( RoutingAlgorithm, >(&state.db, dsl::id.eq(active_routing_algorithm_id.clone())) .await - .map_err(|e| { + .inspect_err(|&e| { logger::error!( ?e, "Failed to fetch RoutingAlgorithm for ID {:?}", active_routing_algorithm_id ); - e }) .change_context(EuclidErrors::StorageError) { Ok(algo) => algo, - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "routing_algorithm_fetch_failed"), }; logger::debug!("Fetched routing algorithm: {:?}", algorithm); @@ -296,13 +391,7 @@ pub async fn routing_evaluate( EuclidErrors::InvalidRequest(format!("Invalid algorithm data format: {}", e)) }) { Ok(data) => data, - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "routing_algorithm_parse_failed"), }; let (output, evaluated_output, rule_name): (Output, Vec, Option) = @@ -312,17 +401,11 @@ pub async fn routing_evaluate( match evaluate_output(&out_enum).map_err(|_| { EuclidErrors::FailedToEvaluateOutput(format!( "{}", - StaticRoutingAlgorithm::Single(conn.clone()).to_string() + StaticRoutingAlgorithm::Single(conn.clone()) )) }) { Ok((_, eval)) => (out_enum, eval, Some("straight_through_rule".into())), - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "preview_output_evaluation_failed"), } } @@ -331,17 +414,11 @@ pub async fn routing_evaluate( match evaluate_output(&out_enum).map_err(|_| { EuclidErrors::FailedToEvaluateOutput(format!( "{}", - StaticRoutingAlgorithm::Priority(connectors.clone()).to_string() + StaticRoutingAlgorithm::Priority(connectors.clone()) )) }) { Ok((_, eval)) => (out_enum, eval, Some("priority_rule".into())), - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "preview_output_evaluation_failed"), } } @@ -350,17 +427,11 @@ pub async fn routing_evaluate( match evaluate_output(&out_enum).map_err(|_| { EuclidErrors::FailedToEvaluateOutput(format!( "{}", - StaticRoutingAlgorithm::VolumeSplit(splits.clone()).to_string() + StaticRoutingAlgorithm::VolumeSplit(splits.clone()) )) }) { Ok((_, eval)) => (out_enum, eval, Some("volume_split_rule".into())), - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "preview_output_evaluation_failed"), } } @@ -373,9 +444,8 @@ pub async fn routing_evaluate( }) { Ok(mut ir) => { // Check if fallback is enabled - if check_default_fallback_present && ir.output == program.default_selection - { - logger::info!( + if default_output_present && ir.output == program.default_selection { + logger::debug!( "Default fallback triggered: Overriding with fallback connector" ); @@ -389,25 +459,26 @@ pub async fn routing_evaluate( } (ir.output, ir.evaluated_output, ir.rule_name) } - Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["routing_evaluate", "failure"]) - .inc(); - timer.observe_duration(); - return Err(e.into()); - } + Err(e) => return fail_preview(e.into(), "preview_interpreter_failed"), } } }; - let eligible_connectors = if let Some(ref cfg) = state.config.routing_config { - let ctx = cgraph::CheckCtx::from(payload.parameters.clone()); - perform_eligibility_analysis(&cfg.constraint_graph, ctx, &evaluated_output) + let pm_filter_bundle = if pm_filter_graph::has_payment_method_type(¶meters) { + state.get_pm_filter_graph_bundle().await } else { - evaluated_output.clone() + None }; + let connectors_for_eligibility = extract_connectors_for_eligibility(&output); + let eligible_connectors = eligibility_for_output( + pm_filter_bundle.as_deref(), + ¶meters, + &connectors_for_eligibility, + ); + let response = RoutingEvaluateResponse { + payment_id: payload.payment_id.clone(), status: match rule_name.as_deref() { Some("default_selection") | Some("default_fallback") => "default_selection".into(), Some(_) => "success".into(), @@ -418,15 +489,82 @@ pub async fn routing_evaluate( eligible_connectors, }; - logger::info!("Response: {response:?}"); + logger::debug!("Response: {response:?}"); + crate::analytics::record_rule_evaluation_preview_event( + Some(payload.created_by.clone()), + payload.payment_id.clone(), + preview_gateway(&response), + rule_name.clone(), + Some(response.status.clone()), + serde_json::to_string(&json!({ + "request": &payload, + "response": &response, + "rule_name": rule_name.clone(), + "preview_kind": "routing_evaluate", + })) + .ok(), + ); API_REQUEST_COUNTER .with_label_values(&["routing_evaluate", "success"]) .inc(); - timer.observe_duration(); + if let Some(timer) = timer.take() { + timer.observe_duration(); + } Ok(Json(response)) } +fn record_routing_evaluate_preview_error( + payload: &RoutingRequest, + error: &ContainerError, + event_stage: &str, +) { + let response_payload = error + .downcast_ref::() + .and_then(|payload| serde_json::to_value(payload).ok()); + let status = error + .get_inner() + .clone() + .into_response() + .status() + .as_u16() + .to_string(); + let error_code = response_payload + .as_ref() + .and_then(|value| value.get("code")) + .and_then(|value| value.as_str()) + .unwrap_or("ROUTING_EVALUATE_FAILED") + .to_string(); + let error_message = response_payload + .as_ref() + .and_then(|value| value.get("message")) + .and_then(|value| value.as_str()) + .map(str::to_string) + .unwrap_or_else(|| error.get_inner().to_string()); + + crate::analytics::record_error_event( + "routing_evaluate", + Some(payload.created_by.clone()), + payload.payment_id.clone(), + None, + None, + Some("RULE_EVALUATE_PREVIEW".to_string()), + error_code, + error_message.clone(), + serde_json::to_string(&json!({ + "request": payload, + "response": { + "status": status, + "error_message": error_message, + "api_error": response_payload, + }, + "preview_kind": "routing_evaluate", + })) + .ok(), + Some(event_stage.to_string()), + ); +} + #[cfg(feature = "mysql")] use crate::storage::schema::routing_algorithm_mapper::dsl as mapper_dsl; #[cfg(feature = "postgres")] @@ -442,6 +580,12 @@ pub async fn activate_routing_rule( .with_label_values(&["activate_routing_rule"]) .inc(); + let update_failure_metrics = || { + API_REQUEST_COUNTER + .with_label_values(&["activate_routing_rule", "failure"]) + .inc(); + }; + let state = get_tenant_app_state().await; let conn = match state .db @@ -451,9 +595,7 @@ pub async fn activate_routing_rule( { Ok(connection) => connection, Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["activate_routing_rule", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); return Err(e.into()); } @@ -471,9 +613,7 @@ pub async fn activate_routing_rule( )) { Ok(algorithm) => algorithm.algorithm_for, Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["activate_routing_rule", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); return Err(e.into()); } @@ -521,9 +661,7 @@ pub async fn activate_routing_rule( return Ok(()); } Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["activate_routing_rule", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); return Err(e.into()); } @@ -555,9 +693,7 @@ pub async fn activate_routing_rule( Ok(()) } Err(e) => { - API_REQUEST_COUNTER - .with_label_values(&["activate_routing_rule", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); Err(e.into()) } @@ -611,6 +747,13 @@ pub async fn list_active_routing_algorithm( metrics::API_REQUEST_TOTAL_COUNTER .with_label_values(&["list_active_routing_algorithm"]) .inc(); + + let update_failure_metrics = || { + API_REQUEST_COUNTER + .with_label_values(&["list_active_routing_algorithm", "failure"]) + .inc(); + }; + let state = get_tenant_app_state().await; let active_mappings = match crate::generics::generic_find_all::< @@ -624,9 +767,7 @@ pub async fn list_active_routing_algorithm( )) { Ok(mappings) => mappings, Err(e) => { - metrics::API_REQUEST_COUNTER - .with_label_values(&["list_active_routing_algorithm", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); return Err(e.into()); } @@ -647,9 +788,7 @@ pub async fn list_active_routing_algorithm( { Ok(algos) => algos, Err(e) => { - metrics::API_REQUEST_COUNTER - .with_label_values(&["list_active_routing_algorithm", "failure"]) - .inc(); + update_failure_metrics(); timer.observe_duration(); return Err(e.into()); } @@ -714,24 +853,112 @@ fn format_output(output: &Output) -> Value { } } -fn perform_eligibility_analysis( - constraint_graph: &cgraph::ConstraintGraph, - ctx: cgraph::CheckCtx, - output: &[ConnectorInfo], +fn preview_gateway(response: &RoutingEvaluateResponse) -> Option { + response + .evaluated_output + .first() + .map(|connector| connector.gateway_name.clone()) + .or_else(|| { + response + .eligible_connectors + .first() + .map(|connector| connector.gateway_name.clone()) + }) +} + +pub(crate) fn eligibility_for_output( + pm_filter_bundle: Option<&pm_filter_graph::PmFilterGraphBundle>, + parameters: &std::collections::HashMap>, + connectors: &[ConnectorInfo], ) -> Vec { - let mut eligible_connectors = Vec::::with_capacity(output.len()); + if !pm_filter_graph::has_payment_method_type(parameters) { + logger::debug!("Skipping pm_filters eligibility; payment_method_type missing"); + return connectors.to_vec(); + } - for out in output { - let clause = cgraph::Clause { - key: "output".to_string(), - comparison: ComparisonType::Equal, - value: ValueType::EnumVariant(out.gateway_name.clone()), - }; + apply_pm_filter_eligibility(pm_filter_bundle, parameters, connectors) +} + +pub fn compute_routing_evaluate_eligibility( + pm_filter_bundle: Option<&pm_filter_graph::PmFilterGraphBundle>, + parameters: &std::collections::HashMap>, + connectors: &[ConnectorInfo], +) -> Vec { + eligibility_for_output(pm_filter_bundle, parameters, connectors) +} - if let Ok(true) = constraint_graph.check_clause_validity(clause, &ctx) { - eligible_connectors.push(out.clone()); +pub(crate) fn apply_pm_filter_eligibility( + bundle: Option<&pm_filter_graph::PmFilterGraphBundle>, + parameters: &std::collections::HashMap>, + eligible_connectors: &[ConnectorInfo], +) -> Vec { + let Some(bundle) = bundle else { + logger::debug!("Skipping pm_filters eligibility; graph unavailable"); + return eligible_connectors.to_vec(); + }; + + pm_filter_graph::filter_eligible_connectors(bundle, parameters, eligible_connectors) +} + +pub(crate) fn extract_connectors_for_eligibility(output: &Output) -> Vec { + let mut connectors = Vec::::new(); + let mut push_unique = |connector: &ConnectorInfo| { + if !connectors.iter().any(|existing| existing == connector) { + connectors.push(connector.clone()); + } + }; + + match output { + Output::Single(connector) => push_unique(connector), + Output::Priority(priority_connectors) => { + for connector in priority_connectors { + push_unique(connector); + } + } + Output::VolumeSplit(splits) => { + for split in splits { + push_unique(&split.output); + } + } + Output::VolumeSplitPriority(splits) => { + for split in splits { + for connector in &split.output { + push_unique(connector); + } + } } } - eligible_connectors + connectors +} + +/// GET endpoint to serve routing keys configuration +/// Returns the routing config with all available keys and their enum values +/// This allows the dashboard to dynamically fetch valid routing keys +pub async fn get_routing_config( +) -> Result, ContainerError> { + let timer = metrics::API_LATENCY_HISTOGRAM + .with_label_values(&["get_routing_config"]) + .start_timer(); + metrics::API_REQUEST_TOTAL_COUNTER + .with_label_values(&["get_routing_config"]) + .inc(); + + let tenant_state = get_tenant_app_state().await; + + // Clone the routing config to return it + let config = tenant_state + .config + .routing_config + .clone() + .ok_or(EuclidErrors::GlobalRoutingConfigsUnavailable)?; + + metrics::API_REQUEST_COUNTER + .with_label_values(&["get_routing_config", "success"]) + .inc(); + timer.observe_duration(); + + logger::info!("Successfully served routing config"); + + Ok(Json(config)) } diff --git a/src/euclid/interpreter.rs b/src/euclid/interpreter.rs index 0160205e..6502e33c 100644 --- a/src/euclid/interpreter.rs +++ b/src/euclid/interpreter.rs @@ -1,4 +1,4 @@ -use crate::euclid::ast::{Output, VolumeSplit}; +use crate::euclid::ast::{Output, ValueType, VolumeSplit}; use crate::euclid::{ast, types}; use rand::distributions::WeightedIndex; use rand::prelude::*; @@ -41,9 +41,12 @@ impl InterpreterBackend { ) -> Result { use ast::{ComparisonType::*, ValueType::*}; - let ctx_value = ctx.get(&comparison.lhs); + let ctx_value = match &comparison.value { + ValueType::MetadataVariant(m) => ctx.get(&m.key), + _ => ctx.get(&comparison.lhs), + }; if ctx_value.is_none() { - crate::logger::warn!( + crate::logger::debug!( missing_context_key = %comparison.lhs, "Context key not found while evaluating condition, skipping rule" ); @@ -203,7 +206,7 @@ pub enum RoutingError { impl fmt::Display for RoutingError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - RoutingError::VolumeSplitFailed => write!(f, "Volume split calculation failed"), + Self::VolumeSplitFailed => write!(f, "Volume split calculation failed"), } } } diff --git a/src/euclid/pm_filter_graph.rs b/src/euclid/pm_filter_graph.rs new file mode 100644 index 00000000..88f454aa --- /dev/null +++ b/src/euclid/pm_filter_graph.rs @@ -0,0 +1,463 @@ +use std::collections::{HashMap, HashSet}; + +use super::{ + ast::{ComparisonType, ConnectorInfo, ValueType}, + cgraph::{ + self, Clause, ConstraintGraph, ConstraintGraphData, Edge, EdgeId, EntityId, Node, NodeId, + NodeKind, NodeValue, Relation, Strength, + }, + types::TomlConfig, +}; +use crate::{ + config::{ConnectorFilters, CurrencyCountryFlowFilter}, + logger, +}; + +pub const PM_FILTER_DEFAULT_OUTPUT_SENTINEL: &str = "__pm_default__"; + +#[derive(Debug, Clone)] +pub struct PmFilterGraphBundle { + pub graph: ConstraintGraph, + pub explicit_connectors: HashSet, + pub has_default_rules: bool, + pub explicit_connector_payment_method_types: HashMap>, + pub default_payment_method_types: HashSet, + pub billing_country_to_iso2: HashMap, + pub node_count: usize, + pub edge_count: usize, +} + +pub fn build_pm_filter_graph_bundle( + pm_filters: &ConnectorFilters, + routing_config: Option<&TomlConfig>, +) -> Result { + let mut compiler = GraphCompiler::default(); + let mut explicit_connectors = HashSet::new(); + let mut has_default_rules = false; + let mut explicit_connector_payment_method_types = HashMap::new(); + let mut default_payment_method_types = HashSet::new(); + + for (connector_name, filters_for_connector) in &pm_filters.0 { + let normalized_connector = connector_name.trim().to_ascii_lowercase(); + let is_default = normalized_connector == "default"; + let output_value = if is_default { + has_default_rules = true; + PM_FILTER_DEFAULT_OUTPUT_SENTINEL.to_string() + } else { + explicit_connectors.insert(normalized_connector.clone()); + normalized_connector + }; + + let output_node = compiler.add_value_node("output", &output_value); + for (payment_method_type, filter) in &filters_for_connector.0 { + let normalized_payment_method_type = payment_method_type.trim().to_ascii_lowercase(); + if normalized_payment_method_type.is_empty() { + continue; + } + + if is_default { + default_payment_method_types.insert(normalized_payment_method_type.clone()); + } else { + explicit_connector_payment_method_types + .entry(output_value.clone()) + .or_insert_with(HashSet::new) + .insert(normalized_payment_method_type.clone()); + } + + let rule_node = compiler.add_aggregator_node(NodeKind::AllAggregator); + let pm_type_node = + compiler.add_value_node("payment_method_type", &normalized_payment_method_type); + compiler.add_edge( + pm_type_node, + rule_node, + Strength::Strong, + Relation::Positive, + ); + + attach_country_filter_nodes(&mut compiler, filter, rule_node); + attach_currency_filter_nodes(&mut compiler, filter, rule_node); + attach_not_available_flow_nodes(&mut compiler, filter, rule_node); + + compiler.add_edge(rule_node, output_node, Strength::Normal, Relation::Positive); + } + } + + let node_count = compiler.nodes.len(); + let edge_count = compiler.edges.len(); + let graph = + ConstraintGraph::try_from(ConstraintGraphData::new(compiler.nodes, compiler.edges))?; + + let billing_country_to_iso2 = build_billing_country_to_iso2_map(routing_config); + + Ok(PmFilterGraphBundle { + graph, + explicit_connectors, + has_default_rules, + explicit_connector_payment_method_types, + default_payment_method_types, + billing_country_to_iso2, + node_count, + edge_count, + }) +} + +pub fn has_payment_method_type(parameters: &HashMap>) -> bool { + get_parameter_string(parameters, "payment_method_type").is_some() +} + +pub fn filter_eligible_connectors( + bundle: &PmFilterGraphBundle, + parameters: &HashMap>, + connectors: &[ConnectorInfo], +) -> Vec { + let Some(base_ctx) = build_pm_filter_context(parameters, &bundle.billing_country_to_iso2) + else { + return connectors.to_vec(); + }; + + connectors + .iter() + .filter(|connector| connector_is_eligible(bundle, &base_ctx, connector)) + .cloned() + .collect() +} + +fn connector_is_eligible( + bundle: &PmFilterGraphBundle, + base_ctx: &HashMap, + connector: &ConnectorInfo, +) -> bool { + let normalized_connector = connector.gateway_name.trim().to_ascii_lowercase(); + let payment_method_type = get_context_enum_value(base_ctx, "payment_method_type"); + let explicit_has_pmt_rule = payment_method_type.is_some_and(|pmt| { + bundle + .explicit_connector_payment_method_types + .get(&normalized_connector) + .is_some_and(|types| types.contains(pmt)) + }); + let default_has_pmt_rule = + payment_method_type.is_some_and(|pmt| bundle.default_payment_method_types.contains(pmt)); + + let (output_clause_value, source) = if explicit_has_pmt_rule { + (normalized_connector, "explicit") + } else if default_has_pmt_rule { + (PM_FILTER_DEFAULT_OUTPUT_SENTINEL.to_string(), "default") + } else { + logger::debug!( + connector = %connector.gateway_name, + payment_method_type = ?payment_method_type, + has_explicit_connector = bundle.explicit_connectors.contains(&normalized_connector), + has_default_rules = bundle.has_default_rules, + "No pm_filters rule for connector/payment_method_type; allowing by default" + ); + return true; + }; + + let mut ctx_vals = base_ctx.clone(); + ctx_vals.insert( + "output".to_string(), + ValueType::EnumVariant(output_clause_value.clone()), + ); + + let clause = Clause { + key: "output".to_string(), + comparison: ComparisonType::Equal, + value: ValueType::EnumVariant(output_clause_value), + }; + + match bundle + .graph + .check_clause_validity(clause, &cgraph::CheckCtx { ctx_vals }) + { + Ok(true) => true, + Ok(false) => { + logger::debug!( + connector = %connector.gateway_name, + rule_source = source, + "Connector filtered by pm_filters constraint graph" + ); + false + } + Err(err) => { + logger::error!( + error = ?err, + connector = %connector.gateway_name, + "pm_filters graph evaluation failed, failing open for connector" + ); + true + } + } +} + +fn get_context_enum_value<'a>(ctx: &'a HashMap, key: &str) -> Option<&'a str> { + ctx.get(key).and_then(|value| match value { + ValueType::EnumVariant(inner) => Some(inner.as_str()), + ValueType::StrValue(inner) => Some(inner.as_str()), + _ => None, + }) +} + +fn build_pm_filter_context( + parameters: &HashMap>, + billing_country_to_iso2: &HashMap, +) -> Option> { + let payment_method_type = get_parameter_string(parameters, "payment_method_type") + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(|v| v.to_ascii_lowercase())?; + + let mut ctx_vals = HashMap::new(); + ctx_vals.insert( + "payment_method_type".to_string(), + ValueType::EnumVariant(payment_method_type), + ); + + if let Some(currency) = get_parameter_string(parameters, "currency") + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(|v| v.to_ascii_uppercase()) + { + ctx_vals.insert("currency".to_string(), ValueType::EnumVariant(currency)); + } + + if let Some(capture_method) = get_parameter_string(parameters, "capture_method") + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(|v| v.to_ascii_lowercase()) + { + ctx_vals.insert( + "capture_method".to_string(), + ValueType::EnumVariant(capture_method), + ); + } + + if let Some(billing_country) = get_parameter_string(parameters, "billing_country") + .and_then(|country| normalize_billing_country_iso2(country, billing_country_to_iso2)) + { + ctx_vals.insert( + "billing_country".to_string(), + ValueType::EnumVariant(billing_country), + ); + } + + Some(ctx_vals) +} + +fn normalize_billing_country_iso2( + billing_country: &str, + billing_country_to_iso2: &HashMap, +) -> Option { + let trimmed = billing_country.trim(); + if trimmed.is_empty() { + return None; + } + + let normalized = trimmed.to_ascii_uppercase(); + if normalized.len() == 2 && normalized.chars().all(|c| c.is_ascii_alphabetic()) { + return Some(normalized); + } + + billing_country_to_iso2.get(trimmed).cloned().or_else(|| { + billing_country_to_iso2 + .get(&trimmed.to_ascii_lowercase()) + .cloned() + }) +} + +fn build_billing_country_to_iso2_map( + routing_config: Option<&TomlConfig>, +) -> HashMap { + let mut map = HashMap::new(); + let Some(routing_config) = routing_config else { + return map; + }; + + let Some(billing_country_key_config) = routing_config.keys.keys.get("billing_country") else { + return map; + }; + + let Some(billing_values) = billing_country_key_config.values.as_ref() else { + return map; + }; + + for billing_country in parse_csv_values(billing_values) { + let iso2 = billing_country.trim().to_ascii_uppercase(); + if iso2.len() == 2 && iso2.chars().all(|c| c.is_ascii_alphabetic()) { + map.insert(billing_country.clone(), iso2.clone()); + map.insert(billing_country.to_ascii_lowercase(), iso2); + } + } + map +} + +fn attach_country_filter_nodes( + compiler: &mut GraphCompiler, + filter: &CurrencyCountryFlowFilter, + parent_node: NodeId, +) { + let Some(allowed_countries) = filter.country.as_ref() else { + return; + }; + + let allowed_countries = allowed_countries + .iter() + .map(|value| value.trim().to_ascii_uppercase()) + .filter(|value| !value.is_empty()) + .collect::>(); + + if allowed_countries.is_empty() { + return; + } + + let country_any_node = compiler.add_aggregator_node(NodeKind::AnyAggregator); + for country in allowed_countries { + let country_node = compiler.add_value_node("billing_country", &country); + compiler.add_edge( + country_node, + country_any_node, + Strength::Weak, + Relation::Positive, + ); + } + + compiler.add_edge( + country_any_node, + parent_node, + Strength::Normal, + Relation::Positive, + ); +} + +fn attach_currency_filter_nodes( + compiler: &mut GraphCompiler, + filter: &CurrencyCountryFlowFilter, + parent_node: NodeId, +) { + let Some(allowed_currencies) = filter.currency.as_ref() else { + return; + }; + + let allowed_currencies = allowed_currencies + .iter() + .map(|value| value.trim().to_ascii_uppercase()) + .filter(|value| !value.is_empty()) + .collect::>(); + + if allowed_currencies.is_empty() { + return; + } + + let currency_any_node = compiler.add_aggregator_node(NodeKind::AnyAggregator); + for currency in allowed_currencies { + let currency_node = compiler.add_value_node("currency", ¤cy); + compiler.add_edge( + currency_node, + currency_any_node, + Strength::Weak, + Relation::Positive, + ); + } + + compiler.add_edge( + currency_any_node, + parent_node, + Strength::Normal, + Relation::Positive, + ); +} + +fn attach_not_available_flow_nodes( + compiler: &mut GraphCompiler, + filter: &CurrencyCountryFlowFilter, + parent_node: NodeId, +) { + let Some(capture_method) = filter + .not_available_flows + .as_ref() + .and_then(|flows| flows.capture_method.as_ref()) + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + .map(|value| value.to_ascii_lowercase()) + else { + return; + }; + + let capture_method_node = compiler.add_value_node("capture_method", &capture_method); + compiler.add_edge( + capture_method_node, + parent_node, + Strength::Strong, + Relation::Negative, + ); +} + +fn get_parameter_string<'a>( + parameters: &'a HashMap>, + key: &str, +) -> Option<&'a str> { + parameters.get(key).and_then(|value| match value.as_ref() { + Some(ValueType::EnumVariant(inner)) => Some(inner.as_str()), + Some(ValueType::StrValue(inner)) => Some(inner.as_str()), + _ => None, + }) +} + +fn parse_csv_values(raw_values: &str) -> Vec { + raw_values + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .collect() +} + +#[derive(Default)] +struct GraphCompiler { + nodes: Vec, + edges: Vec, + value_nodes: HashMap, +} + +impl GraphCompiler { + fn add_value_node(&mut self, key: &str, value: &str) -> NodeId { + let node_value = NodeValue::Value(Clause { + key: key.to_string(), + comparison: ComparisonType::Equal, + value: ValueType::EnumVariant(value.to_string()), + }); + + if let Some(existing_id) = self.value_nodes.get(&node_value).copied() { + return existing_id; + } + + let node_id = self.add_node(NodeKind::Value(node_value.clone())); + self.value_nodes.insert(node_value, node_id); + node_id + } + + fn add_aggregator_node(&mut self, kind: NodeKind) -> NodeId { + self.add_node(kind) + } + + fn add_node(&mut self, kind: NodeKind) -> NodeId { + let node_id = NodeId::with_id(self.nodes.len()); + self.nodes.push(Node { + kind, + preds: Vec::new(), + succs: Vec::new(), + }); + node_id + } + + fn add_edge(&mut self, pred: NodeId, succ: NodeId, strength: Strength, relation: Relation) { + let edge_id = EdgeId::with_id(self.edges.len()); + self.edges.push(Edge { + strength, + relation, + pred, + succ, + }); + self.nodes[pred.get_id()].succs.push(edge_id); + self.nodes[succ.get_id()].preds.push(edge_id); + } +} diff --git a/src/euclid/test.rs b/src/euclid/test.rs new file mode 100644 index 00000000..0fbcdc95 --- /dev/null +++ b/src/euclid/test.rs @@ -0,0 +1,922 @@ +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use crate::{ + config::{ + ConnectorFilters, CurrencyCountryFlowFilter, NotAvailableFlows, PaymentMethodFilters, + }, + euclid::{ + ast::{ConnectorInfo, Output, ValueType, VolumeSplit}, + handlers::routing_rules::{ + apply_pm_filter_eligibility, compute_routing_evaluate_eligibility, + extract_connectors_for_eligibility, + }, + pm_filter_graph, + types::{KeyConfig, KeyDataType, KeysConfig, TomlConfig}, + }, + }; + + fn enum_value(value: &str) -> Option { + Some(ValueType::EnumVariant(value.to_string())) + } + + fn connectors(names: &[&str]) -> Vec { + names + .iter() + .map(|name| ConnectorInfo { + gateway_name: (*name).to_string(), + gateway_id: None, + }) + .collect() + } + + fn routing_config_for_tests() -> TomlConfig { + let mut keys = HashMap::new(); + keys.insert( + "billing_country".to_string(), + KeyConfig { + data_type: KeyDataType::Enum, + values: Some("US,IN".to_string()), + min_value: None, + max_value: None, + min_length: None, + max_length: None, + exact_length: None, + regex: None, + }, + ); + TomlConfig { + keys: KeysConfig { keys }, + } + } + + fn build_bundle_for_tests() -> pm_filter_graph::PmFilterGraphBundle { + let mut connector_configs = HashMap::new(); + connector_configs.insert( + "stripe".to_string(), + PaymentMethodFilters( + [( + "credit".to_string(), + CurrencyCountryFlowFilter { + country: Some(HashSet::from(["US".to_string()])), + currency: None, + not_available_flows: None, + }, + )] + .into_iter() + .collect(), + ), + ); + connector_configs.insert( + "default".to_string(), + PaymentMethodFilters( + [( + "debit".to_string(), + CurrencyCountryFlowFilter { + country: None, + currency: None, + not_available_flows: None, + }, + )] + .into_iter() + .collect(), + ), + ); + + pm_filter_graph::build_pm_filter_graph_bundle( + &ConnectorFilters(connector_configs), + Some(&routing_config_for_tests()), + ) + .expect("pm filter bundle should build") + } + + fn build_priority_matrix_bundle() -> pm_filter_graph::PmFilterGraphBundle { + let mut connector_configs = HashMap::new(); + connector_configs.insert( + "razorpay".to_string(), + PaymentMethodFilters( + [( + "upi_collect".to_string(), + CurrencyCountryFlowFilter { + country: Some(HashSet::from(["IN".to_string()])), + currency: Some(HashSet::from(["INR".to_string()])), + not_available_flows: None, + }, + )] + .into_iter() + .collect(), + ), + ); + connector_configs.insert( + "stripe".to_string(), + PaymentMethodFilters( + [( + "credit".to_string(), + CurrencyCountryFlowFilter { + country: Some(HashSet::from(["US".to_string()])), + currency: Some(HashSet::from(["USD".to_string()])), + not_available_flows: None, + }, + )] + .into_iter() + .collect(), + ), + ); + connector_configs.insert( + "default".to_string(), + PaymentMethodFilters( + [( + "google_pay".to_string(), + CurrencyCountryFlowFilter { + country: Some(HashSet::from(["US".to_string()])), + currency: None, + not_available_flows: None, + }, + )] + .into_iter() + .collect(), + ), + ); + + pm_filter_graph::build_pm_filter_graph_bundle( + &ConnectorFilters(connector_configs), + Some(&routing_config_for_tests()), + ) + .expect("priority matrix bundle should build") + } + + fn params_for_case( + payment_method_type: Option<&str>, + billing_country: Option<&str>, + currency: Option<&str>, + ) -> HashMap> { + let mut params = HashMap::new(); + if let Some(payment_method_type) = payment_method_type { + params.insert( + "payment_method_type".to_string(), + enum_value(payment_method_type), + ); + } + if let Some(billing_country) = billing_country { + params.insert("billing_country".to_string(), enum_value(billing_country)); + } + if let Some(currency) = currency { + params.insert("currency".to_string(), enum_value(currency)); + } + params + } + + fn connector_names(connectors: &[ConnectorInfo]) -> Vec { + connectors + .iter() + .map(|connector| connector.gateway_name.clone()) + .collect() + } + + fn print_priority_test_setup(bundle: &pm_filter_graph::PmFilterGraphBundle) { + println!("=== TEST SETUP: ROUTING + PM ELIGIBILITY ==="); + println!("Routing rule under test:"); + println!(" if payment_method == card => priority [razorpay, stripe]"); + println!(" else => default [adyen]"); + println!("Eligibility input:"); + println!(" all connectors from routing output (single/priority/volume variants)"); + println!("pm_filters under test:"); + println!(" razorpay.upi_collect => country=[IN], currency=[INR]"); + println!(" stripe.credit => country=[US], currency=[USD]"); + println!(" default.google_pay => country=[US]"); + println!("Resolution order:"); + println!(" explicit connector PMT rule -> default PMT rule -> pass-open"); + println!( + "Derived billing_country -> ISO2 map: {:?}", + bundle.billing_country_to_iso2 + ); + println!( + "explicit connector PMT map: {:?}", + bundle.explicit_connector_payment_method_types + ); + println!("default PMT set: {:?}", bundle.default_payment_method_types); + println!("=== END SETUP ==="); + } + + fn pm_filter_test_connectors(names: &[&str]) -> Vec { + names + .iter() + .map(|name| ConnectorInfo { + gateway_name: (*name).to_string(), + gateway_id: None, + }) + .collect() + } + + fn pm_filter_test_routing_config() -> TomlConfig { + let mut keys = HashMap::new(); + keys.insert( + "billing_country".to_string(), + KeyConfig { + data_type: KeyDataType::Enum, + values: Some("US,DE".to_string()), + min_value: None, + max_value: None, + min_length: None, + max_length: None, + exact_length: None, + regex: None, + }, + ); + TomlConfig { + keys: KeysConfig { keys }, + } + } + + fn pm_filter_make_filter( + country: Option<&[&str]>, + currency: Option<&[&str]>, + capture_method: Option<&str>, + ) -> CurrencyCountryFlowFilter { + CurrencyCountryFlowFilter { + country: country.map(|values| values.iter().map(|v| (*v).to_string()).collect()), + currency: currency.map(|values| values.iter().map(|v| (*v).to_string()).collect()), + not_available_flows: capture_method.map(|capture_method| NotAvailableFlows { + capture_method: Some(capture_method.to_string()), + }), + } + } + + fn pm_filter_build_connector_filters() -> ConnectorFilters { + let mut connector_filters = HashMap::new(); + + connector_filters.insert( + "stripe".to_string(), + PaymentMethodFilters( + [( + "credit".to_string(), + pm_filter_make_filter(Some(&["US"]), Some(&["USD"]), None), + )] + .into_iter() + .collect(), + ), + ); + + connector_filters.insert( + "default".to_string(), + PaymentMethodFilters( + [ + ( + "credit".to_string(), + pm_filter_make_filter(Some(&["US"]), Some(&["USD"]), Some("manual")), + ), + ( + "debit".to_string(), + pm_filter_make_filter(Some(&["US"]), Some(&["USD"]), None), + ), + ] + .into_iter() + .collect(), + ), + ); + + ConnectorFilters(connector_filters) + } + + #[test] + fn routing_evaluate_eligibility_applies_pm_filters_on_routing_output() { + let bundle = build_bundle_for_tests(); + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("IN")), + ]); + let initial = connectors(&["stripe", "adyen"]); + + let filtered = compute_routing_evaluate_eligibility(Some(&bundle), ¶ms, &initial); + assert_eq!(filtered, connectors(&["adyen"])); + } + + #[test] + fn routing_evaluate_eligibility_skips_pm_filters_when_payment_method_type_missing() { + let bundle = build_bundle_for_tests(); + let params = HashMap::from([("billing_country".to_string(), enum_value("US"))]); + let initial = connectors(&["stripe", "adyen"]); + + let filtered = compute_routing_evaluate_eligibility(Some(&bundle), ¶ms, &initial); + assert_eq!(filtered, initial); + } + + #[test] + fn routing_evaluate_eligibility_fails_open_for_pm_filters_after_routing_pass() { + let params = HashMap::from([("payment_method_type".to_string(), enum_value("credit"))]); + let initial = connectors(&["stripe", "adyen", "checkout"]); + + let filtered = compute_routing_evaluate_eligibility(None, ¶ms, &initial); + assert_eq!(filtered, connectors(&["stripe", "adyen", "checkout"])); + } + + #[test] + fn apply_pm_filter_eligibility_returns_intersection() { + let bundle = build_bundle_for_tests(); + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("IN")), + ]); + let initial = connectors(&["stripe", "adyen"]); + + let filtered = apply_pm_filter_eligibility(Some(&bundle), ¶ms, &initial); + assert_eq!(filtered, connectors(&["adyen"])); + } + + #[test] + fn apply_pm_filter_eligibility_skips_when_payment_method_type_missing() { + let bundle = build_bundle_for_tests(); + let params = HashMap::from([("billing_country".to_string(), enum_value("US"))]); + let initial = connectors(&["stripe", "adyen"]); + + let filtered = if pm_filter_graph::has_payment_method_type(¶ms) { + apply_pm_filter_eligibility(Some(&bundle), ¶ms, &initial) + } else { + initial.clone() + }; + assert_eq!(filtered, initial); + } + + #[test] + fn apply_pm_filter_eligibility_fails_open_when_bundle_unavailable() { + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("US")), + ]); + let initial = connectors(&["stripe", "adyen"]); + + let filtered = apply_pm_filter_eligibility(None, ¶ms, &initial); + assert_eq!(filtered, initial); + } + + #[test] + fn pm_filter_explicit_connector_pass_and_fail_for_country_currency_pmt() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params_pass = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("US")), + ("currency".to_string(), enum_value("USD")), + ]); + let params_fail_country = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("DE")), + ("currency".to_string(), enum_value("USD")), + ]); + + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms_pass, + &pm_filter_test_connectors(&["stripe"]) + ) + .len(), + 1 + ); + assert!(pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms_fail_country, + &pm_filter_test_connectors(&["stripe"]) + ) + .is_empty()); + } + + #[test] + fn pm_filter_default_rule_applies_for_non_explicit_connector() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("US")), + ("currency".to_string(), enum_value("USD")), + ("capture_method".to_string(), enum_value("automatic")), + ]); + + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms, + &pm_filter_test_connectors(&["adyen"]) + ) + .len(), + 1 + ); + } + + #[test] + fn pm_filter_explicit_connector_without_specific_rule_falls_back_to_default() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("debit")), + ("billing_country".to_string(), enum_value("US")), + ("currency".to_string(), enum_value("USD")), + ]); + + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms, + &pm_filter_test_connectors(&["stripe"]) + ) + .len(), + 1 + ); + } + + #[test] + fn pm_filter_connector_passes_when_rule_absent_in_explicit_and_default() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params = + HashMap::from([("payment_method_type".to_string(), enum_value("upi_collect"))]); + + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms, + &pm_filter_test_connectors(&["stripe"]) + ) + .len(), + 1 + ); + } + + #[test] + fn pm_filter_capture_method_exclusion_works() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params_manual = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("US")), + ("currency".to_string(), enum_value("USD")), + ("capture_method".to_string(), enum_value("manual")), + ]); + let params_auto = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("US")), + ("currency".to_string(), enum_value("USD")), + ("capture_method".to_string(), enum_value("automatic")), + ]); + + assert!(pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms_manual, + &pm_filter_test_connectors(&["adyen"]) + ) + .is_empty()); + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms_auto, + &pm_filter_test_connectors(&["adyen"]) + ) + .len(), + 1 + ); + } + + #[test] + fn pm_filter_missing_country_or_currency_does_not_fail_when_configured() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params = HashMap::from([("payment_method_type".to_string(), enum_value("credit"))]); + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms, + &pm_filter_test_connectors(&["adyen"]) + ) + .len(), + 1 + ); + } + + #[test] + fn pm_filter_billing_country_mapping_is_derived_from_routing_keys() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + assert_eq!( + bundle.billing_country_to_iso2.get("US"), + Some(&"US".to_string()) + ); + assert_eq!( + bundle.billing_country_to_iso2.get("DE"), + Some(&"DE".to_string()) + ); + } + + #[test] + fn pm_filter_unknown_billing_country_is_ignored_and_does_not_false_reject() { + let bundle = pm_filter_graph::build_pm_filter_graph_bundle( + &pm_filter_build_connector_filters(), + Some(&pm_filter_test_routing_config()), + ) + .expect("bundle should build"); + + let params = HashMap::from([ + ("payment_method_type".to_string(), enum_value("credit")), + ("billing_country".to_string(), enum_value("UnknownLand")), + ("currency".to_string(), enum_value("USD")), + ("capture_method".to_string(), enum_value("automatic")), + ]); + + assert_eq!( + pm_filter_graph::filter_eligible_connectors( + &bundle, + ¶ms, + &pm_filter_test_connectors(&["adyen"]) + ) + .len(), + 1 + ); + } + + #[test] + fn extract_connectors_for_priority_output_returns_all_unique_connectors() { + let output = Output::Priority(vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ]); + + let connectors = extract_connectors_for_eligibility(&output); + assert_eq!( + connectors, + vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ] + ); + } + + #[test] + fn extract_connectors_for_volume_split_output_returns_all_split_connectors() { + let output = Output::VolumeSplit(vec![ + VolumeSplit { + split: 70, + output: ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + }, + VolumeSplit { + split: 30, + output: ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + }, + ]); + + let connectors = extract_connectors_for_eligibility(&output); + assert_eq!( + connectors, + vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ] + ); + } + + #[test] + fn extract_connectors_for_volume_split_priority_flattens_all_unique_connectors() { + let output = Output::VolumeSplitPriority(vec![ + VolumeSplit { + split: 50, + output: vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ], + }, + VolumeSplit { + split: 50, + output: vec![ + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ConnectorInfo { + gateway_name: "adyen".to_string(), + gateway_id: Some("mca_ady".to_string()), + }, + ], + }, + ]); + + let connectors = extract_connectors_for_eligibility(&output); + assert_eq!( + connectors, + vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ConnectorInfo { + gateway_name: "adyen".to_string(), + gateway_id: Some("mca_ady".to_string()), + }, + ] + ); + } + + #[test] + fn priority_and_fallback_permutation_matrix_with_logs() { + let bundle = build_priority_matrix_bundle(); + print_priority_test_setup(&bundle); + + #[derive(Clone)] + struct Case { + name: &'static str, + connectors: Vec<&'static str>, + payment_method_type: Option<&'static str>, + billing_country: Option<&'static str>, + currency: Option<&'static str>, + expected: Vec<&'static str>, + } + + let cases = vec![ + Case { + name: "priority_card_no_pmt_type_pm_skipped", + connectors: vec!["razorpay", "stripe"], + payment_method_type: None, + billing_country: None, + currency: None, + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "priority_card_upi_in_inr_both_pass", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("upi_collect"), + billing_country: Some("IN"), + currency: Some("INR"), + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "priority_card_upi_us_usd_only_stripe_passes", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("upi_collect"), + billing_country: Some("US"), + currency: Some("USD"), + expected: vec!["stripe"], + }, + Case { + name: "priority_card_credit_us_usd_both_pass", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("credit"), + billing_country: Some("US"), + currency: Some("USD"), + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "priority_card_google_pay_us_both_pass", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("google_pay"), + billing_country: Some("US"), + currency: None, + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "priority_card_google_pay_in_both_fail", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("google_pay"), + billing_country: Some("IN"), + currency: None, + expected: vec![], + }, + Case { + name: "default_output_no_pmt_type_pm_skipped", + connectors: vec!["adyen"], + payment_method_type: None, + billing_country: None, + currency: None, + expected: vec!["adyen"], + }, + Case { + name: "fallback_razorpay_stripe_no_pmt_type_pm_skipped", + connectors: vec!["razorpay", "stripe"], + payment_method_type: None, + billing_country: None, + currency: None, + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "fallback_razorpay_stripe_upi_in_inr_both_pass", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("upi_collect"), + billing_country: Some("IN"), + currency: Some("INR"), + expected: vec!["razorpay", "stripe"], + }, + Case { + name: "fallback_razorpay_stripe_upi_us_usd_only_stripe_passes", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("upi_collect"), + billing_country: Some("US"), + currency: Some("USD"), + expected: vec!["stripe"], + }, + Case { + name: "fallback_stripe_razorpay_upi_us_usd_only_stripe_passes", + connectors: vec!["stripe", "razorpay"], + payment_method_type: Some("upi_collect"), + billing_country: Some("US"), + currency: Some("USD"), + expected: vec!["stripe"], + }, + Case { + name: "fallback_razorpay_stripe_credit_us_usd_both_pass", + connectors: vec!["razorpay", "stripe"], + payment_method_type: Some("credit"), + billing_country: Some("US"), + currency: Some("USD"), + expected: vec!["razorpay", "stripe"], + }, + ]; + + for (index, case) in cases.iter().enumerate() { + let input_connectors = connectors(&case.connectors); + let params = params_for_case( + case.payment_method_type, + case.billing_country, + case.currency, + ); + let eligible = + compute_routing_evaluate_eligibility(Some(&bundle), ¶ms, &input_connectors); + let expected = connectors(&case.expected); + + println!( + "CASE {index}: {}\n input={:?}\n pmt={:?} country={:?} currency={:?}\n eligible={:?}\n expected={:?}", + case.name, + connector_names(&input_connectors), + case.payment_method_type, + case.billing_country, + case.currency, + connector_names(&eligible), + case.expected + ); + + assert_eq!(eligible, expected, "failed case: {}", case.name); + } + } + + #[test] + fn eligibility_runs_on_all_connectors_for_all_output_forms_with_logs() { + let bundle = build_priority_matrix_bundle(); + print_priority_test_setup(&bundle); + println!("Output forms tested: single, priority, volume_split, volume_split_priority"); + let params = params_for_case(Some("upi_collect"), Some("US"), Some("USD")); + + let test_outputs = vec![ + ( + "single", + Output::Single(ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }), + vec![], + ), + ( + "priority", + Output::Priority(vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ]), + vec!["stripe"], + ), + ( + "volume_split", + Output::VolumeSplit(vec![ + VolumeSplit { + split: 60, + output: ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + }, + VolumeSplit { + split: 40, + output: ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + }, + ]), + vec!["stripe"], + ), + ( + "volume_split_priority", + Output::VolumeSplitPriority(vec![ + VolumeSplit { + split: 50, + output: vec![ + ConnectorInfo { + gateway_name: "razorpay".to_string(), + gateway_id: Some("mca_rzp".to_string()), + }, + ConnectorInfo { + gateway_name: "stripe".to_string(), + gateway_id: Some("mca_strp".to_string()), + }, + ], + }, + VolumeSplit { + split: 50, + output: vec![ConnectorInfo { + gateway_name: "adyen".to_string(), + gateway_id: Some("mca_ady".to_string()), + }], + }, + ]), + vec!["stripe", "adyen"], + ), + ]; + + for (name, output, expected_eligible_names) in test_outputs { + let connectors_for_eligibility = extract_connectors_for_eligibility(&output); + let eligible = compute_routing_evaluate_eligibility( + Some(&bundle), + ¶ms, + &connectors_for_eligibility, + ); + + println!( + "FORM {name}\n extracted={:?}\n eligible={:?}\n expected={:?}", + connector_names(&connectors_for_eligibility), + connector_names(&eligible), + expected_eligible_names + ); + + assert_eq!( + connector_names(&eligible), + expected_eligible_names, + "unexpected eligible connectors for output form {name}" + ); + } + } +} diff --git a/src/euclid/types.rs b/src/euclid/types.rs index 1816726d..380c2a7b 100644 --- a/src/euclid/types.rs +++ b/src/euclid/types.rs @@ -1,6 +1,4 @@ use super::ast::ConnectorInfo; -use super::utils::generate_random_id; -use crate::decider::network_decider; use crate::euclid::ast::{Output, Program, ValueType}; #[cfg(feature = "mysql")] use crate::storage::schema; @@ -16,7 +14,7 @@ use time::PrimitiveDateTime; pub type Metadata = HashMap; -#[derive(Debug, Clone, Serialize, strum::Display)] +#[derive(Debug, Clone, Serialize, strum::Display, PartialEq)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DataType { @@ -61,6 +59,7 @@ pub enum AlgorithmType { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct RoutingRequest { + pub payment_id: Option, pub created_by: String, pub fallback_output: Option>, pub parameters: HashMap>, @@ -100,8 +99,26 @@ impl RoutingDictionaryRecord { } } +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct SrDimensionConfig { + pub merchant_id: String, + pub paymentInfo: SrDimensionInfo, +} +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct SrDimensionInfo { + pub udfs: Vec, + pub fields: Option>, +} +pub const ELIGIBLE_DIMENSIONS: [&str; 5] = [ + "currency", + "country", + "auth_type", + "card_is_in", + "card_network", +]; #[derive(Debug, serde::Serialize)] pub struct RoutingEvaluateResponse { + pub payment_id: Option, pub status: String, pub output: serde_json::Value, pub evaluated_output: Vec, @@ -156,7 +173,7 @@ impl From for JsonifiedRoutingAlgorithm { let algorithm_data: serde_json::Value = serde_json::from_str(&ra.algorithm_data).unwrap_or_else(|_| serde_json::Value::Null); - JsonifiedRoutingAlgorithm { + Self { id: ra.id, created_by: ra.created_by, name: ra.name, @@ -238,6 +255,7 @@ impl fmt::Display for InterpreterError { } } +#[derive(Debug)] pub struct Context(HashMap>); impl Context { pub fn new(parameters: HashMap>) -> Self { @@ -252,57 +270,103 @@ impl Deref for Context { } } +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum KeyDataType { + #[serde(rename = "integer")] + Integer, + #[serde(rename = "enum")] + Enum, + #[serde(rename = "udf")] + Udf, + #[serde(rename = "str_value")] + StrValue, + #[serde(rename = "global_ref")] + GlobalRef, +} + +impl KeyDataType { + pub fn as_str(&self) -> &str { + match self { + Self::Integer => "integer", + Self::Enum => "enum", + Self::Udf => "udf", + Self::StrValue => "str_value", + Self::GlobalRef => "global_ref", + } + } +} + /// Represents a key configuration in the TOML file #[derive(Clone, Debug, Deserialize, Serialize)] pub struct KeyConfig { #[serde(rename = "type")] - pub data_type: String, + pub data_type: KeyDataType, #[serde(default)] pub values: Option, + #[serde(default)] + pub min_value: Option, + #[serde(default)] + pub max_value: Option, + #[serde(default)] + pub min_length: Option, + #[serde(default)] + pub max_length: Option, + #[serde(default)] + pub exact_length: Option, + #[serde(default)] + pub regex: Option, +} + +impl KeyConfig { + pub fn has_validation_constraints(&self) -> bool { + self.min_value.is_some() + || self.max_value.is_some() + || self.min_length.is_some() + || self.max_length.is_some() + || self.exact_length.is_some() + || self.regex.is_some() + } + + pub fn build_validation_rules(&self) -> Result { + let regex_pattern = match &self.regex { + Some(pattern) => Some( + regex::Regex::new(pattern) + .map_err(|e| format!("Invalid regex pattern '{}': {}", pattern, e))?, + ), + None => None, + }; + + Ok(FieldValidationRules { + min_value: self.min_value, + max_value: self.max_value, + min_length: self.min_length, + max_length: self.max_length, + exact_length: self.exact_length, + regex_pattern, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct FieldValidationRules { + pub min_value: Option, + pub max_value: Option, + pub min_length: Option, + pub max_length: Option, + pub exact_length: Option, + pub regex_pattern: Option, } /// Structure for the [keys] section in the TOML -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub struct KeysConfig { #[serde(flatten)] pub keys: HashMap, } -/// Structure for the [default] section in the TOML -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DefaultConfig { - pub output: Vec, -} - /// The complete TOML configuration structure -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub struct TomlConfig { pub keys: KeysConfig, - pub default: DefaultConfig, - #[serde(default)] - pub constraint_graph: crate::euclid::cgraph::ConstraintGraph, -} - -impl Default for TomlConfig { - fn default() -> Self { - Self { - keys: KeysConfig::default(), - default: DefaultConfig::default(), - constraint_graph: crate::euclid::cgraph::ConstraintGraph::default(), - } - } -} - -impl Default for KeysConfig { - fn default() -> Self { - Self { - keys: HashMap::new(), - } - } -} - -impl Default for DefaultConfig { - fn default() -> Self { - Self { output: Vec::new() } - } } diff --git a/src/euclid/utils.rs b/src/euclid/utils.rs index e22dbc8e..c780a273 100644 --- a/src/euclid/utils.rs +++ b/src/euclid/utils.rs @@ -1,11 +1,53 @@ -use super::ast::{self, Comparison, ComparisonType, IfStatement, Rule, ValueType}; -use super::errors::EuclidErrors; -use super::types::StaticRoutingAlgorithm; -use crate::error::{ApiError, ContainerError}; -use crate::euclid::types::{KeyConfig, RoutingRule, TomlConfig}; +use super::ast::{Comparison, ComparisonType, IfStatement, Rule, ValueType}; +use super::errors::{EuclidErrors, ValidationErrorDetails}; +use super::types::{KeyDataType, StaticRoutingAlgorithm}; +use crate::error::ContainerError; +use crate::euclid::types::{FieldValidationRules, KeyConfig, RoutingRule, TomlConfig}; use std::collections::HashMap; use uuid::Uuid; +#[derive(Debug, Clone)] +pub struct ValidationResult { + pub is_valid: bool, + pub errors: Vec, + pub error_summary: Option, +} + +impl ValidationResult { + pub fn success() -> Self { + Self { + is_valid: true, + errors: Vec::new(), + error_summary: None, + } + } + + pub fn failure(errors: Vec) -> Self { + let summary = if errors.is_empty() { + None + } else { + Some( + errors + .iter() + .map(|e| e.message.clone()) + .collect::>() + .join("; "), + ) + }; + Self { + is_valid: false, + errors, + error_summary: summary, + } + } + + pub fn to_error_message(&self) -> String { + self.error_summary + .clone() + .unwrap_or_else(|| "Validation failed".to_string()) + } +} + pub fn generate_random_id(prefix: &str) -> String { let uuid = Uuid::new_v4(); format!("{}_{}", prefix, uuid) @@ -27,7 +69,7 @@ pub fn parse_enum_values(key_config: &KeyConfig) -> Vec { pub fn get_all_enum_definitions(config: &TomlConfig) -> HashMap> { let mut result = HashMap::new(); for (key, key_config) in &config.keys.keys { - if key_config.data_type == "enum" { + if key_config.data_type == KeyDataType::Enum { let values = parse_enum_values(key_config); if !values.is_empty() { result.insert(key.clone(), values); @@ -40,7 +82,7 @@ pub fn get_all_enum_definitions(config: &TomlConfig) -> HashMap bool { if let Some(key_config) = config.keys.keys.get(key) { - if key_config.data_type == "enum" { + if key_config.data_type == KeyDataType::Enum { let valid_values = parse_enum_values(key_config); return valid_values.contains(&value.to_string()); } @@ -54,9 +96,10 @@ pub fn get_keys_by_type(config: &TomlConfig) -> HashMap> { result.insert("enum".to_string(), Vec::new()); result.insert("integer".to_string(), Vec::new()); result.insert("udf".to_string(), Vec::new()); - result.insert("string".to_string(), Vec::new()); + result.insert("str_value".to_string(), Vec::new()); for (key, key_config) in &config.keys.keys { - if let Some(keys) = result.get_mut(&key_config.data_type) { + let type_str = key_config.data_type.as_str().to_string(); + if let Some(keys) = result.get_mut(&type_str) { keys.push(key.clone()); } } @@ -66,7 +109,7 @@ pub fn get_keys_by_type(config: &TomlConfig) -> HashMap> { pub fn validate_routing_rule( rule: &RoutingRule, config: &Option, -) -> Result<(), ContainerError> { +) -> Result> { let config = config .clone() .ok_or_else(|| error_stack::report!(EuclidErrors::GlobalRoutingConfigsUnavailable))?; @@ -74,49 +117,52 @@ pub fn validate_routing_rule( match &rule.algorithm { StaticRoutingAlgorithm::Single(_) | StaticRoutingAlgorithm::Priority(_) - | StaticRoutingAlgorithm::VolumeSplit(_) => return Ok(()), + | StaticRoutingAlgorithm::VolumeSplit(_) => Ok(ValidationResult::success()), StaticRoutingAlgorithm::Advanced(program) => { - let mut errors = Vec::new(); + let mut validation_errors: Vec = Vec::new(); + for rule in &program.rules { - validate_rule(rule, &config, &mut errors); + validate_rule(rule, &config, &mut validation_errors); } - if errors.is_empty() { - Ok(()) + if validation_errors.is_empty() { + Ok(ValidationResult::success()) } else { - let detailed_message = errors.join("; "); - crate::logger::error!( - "Routing rule validation failed with errors: {detailed_message}" - ); - Err(EuclidErrors::InvalidRequest(format!( - "Routing rule validation failed: {}", - detailed_message - )) - .into()) + for error in &validation_errors { + crate::logger::warn!( + field = %error.field, + error_type = %error.error_type, + message = %error.message, + "Field validation error" + ); + } + + let result = ValidationResult::failure(validation_errors); + Ok(result) } } } } -fn validate_rule(rule: &Rule, config: &TomlConfig, errors: &mut Vec) { - for (i, statement) in rule.statements.iter().enumerate() { - validate_statement( - statement, - config, - errors, - &format!("Rule '{}' Statement {}", rule.name, i + 1), - ); +fn validate_rule(rule: &Rule, config: &TomlConfig, errors: &mut Vec) { + for statement in &rule.statements { + validate_statement(statement, config, errors); } } fn validate_statement( statement: &IfStatement, config: &TomlConfig, - errors: &mut Vec, - context: &str, + errors: &mut Vec, ) { for condition in &statement.condition { - validate_condition(condition, config, errors, context); + validate_condition(condition, config, errors); + } + + if let Some(nested) = &statement.nested { + for nested_stmt in nested { + validate_statement(nested_stmt, config, errors); + } } } @@ -127,22 +173,23 @@ fn validate_statement( fn validate_condition( condition: &Comparison, config: &TomlConfig, - errors: &mut Vec, - context: &str, + errors: &mut Vec, ) { let key_exists = config.keys.keys.contains_key(&condition.lhs); if !key_exists { - errors.push(format!( - "{}: Unknown key '{}' in condition", - context, condition.lhs + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "unknown_key", + format!("Invalid key '{}': Unknown key in condition", &condition.lhs), )); return; } + let key_config = &config.keys.keys[&condition.lhs]; - match (key_config.data_type.as_str(), &condition.comparison) { + match (&key_config.data_type, &condition.comparison) { ( - "integer", + KeyDataType::Integer, ComparisonType::Equal | ComparisonType::NotEqual | ComparisonType::LessThan @@ -150,105 +197,318 @@ fn validate_condition( | ComparisonType::GreaterThan | ComparisonType::GreaterThanEqual, ) => {} - ("enum", ComparisonType::Equal | ComparisonType::NotEqual) => {} - - ("enum", _) => { - errors.push(format!( - "{}: Invalid comparison type '{:?}' for enum key '{}'", - context, condition.comparison, condition.lhs + (KeyDataType::Enum, ComparisonType::Equal | ComparisonType::NotEqual) => {} + (KeyDataType::Enum, _) => { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "invalid_comparison", + format!( + "Invalid comparison type '{}': expected Equal or NotEqual, got {:?}", + &condition.lhs, condition.comparison + ), )); } (_, comp) if comp != &ComparisonType::Equal && comp != &ComparisonType::NotEqual => { - errors.push(format!( - "{}: Comparison type '{:?}' may not be appropriate for key '{}' of type '{}'", - context, condition.comparison, condition.lhs, key_config.data_type + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "comparison_warning", + format!( + "Comparison type '{:?}' may not be appropriate for key '{}' of type '{:?}'", + condition.comparison, condition.lhs, key_config.data_type + ), )); } _ => {} } - match (key_config.data_type.as_str(), &condition.value) { - ("enum", ValueType::EnumVariant(value)) => { + match (&key_config.data_type, &condition.value) { + (KeyDataType::Enum, ValueType::EnumVariant(value)) => { if !is_valid_enum_value(config, &condition.lhs, value) { let valid_values = parse_enum_values(key_config); - errors.push(format!( - "{}: Invalid enum value '{}' for key '{}'. Valid values are: {:?}", - context, value, condition.lhs, valid_values + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "invalid_enum_value", + format!( + "Invalid enum value '{}': expected one of {:?}, got '{}'", + &condition.lhs, valid_values, value + ), )); } } - ("enum", ValueType::EnumVariantArray(arr)) => { + (KeyDataType::Enum, ValueType::EnumVariantArray(arr)) => { let invalid: Vec<_> = arr .iter() - .filter(|v| !is_valid_enum_value(config, &condition.lhs, *v)) + .filter(|v| !is_valid_enum_value(config, &condition.lhs, v)) .cloned() .collect(); if !invalid.is_empty() { let valid_values = parse_enum_values(key_config); - errors.push(format!( - "{}: Invalid enum values {:?} for key '{}'. Valid values are: {:?}", - context, invalid, condition.lhs, valid_values + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "invalid_enum_values", + format!( + "Invalid enum values '{}': expected values from {:?}, got {:?}", + &condition.lhs, valid_values, invalid + ), )); } } - ("enum", _) => { - errors.push(format!( - "{}: Key '{}' is of type 'enum' but value is not an enum variant", - context, condition.lhs + (KeyDataType::Enum, _) => { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "type_mismatch", + format!( + "Invalid enum variant '{}': expected enum variant, got {:?}", + &condition.lhs, + condition.value.get_type() + ), )); } - ("integer", ValueType::Number(_)) => { - // Number value is valid for integer type + (KeyDataType::Integer, ValueType::Number(n)) => { + if key_config.has_validation_constraints() { + if let Ok(rules) = key_config.build_validation_rules() { + if let Err(e) = validate_numeric_range(&condition.lhs, *n as i64, &rules) { + let mut expected_parts = Vec::new(); + if let Some(min) = rules.min_value { + expected_parts.push(format!("min: {}", min)); + } + if let Some(max) = rules.max_value { + expected_parts.push(format!("max: {}", max)); + } + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "value_out_of_range", + e, + )); + } + } + } } - // array of literals – only == / != make sense - ("integer", ValueType::NumberArray(_)) => { + (KeyDataType::Integer, ValueType::NumberArray(arr)) => { if !matches!( condition.comparison, ComparisonType::Equal | ComparisonType::NotEqual ) { - errors.push(format!( - "{context}: Only '==' or '!=' allowed with number arrays for key '{}'", - condition.lhs + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "invalid_comparison", + format!( + "Only '==' or '!=' allowed with number arrays for key '{}'", + condition.lhs + ), )); } + + if key_config.has_validation_constraints() { + if let Ok(rules) = key_config.build_validation_rules() { + for (i, n) in arr.iter().enumerate() { + if let Err(e) = validate_numeric_range(&condition.lhs, *n as i64, &rules) { + let mut expected_parts = Vec::new(); + if let Some(min) = rules.min_value { + expected_parts.push(format!("min: {}", min)); + } + if let Some(max) = rules.max_value { + expected_parts.push(format!("max: {}", max)); + } + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "value_out_of_range", + format!("Element {}: {}", i + 1, e), + )); + } + } + } + } } - // comparison array – interpreter supports **only `==`** - ("integer", ValueType::NumberComparisonArray(_)) => { + (KeyDataType::Integer, ValueType::NumberComparisonArray(_)) => { if condition.comparison != ComparisonType::Equal { - errors.push(format!( - "{context}: Only '==' allowed with number comparison arrays for key '{}'", - condition.lhs + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "invalid_comparison", + format!( + "Only '==' allowed with number comparison arrays for key '{}'", + condition.lhs + ), )); } } - - ("integer", _) => { - errors.push(format!( - "{}: Key '{}' is of type 'integer' but value is not a number", - context, condition.lhs + (KeyDataType::Integer, _) => { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "type_mismatch", + format!( + "Invalid key '{}': expected number, got {:?}", + &condition.lhs, + condition.value.get_type() + ), )); } - ("udf", ValueType::MetadataVariant(_)) => { - // Metadata value is valid for udf type + (KeyDataType::Udf, ValueType::MetadataVariant(m)) => { + if key_config.has_validation_constraints() { + if let Ok(rules) = key_config.build_validation_rules() { + if let Err(e) = validate_string_value(&condition.lhs, &m.value, &rules) { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "length_invalid", + e, + )); + } + } + } } - ("udf", _) => { - errors.push(format!( - "{}: Key '{}' is of type 'udf' but value is not a metadata variant", - context, condition.lhs + (KeyDataType::Udf, _) => { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "type_mismatch", + format!( + "Invalid key '{}': expected metadata variant, got {:?}", + &condition.lhs, + condition.value.get_type() + ), )); } + + (KeyDataType::StrValue, ValueType::StrValue(s)) => { + if key_config.has_validation_constraints() { + if let Ok(rules) = key_config.build_validation_rules() { + if let Err(e) = validate_string_value(&condition.lhs, s, &rules) { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "length_invalid", + e, + )); + } + } + } + } + _ => { - if condition.value.get_type().to_string() != key_config.data_type { - errors.push(format!( - "{}: Value type mismatch for key '{}': expected '{}' but got '{}'", - context, - condition.lhs, - key_config.data_type, - condition.value.get_type() + if condition.value.get_type().to_string() != key_config.data_type.as_str() { + errors.push(ValidationErrorDetails::new( + &condition.lhs, + "type_mismatch", + format!( + "Invalid key '{}': expected {}, got {}", + &condition.lhs, + key_config.data_type.as_str(), + condition.value.get_type() + ), )); } } } } + +pub fn validate_numeric_range( + field: &str, + value: i64, + rules: &FieldValidationRules, +) -> Result<(), String> { + if let Some(min) = rules.min_value { + if value < min { + return Err(format!( + "Invalid field '{}': value {} is below minimum {}", + field, value, min + )); + } + } + if let Some(max) = rules.max_value { + if value > max { + return Err(format!( + "Invalid field '{}': value {} exceeds maximum {}", + field, value, max + )); + } + } + Ok(()) +} + +pub fn validate_string_length( + field: &str, + value: &str, + min_length: Option, + max_length: Option, +) -> Result<(), String> { + let len = value.len(); + + if let Some(min) = min_length { + if len < min { + return Err(format!( + "Invalid field '{}': length {} is below minimum {}", + field, len, min + )); + } + } + + if let Some(max) = max_length { + if len > max { + return Err(format!( + "Invalid field '{}': length {} exceeds maximum {}", + field, len, max + )); + } + } + + Ok(()) +} + +pub fn validate_exact_length( + field: &str, + value: &str, + expected_length: usize, +) -> Result<(), String> { + let actual_length = value.len(); + if actual_length != expected_length { + return Err(format!( + "Invalid field '{}': expected {} characters, got {} characters", + field, expected_length, actual_length + )); + } + Ok(()) +} + +pub fn validate_regex_pattern( + field: &str, + value: &str, + pattern: &Option, +) -> Result<(), String> { + if let Some(ref regex) = pattern { + if !regex.is_match(value) { + return Err(format!( + "Invalid field '{}': value does not match required pattern", + field + )); + } + } + Ok(()) +} + +pub fn validate_string_value( + field: &str, + value: &str, + rules: &FieldValidationRules, +) -> Result<(), String> { + let mut errors = Vec::new(); + + if let Some(exact) = rules.exact_length { + if let Err(e) = validate_exact_length(field, value, exact) { + errors.push(e); + } + } else if rules.min_length.is_some() || rules.max_length.is_some() { + if let Err(e) = validate_string_length(field, value, rules.min_length, rules.max_length) { + errors.push(e); + } + } + + if let Err(e) = validate_regex_pattern(field, value, &rules.regex_pattern) { + errors.push(e); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} diff --git a/src/feedback/constants.rs b/src/feedback/constants.rs index a982613e..6862f097 100644 --- a/src/feedback/constants.rs +++ b/src/feedback/constants.rs @@ -24,274 +24,274 @@ pub enum Database { UnknownDB(String), } -// Original Haskell data type: SR_V3_BASED_FLOW_CUTOVER +// Original Haskell data type: SrV3BasedFlowCutover #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SR_V3_BASED_FLOW_CUTOVER; +pub struct SrV3BasedFlowCutover; -impl SC::ServiceConfigKey for SR_V3_BASED_FLOW_CUTOVER { +impl SC::ServiceConfigKey for SrV3BasedFlowCutover { fn get_key(&self) -> std::string::String { "sr_v3_based_flow_cutover".to_string() } } -// Original Haskell data type: ENABLE_DEBUG_MODE_ON_SR_V3 +// Original Haskell data type: EnableDebugModeOnSrV3 #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct ENABLE_DEBUG_MODE_ON_SR_V3; +pub struct EnableDebugModeOnSrV3; -// Original Haskell data type: SR_V3_INPUT_CONFIG_DEFAULT +// Original Haskell data type: SrV3InputConfigDefault #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SR_V3_INPUT_CONFIG_DEFAULT; +pub struct SrV3InputConfigDefault; -// Original Haskell data type: GW_REF_ID_ENABLED_MERCHANTS_SRV2_PRODUCER +// Original Haskell data type: GwRefIdEnabledMerchantsSrv2Producer #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GW_REF_ID_ENABLED_MERCHANTS_SRV2_PRODUCER; +pub struct GwRefIdEnabledMerchantsSrv2Producer; -impl SC::ServiceConfigKey for GW_REF_ID_ENABLED_MERCHANTS_SRV2_PRODUCER { +impl SC::ServiceConfigKey for GwRefIdEnabledMerchantsSrv2Producer { fn get_key(&self) -> std::string::String { "gw_ref_id_enabled_merchants_SRv2_producer".to_string() } } -// Original Haskell data type: AUTH_TYPE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT +// Original Haskell data type: AuthTypeSrRoutingProducerEnabledMerchant #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct AUTH_TYPE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT; +pub struct AuthTypeSrRoutingProducerEnabledMerchant; -impl SC::ServiceConfigKey for AUTH_TYPE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT { +impl SC::ServiceConfigKey for AuthTypeSrRoutingProducerEnabledMerchant { fn get_key(&self) -> std::string::String { "auth_type_sr_routing_producer_enabled_merchant".to_string() } } -// Original Haskell data type: BANK_LEVEL_SR_ROUTING_PRODUCER_ENABLED_MERCHANT +// Original Haskell data type: BankLevelSrRoutingProducerEnabledMerchant #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct BANK_LEVEL_SR_ROUTING_PRODUCER_ENABLED_MERCHANT; +pub struct BankLevelSrRoutingProducerEnabledMerchant; -impl SC::ServiceConfigKey for BANK_LEVEL_SR_ROUTING_PRODUCER_ENABLED_MERCHANT { +impl SC::ServiceConfigKey for BankLevelSrRoutingProducerEnabledMerchant { fn get_key(&self) -> std::string::String { "bank_level_sr_routing_producer_enabled_merchant".to_string() } } -// Original Haskell data type: PSP_APP_SR_ROUTING_PRODUCER_ENABLED_MERCHANT +// Original Haskell data type: PspAppSrRoutingProducerEnabledMerchant #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct PSP_APP_SR_ROUTING_PRODUCER_ENABLED_MERCHANT; +pub struct PspAppSrRoutingProducerEnabledMerchant; -impl SC::ServiceConfigKey for PSP_APP_SR_ROUTING_PRODUCER_ENABLED_MERCHANT { +impl SC::ServiceConfigKey for PspAppSrRoutingProducerEnabledMerchant { fn get_key(&self) -> std::string::String { "psp_app_sr_routing_producer_enabled_merchant".to_string() } } -// Original Haskell data type: PSP_PACKAGE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT +// Original Haskell data type: PspPackageSrRoutingProducerEnabledMerchant #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct PSP_PACKAGE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT; +pub struct PspPackageSrRoutingProducerEnabledMerchant; -impl SC::ServiceConfigKey for PSP_PACKAGE_SR_ROUTING_PRODUCER_ENABLED_MERCHANT { +impl SC::ServiceConfigKey for PspPackageSrRoutingProducerEnabledMerchant { fn get_key(&self) -> std::string::String { "psp_package_sr_routing_producer_enabled_merchant".to_string() } } -// Original Haskell data type: SR_V3_INPUT_CONFIG +// Original Haskell data type: SrV3InputConfig #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SR_V3_INPUT_CONFIG(pub String); +pub struct SrV3InputConfig(pub String); -impl SC::ServiceConfigKey for SR_V3_INPUT_CONFIG { +impl SC::ServiceConfigKey for SrV3InputConfig { fn get_key(&self) -> String { format!("SR_V3_INPUT_CONFIG_{}", self.0) } } -// Original Haskell data type: GLOBAL_GATEWAY_SCORING_ENABLED_MERCHANTS +// Original Haskell data type: GlobalGatewayScoringEnabledMerchants #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GLOBAL_GATEWAY_SCORING_ENABLED_MERCHANTS; +pub struct GlobalGatewayScoringEnabledMerchants; -impl SC::ServiceConfigKey for GLOBAL_GATEWAY_SCORING_ENABLED_MERCHANTS { +impl SC::ServiceConfigKey for GlobalGatewayScoringEnabledMerchants { fn get_key(&self) -> std::string::String { "global_gateway_scoring_enabled_merchants".to_string() } } -// Original Haskell data type: GLOBAL_OUTAGE_GATEWAY_SCORING_ENABLED_MERCHANTS +// Original Haskell data type: GlobalOutageGatewayScoringEnabledMerchants #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GLOBAL_OUTAGE_GATEWAY_SCORING_ENABLED_MERCHANTS; +pub struct GlobalOutageGatewayScoringEnabledMerchants; -impl SC::ServiceConfigKey for GLOBAL_OUTAGE_GATEWAY_SCORING_ENABLED_MERCHANTS { +impl SC::ServiceConfigKey for GlobalOutageGatewayScoringEnabledMerchants { fn get_key(&self) -> std::string::String { "global_outage_gateway_scoring_enabled_merchants".to_string() } } #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct UPDATE_SCORE_LOCK_FEATURE_ENABLED_MERCHANT; +pub struct UpdateScoreLockFeatureEnabledMerchant; -impl SC::ServiceConfigKey for UPDATE_SCORE_LOCK_FEATURE_ENABLED_MERCHANT { +impl SC::ServiceConfigKey for UpdateScoreLockFeatureEnabledMerchant { fn get_key(&self) -> std::string::String { "update_score_lock_feature_enabled_merchant".to_string() } } #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct UPDATE_GATEWAY_SCORE_LOCK_FLAG_TTL; +pub struct UpdateGatewayScoreLockFlagTtl; -impl SC::ServiceConfigKey for UPDATE_GATEWAY_SCORE_LOCK_FLAG_TTL { +impl SC::ServiceConfigKey for UpdateGatewayScoreLockFlagTtl { fn get_key(&self) -> std::string::String { "update_gateway_score_lock_flag_ttl".to_string() } } -// Original Haskell data type: MINIMUM_GATEWAY_SCORE +// Original Haskell data type: MinimumGatewayScore #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct MINIMUM_GATEWAY_SCORE; +pub struct MinimumGatewayScore; -impl SC::ServiceConfigKey for MINIMUM_GATEWAY_SCORE { +impl SC::ServiceConfigKey for MinimumGatewayScore { fn get_key(&self) -> std::string::String { "minimum_gateway_score".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_LATENCY_CHECK_IN_MINS +// Original Haskell data type: GatewayScoreLatencyCheckInMins #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_LATENCY_CHECK_IN_MINS; +pub struct GatewayScoreLatencyCheckInMins; -impl SC::ServiceConfigKey for GATEWAY_SCORE_LATENCY_CHECK_IN_MINS { +impl SC::ServiceConfigKey for GatewayScoreLatencyCheckInMins { fn get_key(&self) -> std::string::String { "gateway_score_latency_check_in_mins".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_LATENCY_CHECK_EXEMPT_GATEWAYS +// Original Haskell data type: GatewayScoreLatencyCheckExemptGateways #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_LATENCY_CHECK_EXEMPT_GATEWAYS; +pub struct GatewayScoreLatencyCheckExemptGateways; -impl SC::ServiceConfigKey for GATEWAY_SCORE_LATENCY_CHECK_EXEMPT_GATEWAYS { +impl SC::ServiceConfigKey for GatewayScoreLatencyCheckExemptGateways { fn get_key(&self) -> std::string::String { "gateway_score_latency_check_exempt_gateways".to_string() } } -// Original Haskell data type: DEFAULT_GW_SCORE_LATENCY_THRESHOLD +// Original Haskell data type: DefaultGwScoreLatencyThreshold #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct DEFAULT_GW_SCORE_LATENCY_THRESHOLD { +pub struct DefaultGwScoreLatencyThreshold { #[serde(rename = "default_gw_score_latency_threshold")] pub default_gw_score_latency_threshold: Option, } -// Original Haskell data type: GATEWAY_PENALTY_FACTOR +// Original Haskell data type: GatewayPenaltyFactor #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_PENALTY_FACTOR; +pub struct GatewayPenaltyFactor; -impl SC::ServiceConfigKey for GATEWAY_PENALTY_FACTOR { +impl SC::ServiceConfigKey for GatewayPenaltyFactor { fn get_key(&self) -> std::string::String { "gateway_penalty_factor".to_string() } } -// Original Haskell data type: GATEWAY_REWARD_FACTOR +// Original Haskell data type: GatewayRewardFactor #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_REWARD_FACTOR; +pub struct GatewayRewardFactor; -impl SC::ServiceConfigKey for GATEWAY_REWARD_FACTOR { +impl SC::ServiceConfigKey for GatewayRewardFactor { fn get_key(&self) -> std::string::String { "gateway_reward_factor".to_string() } } -// Original Haskell data type: OUTAGE_PENALTY_FACTOR +// Original Haskell data type: OutagePenaltyFactor #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct OUTAGE_PENALTY_FACTOR; +pub struct OutagePenaltyFactor; -impl SC::ServiceConfigKey for OUTAGE_PENALTY_FACTOR { +impl SC::ServiceConfigKey for OutagePenaltyFactor { fn get_key(&self) -> std::string::String { "outage_penalty_factor".to_string() } } -// Original Haskell data type: OUTAGE_REWARD_FACTOR +// Original Haskell data type: OutageRewardFactor #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct OUTAGE_REWARD_FACTOR; +pub struct OutageRewardFactor; -impl SC::ServiceConfigKey for OUTAGE_REWARD_FACTOR { +impl SC::ServiceConfigKey for OutageRewardFactor { fn get_key(&self) -> std::string::String { "outage_reward_factor".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_OUTAGE_TTL +// Original Haskell data type: GatewayScoreOutageTtl #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_OUTAGE_TTL; +pub struct GatewayScoreOutageTtl; -impl SC::ServiceConfigKey for GATEWAY_SCORE_OUTAGE_TTL { +impl SC::ServiceConfigKey for GatewayScoreOutageTtl { fn get_key(&self) -> std::string::String { "gateway_score_outage_ttl".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_GLOBAL_TTL +// Original Haskell data type: GatewayScoreGlobalTtl #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_GLOBAL_TTL; +pub struct GatewayScoreGlobalTtl; -impl SC::ServiceConfigKey for GATEWAY_SCORE_GLOBAL_TTL { +impl SC::ServiceConfigKey for GatewayScoreGlobalTtl { fn get_key(&self) -> std::string::String { "gateway_score_global_ttl".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_GLOBAL_OUTAGE_TTL +// Original Haskell data type: GatewayScoreGlobalOutageTtl #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_GLOBAL_OUTAGE_TTL; +pub struct GatewayScoreGlobalOutageTtl; -impl SC::ServiceConfigKey for GATEWAY_SCORE_GLOBAL_OUTAGE_TTL { +impl SC::ServiceConfigKey for GatewayScoreGlobalOutageTtl { fn get_key(&self) -> std::string::String { "gateway_score_global_outage_ttl".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_MERCHANT_ARR_MAX_LENGTH +// Original Haskell data type: GatewayScoreMerchantArrMaxLength #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_MERCHANT_ARR_MAX_LENGTH; +pub struct GatewayScoreMerchantArrMaxLength; -impl SC::ServiceConfigKey for GATEWAY_SCORE_MERCHANT_ARR_MAX_LENGTH { +impl SC::ServiceConfigKey for GatewayScoreMerchantArrMaxLength { fn get_key(&self) -> std::string::String { "gateway_score_merchant_arr_max_length".to_string() } } -// Original Haskell data type: GATEWAY_SCORE_THIRD_DIMENSION_TTL +// Original Haskell data type: GatewayScoreThirdDimensionTtl #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GATEWAY_SCORE_THIRD_DIMENSION_TTL; +pub struct GatewayScoreThirdDimensionTtl; -impl SC::ServiceConfigKey for GATEWAY_SCORE_THIRD_DIMENSION_TTL { +impl SC::ServiceConfigKey for GatewayScoreThirdDimensionTtl { fn get_key(&self) -> std::string::String { "gateway_score_third_dimension_ttl".to_string() } } -// Original Haskell data type: ENFORCE_GW_SCORE_KV_REDIS +// Original Haskell data type: EnforceGwScoreKvRedis #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct ENFORCE_GW_SCORE_KV_REDIS; +pub struct EnforceGwScoreKvRedis; -impl SC::ServiceConfigKey for ENFORCE_GW_SCORE_KV_REDIS { +impl SC::ServiceConfigKey for EnforceGwScoreKvRedis { fn get_key(&self) -> std::string::String { "enforce_gw_score_kv_redis".to_string() } } -// Original Haskell data type: SR_SCORE_REDIS_FALLBACK_LOOKUP_DISABLE +// Original Haskell data type: SrScoreRedisFallbackLookupDisable #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SR_SCORE_REDIS_FALLBACK_LOOKUP_DISABLE; +pub struct SrScoreRedisFallbackLookupDisable; -impl SC::ServiceConfigKey for SR_SCORE_REDIS_FALLBACK_LOOKUP_DISABLE { +impl SC::ServiceConfigKey for SrScoreRedisFallbackLookupDisable { fn get_key(&self) -> std::string::String { "sr_score_redis_fallback_lookup_disable".to_string() } } -// Original Haskell data type: SR_V3_PRODUCER_ISOLATION +// Original Haskell data type: SrV3ProducerIsolation #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SR_V3_PRODUCER_ISOLATION; +pub struct SrV3ProducerIsolation; -impl SC::ServiceConfigKey for SR_V3_PRODUCER_ISOLATION { +impl SC::ServiceConfigKey for SrV3ProducerIsolation { fn get_key(&self) -> std::string::String { "sr_v3_producer_isolation".to_string() } @@ -308,14 +308,14 @@ pub fn kvRedis() -> String { "kv_redis".into() } -// Original Haskell function: pendingTxnsKeyPrefix -pub const pendingTxnsKeyPrefix: &str = "PENDING_TXNS_"; +// Original Haskell function: PENDING_TXNS_KEY_PREFIX +pub const PENDING_TXNS_KEY_PREFIX: &str = "PENDING_TXNS_"; -// Original Haskell function: defaultSrV3BasedBucketSize -pub const defaultSrV3BasedBucketSize: i32 = 125; +// Original Haskell function: DEFAULT_SR_V3_BASED_BUCKET_SIZE +pub const DEFAULT_SR_V3_BASED_BUCKET_SIZE: i32 = 125; -// Original Haskell function: gatewaySelectionV3OrderTypeKeyPrefix -pub const gatewaySelectionV3OrderTypeKeyPrefix: &str = "{gw_sr_v3_score"; +// Original Haskell function: GATEWAY_SELECTION_V3_ORDER_TYPE_KEY_PREFIX +pub const GATEWAY_SELECTION_V3_ORDER_TYPE_KEY_PREFIX: &str = "{gw_sr_v3_score"; // Original Haskell function: ecRedis pub fn ecRedis() -> String { @@ -362,18 +362,18 @@ pub fn defaultMinimumGatewayScore() -> f64 { 0.0 } -// Original Haskell function: gatewayScoringData -pub const gatewayScoringData: &str = "gateway_scoring_data_"; +// Original Haskell function: GATEWAY_SCORING_DATA +pub const GATEWAY_SCORING_DATA: &str = "gateway_scoring_data_"; // Original Haskell function: defaultMerchantArrMaxLength pub fn defaultMerchantArrMaxLength() -> i32 { 40 } -pub fn defaultUpdateGatewayScoreLockFlagTtl() -> i32 { +pub fn default_update_gateway_score_lock_flag_ttl() -> i32 { 300 } -pub fn defaultSrV3LatencyThresholdInSecs() -> f64 { - 300 as f64 +pub fn default_sr_v3_latency_threshold_in_secs() -> f64 { + 300_f64 } diff --git a/src/feedback/gateway_elimination_scoring/flow.rs b/src/feedback/gateway_elimination_scoring/flow.rs index 51d6b926..5946598b 100644 --- a/src/feedback/gateway_elimination_scoring/flow.rs +++ b/src/feedback/gateway_elimination_scoring/flow.rs @@ -4,12 +4,7 @@ // Local Imports use crate::{ app::get_tenant_app_state, - decider::storage::utils::merchant_gateway_account, - feedback::constants::{ - defaultGWScoringPenaltyFactor, defaultGWScoringRewardFactor, defaultMerchantArrMaxLength, - defaultMinimumGatewayScore, defaultScoreGlobalKeysTTL, defaultScoreKeysTTL, ecRedis, - ecRedis2, kvRedis, kvRedis2, ENFORCE_GW_SCORE_KV_REDIS, GATEWAY_SCORE_THIRD_DIMENSION_TTL, - }, + feedback::constants::{defaultGWScoringPenaltyFactor, defaultGWScoringRewardFactor}, redis::types::ServiceConfigKey, types::{ card::txn_card_info::TxnCardInfo, merchant, merchant_config::types::PfMcConfig, @@ -17,16 +12,10 @@ use crate::{ }, }; -#[cfg(feature = "mysql")] -use crate::storage::schema::gateway_bank_emi_support::gateway; -#[cfg(feature = "postgres")] -use crate::storage::schema_pg::gateway_bank_emi_support::gateway; - use crate::feedback::constants as C; -use crate::decider::gatewaydecider::constants::{ENABLE_ELIMINATION_V2, ENABLE_OUTAGE_V2}; +use crate::decider::gatewaydecider::constants::{EnableEliminationV2, EnableOutageV2}; -use crate::types::gateway_routing_input as ETGRI; // use crate::feedback::types as F_TYPES; use crate::decider::gatewaydecider::utils::decode_and_log_error; @@ -48,22 +37,16 @@ use crate::feedback::types::{ // TxnCardInfo, // TxnDetail, CachedGatewayScore, - GatewayScoringKeyType, - KeyType, MerchantScoringDetails, - ScoreType, - ScoringDimension, }; use crate::logger; - +use crate::redis::feature::RedisCompressionConfigCombined; // use eulerhs::language::get_current_date_in_millis; // use eulerhs::language as EL; -use crate::redis::commands::RedisConnectionWrapper; -use crate::redis::feature::isFeatureEnabled; +use crate::redis::feature::is_feature_enabled; -use crate::types::gateway as Gateway; use crate::types::merchant::id as Merchant; // use crate::types::txn_details::types::TxnDetail:: // use types::tenant_config as TenantConfig; @@ -81,7 +64,6 @@ use crate::utils as CUTILS; // Haskell's Double corresponds to Rust's f64, which is built into the language. -use bytes::Bytes; // use encoding_rs as TE; // use lens::set; @@ -93,9 +75,11 @@ pub async fn updateKeyScoreForKeysFromConsumer( txn_detail: TxnDetail, txn_card_info: TxnCardInfo, gateway_scoring_type: GatewayScoreType, - mer_acc_p_id: merchant::id::MerchantPId, - mer_acc: MerchantAccount, + gateway_scoring_data: GatewayScoringData, + _mer_acc_p_id: merchant::id::MerchantPId, + _mer_acc: MerchantAccount, gateway_scoring_key: (ScoreKeyType, Option), + redis_compression_config: Option, ) -> Option<((ScoreKeyType, String), CachedGatewayScore)> { let merchant_id = Merchant::merchant_id_to_text(txn_detail.merchantId.clone()); let (score_key_type, m_key) = gateway_scoring_key; @@ -104,8 +88,8 @@ pub async fn updateKeyScoreForKeysFromConsumer( // let gateway = txn_detail.gateway.unwrap_or_else(|| "".to_string()); let hard_key_ttl = getTTLForKey(score_key_type).await; let timestamp = CUTILS::get_current_date_in_millis(); - // let should_enforce_kv_redis = isFeatureEnabled(C::ENFORCE_GW_SCORE_KV_REDIS.get_key(), merchant_id, C::kvRedis()).await; - // let should_disable_fallback = isFeatureEnabled(C::SR_SCORE_REDIS_FALLBACK_LOOKUP_DISABLE.get_key(), merchant_id, C::kvRedis()).await; + // let should_enforce_kv_redis = isFeatureEnabled(C::GatewayScoreThirdDimensionTtl.get_key(), merchant_id, C::kvRedis()).await; + // let should_disable_fallback = isFeatureEnabled(C::SrScoreRedisFallbackLookupDisable.get_key(), merchant_id, C::kvRedis()).await; // let m_cached_gateway_score: Option = readFromCacheWithFallback(should_enforce_kv_redis, should_disable_fallback, key); let m_cached_gateway_score = readGatewayScoreFromRedis(&key).await; let gw_score_to_be_updated: CachedGatewayScore = match m_cached_gateway_score { @@ -117,9 +101,7 @@ pub async fn updateKeyScoreForKeysFromConsumer( txn_card_info.clone(), ), Some(cached_gateway_score) => { - if (timestamp.clone() - cached_gateway_score.timestamp.clone()) - > (hard_key_ttl.clone()) - 1000 - { + if (timestamp - cached_gateway_score.timestamp) > hard_key_ttl - 1000 { logger::debug!( action = "updateKeyScore", tag = "updateKeyScore", @@ -145,6 +127,7 @@ pub async fn updateKeyScoreForKeysFromConsumer( gateway_scoring_type.clone(), txn_detail.clone(), txn_card_info.clone(), + gateway_scoring_data.clone(), ) .await; let updated_score = match gw_score_to_be_updated.score { @@ -157,6 +140,7 @@ pub async fn updateKeyScoreForKeysFromConsumer( gateway_scoring_type.clone(), score, score_key_type, + gateway_scoring_data, ) .await, ), @@ -174,21 +158,22 @@ pub async fn updateKeyScoreForKeysFromConsumer( transactionCount: transaction_count, } }; - let encoded_json = serde_json::to_string(&updated_cached_gateway_score).unwrap(); + let _encoded_json = serde_json::to_string(&updated_cached_gateway_score).unwrap(); let elapsed_time = timestamp.saturating_sub(updated_cached_gateway_score.timestamp as u128); - let remaining_ttl = (hard_key_ttl as u128).saturating_sub(elapsed_time); + let remaining_ttl = hard_key_ttl.saturating_sub(elapsed_time); let safe_remaining_ttl = if remaining_ttl < 1000 { hard_key_ttl as i64 } else { remaining_ttl as i64 }; - let app_state: std::sync::Arc = + let _app_state: std::sync::Arc = get_tenant_app_state().await; let result = EulerTransforms::writeToCacheWithTTL( key.clone(), updated_cached_gateway_score.clone(), safe_remaining_ttl, + redis_compression_config, ) .await; //To Do: add Ok & Err @@ -227,7 +212,7 @@ fn getTransactionCount( match previous_transaction_count { None => Some(1), Some(transaction_count) => { - if gateway_scoring_type == GatewayScoreType::PENALISE { + if gateway_scoring_type == GatewayScoreType::Penalise { Some(transaction_count + 1) } else { Some(transaction_count) @@ -245,19 +230,16 @@ pub async fn updateKeyScoreForTxnStatus( gateway_scoring_type: GatewayScoreType, current_key_score: f64, score_key_type: ScoreKeyType, + gateway_scoring_data: GatewayScoringData, ) -> f64 { - let is_elimination_v2_enabled = isFeatureEnabled( - ENABLE_ELIMINATION_V2.get_key(), - merchant_id.clone(), - C::kvRedis(), - ) - .await; - let is_elimination_v2_enabled_for_outage = isFeatureEnabled( - ENABLE_OUTAGE_V2.get_key(), + let is_elimination_v2_enabled = is_feature_enabled( + EnableEliminationV2.get_key(), merchant_id.clone(), C::kvRedis(), ) .await; + let is_elimination_v2_enabled_for_outage = + is_feature_enabled(EnableOutageV2.get_key(), merchant_id.clone(), C::kvRedis()).await; let is_outage_key = isKeyOutage(score_key_type); logger::debug!( action = "updateKeyScore", @@ -267,33 +249,35 @@ pub async fn updateKeyScoreForTxnStatus( ); match gateway_scoring_type { - GatewayScoreType::PENALISE => { + GatewayScoreType::Penalise => { return updateScoreWithPenalty( is_elimination_v2_enabled, is_outage_key, is_elimination_v2_enabled_for_outage, - &merchant_id, - &txn_card_info, - &txn_detail, + merchant_id, + txn_card_info, + txn_detail, current_key_score, &score_key_type, + gateway_scoring_data.clone(), ) .await; } - GatewayScoreType::REWARD => { + GatewayScoreType::Reward => { return updateScoreWithReward( is_elimination_v2_enabled, is_outage_key, is_elimination_v2_enabled_for_outage, - &merchant_id, - &txn_card_info, - &txn_detail, + merchant_id, + txn_card_info, + txn_detail, current_key_score, &score_key_type, + gateway_scoring_data.clone(), ) .await; } - _ => return current_key_score, + _ => current_key_score, } } @@ -306,6 +290,7 @@ async fn updateScoreWithPenalty( txn_detail: &TxnDetail, current_key_score: f64, score_key_type: &ScoreKeyType, + gateway_scoring_data: GatewayScoringData, ) -> f64 { match ( is_elimination_v2_enabled, @@ -313,14 +298,20 @@ async fn updateScoreWithPenalty( is_elimination_v2_enabled_for_outage, ) { (true, true, true) | (true, _, _) => { - let m_reward_factor = - eliminationV2RewardFactor(merchant_id, txn_card_info, txn_detail).await; + let m_reward_factor = eliminationV2RewardFactor( + merchant_id, + txn_card_info, + txn_detail, + gateway_scoring_data.isGriEnabledForElimination, + gateway_scoring_data.gatewayReferenceId, + ) + .await; match m_reward_factor { None => { getFailureKeyScore( false, current_key_score, - getPenaltyFactor(score_key_type.clone()).await, + getPenaltyFactor(*score_key_type).await, ) .await } @@ -331,7 +322,7 @@ async fn updateScoreWithPenalty( getFailureKeyScore( false, current_key_score, - getPenaltyFactor(score_key_type.clone()).await, + getPenaltyFactor(*score_key_type).await, ) .await } @@ -347,6 +338,7 @@ async fn updateScoreWithReward( txn_detail: &TxnDetail, current_key_score: f64, score_key_type: &ScoreKeyType, + gateway_scoring_data: GatewayScoringData, ) -> f64 { match ( is_elimination_v2_enabled, @@ -354,13 +346,19 @@ async fn updateScoreWithReward( is_elimination_v2_enabled_for_outage, ) { (true, true, true) | (true, _, _) => { - let m_reward_factor = - eliminationV2RewardFactor(merchant_id, txn_card_info, txn_detail).await; + let m_reward_factor = eliminationV2RewardFactor( + merchant_id, + txn_card_info, + txn_detail, + gateway_scoring_data.isGriEnabledForElimination, + gateway_scoring_data.gatewayReferenceId, + ) + .await; match m_reward_factor { None => getSuccessKeyScore( false, current_key_score, - getRewardFactor(score_key_type.clone()).await, + getRewardFactor(*score_key_type).await, ), Some(factor) => getSuccessKeyScore(true, current_key_score, factor), } @@ -368,7 +366,7 @@ async fn updateScoreWithReward( _ => getSuccessKeyScore( false, current_key_score, - getRewardFactor(score_key_type.clone()).await, + getRewardFactor(*score_key_type).await, ), } } @@ -393,7 +391,7 @@ pub async fn getFailureKeyScore( current_score: f64, penalty_factor: f64, ) -> f64 { - let m_score: Option = findByNameFromRedis(C::MINIMUM_GATEWAY_SCORE.get_key()) + let m_score: Option = findByNameFromRedis(C::MinimumGatewayScore.get_key()) .await .unwrap_or_default(); let minimum_failure_score = m_score.unwrap_or(C::defaultMinimumGatewayScore()); @@ -412,13 +410,13 @@ pub async fn getFailureKeyScore( // Original Haskell function: getPenaltyFactor pub async fn getPenaltyFactor(scoreKeyType: ScoreKeyType) -> f64 { let penalty_factor = if isKeyOutage(scoreKeyType) { - findByNameFromRedis(C::OUTAGE_PENALTY_FACTOR.get_key()) + findByNameFromRedis(C::OutagePenaltyFactor.get_key()) .await - .unwrap_or_else(|| defaultGWScoringPenaltyFactor()) + .unwrap_or_else(defaultGWScoringPenaltyFactor) } else { - findByNameFromRedis(C::GATEWAY_PENALTY_FACTOR.get_key()) + findByNameFromRedis(C::GatewayPenaltyFactor.get_key()) .await - .unwrap_or_else(|| defaultGWScoringPenaltyFactor()) + .unwrap_or_else(defaultGWScoringPenaltyFactor) }; penalty_factor } @@ -426,13 +424,13 @@ pub async fn getPenaltyFactor(scoreKeyType: ScoreKeyType) -> f64 { // Original Haskell function: getRewardFactor pub async fn getRewardFactor(scoreKeyType: ScoreKeyType) -> f64 { let reward_factor = if isKeyOutage(scoreKeyType) { - findByNameFromRedis(C::OUTAGE_REWARD_FACTOR.get_key()) + findByNameFromRedis(C::OutageRewardFactor.get_key()) .await - .unwrap_or_else(|| defaultGWScoringRewardFactor()) + .unwrap_or_else(defaultGWScoringRewardFactor) } else { - findByNameFromRedis(C::OUTAGE_REWARD_FACTOR.get_key()) + findByNameFromRedis(C::OutageRewardFactor.get_key()) .await - .unwrap_or_else(|| defaultGWScoringRewardFactor()) + .unwrap_or_else(defaultGWScoringRewardFactor) }; reward_factor } @@ -444,6 +442,7 @@ pub async fn getUpdatedMerchantDetailsForGlobalKey( gateway_scoring_type: GatewayScoreType, txn_detail: TxnDetail, txn_card_info: TxnCardInfo, + gateway_scoring_data: GatewayScoringData, ) -> Option> { let merchant_id = Merchant::merchant_id_to_text(txn_detail.merchantId.clone()); if isGlobalKey(score_key_type) { @@ -454,11 +453,11 @@ pub async fn getUpdatedMerchantDetailsForGlobalKey( if filtered_merchant_details_array.is_empty() { let arr_max_length = getMerchantArrMaxLength().await; if merchant_details_array.len() as i32 >= arr_max_length { - return (Some(merchant_details_array)); + Some(merchant_details_array) } else { let merchant_detail = getDefaultMerchantScoringDetailsArray(merchant_id, 1.0, 1, None); - return (Some([merchant_details_array, vec![merchant_detail]].concat())); + Some([merchant_details_array, vec![merchant_detail]].concat()) } } else { let mut results = Vec::new(); @@ -469,21 +468,22 @@ pub async fn getUpdatedMerchantDetailsForGlobalKey( &txn_card_info, gateway_scoring_type.clone(), score_key_type, + gateway_scoring_data.clone(), ) .await; results.push(result); } - return Some(results); + Some(results) } } None => { let merchant_scoring_details = getDefaultMerchantScoringDetailsArray(merchant_id, 1.0, 1, None); - return (Some(vec![merchant_scoring_details])); + Some(vec![merchant_scoring_details]) } } } else { - return (None); + None } } @@ -493,6 +493,7 @@ pub async fn replaceTransactionCount( txn_card_info: &TxnCardInfo, gateway_scoring_type: GatewayScoreType, score_key_type: ScoreKeyType, + gateway_scoring_data: GatewayScoringData, ) -> MerchantScoringDetails { let merchant_id = Merchant::merchant_id_to_text(txn_detail.merchantId.clone()); if merchant_scoring_details.merchantId == merchant_id { @@ -503,30 +504,31 @@ pub async fn replaceTransactionCount( gateway_scoring_type.clone(), merchant_scoring_details.score, score_key_type, + gateway_scoring_data.clone(), ) .await; - let new_count = if gateway_scoring_type == GatewayScoreType::PENALISE { + let new_count = if gateway_scoring_type == GatewayScoreType::Penalise { merchant_scoring_details.transactionCount + 1 } else { merchant_scoring_details.transactionCount }; - (MerchantScoringDetails { + MerchantScoringDetails { score: updated_score, transactionCount: new_count, ..merchant_scoring_details - }) + } } else { - (merchant_scoring_details) + merchant_scoring_details } } // Original Haskell function: getNewCachedGatewayScore pub fn getNewCachedGatewayScore( - key: String, - gateway_scoring_type: GatewayScoreType, + _key: String, + _gateway_scoring_type: GatewayScoreType, score_key_type: ScoreKeyType, txn_detail: TxnDetail, - txn_card_info: TxnCardInfo, + _txn_card_info: TxnCardInfo, ) -> CachedGatewayScore { let merchant_id = Merchant::merchant_id_to_text(txn_detail.merchantId); let current_date: u128 = CUTILS::get_current_date_in_millis(); @@ -535,7 +537,7 @@ pub fn getNewCachedGatewayScore( getDefaultMerchantScoringDetailsArray(merchant_id, 1.0, 0, None); CachedGatewayScore { score: None, - timestamp: current_date.clone(), + timestamp: current_date, lastResetTimestamp: None, merchants: Some(vec![merchant_scoring_details]), transactionCount: None, @@ -543,8 +545,8 @@ pub fn getNewCachedGatewayScore( } else { CachedGatewayScore { score: Some(1.0), - timestamp: current_date.clone(), - lastResetTimestamp: Some(current_date.clone()), + timestamp: current_date, + lastResetTimestamp: Some(current_date), merchants: None, transactionCount: Some(0), } @@ -560,7 +562,7 @@ pub fn getDefaultMerchantScoringDetailsArray( ) -> MerchantScoringDetails { let current_date = CUTILS::get_current_date_in_millis(); MerchantScoringDetails { - score: score, + score, merchantId: merchant_id, transactionCount: transaction_count, lastResetTimestamp: m_last_reset_timestamp.unwrap_or(current_date as i32), @@ -570,16 +572,16 @@ pub fn getDefaultMerchantScoringDetailsArray( // Original Haskell function: getAllUnifiedKeys pub async fn getAllUnifiedKeys( txn_detail: TxnDetail, - txn_card_info: TxnCardInfo, + _txn_card_info: TxnCardInfo, mer_acc_p_id: Merchant::MerchantPId, - m_pf_mc_config: Option, + _m_pf_mc_config: Option, mer_acc: MerchantAccount, gateway_scoring_data: GatewayScoringData, gateway_reference_id: Option, ) -> Vec<(ScoreKeyType, Option)> { let merchant_id = Merchant::merchant_id_to_text(txn_detail.merchantId.clone()); - let is_key_enabled_for_global_gateway_scoring = isFeatureEnabled( - C::GLOBAL_GATEWAY_SCORING_ENABLED_MERCHANTS.get_key(), + let is_key_enabled_for_global_gateway_scoring = is_feature_enabled( + C::GlobalGatewayScoringEnabledMerchants.get_key(), merchant_id.clone(), C::kvRedis(), ) @@ -588,16 +590,16 @@ pub async fn getAllUnifiedKeys( || MCU::isPaymentFlowEnabledWithHierarchyCheck( mer_acc_p_id, mer_acc.tenantAccountId.clone(), - ModuleEnum::MERCHANT_CONFIG, - PaymentFlow::PaymentFlow::ELIMINATION_BASED_ROUTING, + ModuleEnum::MerchantConfig, + PaymentFlow::PaymentFlow::EliminationBasedRouting, crate::types::country::country_iso::text_db_to_country_iso( mer_acc.country.as_deref().unwrap_or_default(), ) .ok(), ) .await; - let is_gateway_scoring_enabled_for_global_outage = isFeatureEnabled( - C::GLOBAL_OUTAGE_GATEWAY_SCORING_ENABLED_MERCHANTS.get_key(), + let is_gateway_scoring_enabled_for_global_outage = is_feature_enabled( + C::GlobalOutageGatewayScoringEnabledMerchants.get_key(), merchant_id.clone(), C::kvRedis(), ) @@ -606,8 +608,8 @@ pub async fn getAllUnifiedKeys( MCU::isPaymentFlowEnabledWithHierarchyCheck( mer_acc_p_id, mer_acc.tenantAccountId, - ModuleEnum::MERCHANT_CONFIG, - PaymentFlow::PaymentFlow::OUTAGE, + ModuleEnum::MerchantConfig, + PaymentFlow::PaymentFlow::Outage, crate::types::country::country_iso::text_db_to_country_iso( mer_acc.country.as_deref().unwrap_or_default(), ) @@ -619,12 +621,12 @@ pub async fn getAllUnifiedKeys( let key = EulerTransforms::getProducerKey( txn_detail.clone(), Some(gateway_scoring_data.clone()), - ScoreKeyType::ELIMINATION_GLOBAL_KEY, + ScoreKeyType::EliminationGlobalKey, false, gateway_reference_id.clone(), ) .await; - vec![(ScoreKeyType::ELIMINATION_GLOBAL_KEY, key)] + vec![(ScoreKeyType::EliminationGlobalKey, key)] } else { logger::debug!( action = "getGlobalKeys", @@ -632,19 +634,19 @@ pub async fn getAllUnifiedKeys( "Global gateway scoring not enabled for merchant {:?}", merchant_id ); - vec![(ScoreKeyType::ELIMINATION_GLOBAL_KEY, None)] + vec![(ScoreKeyType::EliminationGlobalKey, None)] }; let merchant_key = if is_key_enabled_for_merchant_gateway_scoring { let key = EulerTransforms::getProducerKey( txn_detail.clone(), Some(gateway_scoring_data.clone()), - ScoreKeyType::ELIMINATION_MERCHANT_KEY, + ScoreKeyType::EliminationMerchantKey, false, gateway_reference_id.clone(), ) .await; - vec![(ScoreKeyType::ELIMINATION_MERCHANT_KEY, key)] + vec![(ScoreKeyType::EliminationMerchantKey, key)] } else { logger::debug!( action = "getMerchantBasedKeys", @@ -652,19 +654,19 @@ pub async fn getAllUnifiedKeys( "Merchant gateway scoring not enabled for merchant {:?}", merchant_id ); - vec![(ScoreKeyType::ELIMINATION_MERCHANT_KEY, None)] + vec![(ScoreKeyType::EliminationMerchantKey, None)] }; let global_outage_keys = if is_gateway_scoring_enabled_for_global_outage { let key = EulerTransforms::getProducerKey( txn_detail.clone(), Some(gateway_scoring_data.clone()), - ScoreKeyType::OUTAGE_GLOBAL_KEY, + ScoreKeyType::OutageGlobalKey, false, gateway_reference_id.clone(), ) .await; - vec![(ScoreKeyType::OUTAGE_GLOBAL_KEY, key)] + vec![(ScoreKeyType::OutageGlobalKey, key)] } else { logger::debug!( action = "getGlobalKeys", @@ -672,19 +674,19 @@ pub async fn getAllUnifiedKeys( "Global gateway scoring not enabled for merchant {:?}", merchant_id ); - vec![(ScoreKeyType::OUTAGE_GLOBAL_KEY, None)] + vec![(ScoreKeyType::OutageGlobalKey, None)] }; let merchant_outage_keys = if is_gateway_scoring_enabled_for_merchant_outage { let key = EulerTransforms::getProducerKey( txn_detail.clone(), Some(gateway_scoring_data), - ScoreKeyType::OUTAGE_MERCHANT_KEY, + ScoreKeyType::OutageMerchantKey, false, gateway_reference_id.clone(), ) .await; - vec![(ScoreKeyType::OUTAGE_MERCHANT_KEY, key)] + vec![(ScoreKeyType::OutageMerchantKey, key)] } else { logger::debug!( action = "getMerchantScopedOutageKeys", @@ -692,7 +694,7 @@ pub async fn getAllUnifiedKeys( "Outage scoring not enabled for merchant {:?}", merchant_id ); - vec![(ScoreKeyType::OUTAGE_MERCHANT_KEY, None)] + vec![(ScoreKeyType::OutageMerchantKey, None)] }; global_key @@ -708,10 +710,10 @@ pub async fn getTTLForKey(score_key_type: ScoreKeyType) -> u128 { let is_key_global = isGlobalKey(score_key_type); let is_outage_key = isKeyOutage(score_key_type); let key: Option = match (is_key_global, is_outage_key) { - (true, true) => findByNameFromRedis(C::GATEWAY_SCORE_GLOBAL_OUTAGE_TTL.get_key()).await, - (false, true) => findByNameFromRedis(C::GATEWAY_SCORE_OUTAGE_TTL.get_key()).await, - (true, false) => findByNameFromRedis(C::GATEWAY_SCORE_GLOBAL_TTL.get_key()).await, - _ => findByNameFromRedis(C::GATEWAY_SCORE_THIRD_DIMENSION_TTL.get_key()).await, + (true, true) => findByNameFromRedis(C::GatewayScoreGlobalOutageTtl.get_key()).await, + (false, true) => findByNameFromRedis(C::GatewayScoreOutageTtl.get_key()).await, + (true, false) => findByNameFromRedis(C::GatewayScoreGlobalTtl.get_key()).await, + _ => findByNameFromRedis(C::GatewayScoreThirdDimensionTtl.get_key()).await, }; key.map_or_else(|| getDefaultTTL(score_key_type), |k| k.floor() as u128) } @@ -728,9 +730,9 @@ pub async fn readGatewayScoreFromRedis(key: &str) -> Option let app_state = get_tenant_app_state().await; app_state .redis_conn - .get_key::(&key, "gateway_score_key") + .get_key::(key, "gateway_score_key") .await - .map_or_else(|_| None, Some) + .ok() } // pub async fn readFromCacheWithFallback( @@ -782,6 +784,8 @@ pub async fn eliminationV2RewardFactor( merchant_id: &str, txn_card_info: &TxnCardInfo, txn_detail: &TxnDetail, + is_gri_enabled_for_elimination: bool, + gateway_reference_id: Option, ) -> Option { let merch_acc: MerchantAccount = MA::load_merchant_by_merchant_id(MID::merchant_id_to_text(txn_detail.clone().merchantId)) @@ -804,16 +808,19 @@ pub async fn eliminationV2RewardFactor( merchant_id.to_string(), txn_card_info.clone(), txn_detail.clone(), + is_gri_enabled_for_elimination, + gateway_reference_id.clone(), ) .await; match sr1_and_sr2_and_n { - Some((sr1, sr2, n, m_pmt, m_pm, m_txn_object_type, source)) => { + Some((sr1, sr2, n, n_, m_pmt, m_pm, m_txn_object_type, source)) => { logger::info!( - "CALCULATING_ALPHA:SR1_SR2_N_PMT_PM_TXNOBJECTTYPE_CONFIGSOURCE {} {} {} {} {} {} {:?}", + "CALCULATING_ALPHA:SR1_SR2_N_PMT_PM_TXNOBJECTTYPE_CONFIGSOURCE {} {} {} {:?} {} {} {} {:?}", sr1, sr2, n, + n_, m_pmt.unwrap_or_else(|| "Nothing".to_string()), m_pm.unwrap_or_else(|| "Nothing".to_string()), m_txn_object_type.unwrap_or_else(|| "Nothing".to_string()), @@ -822,13 +829,13 @@ pub async fn eliminationV2RewardFactor( logger::info!( action = "calculateAlpha", tag = "ALPHA_VALUE", - alpha_value = calculate_alpha(sr1, sr2, n), + alpha_value = calculate_alpha(sr1, sr2, n, n_), ); - Some(calculate_alpha(sr1, sr2, n)) + Some(calculate_alpha(sr1, sr2, n, n_)) } None => { - logger::info!("ELIMINATION_V2_VALUES_NOT_FOUND:ALPHA:PMT_PM_TXNOBJECTTYPE_SOURCEOBJECT {:?} {:?} {} {:?}", + logger::info!("ELIMINATION_V2_VALUES_NOT_FOUND:ALPHA:PMT_PM_TXNOBJECTTYPE_SOURCEOBJECT {:?} {:?} {:?} {:?}", txn_card_info.paymentMethodType, if txn_card_info.paymentMethod.is_empty() { "Nothing".to_string() } else { txn_card_info.paymentMethod.clone() }, txn_detail.txnObjectType, @@ -839,8 +846,19 @@ pub async fn eliminationV2RewardFactor( } } -fn calculate_alpha(sr1: f64, sr2: f64, n: f64) -> f64 { - ((sr1 - sr2) * (sr1 - sr2)) / ((n * n) * (sr1 * (100.0 - sr1))) +fn calculate_alpha(sr1: f64, sr2: f64, n: f64, n_prime: Option) -> f64 { + match n_prime { + None => ((sr1 - sr2) * (sr1 - sr2)) / ((n * n) * (sr1 * (100.0 - sr1))), + Some(n_val) => { + // These weights should be fetched from Env or config as per your environment + let sr1_th_weight = 0.29; + let sr2_th_weight = 0.71; + let threshold = ((sr1_th_weight * sr1) + (sr2_th_weight * sr2)) / 100.0; + let val1 = ((sr1 - sr2) * (sr1 - sr2)) / ((n * n) * (sr1 * (100.0 - sr1))); + let val2 = (threshold / (sr1 / 100.0)).powf(1.0 / n_val); + val1.min(val2) + } + } } // Original Haskell function: findMerchantFromMerchantArray @@ -857,22 +875,21 @@ pub fn findMerchantFromMerchantArray( // Original Haskell function: getMerchantArrMaxLength pub async fn getMerchantArrMaxLength() -> i32 { - let max_length = findByNameFromRedis(C::GATEWAY_SCORE_MERCHANT_ARR_MAX_LENGTH.get_key()) + let max_length = findByNameFromRedis(C::GatewayScoreMerchantArrMaxLength.get_key()) .await - .unwrap_or_else(|| C::defaultMerchantArrMaxLength()); + .unwrap_or_else(C::defaultMerchantArrMaxLength); max_length } // Original Haskell function: isGlobalKey pub fn isGlobalKey(scoreKeyType: ScoreKeyType) -> bool { - scoreKeyType == ScoreKeyType::ELIMINATION_GLOBAL_KEY - || scoreKeyType == ScoreKeyType::OUTAGE_GLOBAL_KEY + scoreKeyType == ScoreKeyType::EliminationGlobalKey + || scoreKeyType == ScoreKeyType::OutageGlobalKey } // Original Haskell function: isKeyOutage pub fn isKeyOutage(scoreKeyType: ScoreKeyType) -> bool { - scoreKeyType == ScoreKeyType::OUTAGE_GLOBAL_KEY - || scoreKeyType == ScoreKeyType::OUTAGE_MERCHANT_KEY + scoreKeyType == ScoreKeyType::OutageGlobalKey || scoreKeyType == ScoreKeyType::OutageMerchantKey } // Original Haskell function: filterAndTransformOutageKeys diff --git a/src/feedback/gateway_scoring_service.rs b/src/feedback/gateway_scoring_service.rs index db4443a5..435dc021 100644 --- a/src/feedback/gateway_scoring_service.rs +++ b/src/feedback/gateway_scoring_service.rs @@ -2,15 +2,16 @@ // Generated on 2025-03-23 10:24:42 use crate::redis::cache::findByNameFromRedis; -use crate::redis::feature::isFeatureEnabled; +use crate::redis::feature::{is_feature_enabled, RedisCompressionConfigCombined}; +use masking::PeekInterface; // Converted imports -// use gateway_decider::constants as c::{enable_elimination_v2, gateway_scoring_data, ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3, SR_V3_INPUT_CONFIG, GATEWAY_SCORE_FIRST_DIMENSION_SOFT_TTL}; +// use gateway_decider::constants as c::{enable_elimination_v2, gateway_scoring_data, EnableExploreAndExploitOnSrv3, SrV3InputConfig, GatewayScoreFirstDimensionSoftTtl}; // use feedback::constants as c; // use data::text::encoding as de::encode_utf8; // use db::storage::types::merchant_account as merchant_account; // use types::gateway_routing_input as etgri; // use gateway_decider::utils::decode_and_log_error; -// use gateway_decider::gw_scoring::get_sr1_and_sr2_and_n; +use crate::decider::gatewaydecider::gw_scoring::get_metric_entry_data; // use feedback::utils as euler_transforms; // use feedback::types::*; // use feedback::types::txn_card_info; @@ -23,11 +24,11 @@ use crate::redis::feature::isFeatureEnabled; // use eulerhs::language as el; // use eulerhs::types as et; // use eulerhs::tenant_redis_layer as rc; -use crate::app::{get_tenant_app_state, APP_STATE}; -use crate::decider::gatewaydecider::constants::{self as DC, srV3DefaultInputConfig}; +use crate::app::get_tenant_app_state; +use crate::decider::gatewaydecider::constants::{self as DC, SR_V3_DEFAULT_INPUT_CONFIG}; use crate::decider::gatewaydecider::types as T; use crate::decider::gatewaydecider::types::GatewayScoringData; -use crate::decider::gatewaydecider::types::RoutingFlowType as RF; +use crate::decider::gatewaydecider::types::{RoutingFlowType as RF, SrRoutingDimensions}; use crate::decider::gatewaydecider::utils::{ self as GU, get_m_id, get_payment_method, get_sr_v3_latency_threshold, }; @@ -38,9 +39,11 @@ use crate::feedback::utils::GatewayScoringType as GST; use crate::merchant_config_util as MerchantConfig; use crate::redis::{feature as Cutover, types::ServiceConfigKey}; use crate::types::card::txn_card_info::TxnCardInfo; +use crate::types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput; use crate::types::merchant::id as MID; use crate::types::merchant::merchant_account as MA; use crate::types::merchant::merchant_account::MerchantAccount; +use std::collections::HashMap; // use utils::redis::feature as cutover::is_feature_enabled; // use prelude::{from_integral, foldable::length, map_m, error}; // use data::foldable::{for_, foldl}; @@ -50,7 +53,6 @@ use crate::types::merchant::merchant_account::MerchantAccount; // use control::monad::extra::maybe_m; // use control::category; use crate::types::merchant as ETM; -use crate::types::transaction::id::transaction_id_to_text; // use utils::redis as redis; // use db::common::types::payment_flows as pf; // use utils::config::merchant_config as merchant_config; @@ -65,36 +67,34 @@ use crate::types::transaction::id::transaction_id_to_text; // use data::aeson as a; // use types::merchant as etm; -use fred::types::SetOptions; -use serde::{Deserialize, Serialize}; - +use crate::redis::feature::RedisDataStruct; use crate::{ feedback::{ constants as C, gateway_elimination_scoring::flow as GEF, utils::{isPennyMandateRegTxn, isRecurringTxn, GatewayScoringType}, }, - types::txn_details::types::TxnDetail, - types::txn_details::types::TxnStatus, - types::txn_details::types::TxnStatus as TS, + types::txn_details::types::{TransactionLatency, TxnDetail, TxnStatus, TxnStatus as TS}, }; +use fred::types::SetOptions; +use serde::{Deserialize, Serialize}; use super::constants::{ - defaultSrV3LatencyThresholdInSecs, SR_V3_INPUT_CONFIG, UPDATE_GATEWAY_SCORE_LOCK_FLAG_TTL, - UPDATE_SCORE_LOCK_FEATURE_ENABLED_MERCHANT, + default_sr_v3_latency_threshold_in_secs, SrV3InputConfig, UpdateGatewayScoreLockFlagTtl, + UpdateScoreLockFeatureEnabledMerchant, }; -use super::utils::getTimeFromTxnCreatedInMills; +use super::utils::get_time_from_txn_created_in_mills; use crate::logger; use crate::types::payment::payment_method_type_const::*; // Converted data types // Original Haskell data type: GatewayLatencyForScoring #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct GatewayLatencyForScoring { - #[serde(rename = "defaultLatencyThreshold")] - pub defaultLatencyThreshold: f64, + #[serde(rename = "default_latency_threshold")] + pub default_latency_threshold: f64, - #[serde(rename = "merchantLatencyGatewayWiseInput")] - pub merchantLatencyGatewayWiseInput: Option>, + #[serde(rename = "merchant_latency_gateway_wise_input")] + pub merchant_latency_gateway_wise_input: Option>, } // Original Haskell data type: GatewayWiseLatencyInput @@ -136,10 +136,10 @@ pub struct UpdateGatewayScoreRequest { } // Original Haskell data type: MetricEntry -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MetricEntry { - #[serde(rename = "total_volume")] - pub total_volume: f32, + #[serde(rename = "n_value")] + pub n_value: f32, #[serde(rename = "success_rate")] pub success_rate: f32, @@ -152,6 +152,9 @@ pub struct MetricEntry { #[serde(rename = "tp99_latency")] pub tp99_latency: f32, + + #[serde(rename = "default_success_threshold")] + pub default_success_threshold: f32, } // Original Haskell data type: SrMetrics @@ -206,17 +209,17 @@ pub struct ResetGatewayScoreBulkRequest { pub order_id: Option, #[serde(rename = "resetGatewayScoreReqArr")] - pub resetGatewayScoreReqArr: Vec, + pub reset_gateway_score_req_arr: Vec, } -pub fn defaultGwLatencyCheckInMins() -> GatewayLatencyForScoring { +pub fn default_gw_latency_check_in_mins() -> GatewayLatencyForScoring { GatewayLatencyForScoring { - defaultLatencyThreshold: 10.0, - merchantLatencyGatewayWiseInput: None, + default_latency_threshold: 10.0, + merchant_latency_gateway_wise_input: None, } } -pub fn txnSuccessStates() -> Vec { +pub fn txn_success_states() -> Vec { vec![ TS::Charged, TS::Authorized, @@ -232,7 +235,7 @@ pub fn txnSuccessStates() -> Vec { ] } -pub fn txnFailureStates() -> Vec { +pub fn txn_failure_states() -> Vec { vec![ TxnStatus::AuthenticationFailed, TxnStatus::AuthorizationFailed, @@ -241,7 +244,11 @@ pub fn txnFailureStates() -> Vec { ] } -pub async fn checkAndSendShouldUpdateGatewayScore(lock_key: String, lock_key_ttl: i32) -> bool { +pub async fn check_and_send_should_update_gateway_score( + lock_key: String, + lock_key_ttl: i32, + redis_comp_config: Option, +) -> bool { let app_state = get_tenant_app_state().await; let is_set_either = app_state .redis_conn @@ -250,100 +257,160 @@ pub async fn checkAndSendShouldUpdateGatewayScore(lock_key: String, lock_key_ttl "true", lock_key_ttl as i64, SetOptions::NX, + redis_comp_config, + RedisDataStruct::STRING, ) .await; - match is_set_either { - Ok(value) => value, - Err(_) => false, - } + is_set_either.unwrap_or(false) +} + +pub fn is_transaction_success(txn_status: TxnStatus) -> bool { + txn_success_states().contains(&txn_status) } -pub fn isTransactionSuccess(txn_status: TxnStatus) -> bool { - txnSuccessStates().contains(&txn_status) +pub fn is_transaction_failure(txn_status: TxnStatus) -> bool { + txn_failure_states().contains(&txn_status) } -pub fn isTransactionFailure(txn_status: TxnStatus) -> bool { - txnFailureStates().contains(&txn_status) +pub fn isGwLatencyWithinConfiguredThreshold( + txn_latency: Option, + merchant_latency_threshold: Option, +) -> bool { + logger::debug!( + action = "txn_latency_within_threshold", + tag = "txn_latency_within_threshold", + "Latency & Threshold: {:?} {:?}", + txn_latency, + merchant_latency_threshold + ); + if let Some((latency, threshold)) = txn_latency.zip(merchant_latency_threshold) { + latency <= threshold + } else { + true + } } -pub async fn getGatewayScoringType( +pub async fn get_gateway_scoring_type( txn_detail: TxnDetail, txn_card_info: TxnCardInfo, flag: bool, ) -> GatewayScoringType { if flag { - return GatewayScoringType::PENALISE_SRV3; + return GatewayScoringType::PenaliseSrv3; } let txn_status = txn_detail.status.clone(); let merchant_id = txn_detail.merchantId.clone(); - let is_success = isTransactionSuccess(txn_status.clone()); - let is_failure = isTransactionFailure(txn_status.clone()); - let time_difference = getTimeFromTxnCreatedInMills(txn_detail.clone()); + let is_success = is_transaction_success(txn_status.clone()); + let is_failure = is_transaction_failure(txn_status.clone()); + let time_difference = get_time_from_txn_created_in_mills(txn_detail.clone()); let merchant_sr_v3_input_config = - findByNameFromRedis(SR_V3_INPUT_CONFIG(get_m_id(merchant_id)).get_key()).await; + findByNameFromRedis(SrV3InputConfig(get_m_id(merchant_id)).get_key()).await; let pmt = txn_card_info.paymentMethodType; let pm = get_payment_method( pmt.to_string(), txn_card_info.paymentMethod, txn_detail.sourceObject.unwrap_or_default(), ); - let maybe_latency_threshold = - get_sr_v3_latency_threshold(merchant_sr_v3_input_config, &pmt, &pm); + // Extract the new parameters from txn_card_info + + let sr_routing_dimensions = SrRoutingDimensions { + card_network: txn_card_info + .cardSwitchProvider + .as_ref() + .map(|s| s.peek().to_string()), + card_isin: txn_card_info.card_isin.clone(), + currency: Some(txn_detail.currency.to_string()), + country: txn_detail.country.as_ref().map(|c| c.to_string()), + auth_type: txn_card_info.authType.as_ref().map(|a| a.to_string()), + }; + + let maybe_latency_threshold = get_sr_v3_latency_threshold( + merchant_sr_v3_input_config.clone(), + &pmt, + &pm, + &sr_routing_dimensions, + ); let time_difference_threshold = match maybe_latency_threshold { None => { let default_sr_v3_input_config = - findByNameFromRedis(srV3DefaultInputConfig.get_key()).await; - let maybe_default_latency_threshold = - get_sr_v3_latency_threshold(default_sr_v3_input_config, &pmt, &pm); - maybe_default_latency_threshold.unwrap_or(defaultSrV3LatencyThresholdInSecs()) + findByNameFromRedis(SR_V3_DEFAULT_INPUT_CONFIG.get_key()).await; + let maybe_default_latency_threshold = get_sr_v3_latency_threshold( + default_sr_v3_input_config, + &pmt, + &pm, + &sr_routing_dimensions, + ); + maybe_default_latency_threshold.unwrap_or(default_sr_v3_latency_threshold_in_secs()) } Some(latency_threshold) => latency_threshold, }; - logger::info!( + logger::debug!( action = "sr_v3_latency_threshold", tag = "sr_v3_latency_threshold", - "Latency Threshold: {}", - time_difference_threshold + "Latency Threshold: {} Time Difference: {}", + time_difference_threshold, + time_difference ); if is_success { - GatewayScoringType::REWARD + GatewayScoringType::Reward } else if is_failure { - GatewayScoringType::PENALISE_SRV3 + GatewayScoringType::PenaliseSrv3 } else if time_difference < ((time_difference_threshold * 1000.0) as u128) { - GatewayScoringType::PENALISE + GatewayScoringType::Penalise } else { - GatewayScoringType::PENALISE_SRV3 + GatewayScoringType::PenaliseSrv3 } } -pub fn updateGatewayScoreLock( +pub fn update_gateway_score_lock( gateway_scoring_type: GatewayScoringType, txn_uuid: String, gateway: String, ) -> String { - match (gateway_scoring_type) { - (GatewayScoringType::PENALISE) => { + match gateway_scoring_type { + GatewayScoringType::Penalise => { format!("gateway_scores_lock_PENALISE_{}_{}", txn_uuid, gateway) } - (GatewayScoringType::PENALISE_SRV3) => { + GatewayScoringType::PenaliseSrv3 => { format!("gateway_scores_lock_PENALISE_SRV3_{}_{}", txn_uuid, gateway) } - (GatewayScoringType::REWARD) => { + GatewayScoringType::Reward => { format!("gateway_scores_lock_REWARD_{}_{}", txn_uuid, gateway) } - _ => String::new(), + } +} + +pub fn invalid_request_error(detail: &str, e: &impl std::fmt::Display) -> T::ErrorResponse { + T::ErrorResponse { + status: "400".to_string(), + error_code: "INVALID_REQUEST".to_string(), + error_message: format!("Failed to extract {}: {}", detail, e), + priority_logic_tag: None, + routing_approach: None, + filter_wise_gateways: None, + error_info: T::UnifiedError { + code: "INVALID_REQUEST".to_string(), + user_message: "Invalid request data provided".to_string(), + developer_message: format!("Error extracting {}: {}", detail, e), + }, + priority_logic_output: None, + is_dynamic_mga_enabled: false, } } pub async fn check_and_update_gateway_score_( - apiPayload: FT::UpdateScorePayload, + api_payload: FT::UpdateScorePayload, ) -> Result { - let redis_key = format!("{}{}", C::gatewayScoringData, apiPayload.clone().paymentId); + let redis_key = format!( + "{}{}", + C::GATEWAY_SCORING_DATA, + api_payload.clone().payment_id + ); let app_state = get_tenant_app_state().await; // Attempt to fetch gateway scoring data from Redis @@ -358,13 +425,22 @@ pub async fn check_and_update_gateway_score_( match m_gateway_scoring_data { Ok(gateway_scoring_data) => { // Extract transaction details and card info from the API payload - let txn_detail: TxnDetail = - Fbu::getTxnDetailFromApiPayload(apiPayload.clone(), gateway_scoring_data.clone()); - let txn_card_info: TxnCardInfo = - Fbu::getTxnCardInfoFromApiPayload(apiPayload.clone(), gateway_scoring_data.clone()); + let txn_detail: TxnDetail = match Fbu::get_txn_detail_from_api_payload( + api_payload.clone(), + gateway_scoring_data.clone(), + ) { + Ok(detail) => detail, + Err(e) => { + return Err(invalid_request_error("transaction details", &e)); + } + }; + let txn_card_info: TxnCardInfo = Fbu::get_txn_card_info_from_api_payload( + api_payload.clone(), + gateway_scoring_data.clone(), + ); let log_message = "update_gateway_score"; - let enforce_failure = apiPayload.enforceDynamicRoutingFailure.unwrap_or(false); + let enforce_failure = api_payload.enforce_dynamic_routing_failure.unwrap_or(false); // Call the function to check and update the gateway score check_and_update_gateway_score( @@ -372,7 +448,9 @@ pub async fn check_and_update_gateway_score_( txn_card_info, log_message, enforce_failure, - apiPayload.gatewayReferenceId.clone(), + api_payload.gateway_reference_id.clone(), + api_payload.txn_latency.clone(), + None, ) .await; @@ -408,30 +486,36 @@ pub async fn check_and_update_gateway_score( log_message: &str, enforce_failure: bool, gateway_reference_id: Option, + txn_latency: Option, + redis_comp_config: Option, ) -> () { // Get gateway scoring type let gateway_scoring_type = - getGatewayScoringType(txn_detail.clone(), txn_card_info.clone(), enforce_failure).await; + get_gateway_scoring_type(txn_detail.clone(), txn_card_info.clone(), enforce_failure).await; let gateway_in_string = txn_detail.gateway.clone().unwrap_or_default(); // Create update score lock key - let update_score_lock_key = updateGatewayScoreLock( + let update_score_lock_key = update_gateway_score_lock( gateway_scoring_type.clone(), txn_detail.txnUuid.clone(), // This is Maybe type in haskell and here it is not Option gateway_in_string, // Convert Option to String ); - let lock_key_ttl = findByNameFromRedis(UPDATE_GATEWAY_SCORE_LOCK_FLAG_TTL.get_key()) + let lock_key_ttl = findByNameFromRedis(UpdateGatewayScoreLockFlagTtl.get_key()) .await .unwrap_or(300); - let should_compute_gw_score = - checkAndSendShouldUpdateGatewayScore(update_score_lock_key, lock_key_ttl).await; + let should_compute_gw_score = check_and_send_should_update_gateway_score( + update_score_lock_key, + lock_key_ttl, + redis_comp_config.clone(), + ) + .await; // Check if feature is enabled for merchant - let feature_enabled = isFeatureEnabled( - UPDATE_SCORE_LOCK_FEATURE_ENABLED_MERCHANT.get_key(), + let feature_enabled = is_feature_enabled( + UpdateScoreLockFeatureEnabledMerchant.get_key(), get_m_id(txn_detail.merchantId.clone()), "kv_redis".to_string(), ) @@ -440,7 +524,7 @@ pub async fn check_and_update_gateway_score( // Logging and score update logic if feature_enabled { if should_compute_gw_score { - logger::info!( + logger::debug!( action = "UPDATE_GATEWAY_SCORE_LOCK", tag = "UPDATE_GATEWAY_SCORE_LOCK", "Updating Gateway Score in {} flow with status as {:?} and scoring type as {:?}", @@ -448,16 +532,18 @@ pub async fn check_and_update_gateway_score( txn_detail.status, gateway_scoring_type ); - updateGatewayScore( + update_gateway_score( gateway_scoring_type.clone(), txn_detail.clone(), txn_card_info.clone(), gateway_reference_id.clone(), + txn_latency.clone(), + redis_comp_config.clone(), ) .await; } } else { - logger::info!( + logger::debug!( action = "GW_SCORE_LOCK_FEATURE_NOT_ENABLED", tag = "GW_SCORE_LOCK_FEATURE_NOT_ENABLED", "Updating Gateway Score in {} flow with status as {:?} and scoring type as {:?}", @@ -465,24 +551,26 @@ pub async fn check_and_update_gateway_score( txn_detail.status, gateway_scoring_type ); - updateGatewayScore( + update_gateway_score( gateway_scoring_type.clone(), txn_detail, txn_card_info.clone(), gateway_reference_id.clone(), + txn_latency.clone(), + redis_comp_config, ) .await; } - - () } // Original Haskell function: updateGatewayScore -pub async fn updateGatewayScore( +pub async fn update_gateway_score( gateway_scoring_type: GatewayScoringType, txn_detail: TxnDetail, txn_card_info: TxnCardInfo, gateway_reference_id: Option, + txn_latency: Option, + redis_compression_config: Option, ) -> () { let mer_acc: MerchantAccount = MA::load_merchant_by_merchant_id(MID::merchant_id_to_text(txn_detail.clone().merchantId)) @@ -490,77 +578,190 @@ pub async fn updateGatewayScore( .expect("Merchant account not found"); //let mer_acc = - let routing_approach = getRoutingApproach(txn_detail.clone()); - logger::info!( + let routing_approach = get_routing_approach(txn_detail.clone()); + logger::debug!( action = "routing_approach_value", tag = "routing_approach_value", "{:?}", routing_approach ); - let should_update_gateway_score = if gateway_scoring_type.clone() == GST::PENALISE_SRV3 { - false - } else if gateway_scoring_type.clone() == GST::PENALISE { - isTransactionPending(txn_detail.clone().status) + let m_source_object = if txn_card_info.paymentMethodType == UPI { + txn_detail.sourceObject.clone() } else { - true + Some(txn_card_info.paymentMethod.clone()) }; - //let is_pm_and_pmt_present = Fbu::isTrueString(txn_card_info.paymentMethod) && txn_card_info.paymentMethodType.is_some(); - let should_update_srv3_gateway_score = if gateway_scoring_type.clone() == GST::PENALISE { + let should_update_gateway_score = if gateway_scoring_type.clone() == GST::PenaliseSrv3 { false + } else if gateway_scoring_type.clone() == GST::Penalise { + is_transaction_pending(txn_detail.clone().status) } else { true }; - let is_update_within_window = isUpdateWithinLatencyWindow( - txn_detail.clone(), - txn_card_info.clone(), - gateway_scoring_type.clone(), - mer_acc.clone(), - ) - .await; + //let is_pm_and_pmt_present = Fbu::isTrueString(txn_card_info.paymentMethod) && txn_card_info.paymentMethodType.is_some(); + let should_update_srv3_gateway_score = gateway_scoring_type.clone() != GST::Penalise; - let should_isolate_srv3_producer = if Cutover::isFeatureEnabled( - C::SR_V3_PRODUCER_ISOLATION.get_key(), + let should_isolate_srv3_producer = if Cutover::is_feature_enabled( + C::SrV3ProducerIsolation.get_key(), MID::merchant_id_to_text(txn_detail.clone().merchantId), C::kvRedis(), ) .await { - if isRoutingApproachInSRV3(routing_approach.clone()) { - true - } else { - false - } + is_routing_approach_in_srv3(routing_approach.clone()) } else { true }; - let should_update_explore_txn = if Cutover::isFeatureEnabled( - DC::ENABLE_EXPLORE_AND_EXPLOIT_ON_SRV3(txn_card_info.clone().paymentMethodType.to_string()) + let should_update_explore_txn = if Cutover::is_feature_enabled( + DC::EnableExploreAndExploitOnSrv3(txn_card_info.clone().paymentMethodType.to_string()) .get_key(), MID::merchant_id_to_text(txn_detail.clone().merchantId), C::kvRedis(), ) .await { - if isRoutingApproachInExplore(routing_approach.clone()) { - true + is_routing_approach_in_explore(routing_approach.clone()) + } else { + true + }; + + let redis_key = format!("{}{}", C::GATEWAY_SCORING_DATA, txn_detail.clone().txnUuid); + let app_state = get_tenant_app_state().await; + + let redis_gateway_score_data_initial: Option = + if should_update_srv3_gateway_score + && should_isolate_srv3_producer + && should_update_explore_txn + { + let mb_gateway_scoring_data: Option = app_state + .redis_conn + .get_key(&redis_key, "GatewayScoringData") + .await + .ok(); + mb_gateway_scoring_data } else { - false + None + }; + + let mb_gateway_scoring_data: Option = if should_update_gateway_score { + match redis_gateway_score_data_initial { + None => { + let redis_data: Option = app_state + .redis_conn + .get_key(&redis_key, "GatewayScoringData") + .await + .ok(); + redis_data + } + Some(_) => redis_gateway_score_data_initial, } } else { - true + None }; - let redis_key = format!("{}{}", C::gatewayScoringData, txn_detail.clone().txnUuid); - let redis_gateway_score_data = if should_update_srv3_gateway_score - && is_update_within_window + let m_metric_entry: Option = match mb_gateway_scoring_data.clone() { + None => { + let merchant_id_str = MID::merchant_id_to_text(txn_detail.clone().merchantId); + let pmt_str = txn_card_info.paymentMethodType.to_string(); + let txn_obj_type_str = txn_detail + .txnObjectType + .clone() + .map(|t| t.to_string()) + .unwrap_or_default(); + let card_type_str = txn_card_info.card_type.clone().map(|t| t.to_string()); + get_metric_entry_data( + merchant_id_str, + pmt_str, + m_source_object.clone(), + txn_obj_type_str, + card_type_str, + false, + None, + ) + .await + } + Some(gateway_scoring_data) => { + let merchant_id_str = MID::merchant_id_to_text(txn_detail.clone().merchantId); + let pmt_str = txn_card_info.paymentMethodType.to_string(); + let txn_obj_type_str = txn_detail + .txnObjectType + .clone() + .map(|t| t.to_string()) + .unwrap_or_default(); + let card_type_str = txn_card_info.card_type.clone().map(|t| t.to_string()); + get_metric_entry_data( + merchant_id_str, + pmt_str, + m_source_object.clone(), + txn_obj_type_str, + card_type_str, + gateway_scoring_data.isGriEnabledForElimination, + gateway_scoring_data.gatewayReferenceId, + ) + .await + } + }; + + let is_update_within_window = is_update_within_latency_window( + txn_detail.clone(), + txn_card_info.clone(), + gateway_scoring_type.clone(), + mer_acc.clone(), + txn_latency.clone(), + m_metric_entry.clone(), + ) + .await; + + let should_record_srv3_post_update = should_update_srv3_gateway_score && should_isolate_srv3_producer && should_update_explore_txn - { - logger::info!( + && is_update_within_window; + + if !should_record_srv3_post_update { + if let Some(metric_entry) = m_metric_entry.clone() { + crate::analytics::record_score_snapshot_event( + Some(MID::merchant_id_to_text(txn_detail.clone().merchantId)), + Some(txn_card_info.paymentMethodType.to_string()), + Some(m_source_object.clone().unwrap_or_default()), + txn_card_info + .cardSwitchProvider + .as_ref() + .map(|provider| provider.peek().to_string()), + txn_card_info.card_isin.clone(), + Some(txn_detail.currency.to_string()), + txn_detail + .country + .as_ref() + .map(|country| country.to_string()), + txn_card_info + .authType + .as_ref() + .map(|auth_type| auth_type.to_string()), + txn_detail.gateway.clone().or(gateway_reference_id.clone()), + Some(metric_entry.success_rate.into()), + Some(metric_entry.sigma_factor.into()), + Some(metric_entry.average_latency.into()), + Some(metric_entry.tp99_latency.into()), + Some(metric_entry.n_value as i64), + "update_gateway_score", + serde_json::to_string(&serde_json::json!({ + "routing_approach": format!("{:?}", routing_approach), + "gateway_scoring_type": format!("{:?}", gateway_scoring_type), + "message": "Gateway score updated successfully", + })) + .ok(), + Some(txn_detail.txnUuid.clone()), + None, + Some("score_updated".to_string()), + ); + } + } + + if should_record_srv3_post_update { + logger::debug!( action = "updateGatewayScore", tag = "updateGatewayScore", "Updating sr v3 score for the txn with scoring type as {:?} and status as {:?}", @@ -573,7 +774,7 @@ pub async fn updateGatewayScore( .get_key(&redis_key, "GatewayScoringData") .await .ok(); - GSSV3::flow::updateSrV3Score( + GSSV3::flow::update_sr_v3_score( gateway_scoring_type.clone(), txn_detail.clone(), txn_card_info.clone(), @@ -582,15 +783,96 @@ pub async fn updateGatewayScore( gateway_reference_id.clone(), ) .await; - mb_gateway_scoring_data - } else { - None - }; + + if let Some(gateway_scoring_data) = mb_gateway_scoring_data { + let gateway_name = txn_detail.gateway.clone().unwrap_or_default(); + if !gateway_name.is_empty() { + let merchant_id = MID::merchant_id_to_text(txn_detail.clone().merchantId); + let pmt_str = txn_card_info.paymentMethodType.to_string(); + let pm_str = m_source_object.clone().unwrap_or_default(); + let sr_routing_dimensions = SrRoutingDimensions { + card_network: txn_card_info + .cardSwitchProvider + .as_ref() + .map(|s| s.peek().to_string()), + card_isin: txn_card_info.card_isin.clone(), + currency: Some(txn_detail.currency.to_string()), + country: txn_detail.country.as_ref().map(|c| c.to_string()), + auth_type: txn_card_info.authType.as_ref().map(|a| a.to_string()), + }; + let merchant_sr_v3_input_config = + findByNameFromRedis(SrV3InputConfig(merchant_id.clone()).get_key()).await; + let default_sr_v3_input_config = + findByNameFromRedis(SR_V3_DEFAULT_INPUT_CONFIG.get_key()).await; + let bucket_size = GU::get_sr_v3_bucket_size( + merchant_sr_v3_input_config, + &pmt_str, + &pm_str, + &sr_routing_dimensions, + ) + .or_else(|| { + GU::get_sr_v3_bucket_size( + default_sr_v3_input_config, + &pmt_str, + &pm_str, + &sr_routing_dimensions, + ) + }) + .unwrap_or(DC::DEFAULT_SR_V3_BASED_BUCKET_SIZE); + + let gateway_ref_id_map = + HashMap::from([(gateway_name.clone(), gateway_reference_id.clone())]); + let redis_key_map = GU::get_unified_key( + gateway_scoring_data, + None, + T::ScoreKeyType::SrV3Key, + false, + gateway_ref_id_map, + ) + .await; + + if let Some(redis_key) = redis_key_map.get(&gateway_name) { + let score_value = + crate::decider::gatewaydecider::gw_scoring::get_score_from_redis( + bucket_size, + redis_key, + ) + .await; + crate::analytics::record_score_snapshot_event( + Some(merchant_id), + Some(pmt_str), + Some(pm_str), + sr_routing_dimensions.card_network.clone(), + sr_routing_dimensions.card_isin.clone(), + sr_routing_dimensions.currency.clone(), + sr_routing_dimensions.country.clone(), + sr_routing_dimensions.auth_type.clone(), + Some(gateway_name), + Some(score_value), + None, + None, + None, + Some(bucket_size as i64), + "update_gateway_score", + serde_json::to_string(&serde_json::json!({ + "routing_approach": format!("{:?}", routing_approach), + "gateway_scoring_type": format!("{:?}", gateway_scoring_type), + "message": "Gateway score snapshot derived from sr_v3 redis score", + })) + .ok(), + Some(txn_detail.txnUuid.clone()), + None, + Some("score_updated".to_string()), + ); + } + } + } + } if should_update_gateway_score && is_update_within_window { - let mer_acc_p_id: ETM::id::MerchantPId = mer_acc.id.clone(); + let mer_acc_p_id: ETM::id::MerchantPId = mer_acc.id; let m_pf_mc_config = MerchantConfig::getMerchantConfigEntityLevelLookupConfig().await; - let mb_gateway_scoring_data = match redis_gateway_score_data { + let mb_gateway_scoring_data = match mb_gateway_scoring_data { None => { let app_state = get_tenant_app_state().await; let redis_data: Option = app_state @@ -600,9 +882,9 @@ pub async fn updateGatewayScore( .ok(); redis_data } - Some(_) => redis_gateway_score_data, + Some(_) => mb_gateway_scoring_data, }; - logger::info!(tag = "GatewayScoringData", "{:?}", mb_gateway_scoring_data); + logger::debug!(tag = "GatewayScoringData", "{:?}", mb_gateway_scoring_data); match mb_gateway_scoring_data { None => { logger::error!( @@ -612,7 +894,7 @@ pub async fn updateGatewayScore( ); } Some(gateway_scoring_data) => { - logger::info!( + logger::debug!( action = "Downtime-EmailNotification", tag = "Downtime-EmailNotification", "Proceed to updateKeyScoreForKeysFromConsumer" @@ -620,14 +902,14 @@ pub async fn updateGatewayScore( let key_array = GEF::getAllUnifiedKeys( txn_detail.clone(), txn_card_info.clone(), - mer_acc_p_id.clone(), + mer_acc_p_id, m_pf_mc_config.clone(), mer_acc.clone(), gateway_scoring_data.clone(), gateway_reference_id.clone(), ) .await; - logger::info!( + logger::debug!( action = "Downtime-EmailNotification", tag = "Downtime-EmailNotification", "{:?}", @@ -638,16 +920,18 @@ pub async fn updateGatewayScore( txn_detail.clone(), txn_card_info.clone(), gateway_scoring_type.clone(), + gateway_scoring_data.clone(), mer_acc_p_id, mer_acc.clone(), key, + redis_compression_config.clone(), )); } } } - Fbu::logGatewayScoreType( + Fbu::log_gateway_score_type( gateway_scoring_type, - RF::ELIMINATION_FLOW, + RF::EliminationFlow, txn_detail.clone(), ); } else { @@ -678,18 +962,20 @@ pub async fn updateGatewayScore( } // Original Haskell function: getRoutingApproach -pub fn getRoutingApproach(txnDetail: TxnDetail) -> Option { - let internalMeta: Option = getValueFromMetaData(&txnDetail); - match internalMeta { +pub fn get_routing_approach(txn_detail: TxnDetail) -> Option { + let internal_meta: Option = get_value_from_meta_data(&txn_detail); + match internal_meta { Some(meta) => Some(meta.internal_tracking_info.routing_approach), None => None, } } // Original Haskell function: getValueFromMetaData -pub fn getValueFromMetaData(txn_detail: &TxnDetail) -> Option { +pub fn get_value_from_meta_data( + txn_detail: &TxnDetail, +) -> Option { let metadata = txn_detail.internalMetadata.clone()?; - serde_json::from_str(&metadata).ok() + serde_json::from_str(metadata.peek()).ok() } // Original Haskell function: isRoutingApproachInSRV2 @@ -701,7 +987,7 @@ pub fn isRoutingApproachInSRV2(maybe_text: Option) -> bool { } // Original Haskell function: isRoutingApproachInSRV3 -pub fn isRoutingApproachInSRV3(maybe_text: Option) -> bool { +pub fn is_routing_approach_in_srv3(maybe_text: Option) -> bool { match maybe_text { Some(text) => text.contains("V3"), None => false, @@ -709,7 +995,7 @@ pub fn isRoutingApproachInSRV3(maybe_text: Option) -> bool { } // Original Haskell function: isRoutingApproachInExplore -pub fn isRoutingApproachInExplore(maybe_text: Option) -> bool { +pub fn is_routing_approach_in_explore(maybe_text: Option) -> bool { match maybe_text { Some(text) => text.contains("HEDGING"), None => false, @@ -717,14 +1003,16 @@ pub fn isRoutingApproachInExplore(maybe_text: Option) -> bool { } // Original Haskell function: isUpdateWithinLatencyWindow -pub async fn isUpdateWithinLatencyWindow( +pub async fn is_update_within_latency_window( txn_detail: TxnDetail, txn_card_info: TxnCardInfo, gateway_scoring_type: GatewayScoringType, mer_acc: MerchantAccount, + txn_latency: Option, + m_metric_entry: Option, ) -> bool { match gateway_scoring_type { - GatewayScoringType::PENALISE => true, + GatewayScoringType::Penalise => true, _ => { let exempt_for_mandate_txn = checkExemptIfMandateTxn(&txn_detail, &txn_card_info).await; if exempt_for_mandate_txn @@ -737,45 +1025,174 @@ pub async fn isUpdateWithinLatencyWindow( } else { // let m_auto_refund_conflict_threshold_in_mins: Option = None; // Placeholder for actual implementation let gw_latency_check_threshold = - findByNameFromRedis(C::GATEWAY_SCORE_LATENCY_CHECK_IN_MINS.get_key()) + findByNameFromRedis(C::GatewayScoreLatencyCheckInMins.get_key()) .await .unwrap_or(C::defaultGatewayScoreLatencyCheckInMins()); + let gw_wise_latency_threshold = get_gateway_wise_latency( + &default_gw_latency_check_in_mins(), + &txn_card_info.paymentMethodType.to_string(), + &GU::get_payment_method( + txn_card_info.paymentMethodType.to_string(), + txn_card_info.paymentMethod.clone(), + txn_detail.sourceObject.clone().unwrap_or_default(), + ), + &txn_detail.gateway.clone().unwrap_or_default(), // Convert Option to String + ); + logger::info!( + action = "gw_wise_latency_threshold", + tag = "gw_wise_latency_threshold", + "gw_wise_latency_threshold: {}", + gw_wise_latency_threshold + ); + + // check if the transaction latency calculated by orchestration is within the configured threshold + let is_gw_latency_within_threshold = isGwLatencyWithinConfiguredThreshold( + txn_latency.and_then(|m| m.gateway_latency), + GatewaySuccessRateBasedRoutingInput::from_str( + &mer_acc.gatewaySuccessRateBasedDeciderInput, + ) + .ok() + .and_then(|m| m.txnLatency.and_then(|l| l.gatewayLatency)), + ); // Cutover::findByNameFromRedis(C.gatewayScoreLatencyCheckInMins) // .await // .unwrap_or(C.defaultGatewayScoreLatencyCheckInMins); - let merchant_id = MID::merchant_id_to_text(txn_detail.merchantId.clone()); + let _merchant_id = MID::merchant_id_to_text(txn_detail.merchantId.clone()); let pmt = txn_card_info.paymentMethodType; - let pm = GU::get_payment_method( + let _pm = GU::get_payment_method( pmt, txn_card_info.paymentMethod, txn_detail.sourceObject.clone().unwrap_or_default(), ); - let gw_score_update_latency = Fbu::getTimeFromTxnCreatedInMills(txn_detail.clone()); - logger::info!( + let gw_score_update_latency = + Fbu::get_time_from_txn_created_in_mills(txn_detail.clone()); + let gw_latency_check_threshold_ = + gw_wise_latency_threshold.min(gw_latency_check_threshold as f64); + let gw_latency_check_threshold = match m_metric_entry { + Some(metric_entry) => { + gw_latency_check_threshold_.min(metric_entry.tp99_latency.into()) + } + None => gw_latency_check_threshold_, + }; + logger::debug!( action = "gwLatencyCheckThreshold", tag = "gwLatencyCheckThreshold", "gwLatencyCheckThreshold: {}", gw_latency_check_threshold ); - if gw_score_update_latency < gw_latency_check_threshold * 60000u128 { - true - } else { - false - } + (gw_score_update_latency < (gw_latency_check_threshold * 60000.0) as u128) + && is_gw_latency_within_threshold } } } } async fn checkExemptIfMandateTxn(txn_detail: &TxnDetail, txn_card_info: &TxnCardInfo) -> bool { - let is_recurring = isRecurringTxn(Some(txn_detail.txnObjectType.clone())); + let is_recurring = isRecurringTxn(txn_detail.txnObjectType.clone()); let is_nb_pmt = txn_card_info.paymentMethodType == (NB); let is_penny_reg_txn = isPennyMandateRegTxn(txn_detail.clone()); is_recurring || (is_nb_pmt && is_penny_reg_txn) } // Original Haskell function: isTransactionPending -pub fn isTransactionPending(txnStatus: TxnStatus) -> bool { - txnStatus == TS::PendingVBV || txnStatus == TS::Started +pub fn is_transaction_pending(txn_status: TxnStatus) -> bool { + txn_status == TS::PendingVBV || txn_status == TS::Started +} + +// Helper function to filter by gateway only +fn filter_upto_gw<'a>( + latency_input: &'a [GatewayWiseLatencyInput], + gw: &'a str, +) -> Option<&'a GatewayWiseLatencyInput> { + latency_input + .iter() + .find(|x| x.gateway == gw && x.paymentMethodType.is_none() && x.paymentMethod.is_none()) +} + +// Helper function to filter by gateway and payment method type +fn filter_upto_pmt<'a>( + latency_input: &'a [GatewayWiseLatencyInput], + gw: &'a str, + pmt: &'a str, +) -> Option<&'a GatewayWiseLatencyInput> { + latency_input.iter().find(|x| { + x.gateway == gw + && x.paymentMethodType + .as_ref() + .map_or("".to_string(), |s| s.clone()) + == pmt + && x.paymentMethod.is_none() + }) +} + +// Helper function to filter by gateway, payment method type, and payment method +fn filter_upto_pm<'a>( + latency_input: &'a [GatewayWiseLatencyInput], + gw: &'a str, + pmt: &'a str, + pm: &'a str, +) -> Option<&'a GatewayWiseLatencyInput> { + latency_input.iter().find(|x| { + x.gateway == gw + && x.paymentMethodType + .as_ref() + .map_or("".to_string(), |s| s.clone()) + == pmt + && x.paymentMethod + .as_ref() + .map_or("".to_string(), |s| s.clone()) + == pm + }) +} + +// Helper function to get gateway latency threshold +fn get_gw_latency_threshold( + merchant_latency_gateway_wise_input: &Option>, +) -> Option<&GatewayWiseLatencyInput> { + match merchant_latency_gateway_wise_input { + None => None, + Some(_latency_input) => { + // This will be called with specific parameters in the main function + // For now, return None as the actual filtering happens in the main function + None + } + } } + +// Main function to get gateway-wise latency +pub fn get_gateway_wise_latency( + gateway_latency_threshold: &GatewayLatencyForScoring, + pmt: &str, + pm: &str, + gw: &str, +) -> f64 { + let _m_gateway_wise_input = + get_gw_latency_threshold(&gateway_latency_threshold.merchant_latency_gateway_wise_input); + + // Log the input (similar to EL.logDebugV in Haskell) + logger::debug!( + action = "get_gateway_wise_latency", + tag = "get_gateway_wise_latency", + "mGatewayWiseInput: {:?}", + gateway_latency_threshold.merchant_latency_gateway_wise_input + ); + + match &gateway_latency_threshold.merchant_latency_gateway_wise_input { + Some(gw_wise_input) => { + // Try to find the most specific match first, then fall back to less specific + if let Some(result) = filter_upto_pm(gw_wise_input, gw, pmt, pm) { + result.latencyThreshold + } else if let Some(result) = filter_upto_pmt(gw_wise_input, gw, pmt) { + result.latencyThreshold + } else if let Some(result) = filter_upto_gw(gw_wise_input, gw) { + result.latencyThreshold + } else { + gateway_latency_threshold.default_latency_threshold + } + } + None => gateway_latency_threshold.default_latency_threshold, + } +} + +// Helper function to filter by gateway only diff --git a/src/feedback/gateway_selection_scoring_v3/flow.rs b/src/feedback/gateway_selection_scoring_v3/flow.rs index 7da495d8..7b907fa3 100644 --- a/src/feedback/gateway_selection_scoring_v3/flow.rs +++ b/src/feedback/gateway_selection_scoring_v3/flow.rs @@ -31,12 +31,10 @@ // use feedback::types::{TxnCardInfo, PaymentMethodType, MerchantGatewayAccount}; use crate::decider::gatewaydecider::utils as GU; use crate::logger; -use crate::merchant_config_util as MC; use crate::redis::cache::findByNameFromRedis; use crate::types::payment::payment_method_type_const::*; use crate::{ - app, - decider::gatewaydecider::types::GatewayScoringData, + decider::gatewaydecider::types::{GatewayScoringData, SrRoutingDimensions}, decider::{ gatewaydecider::constants as DC, gatewaydecider::types::RoutingFlowType as RF, gatewaydecider::types::ScoreKeyType as SK, gatewaydecider::types::SrV3InputConfig, @@ -45,48 +43,43 @@ use crate::{ constants as C, types::SrV3DebugBlock, utils::{ - dateInIST, getCurrentIstDateWithFormat, getProducerKey, getTrueString, - isKeyExistsRedis, logGatewayScoreType, updateMovingWindow, updateScore, - GatewayScoringType, + dateInIST, getCurrentIstDateWithFormat, getProducerKey, isKeyExistsRedis, + log_gateway_score_type, updateMovingWindow, updateScore, GatewayScoringType, }, }, - redis::{feature::isFeatureEnabled, types::ServiceConfigKey}, + redis::types::ServiceConfigKey, types::{ - card::txn_card_info::TxnCardInfo, - merchant::id as MID, - merchant::{ - merchant_account::MerchantAccount, merchant_gateway_account::MerchantGatewayAccount, - }, - payment_flow::PaymentFlow as PF, - txn_details::types::TxnDetail, + card::txn_card_info::TxnCardInfo, merchant::id as MID, + merchant::merchant_account::MerchantAccount, txn_details::types::TxnDetail, }, - utils as U, }; +use masking::PeekInterface; +use serde_json; // Converted functions // Original Haskell function: updateSrV3Score -pub async fn updateSrV3Score( +pub async fn update_sr_v3_score( gateway_scoring_type: GatewayScoringType, txn_detail: TxnDetail, txn_card_info: TxnCardInfo, - merchant_acc: MerchantAccount, + _merchant_acc: MerchantAccount, mb_gateway_scoring_data: Option, gateway_reference_id: Option, ) { - // let is_merchant_enabled_globally = MC::isMerchantEnabledForPaymentFlows(merchant_acc.id, [PF::SR_BASED_ROUTING].to_vec()).await; - match (txn_detail.gateway.clone()) { - (None) => { - logger::info!( + // let is_merchant_enabled_globally = MC::isMerchantEnabledForPaymentFlows(merchant_acc.id, [PF::SrBasedRouting].to_vec()).await; + match txn_detail.gateway.clone() { + None => { + logger::debug!( action = "gateway not found", tag = "gateway not found", "gateway not found for this transaction having id" ); } - (Some(gateway)) => { + Some(_gateway) => { let unified_sr_v3_key = getProducerKey( txn_detail.clone(), mb_gateway_scoring_data, - SK::SR_V3_KEY, + SK::SrV3Key, false, gateway_reference_id.clone(), ) @@ -110,7 +103,7 @@ pub async fn updateSrV3Score( let key3d_for_gateway_selection = unified_sr_v3_key.clone().unwrap_or_else(|| "".to_string()); if key3d_for_gateway_selection != key_for_gateway_selection { - logger::info!( + logger::debug!( tag = "SR V3 Based threeD Producer Key", action = "SR V3 Based threeD Producer Key", "{:?}", @@ -130,7 +123,7 @@ pub async fn updateSrV3Score( .await; } } - logGatewayScoreType(gateway_scoring_type, RF::SRV3_FLOW, txn_detail); + log_gateway_score_type(gateway_scoring_type, RF::Srv3Flow, txn_detail); } } } @@ -144,7 +137,7 @@ pub async fn createKeysIfNotExist( ) { let is_queue_key_exists = isKeyExistsRedis(key_for_gateway_selection_queue.clone()).await; let is_score_key_exists = isKeyExistsRedis(key_for_gateway_selection_score.clone()).await; - logger::info!( + logger::debug!( tag = "createKeysIfNotExist", action = "createKeysIfNotExist", "Value for isQueueKeyExists is {} and isScoreKeyExists is {}", @@ -152,16 +145,15 @@ pub async fn createKeysIfNotExist( is_score_key_exists ); if is_queue_key_exists && is_score_key_exists { - return; } else { let merchant_bucket_size = getSrV3MerchantBucketSize(txn_detail, txn_card_info).await; - logger::info!( + logger::debug!( tag = "createKeysIfNotExist", action = "createKeysIfNotExist", "Creating keys with bucket size as {}", merchant_bucket_size ); - let score_list = vec!["1".to_string(); merchant_bucket_size.clone().try_into().unwrap()]; + let score_list = vec!["1".to_string(); merchant_bucket_size.try_into().unwrap()]; let redis = C::kvRedis(); GU::create_moving_window_and_score( redis, @@ -182,7 +174,7 @@ pub async fn updateScoreAndQueue( txn_detail: TxnDetail, txn_card_info: TxnCardInfo, ) { - logger::info!( + logger::debug!( action = "updateScoreAndQueue", tag = "updateScoreAndQueue", "Updating sr v3 score and queue" @@ -195,8 +187,8 @@ pub async fn updateScoreAndQueue( ) .await; let (value, should_score_increase): (String, bool) = match gateway_scoring_type { - GatewayScoringType::PENALISE_SRV3 => ("0".into(), false), - GatewayScoringType::REWARD => ("1".into(), true), + GatewayScoringType::PenaliseSrv3 => ("0".into(), false), + GatewayScoringType::Reward => ("1".into(), true), _ => ("0".into(), false), }; // let is_debug_mode_enabled = isFeatureEnabled( @@ -238,8 +230,8 @@ pub async fn updateScoreAndQueue( // } // } // } - let current_ist_time = getCurrentIstDateWithFormat("YYYY-MM-DD HH:mm:SS.sss".to_string()); - let date_created = dateInIST( + let _current_ist_time = getCurrentIstDateWithFormat("YYYY-MM-DD HH:mm:SS.sss".to_string()); + let _date_created = dateInIST( txn_detail.clone().dateCreated.to_string(), "YYYY-MM-DD HH:mm:SS.sss".to_string(), ) @@ -262,7 +254,7 @@ pub async fn updateScoreAndQueue( value.clone(), ) .await; - logger::info!( + logger::debug!( action = "updateScoreAndQueue", tag = "updateScoreAndQueue", "Popped Redis Value {}", @@ -273,14 +265,13 @@ pub async fn updateScoreAndQueue( Ok(maybe_popped_status_block) => get_status(maybe_popped_status_block, popped_status), Err(_) => popped_status, }; - logger::info!( + logger::debug!( action = "updateScoreAndQueue", tag = "updateScoreAndQueue", "Popped Returned Value {}", returned_value ); if returned_value == value { - return; } else { updateScore( C::kvRedis(), @@ -291,51 +282,52 @@ pub async fn updateScoreAndQueue( } } -fn debugBlock( - txn_detail: TxnDetail, - current_time: String, - date_created: String, - value: String, -) -> SrV3DebugBlock { - SrV3DebugBlock { - txn_uuid: txn_detail.txnUuid, - order_id: txn_detail.orderId.0, - date_created, - current_time, - txn_status: value, - } -} - -fn getStatus(maybe_popped_status_block: Option, popped_status: String) -> String { - match maybe_popped_status_block { - Some(popped_status_block) => popped_status_block.txn_status.clone(), - None => popped_status, - } -} - //Original Haskell function: getSrV3MerchantBucketSize pub async fn getSrV3MerchantBucketSize(txn_detail: TxnDetail, txn_card_info: TxnCardInfo) -> i32 { let merchant_sr_v3_input_config: Option = findByNameFromRedis( - C::SR_V3_INPUT_CONFIG(MID::merchant_id_to_text(txn_detail.merchantId)).get_key(), + C::SrV3InputConfig(MID::merchant_id_to_text(txn_detail.merchantId)).get_key(), ) .await; let pmt = txn_card_info.paymentMethodType; let pm = GU::get_payment_method( - (&pmt).to_string(), + pmt.to_string(), txn_card_info.paymentMethod, txn_detail.sourceObject.unwrap_or_default(), ); - let maybe_bucket_size = GU::get_sr_v3_bucket_size(merchant_sr_v3_input_config, &pmt, &pm); + // Extract the new parameters from txn_card_info + + let sr_routing_dimensions = SrRoutingDimensions { + card_network: txn_card_info + .cardSwitchProvider + .as_ref() + .map(|s| s.peek().to_string()), + card_isin: txn_card_info.card_isin, + currency: Some(txn_detail.currency.to_string()), + country: txn_detail.country.as_ref().map(|c| c.to_string()), + auth_type: txn_card_info.authType.as_ref().map(|a| a.to_string()), + }; + + let maybe_bucket_size = GU::get_sr_v3_bucket_size( + merchant_sr_v3_input_config, + &pmt, + &pm, + &sr_routing_dimensions, + ); let merchant_bucket_size = match maybe_bucket_size { None => { let default_sr_v3_input_config: Option = - findByNameFromRedis(DC::srV3DefaultInputConfig.get_key()).await; - GU::get_sr_v3_bucket_size(default_sr_v3_input_config, &pmt, &pm) - .unwrap_or(C::defaultSrV3BasedBucketSize) + findByNameFromRedis(DC::SR_V3_DEFAULT_INPUT_CONFIG.get_key()).await; + GU::get_sr_v3_bucket_size( + default_sr_v3_input_config, + &pmt, + &pm, + &sr_routing_dimensions, + ) + .unwrap_or(C::DEFAULT_SR_V3_BASED_BUCKET_SIZE) } Some(bucket_size) => bucket_size, }; - logger::info!( + logger::debug!( action = "sr_v3_bucket_size", tag = "sr_v3_bucket_size", "Bucket Size: {}", diff --git a/src/feedback/types.rs b/src/feedback/types.rs index 905eeb5d..a6bf0582 100644 --- a/src/feedback/types.rs +++ b/src/feedback/types.rs @@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; // use database::beam as B; // use chrono::{Local, Utc}; -use crate::types::gateway as ETG; -use crate::types::txn_details::types::{Offset, TxnStatus}; +use crate::decider::gatewaydecider::utils::mask_secret_option; +use crate::types::txn_details::types::{TransactionLatency, TxnStatus}; +use masking::Secret; use std::string::String; -use time::OffsetDateTime; // use eulerhs::types::MeshError; // // Converted type synonyms @@ -42,10 +42,11 @@ pub struct TxnInfo { // Original Haskell data type: MandateTxnType #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum MandateTxnType { - REGISTER, - REGISTER_AND_DEBIT, - DEFAULT, + Register, + RegisterAndDebit, + Default, } // Original Haskell data type: MerchantScoringDetails @@ -107,7 +108,8 @@ pub struct GatewayScoringKeyType { pub sourceObject: Option, #[serde(rename = "paymentSource")] - pub paymentSource: Option, + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, #[serde(rename = "cardType")] pub cardType: Option, @@ -120,6 +122,9 @@ pub struct GatewayScoringKeyType { #[serde(rename = "softTTL")] pub softTTL: Option, + + #[serde(rename = "gatewayReferenceId")] + pub gatewayReferenceId: Option, } // Original Haskell data type: TxnDetailT @@ -217,35 +222,40 @@ pub struct GatewayScoringKeyType { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TxnObjectType { - MANDATE_REGISTER, - MANDATE_PAYMENT, - ORDER_PAYMENT, - EMANDATE_REGISTER, - EMANDATE_PAYMENT, - TPV_PAYMENT, - PARTIAL_CAPTURE, - PARTIAL_VOID, - TPV_EMANDATE_REGISTER, - TPV_MANDATE_REGISTER, - TPV_EMANDATE_PAYMENT, - TPV_MANDATE_PAYMENT, - VAN_PAYMENT, - MOTO_PAYMENT, + MandateRegister, + MandatePayment, + OrderPayment, + EmandateRegister, + EmandatePayment, + TpvPayment, + PartialCapture, + PartialVoid, + TpvEmandateRegister, + TpvMandateRegister, + TpvEmandatePayment, + TpvMandatePayment, + VanPayment, + MotoPayment, } #[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] pub struct UpdateScorePayload { - pub merchantId: String, + pub merchant_id: String, pub gateway: String, - pub gatewayReferenceId: Option, + pub gateway_reference_id: Option, pub status: TxnStatus, - pub paymentId: String, - pub enforceDynamicRoutingFailure: Option, + pub payment_id: String, + pub enforce_dynamic_routing_failure: Option, + pub txn_latency: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateScoreResponse { pub message: String, + pub merchant_id: String, + pub gateway: String, + pub payment_id: String, } #[derive(Debug, Serialize, Deserialize)] @@ -260,6 +270,12 @@ pub struct InternalTrackingInfo { pub routing_approach: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TransactionLatencyThreshold { + #[serde(rename = "gatewayLatency")] + pub gatewayLatency: Option, +} + // Original Haskell data type: Error // #[derive(Debug, Serialize, Deserialize, PartialEq)] // #[serde(tag = "type", content = "value")] @@ -433,7 +449,7 @@ pub struct SrV3DebugBlock { // AADHAAR, // PAPERNACH, // PAN, -// MERCHANT_CONTAINER, +// MerchantContainer, // Virtual_Account, // OTC, // RTP, @@ -509,7 +525,7 @@ pub struct SrV3DebugBlock { // Converted newtypes // Original Haskell newtype: Milliseconds -#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] pub struct Milliseconds { #[serde(rename = "milliseconds")] pub milliseconds: f64, @@ -537,7 +553,7 @@ pub struct Date { // PaymentMethodType::AADHAAR => "AADHAAR".into(), // PaymentMethodType::PAPERNACH => "PAPERNACH".into(), // PaymentMethodType::PAN => "PAN".into(), -// PaymentMethodType::MERCHANT_CONTAINER => "MERCHANT_CONTAINER".into(), +// PaymentMethodType::MerchantContainer => "MERCHANT_CONTAINER".into(), // PaymentMethodType::Virtual_Account => "VIRTUAL_ACCOUNT".into(), // PaymentMethodType::OTC => "OTC".into(), // PaymentMethodType::RTP => "RTP".into(), @@ -561,7 +577,7 @@ pub struct Date { // "AADHAAR" => PaymentMethodType::AADHAAR, // "PAPERNACH" => PaymentMethodType::PAPERNACH, // "PAN" => PaymentMethodType::PAN, -// "MERCHANT_CONTAINER" => PaymentMethodType::MERCHANT_CONTAINER, +// "MERCHANT_CONTAINER" => PaymentMethodType::MerchantContainer, // "VIRTUAL_ACCOUNT" => PaymentMethodType::Virtual_Account, // "OTC" => PaymentMethodType::OTC, // "RTP" => PaymentMethodType::RTP, diff --git a/src/feedback/utils.rs b/src/feedback/utils.rs index 08f2afa8..3a044ff4 100644 --- a/src/feedback/utils.rs +++ b/src/feedback/utils.rs @@ -3,6 +3,7 @@ use crate::app::get_tenant_app_state; use crate::error::StorageError; +use masking::{PeekInterface, Secret}; // Converted imports // use eulerhs::prelude::*; // use eulerhs::language as L; @@ -17,8 +18,6 @@ use crate::feedback::types::{ CachedGatewayScore, InternalMetadata, InternalTrackingInfo, MandateTxnInfo, MandateTxnType, UpdateScorePayload, }; -use crate::storage::types::TxnCardInfo; -use crate::types::currency::Currency; use crate::types::money::internal::Money; use crate::types::order as ETO; use crate::types::transaction::id as ETId; @@ -39,7 +38,7 @@ use time::PrimitiveDateTime; // use data::list as DL; // use ghc::records::extra::HasField; use crate::decider::gatewaydecider::types::{ - DetailedGatewayScoringType, GatewayReferenceIdMap, GatewayScoringData, GatewayScoringTypeLog, + DetailedGatewayScoringType, GatewayReferenceIdMap, GatewayScoringData, GatewayScoringTypeLogData, RoutingFlowType, ScoreKeyType, }; use crate::types::money::internal as ETMo; @@ -48,7 +47,7 @@ use crate::types::money::internal as ETMo; // use data::time::local_time as DTL; // use data::time::format as DTF; // use juspay::extra::json::decode_json; -use crate::decider::gatewaydecider::utils::{get_unified_key, get_value}; +use crate::decider::gatewaydecider::utils::get_unified_key; // use control::monad::except::{run_except, ExceptT}; // use data::byte_string::lazy as BSL; // use ghc::generics::Generic; @@ -65,22 +64,22 @@ use crate::types::txn_details::types::{self as ETTD, TxnDetail, TxnObjectType}; // use crate::control::exception as CE; // use juspay::extra::non_empty_text as NE; use crate::types::merchant as ETM; -use crate::types::payment::payment_method as ETP; // use types::money::{from_double, Money}; // use optics::core::{preview, review}; // use control::category::<<<; -use super::constants::gatewayScoringData; -use crate::types::gateway::{gateway_any_to_text, GatewayAny}; // use prelude::real_to_frac; // use data::time::clock::posix as DTP; use crate::logger; +use crate::redis::feature::{RedisCompressionConfigCombined, RedisDataStruct}; +use time::format_description::well_known::Iso8601; // Converted data types // Original Haskell data type: GatewayScoringType #[derive(Debug, Serialize, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum GatewayScoringType { - PENALISE, - PENALISE_SRV3, - REWARD, + Penalise, + PenaliseSrv3, + Reward, } // Original Haskell data type: JuspayBankCode @@ -160,57 +159,63 @@ pub fn convertSuccessResponseIdFlip(x: i32) -> ETTD::SuccessResponseId { ETTD::SuccessResponseId(x as i64) } -pub fn getTxnDetailFromApiPayload( - apiPayload: UpdateScorePayload, +pub fn get_txn_detail_from_api_payload( + api_payload: UpdateScorePayload, gateway_scoring_data: GatewayScoringData, -) -> ETTD::TxnDetail { +) -> Result { let txn_detail = ETTD::TxnDetail { id: ETTD::to_txn_detail_id(1), dateCreated: gateway_scoring_data.dateCreated, - orderId: ETO::id::to_order_id(apiPayload.paymentId.clone()), - status: apiPayload.status.clone(), - txnId: ETId::to_transaction_id(apiPayload.paymentId.clone()), - txnType: "NOT_DEFINED".to_string(), - addToLocker: false, - merchantId: ETM::id::to_merchant_id(apiPayload.merchantId.clone()), - gateway: Some(apiPayload.gateway), - expressCheckout: false, - isEmi: false, + orderId: ETO::id::to_order_id(api_payload.payment_id.clone()), + status: api_payload.status.clone(), + txnId: ETId::to_transaction_id(api_payload.payment_id.clone()), + txnType: Some("NOT_DEFINED".to_string()), + addToLocker: Some(false), + merchantId: ETM::id::to_merchant_id(api_payload.merchant_id.clone()), + gateway: Some(api_payload.gateway), + expressCheckout: Some(false), + isEmi: Some(false), emiBank: None, emiTenure: None, - txnUuid: apiPayload.paymentId.clone(), + txnUuid: api_payload.payment_id.clone(), merchantGatewayAccountId: None, - txnAmount: ETMo::Money::from_double(0.0), - txnObjectType: ETTD::TxnObjectType::from_text(gateway_scoring_data.orderType.clone()) - .unwrap_or_else(|| ETTD::TxnObjectType::OrderPayment), + txnAmount: Some(ETMo::Money::from_double(0.0)), + txnObjectType: Some( + ETTD::TxnObjectType::from_text(gateway_scoring_data.orderType.clone()) + .unwrap_or(ETTD::TxnObjectType::OrderPayment), + ), sourceObject: Some(gateway_scoring_data.paymentMethod.clone()), sourceObjectId: None, - currency: Currency::INR, + currency: gateway_scoring_data + .currency + .clone() + .ok_or(crate::error::ApiError::MissingRequiredField("currency"))?, + country: gateway_scoring_data.country, surchargeAmount: None, taxAmount: None, - internalMetadata: Some( + internalMetadata: Some(Secret::new( serde_json::to_string(&InternalMetadata { internal_tracking_info: InternalTrackingInfo { routing_approach: gateway_scoring_data.routingApproach.unwrap_or_default(), }, }) .unwrap(), - ), - netAmount: ETMo::Money::from_double(0.0), + )), + netAmount: Some(ETMo::Money::from_double(0.0)), metadata: None, offerDeductionAmount: None, internalTrackingInfo: None, partitionKey: None, txnAmountBreakup: None, }; - txn_detail + Ok(txn_detail) } -pub fn getTxnCardInfoFromApiPayload( - apiPayload: UpdateScorePayload, +pub fn get_txn_card_info_from_api_payload( + _api_payload: UpdateScorePayload, gateway_scoring_data: GatewayScoringData, ) -> ETCa::txn_card_info::TxnCardInfo { - let txnCardInfo = ETCa::txn_card_info::TxnCardInfo { + ETCa::txn_card_info::TxnCardInfo { id: ETCa::txn_card_info::to_txn_card_info_pid(1), card_isin: None, cardIssuerBankName: None, @@ -228,8 +233,7 @@ pub fn getTxnCardInfoFromApiPayload( ETCa::txn_card_info::text_to_auth_type(&auth_type_text).ok() }), partitionKey: None, - }; - txnCardInfo + } } // Original Haskell function: convertMerchantIdFlip // pub fn convertMerchantIdFlip(s: &str) -> Option { @@ -249,17 +253,17 @@ pub fn getTxnCardInfoFromApiPayload( // Original Haskell function: convertTxnObjectTypeFli:: // pub fn convertTxnObjectTypeFlip(txn_object_type: Option) -> ETTD::TxnObjectType { // match txn_object_type { -// Some(TxnObjectType::ORDER_PAYMENT) => ETTD::TxnObjectType::OrderPayment, -// Some(TxnObjectType::MANDATE_REGISTER) => ETTD::TxnObjectType::MandateRegister, -// Some(TxnObjectType::EMANDATE_REGISTER) => ETTD::TxnObjectType::EmandateRegister, -// Some(TxnObjectType::EMANDATE_PAYMENT) => ETTD::TxnObjectType::EmandatePayment, -// Some(TxnObjectType::MANDATE_PAYMENT) => ETTD::TxnObjectType::MandatePayment, -// Some(TxnObjectType::TPV_PAYMENT) => ETTD::TxnObjectType::TpvPayment, -// Some(TxnObjectType::PARTIAL_CAPTURE) => ETTD::TxnObjectType::PartialCapture, -// Some(TxnObjectType::TPV_EMANDATE_REGISTER) => ETTD::TxnObjectType::TpvEmandateRegister, -// Some(TxnObjectType::TPV_MANDATE_REGISTER) => ETTD::TxnObjectType::TpvMandateRegister, -// Some(TxnObjectType::TPV_EMANDATE_PAYMENT) => ETTD::TxnObjectType::TpvEmandatePayment, -// Some(TxnObjectType::TPV_MANDATE_PAYMENT) => ETTD::TxnObjectType::TpvMandatePayment, +// Some(TxnObjectType::OrderPayment) => ETTD::TxnObjectType::OrderPayment, +// Some(TxnObjectType::MandateRegister) => ETTD::TxnObjectType::MandateRegister, +// Some(TxnObjectType::EmandateRegister) => ETTD::TxnObjectType::EmandateRegister, +// Some(TxnObjectType::EmandatePayment) => ETTD::TxnObjectType::EmandatePayment, +// Some(TxnObjectType::MandatePayment) => ETTD::TxnObjectType::MandatePayment, +// Some(TxnObjectType::TpvPayment) => ETTD::TxnObjectType::TpvPayment, +// Some(TxnObjectType::PartialCapture) => ETTD::TxnObjectType::PartialCapture, +// Some(TxnObjectType::TpvEmandateRegister) => ETTD::TxnObjectType::TpvEmandateRegister, +// Some(TxnObjectType::TpvMandateRegister) => ETTD::TxnObjectType::TpvMandateRegister, +// Some(TxnObjectType::TpvEmandatePayment) => ETTD::TxnObjectType::TpvEmandatePayment, +// Some(TxnObjectType::TpvMandatePayment) => ETTD::TxnObjectType::TpvMandatePayment, // _ => ETTD::TxnObjectType::OrderPayment, // } // } @@ -328,17 +332,17 @@ pub fn getTxnCardInfoFromApiPayload( // Original Haskell function: textToAuthType // pub fn textToAuthType(auth_type: Option) -> Option { // match auth_type.as_deref() { -// Some("ATMPIN") => Some(ETCa::AuthType::ATMPIN), -// Some("THREE_DS") => Some(ETCa::AuthType::THREE_DS), -// Some("THREE_DS_2") => Some(ETCa::AuthType::THREE_DS_2), -// Some("OTP") => Some(ETCa::AuthType::OTP), -// Some("OBO_OTP") => Some(ETCa::AuthType::OBO_OTP), -// Some("VIES") => Some(ETCa::AuthType::VIES), -// Some("NO_THREE_DS") => Some(ETCa::AuthType::NO_THREE_DS), -// Some("NETWORK_TOKEN") => Some(ETCa::AuthType::NETWORK_TOKEN), -// Some("MOTO") => Some(ETCa::AuthType::MOTO), -// Some("FIDO") => Some(ETCa::AuthType::FIDO), -// Some("CTP") => Some(ETCa::AuthType::CTP), +// Some("ATMPIN") => Some(ETCa::AuthType::Atmpin), +// Some("THREE_DS") => Some(ETCa::AuthType::ThreeDs), +// Some("THREE_DS_2") => Some(ETCa::AuthType::ThreeDs2), +// Some("OTP") => Some(ETCa::AuthType::Otp), +// Some("OBO_OTP") => Some(ETCa::AuthType::OboOtp), +// Some("VIES") => Some(ETCa::AuthType::Vies), +// Some("NO_THREE_DS") => Some(ETCa::AuthType::NoThreeDs), +// Some("NETWORK_TOKEN") => Some(ETCa::AuthType::NetworkToken), +// Some("MOTO") => Some(ETCa::AuthType::Moto), +// Some("FIDO") => Some(ETCa::AuthType::Fido), +// Some("CTP") => Some(ETCa::AuthType::Ctp), // _ => None, // } // } @@ -360,7 +364,7 @@ pub fn getTxnCardInfoFromApiPayload( // Some(FT::PaymentMethodType::PAPERNACH) => ETP::PaymentMethodType::Papernach, // Some(FT::PaymentMethodType::PAN) => ETP::PaymentMethodType::PAN, // Some(FT::PaymentMethodType::UNKNOWN(ref val)) if val == "ATM_CARD" => ETP::PaymentMethodType::AtmCard, -// Some(FT::PaymentMethodType::MERCHANT_CONTAINER) => ETP::PaymentMethodType::MerchantContainer, +// Some(FT::PaymentMethodType::MerchantContainer) => ETP::PaymentMethodType::MerchantContainer, // Some(FT::PaymentMethodType::Virtual_Account) => ETP::PaymentMethodType::VirtualAccount, // Some(FT::PaymentMethodType::OTC) => ETP::PaymentMethodType::Otc, // Some(FT::PaymentMethodType::RTP) => ETP::PaymentMethodType::Rtp, @@ -371,7 +375,7 @@ pub fn getTxnCardInfoFromApiPayload( // } // Original Haskell function: updateScore -pub async fn updateScore(redis: String, key: String, should_score_increase: bool) -> () { +pub async fn updateScore(_redis: String, key: String, should_score_increase: bool) -> () { let app_state = get_tenant_app_state().await; let either_res = if should_score_increase { app_state.redis_conn.increment_key(&key).await @@ -410,16 +414,15 @@ pub async fn isKeyExistsRedis(key: String) -> bool { } } } - // Original Haskell function: updateQueue pub async fn updateQueue( - redis_name: String, + _redis_name: String, queue_key: String, score_key: String, value: String, ) -> Result, error_stack::Report> { let app_state = get_tenant_app_state().await; - let value_clone = value.clone(); + let _value_clone = value.clone(); let r: Result, error_stack::Report> = app_state .redis_conn @@ -447,7 +450,7 @@ pub async fn updateQueue( match r { Ok(result) => { - logger::info!( + logger::debug!( action = "updateQueue", tag = "updateQueue", "Successfully updated queue in Redis: {:?}", @@ -509,7 +512,7 @@ pub fn isTrueString(val: Option) -> bool { // Original Haskell function: dateInIST pub fn dateInIST(db_date: String, format: String) -> Option { - // Parse input date uisng primitivedatetime + // Parse input date using primitivedatetime let format_description = match time::format_description::parse(&format) { Ok(desc) => desc, Err(_) => return None, @@ -615,11 +618,10 @@ pub async fn getProducerKey( ) -> Option { match redis_gateway_score_data { Some(gateway_score_data) => { - let is_gri_enabled = if [ScoreKeyType::ELIMINATION_MERCHANT_KEY] - .contains(&score_key_type) + let is_gri_enabled = if [ScoreKeyType::EliminationMerchantKey].contains(&score_key_type) { gateway_score_data.isGriEnabledForElimination - } else if [ScoreKeyType::SR_V2_KEY, ScoreKeyType::SR_V3_KEY].contains(&score_key_type) { + } else if [ScoreKeyType::SrV2Key, ScoreKeyType::SrV3Key].contains(&score_key_type) { gateway_score_data.isGriEnabledForSrRouting } else { false @@ -642,6 +644,7 @@ pub async fn getProducerKey( let gateway_key = get_unified_key( gateway_score_data, + None, score_key_type, enforce1d, gateway_and_reference_id, @@ -649,7 +652,7 @@ pub async fn getProducerKey( .await; let (_, key) = gateway_key.into_iter().next().unwrap(); - logger::info!(tag = "getProducerKey", "UNIFIED_KEY {}", key); + logger::debug!(tag = "getProducerKey", "UNIFIED_KEY {}", key); Some(key) } None => { @@ -664,48 +667,45 @@ pub async fn getProducerKey( } // Original Haskell function: logGatewayScoreType -pub fn logGatewayScoreType( +pub fn log_gateway_score_type( gateway_score_type: GatewayScoringType, routing_flow_type: RoutingFlowType, - txn_detail: TxnDetail, + _txn_detail: TxnDetail, ) { let detailed_gateway_score_type = match routing_flow_type { - RoutingFlowType::ELIMINATION_FLOW => match gateway_score_type { - GatewayScoringType::REWARD => DetailedGatewayScoringType::ELIMINATION_REWARD, - _ => DetailedGatewayScoringType::ELIMINATION_PENALISE, + RoutingFlowType::EliminationFlow => match gateway_score_type { + GatewayScoringType::Reward => DetailedGatewayScoringType::EliminationReward, + _ => DetailedGatewayScoringType::EliminationPenalise, }, - RoutingFlowType::SRV2_FLOW => match gateway_score_type { - GatewayScoringType::REWARD => DetailedGatewayScoringType::SRV2_REWARD, - _ => DetailedGatewayScoringType::SRV2_PENALISE, + RoutingFlowType::Srv2Flow => match gateway_score_type { + GatewayScoringType::Reward => DetailedGatewayScoringType::Srv2Reward, + _ => DetailedGatewayScoringType::Srv2Penalise, }, _ => match gateway_score_type { - GatewayScoringType::REWARD => DetailedGatewayScoringType::SRV3_REWARD, - _ => DetailedGatewayScoringType::SRV3_PENALISE, + GatewayScoringType::Reward => DetailedGatewayScoringType::Srv3Reward, + _ => DetailedGatewayScoringType::Srv3Penalise, }, }; - let txn_creation_time = txn_detail - .dateCreated - .to_string() - .replace(" ", "T") - .replace(" UTC", "Z"); + let txn_creation_time = match &time::OffsetDateTime::now_utc().format(&Iso8601::DEFAULT) { + Ok(dt) => dt.to_string(), + Err(_) => "Invalid format".to_string(), + }; let log_data = GatewayScoringTypeLogData { dateCreated: txn_creation_time, score_type: detailed_gateway_score_type, }; - let log_entry = GatewayScoringTypeLog { - log_data: serde_json::Value::String( - serde_json::to_string(&log_data).unwrap_or_else(|_| "Serialization error".to_string()), - ), - }; + let log_json = serde_json::json!({ + "data": log_data, + }); logger::info!( action = "GATEWAY_SCORE_UPDATED", tag = "GATEWAY_SCORE_UPDATED", - "Logging gateway score type: {:?}", - log_entry + "{}", + log_json.to_string() ); } @@ -714,13 +714,20 @@ pub async fn writeToCacheWithTTL( key: String, cached_gateway_score: CachedGatewayScore, ttl: i64, + redis_compression_config: Option, ) -> Result { - //from CachedGatewayScore comvert encoded_score to a encoded jasson that can be used as a value for redis sextx + //from CachedGatewayScore convert encoded_score to a encoded json that can be used as a value for redis sextx let encoded_score = serde_json::to_string(&cached_gateway_score).unwrap_or_else(|_| "".to_string()); - let primary_write = - addToCacheWithExpiry("kv_redis".to_string(), key.clone(), encoded_score, ttl).await; + let primary_write = addToCacheWithExpiry( + "kv_redis".to_string(), + key.clone(), + encoded_score, + ttl, + redis_compression_config, + ) + .await; match primary_write { Ok(_) => Ok(0), @@ -730,16 +737,26 @@ pub async fn writeToCacheWithTTL( // Original Haskell function: addToCacheWithExpiry pub async fn addToCacheWithExpiry( - redis_name: String, + _redis_name: String, key: String, value: String, ttl: i64, + redis_compression_config: Option, ) -> Result<(), StorageError> { let app_state = get_tenant_app_state().await; - let cached_resp = app_state.redis_conn.setx(&key, &value, ttl).await; + let cached_resp = app_state + .redis_conn + .setx( + &key, + &value, + ttl, + redis_compression_config, + RedisDataStruct::STRING, + ) + .await; match cached_resp { Ok(_) => Ok(()), - Err(error) => Err(StorageError::InsertError), + Err(_error) => Err(StorageError::InsertError), } } @@ -751,7 +768,7 @@ pub async fn deleteFromCache(redis_name: String, key: String) -> Result Result { +pub async fn delCache(_dbName: String, key: String) -> Result { let app_state = get_tenant_app_state().await; let data = app_state.redis_conn.conn.delete_key(&key).await; // convert data to Result @@ -851,15 +868,19 @@ pub fn mandateRegisterTxnObjectTypes() -> Vec { } pub fn isPennyMandateRegTxn(txn_detail: TxnDetail) -> bool { - if isMandateRegTxn(txn_detail.clone().txnObjectType) { - isPennyTxnType(txn_detail.clone()) + if let Some(txn_object_type) = txn_detail.clone().txnObjectType { + if isMandateRegTxn(txn_object_type) { + isPennyTxnType(txn_detail.clone()) + } else { + false + } } else { false } } // Original Haskell function: getTxnTypeFromInternalMetadata -pub fn getTxnTypeFromInternalMetadata(internal_metadata: Option) -> MandateTxnType { +pub fn getTxnTypeFromInternalMetadata(internal_metadata: Option>) -> MandateTxnType { match internal_metadata { None => { logger::debug!( @@ -867,12 +888,12 @@ pub fn getTxnTypeFromInternalMetadata(internal_metadata: Option) -> Mand tag = "APP_DEBUG", "FETCH_TXN_TYPE_FROM_IM_FLOW" ); - (MandateTxnType::DEFAULT) + MandateTxnType::Default } Some(internal_metadata) => { - match serde_json::from_str::(&internal_metadata) { + match serde_json::from_str::(internal_metadata.peek()) { Ok(txn_info) => txn_info.mandateTxnInfo.txnType, - Err(_) => MandateTxnType::DEFAULT, + Err(_) => MandateTxnType::Default, } } } @@ -887,7 +908,7 @@ pub fn isMandateRegTxn(txn_object_type: TxnObjectType) -> bool { pub fn isPennyTxnType(txn_detail: TxnDetail) -> bool { let mandate = getTxnTypeFromInternalMetadata(txn_detail.internalMetadata); match mandate { - MandateTxnType::REGISTER => true, + MandateTxnType::Register => true, _ => false, } } @@ -910,10 +931,10 @@ pub fn isRecurringTxn(txn_object_type: Option) -> bool { // } // Original Haskell function: getTimeFromTxnCreatedInMills -pub fn getTimeFromTxnCreatedInMills(txn: TxnDetail) -> u128 { - let dateCreated = txn.dateCreated.unix_timestamp_nanos() as u128 / 1_000_000; - let currentTime = EU::get_current_date_in_millis(); - currentTime.saturating_sub(dateCreated) +pub fn get_time_from_txn_created_in_mills(txn: TxnDetail) -> u128 { + let date_created = txn.dateCreated.unix_timestamp_nanos() as u128 / 1_000_000; + let current_time = EU::get_current_date_in_millis(); + current_time.saturating_sub(date_created) } // Original Haskell function: dateToMilliSeconds diff --git a/src/generics.rs b/src/generics.rs index f9e6600f..5918420d 100644 --- a/src/generics.rs +++ b/src/generics.rs @@ -86,8 +86,8 @@ where InsertStatement>::Values>: AsQuery + ExecuteDsl + Send, { - let mut conn = storage.get_conn().await.map_err(|_| MeshError::Others)?; - generic_insert_core::(&mut conn, values).await + let conn = storage.get_conn().await.map_err(|_| MeshError::Others)?; + generic_insert_core::(&conn, values).await } #[cfg(feature = "postgres")] @@ -99,8 +99,8 @@ where >::Values: CanInsertInSingleQuery + QueryFragment + 'static, InsertStatement>::Values>: AsQuery + ExecuteDsl + Send, { - let mut conn = storage.get_conn().await.map_err(|_| MeshError::Others)?; - generic_insert_core::(&mut conn, values).await + let conn = storage.get_conn().await.map_err(|_| MeshError::Others)?; + generic_insert_core::(&conn, values).await } #[cfg(feature = "mysql")] @@ -116,7 +116,7 @@ where InsertStatement>::Values>: AsQuery + ExecuteDsl + Send, { - let debug_values = format!("{values:?}"); + let _debug_values = format!("{values:?}"); let query = diesel::insert_into(::table()).values(values); logger::debug!( action = "generic_insert", @@ -141,7 +141,7 @@ where >::Values: CanInsertInSingleQuery + QueryFragment + 'static, InsertStatement>::Values>: AsQuery + ExecuteDsl + Send, { - let debug_values = format!("{values:?}"); + let _debug_values = format!("{values:?}"); let query = diesel::insert_into(::table()).values(values); logger::debug!( action = "generic_insert", @@ -153,7 +153,7 @@ where .await .change_context(MeshError::Others) } -// Returns error incase of entry not found in DB or due to other issues +// Returns error in case of entry not found in DB or due to other issues #[cfg(feature = "mysql")] pub async fn generic_update( conn: &MysqlPoolConn, @@ -206,7 +206,7 @@ where Ok(res) }) } -// Returns 0 incase of entry not found in DB and errors due to other issues +// Returns 0 in case of entry not found in DB and errors due to other issues #[cfg(feature = "mysql")] pub async fn generic_update_if_present( conn: &MysqlPoolConn, @@ -342,7 +342,7 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; generic_filter::(&conn, predicate).await } @@ -355,7 +355,7 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; generic_filter::(&conn, predicate).await } @@ -456,7 +456,7 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; generic_find_one_core::(&conn, predicate).await } @@ -469,7 +469,7 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; generic_find_one_core::(&conn, predicate).await } @@ -532,7 +532,7 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; to_optional(generic_find_by_id_core::(&conn, id).await) } @@ -551,16 +551,16 @@ where { let conn = match storage.get_conn().await { Ok(conn) => Ok(conn), - Err(err) => Err(MeshError::Others), + Err(_err) => Err(MeshError::Others), }?; to_optional(generic_find_by_id_core::(&conn, id).await) } -pub async fn track_database_call(future: Fut, operation: DatabaseOperation) -> U +pub async fn track_database_call(future: Fut, _operation: DatabaseOperation) -> U where Fut: std::future::Future, { - let start = std::time::Instant::now(); + let _start = std::time::Instant::now(); let output = future.await; output } diff --git a/src/lib.rs b/src/lib.rs index 564b2735..7b2ab032 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,30 @@ +#![allow(non_snake_case)] +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_precision_loss)] +#![allow(clippy::ptr_arg)] +#![allow(clippy::type_complexity)] +#![allow(clippy::as_conversions)] +#![allow(clippy::useless_conversion)] +#![allow(clippy::module_inception)] +#![allow(clippy::should_implement_trait)] +#![allow(clippy::panic)] +#![allow(clippy::unwrap_in_result)] +#![allow(clippy::match_like_matches_macro)] +#![allow(clippy::match_single_binding)] +#![allow(clippy::if_same_then_else)] +#![allow(clippy::map_identity)] +#![allow(clippy::redundant_pattern_matching)] +#![allow(clippy::manual_filter_map)] +#![allow(clippy::single_match)] +#![allow(clippy::manual_ok_err)] +pub mod analytics; pub mod api_client; pub mod app; pub mod config; diff --git a/src/logger/formatter.rs b/src/logger/formatter.rs index e71d0b8b..c643c921 100644 --- a/src/logger/formatter.rs +++ b/src/logger/formatter.rs @@ -31,7 +31,6 @@ use tracing_subscriber::{ // Implicit keys -const MESSAGE: &str = "message"; const HOSTNAME: &str = "hostname"; const PID: &str = "pid"; const LEVEL: &str = "level"; @@ -91,6 +90,7 @@ impl fmt::Display for RecordType { /// `FormattingLayer` relies on the `tracing_bunyan_formatter::JsonStorageLayer` which is storage of entries. /// #[derive(Debug)] +#[allow(dead_code)] pub struct FormattingLayer where W: for<'a> MakeWriter<'a> + 'static, @@ -167,7 +167,7 @@ where Value::String(s) => { // Try parsing string as JSON match serde_json::from_str::(&s) { - Ok(inner_json) => FormattingLayer::::normalize_json(inner_json), + Ok(inner_json) => Self::normalize_json(inner_json), Err(_) => Value::String(s), } } @@ -224,10 +224,16 @@ where let domain_keys = [ "message_number", "error_category", + "error", + "error_message", + "jp_error_code", + "jp_error_message", + "source", "x-request-id", "env", "@timestamp", "udf_txn_uuid", + "txn_uuid", "flow_guid", "action", "is_audit_trail_log", @@ -341,7 +347,11 @@ where explicit_entries_set.insert("resp_code"); } if !explicit_entries_set.contains("level") { - map_serializer.serialize_entry("level", &Value::String("Info".to_string()))?; + if metadata.level() == &tracing::Level::ERROR { + map_serializer.serialize_entry("level", &Value::String("Error".to_string()))?; + } else { + map_serializer.serialize_entry("level", &Value::String("Info".to_string()))?; + } explicit_entries_set.insert("level"); } if !explicit_entries_set.contains("cell_id") { @@ -349,8 +359,6 @@ where explicit_entries_set.insert("cell_id"); } } else { - // DOMAIN category logic. - // Serialize keys from the span that match the domain keys array. if let Some(span) = span { let extensions = span.extensions(); if let Some(visitor) = extensions.get::>() { @@ -368,7 +376,7 @@ where if !explicit_entries_set.contains(*key) { if let Some(value) = storage.values.get(*key) { if key == &"message" { - let normalized_value = get_normalized_message::(&storage); + let normalized_value = get_normalized_message::(storage); map_serializer.serialize_entry("message", &normalized_value)?; explicit_entries_set.insert("message"); @@ -380,6 +388,13 @@ where } } + if !explicit_entries_set.contains("startup_config") { + if let Some(value) = storage.values.get("startup_config") { + map_serializer.serialize_entry("startup_config", value)?; + explicit_entries_set.insert("startup_config"); + } + } + // Set additional fields for DOMAIN logs. if metadata.level() == &tracing::Level::ERROR { if !explicit_entries_set.contains("category") { @@ -419,7 +434,7 @@ where } if !explicit_entries_set.contains("message") { - let normalized_value = get_normalized_message::(&storage); + let normalized_value = get_normalized_message::(storage); map_serializer.serialize_entry("message", &normalized_value)?; explicit_entries_set.insert("message"); @@ -430,6 +445,7 @@ where explicit_entries_set.insert("@timestamp"); } } + if !explicit_entries_set.contains("is_art_enabled") { map_serializer.serialize_entry("is_art_enabled", "false")?; explicit_entries_set.insert("is_art_enabled"); @@ -501,7 +517,7 @@ where /// /// Flush memory buffer into an output stream trailing it with next line. /// - /// Should be done by single `write_all` call to avoid fragmentation of log because of mutlithreading. + /// Should be done by single `write_all` call to avoid fragmentation of log because of multithreading. /// fn flush(&self, mut buffer: Vec) -> Result<(), std::io::Error> { buffer.write_all(b"\n")?; @@ -509,10 +525,11 @@ where } /// Serialize entries of span. + #[allow(dead_code)] fn span_serialize( &self, span: &SpanRef<'_, S>, - ty: RecordType, + _ty: RecordType, ) -> Result, std::io::Error> where S: Subscriber + for<'a> LookupSpan<'a>, @@ -520,7 +537,7 @@ where let mut buffer = Vec::new(); let mut serializer = serde_json::Serializer::new(&mut buffer); let mut map_serializer = serializer.serialize_map(None)?; - let mut storage = Storage::default(); + let storage = Storage::default(); self.common_serialize( &mut map_serializer, @@ -566,6 +583,9 @@ where let value = storage .values .get("message") + .or_else(|| storage.values.get("error")) + .or_else(|| storage.values.get("error_message")) + .or_else(|| storage.values.get("jp_error_message")) .cloned() .unwrap_or(Value::String("null".to_string())); @@ -612,17 +632,7 @@ where } } - fn on_enter(&self, id: &tracing::Id, ctx: Context<'_, S>) { - let span = ctx.span(id).expect("No span"); - if let Ok(serialized) = self.span_serialize(&span, RecordType::EnterSpan) { - let _ = self.flush(serialized); - } - } + fn on_enter(&self, _id: &tracing::Id, _ctx: Context<'_, S>) {} - fn on_close(&self, id: tracing::Id, ctx: Context<'_, S>) { - let span = ctx.span(&id).expect("No span"); - if let Ok(serialized) = self.span_serialize(&span, RecordType::ExitSpan) { - let _ = self.flush(serialized); - } - } + fn on_close(&self, _id: tracing::Id, _ctx: Context<'_, S>) {} } diff --git a/src/logger/setup.rs b/src/logger/setup.rs index f5da22c4..3b3872d0 100644 --- a/src/logger/setup.rs +++ b/src/logger/setup.rs @@ -1,7 +1,7 @@ //! Setup logging subsystem. use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::{fmt, prelude::*, util::SubscriberInitExt, EnvFilter, Layer}; +use tracing_subscriber::{prelude::*, util::SubscriberInitExt, EnvFilter, Layer}; use super::{config, formatter::FormattingLayer, storage::StorageSubscription}; diff --git a/src/logger/storage.rs b/src/logger/storage.rs index 00a84cfb..ce3fb605 100644 --- a/src/logger/storage.rs +++ b/src/logger/storage.rs @@ -18,7 +18,7 @@ use crate::logger; pub struct StorageSubscription; /// Storage to store key value pairs of spans. -/// When new entry is crated it stores it in [HashMap] which is owned by `extensions`. +/// When new entry is created it stores it in [HashMap] which is owned by `extensions`. #[derive(Clone, Debug)] pub struct Storage<'a> { /// Hash map to store values. diff --git a/src/merchant_config_util.rs b/src/merchant_config_util.rs index 12b47814..e3fde3c9 100644 --- a/src/merchant_config_util.rs +++ b/src/merchant_config_util.rs @@ -27,7 +27,7 @@ use josekit::Value; use crate::{ decider::{ configs::env_vars::enable_merchant_config_entity_lookup, - gatewaydecider::constants::MERCHANT_CONFIG_ENTITY_LEVEL_LOOKUP_CUTOVER, + gatewaydecider::constants::MerchantConfigEntityLevelLookupCutover, }, logger, redis::{cache::findByNameFromRedis, types::ServiceConfigKey}, @@ -40,12 +40,14 @@ use crate::{ load_merchant_config_by_mpid_category_name_and_status, MerchantConfig, }, types::{ - config_category_to_text, config_status_to_text, to_config_status, ConfigCategory, - ConfigStatus, PfMcConfig, + config_category_to_text, config_status_to_text, ConfigCategory, ConfigStatus, + PfMcConfig, }, }, payment_flow::{payment_flows_to_text, PaymentFlow}, - tenant::tenant_config::{ConfigType, ModuleName}, + tenant::tenant_config::{ + ConfigType, ModuleName, TenantConfigStatus, TenantConfigValueType, + }, tenant_config::{ get_arr_active_tenant_config_by_tenant_id_module_name_module_key_and_arr_type, get_arr_active_tenant_config_by_tenant_id_module_name_module_key_and_arr_type_and_country, @@ -75,7 +77,7 @@ use crate::{ // // Original Haskell function: getMerchantConfigEntityLevelLookupConfig pub async fn getMerchantConfigEntityLevelLookupConfig() -> Option { if enable_merchant_config_entity_lookup() { - findByNameFromRedis(MERCHANT_CONFIG_ENTITY_LEVEL_LOOKUP_CUTOVER.get_key()).await + findByNameFromRedis(MerchantConfigEntityLevelLookupCutover.get_key()).await } else { None } @@ -146,7 +148,7 @@ pub async fn isMerchantEnabledForPaymentFlows( ) -> bool { let mc_arr = load_merchant_config_by_mpid_category_and_name( merchant_id, - config_category_to_text(ConfigCategory::PAYMENT_FLOW), + config_category_to_text(ConfigCategory::PaymentFlow), payment_flows .iter() .map(|pf: &PaymentFlow| payment_flows_to_text(pf)) @@ -159,10 +161,7 @@ pub async fn isMerchantEnabledForPaymentFlows( if !is_valid_length { logMerConfigLengthMisMatchError( payment_flows.iter().map(payment_flows_to_text).collect(), - mc_arr - .into_iter() - .filter_map(|mc| Some(mc.clone())) - .collect(), + mc_arr.into_iter().collect(), ); } is_valid_length && are_all_pfs_enabled @@ -238,7 +237,7 @@ pub async fn isMerchantEnabledForPaymentFlows( // } // let mc_arr = MerchantConfig::loadArrMerchantConfigByMPidCategoryAndName( // mer_acc_id, -// MCTypes::PAYMENT_FLOW, +// MCTypes::PaymentFlow, // &enabled_pfs.iter().map(|pf| MCTypes::ConfigName(pf.clone())).collect::>(), // ); // let result: Vec<(String, RedisMerchantConfigStatus)> = enabled_pfs @@ -281,7 +280,7 @@ pub async fn isPaymentFlowEnabledForMerchant( payment_flow: PaymentFlow, ) -> bool { let config_name = payment_flows_to_text(&payment_flow); - let config_category = config_category_to_text(ConfigCategory::PAYMENT_FLOW); + let config_category = config_category_to_text(ConfigCategory::PaymentFlow); let config_status = config_status_to_text(ConfigStatus::ENABLED); load_merchant_config_by_mpid_category_name_and_status( @@ -340,7 +339,7 @@ pub async fn getMerchantConfigValueForPaymentFlow( ) -> Option { let m_mer_config = load_merchant_config_by_mpid_category_name_and_status( merchant_p_id_val, - config_category_to_text(ConfigCategory::PAYMENT_FLOW), + config_category_to_text(ConfigCategory::PaymentFlow), payment_flows_to_text(&pf), config_status_to_text(ConfigStatus::ENABLED), ) @@ -363,7 +362,7 @@ pub async fn getMerchantConfigValueForPaymentFlow( // ) -> Option { // let m_mer_config = load_merchant_config_by_mpid_category_name_and_status( // merchant_p_id_val, -// ConfigCategory::PAYMENT_FLOW, +// ConfigCategory::PaymentFlow, // payment_flows_to_text(&pf), // ConfigStatus::ENABLED, // ); @@ -401,13 +400,13 @@ pub async fn getMerchantConfigValueForPaymentFlow( // // Original Haskell function: getMerchantConfigStatusAndvalueForPaymentFlow pub async fn getMerchantConfigStatusAndvalueForPaymentFlow( merchant_p_id: MerchantPId, - merchant_id: MerchantPId, + _merchant_id: MerchantPId, pf: PaymentFlow, - m_pf_mc_config: Option, + _m_pf_mc_config: Option, ) -> (ConfigStatus, Option) { let m_mer_config: Option = load_merchant_config_by_mpid_category_and_name( merchant_p_id, - config_category_to_text(ConfigCategory::PAYMENT_FLOW), + config_category_to_text(ConfigCategory::PaymentFlow), payment_flows_to_text(&pf), ) .await; @@ -435,7 +434,7 @@ pub async fn getMerchantConfigStatusAndvalueForPaymentFlow( // ) -> (Option) { // let m_mer_config = load_merchant_config_by_mpid_category_and_name( // merchant_p_id, -// config_category_to_text(ConfigCategory::PAYMENT_FLOW), +// config_category_to_text(ConfigCategory::PaymentFlow), // payment_flows_to_text(&pf), // ).await; // match m_mer_config { @@ -517,7 +516,7 @@ pub async fn isPaymentFlowEnabledWithHierarchyCheck( ); } - let category = config_category_to_text(ConfigCategory::PAYMENT_FLOW); + let category = config_category_to_text(ConfigCategory::PaymentFlow); let m_mer_config = load_merchant_config_by_mpid_category_and_name( merchant_p_id, @@ -544,10 +543,9 @@ pub async fn isPaymentFlowEnabledWithHierarchyCheck( } fn checkIfEnabledByTenant(config_value: &str, error_tag: &str) -> bool { - let config_status = to_config_status(config_value); - match config_status { - Ok(ConfigStatus::ENABLED) => true, - Ok(ConfigStatus::DISABLED) => false, + let result: Result, _> = serde_json::from_str(config_value); + match result { + Ok(config) => config.status == TenantConfigStatus::ENABLED, Err(e) => { logger::error!(action = error_tag, tag = error_tag, "Error: {}", e); false @@ -606,7 +604,7 @@ pub async fn getPaymentFlowInfoFromTenantConfig( // ) -> (Redis::MerchantConfigStatus, MAConfigs) { // let m_mer_config = MerchantConfig::loadMerchantConfigByMPidCategoryAndName( // &merchant_p_id, -// MCTypes::PAYMENT_FLOW, +// MCTypes::PaymentFlow, // MCTypes::ConfigName(pf.to_string()), // ); @@ -677,7 +675,7 @@ pub async fn getPaymentFlowInfoFromTenantConfig( // ma_configs.clone() // } // }, -// PaymentFlow::AUTO_REFUND => match mer_config.config_value.as_ref().and_then(|v| { +// PaymentFlow::AutoRefund => match mer_config.config_value.as_ref().and_then(|v| { // A::eitherDecodeStrict::(&encodeUtf8(v)).ok() // }) { // Some(v) => MAConfigs::ARC(v), @@ -701,8 +699,8 @@ pub async fn getPaymentFlowInfoFromTenantConfig( // } // } -// // Original Haskell function: getConfigValueWithGCCatagory -// pub fn getConfigValueWithGCCatagory( +// // Original Haskell function: getConfigValueWithGCCategory +// pub fn getConfigValueWithGCCategory( // arg1: i64, // arg2: String, // arg3: String, @@ -711,17 +709,17 @@ pub async fn getPaymentFlowInfoFromTenantConfig( // where // T: serde::de::DeserializeOwned, // { -// getConfigValueFromMerchantConfig(MCTypes::GENERAL_CONFIG, arg1, arg2, arg3, arg4) +// getConfigValueFromMerchantConfig(MCTypes::GeneralConfig, arg1, arg2, arg3, arg4) // } -// // Original Haskell function: getConfigValueWithPFCatagory -// pub fn getConfigValueWithPFCatagory( +// // Original Haskell function: getConfigValueWithPFCategory +// pub fn getConfigValueWithPFCategory( // arg1: i64, // arg2: String, // arg3: String, // arg4: Option, // ) -> Option { -// getConfigValueFromMerchantConfig(MCTypes::PAYMENT_FLOW, arg1, arg2, arg3, arg4) +// getConfigValueFromMerchantConfig(MCTypes::PaymentFlow, arg1, arg2, arg3, arg4) // } // // Original Haskell function: getConfigValueFromMerchantConfig @@ -734,7 +732,7 @@ pub async fn getPaymentFlowInfoFromTenantConfig( // ) -> Option { // let tenant_config = getPaymentFlowInfoFromTenantConfig( // Some(&tenant_acc_id), -// TC::MERCHANT_CONFIG, +// TC::MerchantConfig, // &merchant_config_key, // m_iso_country_code, // ); diff --git a/src/metrics.rs b/src/metrics.rs index 2ce6aeaf..ec142985 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -28,6 +28,27 @@ lazy_static! { &["endpoint"], exponential_buckets(0.0005, 2.0, 10).unwrap() ).unwrap(); + + /// Count of routing decisions grouped by routing approach and result status + pub static ref ROUTING_DECISION_COUNTER: IntCounterVec = register_int_counter_vec!( + "routing_decisions_total", + "Count of routing decisions grouped by routing approach and result status", + &["approach", "status"] + ).unwrap(); + + /// Count of priority logic rule hits grouped by rule name + pub static ref ROUTING_RULE_HIT_COUNTER: IntCounterVec = register_int_counter_vec!( + "routing_rule_hits_total", + "Count of priority logic rule hits grouped by rule name", + &["rule_name"] + ).unwrap(); + + /// Count of analytics events captured by type + pub static ref ANALYTICS_EVENT_COUNTER: IntCounterVec = register_int_counter_vec!( + "analytics_events_total", + "Count of analytics events captured by type", + &["event_type"] + ).unwrap(); } pub async fn metrics_handler() -> error_stack::Result { @@ -83,7 +104,7 @@ pub async fn metrics_server_builder( axum::serve(listener, router.into_make_service()) .with_graceful_shutdown(async move { let _ = sigterm.recv().await; - tracing::info!("Metrics server shutting down gracefully"); + tracing::debug!("Metrics server shutting down gracefully"); }) .await?; @@ -99,10 +120,10 @@ impl crate::config::GlobalConfig { tracing::info!( category = "SERVER", - "{} started [{:?}] [{:?}]", + action = "metrics_server_startup", server, - self.metrics, - self.log + bind_address = %loc, + "Metrics server listening" ); Ok(tokio::net::TcpListener::bind(loc).await?) diff --git a/src/redis.rs b/src/redis.rs index df780bc5..24ac6eb1 100644 --- a/src/redis.rs +++ b/src/redis.rs @@ -1,4 +1,5 @@ pub mod cache; pub mod commands; pub mod feature; +pub mod mem_cache; pub mod types; diff --git a/src/redis/cache.rs b/src/redis/cache.rs index 8bc6e7ae..2ef6d444 100644 --- a/src/redis/cache.rs +++ b/src/redis/cache.rs @@ -1,3 +1,7 @@ +use super::mem_cache::GLOBAL_CACHE; +use crate::app::get_tenant_app_state; +use crate::logger; +use crate::redis::feature::RedisDataStruct; use crate::types::service_configuration; use crate::utils::StringExt; use serde::Deserialize; @@ -6,6 +10,62 @@ use serde::Deserialize; // Original Haskell type: KVDBName pub type KVDBName = String; +async fn get_from_memory_cache(prefixed_key: &str) -> Result { + match GLOBAL_CACHE.get::(prefixed_key) { + Ok(value) => Ok(value), + Err(e) => Err(format!("Memory cache get failed: {}", e)), + } +} + +async fn set_to_memory_cache(prefixed_key: &str, value: &str, ttl_seconds: Option) { + match GLOBAL_CACHE.store(prefixed_key.to_string(), value.to_string(), ttl_seconds) { + Ok(_) => {} + Err(e) => { + crate::logger::warn!( + tag = "memory_cache_write_failed", + action = "memory_cache_write_failed", + "Failed to write cache for key: {}, error: {:?}", + prefixed_key, + e + ); + } + } +} + +async fn get_from_redis_cache(prefixed_key: &str) -> Result { + let app_state = get_tenant_app_state().await; + match app_state.redis_conn.get_key_string(prefixed_key).await { + Ok(value) => Ok(value), + Err(e) => Err(format!("Redis cache get failed: {:?}", e)), + } +} + +async fn set_to_redis_cache(prefixed_key: &str, value: &str, ttl_seconds: i64) { + let app_state = get_tenant_app_state().await; + match app_state + .redis_conn + .setx( + prefixed_key, + value, + ttl_seconds, + None, + RedisDataStruct::STRING, + ) + .await + { + Ok(_) => {} + Err(e) => { + crate::logger::warn!( + tag = "redis_cache_write_failed", + action = "redis_cache_write_failed", + "Failed to write Redis cache for key: {}, error: {:?}", + prefixed_key, + e + ); + } + } +} + // // Converted data types // // Original Haskell data type: Multi // #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -54,7 +114,6 @@ where findByNameFromRedisHelper(key, Some(decode_fn)).await } -// Original Haskell function: findByNameFromRedisHelper pub async fn findByNameFromRedisHelper( key: String, decode_fn: Option Option>, @@ -62,23 +121,140 @@ pub async fn findByNameFromRedisHelper( where A: for<'de> Deserialize<'de>, { - let res = service_configuration::find_config_by_name(key).await; + let app_state = get_tenant_app_state().await; + let prefixed_key = app_state.config.cache_config.add_prefix(&key); + let ttl_seconds = app_state.config.cache_config.service_config_ttl; + let ttl_seconds_u64 = Some(ttl_seconds as u64); + + match get_from_memory_cache(&prefixed_key).await { + Ok(cache_value) => { + logger::debug!( + tag = "memory_cache", + action = "hit", + "Memory cache hit for key: {}", + key + ); + if cache_value.is_empty() { + return None; + } + return match &decode_fn { + Some(func) => func(cache_value), + None => extractValue(cache_value), + }; + } + Err(_) => { + logger::debug!( + tag = "memory_cache", + action = "miss", + "Memory cache miss for key: {}, checking Redis", + key + ); + } + } + + match get_from_redis_cache(&prefixed_key).await { + Ok(redis_value) => { + logger::debug!( + tag = "redis_cache", + action = "hit", + "Redis cache hit for key: {}", + key + ); + if redis_value.is_empty() { + set_to_memory_cache(&prefixed_key, "", ttl_seconds_u64).await; + return None; + } + + set_to_memory_cache(&prefixed_key, &redis_value, ttl_seconds_u64).await; + return match &decode_fn { + Some(func) => func(redis_value), + None => extractValue(redis_value), + }; + } + Err(_) => { + logger::debug!( + tag = "redis_cache", + action = "miss", + "Redis cache miss for key: {}, falling back to database", + key + ); + } + } + + let res = service_configuration::find_config_by_name(key.clone()).await; match res { - Ok(m_service_config) => match m_service_config { - Some(service_config) => match service_config.value { - Some(value) => match decode_fn { + Ok(Some(service_config)) => match service_config.value { + Some(value) => { + set_to_redis_cache(&prefixed_key, &value, ttl_seconds).await; + set_to_memory_cache(&prefixed_key, &value, ttl_seconds_u64).await; + + match decode_fn { Some(func) => func(value), - None => None, - }, - None => None, - }, - None => None, + None => extractValue(value), + } + } + None => { + set_to_redis_cache(&prefixed_key, "", ttl_seconds).await; + set_to_memory_cache(&prefixed_key, "", ttl_seconds_u64).await; + None + } }, + Ok(None) => { + set_to_redis_cache(&prefixed_key, "", ttl_seconds).await; + set_to_memory_cache(&prefixed_key, "", ttl_seconds_u64).await; + None + } Err(_) => None, } } +// Function to find value from memory cache/DB and return default if not present +pub async fn findByNameFromRedisWithDefault(key: String, default_value: A) -> A +where + A: for<'de> Deserialize<'de> + serde::Serialize + Clone, +{ + // First try to get the value using the helper function + let result = findByNameFromRedisHelper(key.clone(), Some(extractValue::)).await; + + match result { + Some(value) => value, + None => { + // Serialize the default value to JSON string + match serde_json::to_string(&default_value) { + Ok(default_json) => { + // Cache the default value for future use + let app_state = get_tenant_app_state().await; + let prefixed_key = app_state.config.cache_config.add_prefix(&key); + let ttl_seconds = app_state.config.cache_config.service_config_ttl; + let ttl_seconds_u64 = Some(ttl_seconds as u64); + + set_to_redis_cache(&prefixed_key, &default_json, ttl_seconds).await; + set_to_memory_cache(&prefixed_key, &default_json, ttl_seconds_u64).await; + + logger::debug!( + tag = "cache", + action = "default_cached", + "Cached default value for key: {}", + key + ); + } + Err(e) => { + logger::warn!( + tag = "cache", + action = "serialize_failed", + "Failed to serialize default value for key: {}, error: {:?}", + key, + e + ); + } + } + + default_value + } + } +} + pub fn extractValue(value: String) -> Option where A: for<'de> Deserialize<'de>, diff --git a/src/redis/commands.rs b/src/redis/commands.rs index ac35b0ed..cf690190 100644 --- a/src/redis/commands.rs +++ b/src/redis/commands.rs @@ -7,28 +7,242 @@ use fred::prelude::RedisKey; use fred::types::SetOptions; use fred::{ clients::Transaction, - interfaces::{KeysInterface, ListInterface, TransactionInterface}, - types::{Expiration, FromRedis, MultipleValues, Scanner}, + interfaces::{HashesInterface, KeysInterface, ListInterface, TransactionInterface}, + types::{Expiration, FromRedis, MultipleValues}, }; use redis_interface::{errors, types::DelReply, RedisConnectionPool}; use std::fmt::Debug; +use std::str; + +use crate::config::CompressionFilepath; +#[cfg(feature = "redis_compression")] +use crate::logger; +#[cfg(feature = "redis_compression")] +use crate::redis::feature::{ + RedisCompressionConfig, RedisCompressionConfigCombined, RedisDataStruct, +}; +#[cfg(not(feature = "redis_compression"))] +use crate::redis::feature::{RedisCompressionConfigCombined, RedisDataStruct}; +#[cfg(feature = "redis_compression")] +use fred::types::RedisValue; +#[cfg(feature = "redis_compression")] +use serde::de::DeserializeOwned; +#[cfg(feature = "redis_compression")] +use std::env; +#[cfg(feature = "redis_compression")] +use std::fs::File; +#[cfg(feature = "redis_compression")] +use std::io::{Cursor, Read}; +#[cfg(feature = "redis_compression")] +use zstd::bulk::Compressor; +#[cfg(feature = "redis_compression")] +use zstd::stream::read::Decoder; pub struct RedisConnectionWrapper { pub conn: RedisConnectionPool, + pub compression_file_path: Option, } +#[allow(dead_code)] +const ZSTD_MAGIC_BYTES: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD]; + impl RedisConnectionWrapper { - pub fn new(redis_conn: RedisConnectionPool) -> Self { - Self { conn: redis_conn } + pub fn new( + redis_conn: RedisConnectionPool, + compression_file_path: Option, + ) -> Self { + Self { + conn: redis_conn, + compression_file_path, + } + } + + #[cfg(feature = "redis_compression")] + fn compress_string_with_config( + &self, + value: &str, + key: &str, + redis_compression_config: Option<&RedisCompressionConfigCombined>, + redis_type: RedisDataStruct, + ) -> Vec { + logger::debug!( + "REDIS_ZSTD_COMPRESS - compress_string_with_config called with key: {}, value length: {}", + key, + value.len() + ); + + let json = value.as_bytes().to_vec(); + + let redis_compression_eligible_length = env::var("REDIS_COMPRESSION_ELIGIBLE_LENGTH") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(200); + + let redis_type_key = redis_type.as_str(); + + logger::debug!( + "REDIS_ZSTD_COMPRESS - Redis Type Key: {}, JSON Length: {}, redis_compression_config: {:?}, key: {} , eligible_length: {}", + redis_type_key, + json.len(), + redis_compression_config, + key, + redis_compression_eligible_length + ); + + let final_value = match redis_compression_config { + Some(config_combined) if config_combined.isRedisCompEnabled => { + match config_combined + .redisCompressionConfig + .as_ref() + .and_then(|config| config.get(redis_type_key)) + { + Some(comp_conf) + if json.len() > redis_compression_eligible_length + && comp_conf.compEnabled => + { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Compressing data for key: {}, dictId: {}", + key, + comp_conf.dictId + ); + self.compress_with_dict(&json, comp_conf, key) + } + _ => { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Skipping compression for key: {} (length or compEnabled check failed)", + key + ); + json + } + } + } + _ => { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Skipping compression for key: {} (redis compression not enabled or config not present)", + key + ); + json + } + }; + + logger::debug!( + "REDIS_ZSTD_COMPRESS - Compressed/processed key: {}, final value length: {}, is_compressed: {}", + key, + final_value.len(), + Self::is_zstd_compressed(&final_value) + ); + + final_value } - pub async fn set_key(&self, key: &str, value: V) -> Result<(), errors::RedisError> + #[cfg(feature = "redis_compression")] + fn compress_with_dict( + &self, + json: &[u8], + comp_conf: &RedisCompressionConfig, + key: &str, + ) -> Vec { + let dict_file_path = match &self.compression_file_path { + Some(compression_config) => format!( + "{}/{}.dict", + compression_config.zstd_compression_filepath, comp_conf.dictId + ), + None => { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Compression filepath not configured for key: {}", + key + ); + return json.to_vec(); + } + }; + + match std::fs::read(&dict_file_path) { + Ok(dict_bytes) => { + let compression_level = comp_conf + .compLevel + .as_ref() + .and_then(|s| s.parse::().ok()) + .unwrap_or(3); + + match Compressor::with_dictionary(compression_level, &dict_bytes) { + Ok(mut compressor) => match compressor.compress(json) { + Ok(compressed) => { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Successfully compressed data for key: {}, original length: {}, compressed length: {}", + key, + json.len(), + compressed.len() + ); + return compressed; + } + Err(e) => { + logger::error!("Compression failed for key {}: {:?}", key, e); + } + }, + Err(e) => { + logger::error!("Failed to create compressor for key {}: {:?}", key, e); + } + } + } + Err(e) => { + logger::error!( + "Failed to read dictionary file {} for key {}: {:?}", + dict_file_path, + key, + e + ); + } + } + + json.to_vec() + } + + #[cfg(not(feature = "redis_compression"))] + pub async fn set_key( + &self, + key: &str, + value: V, + _redis_compression_config: Option, + _redis_type: RedisDataStruct, + ) -> Result<(), errors::RedisError> where V: serde::Serialize + Debug, { self.conn.serialize_and_set_key(key, value).await } + #[cfg(feature = "redis_compression")] + pub async fn set_key( + &self, + key: &str, + value: &str, + redis_compression_config: Option, + redis_type: RedisDataStruct, + ) -> Result<(), errors::RedisError> { + let final_value = self.compress_string_with_config( + value, + key, + redis_compression_config.as_ref(), + redis_type, + ); + + // Convert Vec to RedisValue::Bytes for proper serialization + let redis_value = RedisValue::Bytes(final_value.into()); + + self.conn + .pool + .set::<(), _, _>(key, redis_value, None, None, false) + .await + .change_context(errors::RedisError::SetHashFailed)?; + + logger::debug!( + "REDIS_ZSTD_COMPRESS - REDIS_COMPRESSION - Successfully set key: {} (string type)", + key + ); + + Ok(()) + } + pub async fn set_key_with_ttl( &self, key: &str, @@ -43,6 +257,151 @@ impl RedisConnectionWrapper { .await } + #[cfg(feature = "redis_compression")] + fn is_zstd_compressed(data: &[u8]) -> bool { + data.len() >= 4 && data[..4] == [0x28, 0xB5, 0x2F, 0xFD] + } + #[cfg(feature = "redis_compression")] + fn extract_dict_id_from_cdata(cdata: &[u8]) -> Option { + // Haskell magic: "(\181/\253" + let magic_number: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD]; + + if cdata.len() > 9 && cdata[0..4] == magic_number { + let cdata_wo_magic = &cdata[4..]; + + let frame_header_w = cdata_wo_magic[0]; + + // getDictionaryLengthFromDictID (bit1, bit0) + let bit1 = ((frame_header_w >> 1) & 1) == 1; + let bit0 = (frame_header_w & 1) == 1; + + let dict_id_size = match (bit1, bit0) { + (false, false) => 1, + (false, true) => 1, + (true, false) => 2, + (true, true) => 4, + }; + + // singleSegmentFlag = bit 5 + let single_segment_flag = ((frame_header_w >> 5) & 1) == 1; + + let start_offset = if single_segment_flag { 1 } else { 2 }; + + if cdata_wo_magic.len() < start_offset + dict_id_size { + return None; + } + + let dict_id_bytes = &cdata_wo_magic[start_offset..start_offset + dict_id_size]; + + // decodeDictId → convert bytes to hex string + let decoded = Self::decode_dict_id(dict_id_bytes); + + Some(decoded) + } else { + None + } + } + + #[cfg(feature = "redis_compression")] + fn decode_dict_id(bytes: &[u8]) -> String { + let val = match bytes.len() { + 1 => bytes[0] as u32, + 2 => { + let s8 = bytes[0] as u32; + let f8 = bytes[1] as u32; + (f8 << 8) | s8 + } + 3 => { + let a8 = bytes[0] as u32; + let b8 = bytes[1] as u32; + let c8 = bytes[2] as u32; + (c8 << 16) | (b8 << 8) | a8 + } + 4 => { + let a8 = bytes[0] as u32; + let b8 = bytes[1] as u32; + let c8 = bytes[2] as u32; + let d8 = bytes[3] as u32; + ((d8 << 8) | c8) << 16 | (b8 << 8) | a8 + } + _ => { + logger::error!("Unexpected dict_id size: {}", bytes.len()); + return "0".to_string(); + } + }; + + val.to_string() + } + + #[cfg(feature = "redis_compression")] + pub async fn get_key( + &self, + key: &str, + _type_name: &'static str, + ) -> Result + where + T: DeserializeOwned, + { + let raw_bytes: Vec = self.conn.get_key(key).await?; + + if raw_bytes.is_empty() { + return Err(errors::RedisError::GetFailed.into()); + } + + match Self::extract_dict_id_from_cdata(&raw_bytes) { + Some(dict_id) => { + let dict_file_path = match &self.compression_file_path { + Some(compression_config) => format!( + "{}/{}.dict", + compression_config.zstd_compression_filepath, dict_id + ), + None => { + logger::debug!( + "REDIS_ZSTD_COMPRESS - Compression filepath not configured for key: {}", + key + ); + return serde_json::from_slice(&raw_bytes) + .change_context(errors::RedisError::GetFailed); + } + }; + + let mut dict_file = File::open(&dict_file_path).map_err(|e| { + logger::error!( + "Failed to open dictionary file {} for key {}: {:?}", + dict_file_path, + key, + e + ); + errors::RedisError::UnknownResult + })?; + + let mut dict_bytes = Vec::new(); + dict_file.read_to_end(&mut dict_bytes).map_err(|e| { + logger::error!("Failed to read dictionary file for key {}: {:?}", key, e); + errors::RedisError::GetFailed + })?; + + let mut decoder = Decoder::with_dictionary(Cursor::new(&raw_bytes), &dict_bytes) + .map_err(|e| { + logger::error!("Failed to create ZSTD decoder for key {}: {:?}", key, e); + errors::RedisError::GetFailed + })?; + + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed).map_err(|e| { + logger::error!("Failed to decompress data for key {}: {:?}", key, e); + errors::RedisError::GetFailed + })?; + + serde_json::from_slice(&decompressed).change_context(errors::RedisError::GetFailed) + } + None => { + serde_json::from_slice(&raw_bytes).change_context(errors::RedisError::GetFailed) + } + } + } + + #[cfg(not(feature = "redis_compression"))] pub async fn get_key( &self, key: &str, @@ -54,6 +413,12 @@ impl RedisConnectionWrapper { self.conn.get_and_deserialize_key(key, type_name).await } + #[cfg(feature = "redis_compression")] + pub async fn get_key_string(&self, key: &str) -> Result { + self.get_key::(key, "").await + } + + #[cfg(not(feature = "redis_compression"))] pub async fn get_key_string(&self, key: &str) -> Result { self.conn .get_key(key) @@ -69,6 +434,19 @@ impl RedisConnectionWrapper { .change_context(errors::RedisError::GetListLengthFailed) } + pub async fn get_list_range( + &self, + key: &str, + start: i64, + stop: i64, + ) -> Result, errors::RedisError> { + self.conn + .pool + .lrange(key, start, stop) + .await + .change_context(errors::RedisError::GetListLengthFailed) + } + pub async fn append_to_list_start( &self, key: &RedisKey, @@ -129,20 +507,63 @@ impl RedisConnectionWrapper { .change_context(errors::RedisError::IncrementHashFieldFailed) } + #[cfg(not(feature = "redis_compression"))] pub async fn setXWithOption( &self, key: &str, value: &str, ttl: i64, option: SetOptions, + _redis_compression_config: Option, + _redis_type: RedisDataStruct, ) -> Result { - // implement the redis query to set if it doesn't exist self.conn .pool .set(key, value, Some(Expiration::EX(ttl)), Some(option), false) .await .change_context(errors::RedisError::SetHashFailed) } + #[cfg(feature = "redis_compression")] + pub async fn setXWithOption( + &self, + key: &str, + value: &str, + ttl: i64, + option: SetOptions, + redis_compression_config: Option, + redis_type: RedisDataStruct, + ) -> Result { + let final_value = self.compress_string_with_config( + value, + key, + redis_compression_config.as_ref(), + redis_type, + ); + + // Convert Vec to RedisValue::Bytes for proper serialization + let redis_value = RedisValue::Bytes(final_value.into()); + + let result = self + .conn + .pool + .set( + key, + redis_value, + Some(Expiration::EX(ttl)), + Some(option), + false, + ) + .await + .change_context(errors::RedisError::SetHashFailed)?; + + logger::debug!( + "REDIS_ZSTD_COMPRESS - REDIS_COMPRESSION - Successfully set key: {}, result: {}", + key, + result + ); + + Ok(result) + } pub async fn exists(&self, key: &str) -> Result { self.conn .pool @@ -150,13 +571,53 @@ impl RedisConnectionWrapper { .await .change_context(errors::RedisError::GetFailed) } - pub async fn setx(&self, key: &str, value: &str, ttl: i64) -> Result<(), errors::RedisError> { + #[cfg(not(feature = "redis_compression"))] + pub async fn setx( + &self, + key: &str, + value: &str, + ttl: i64, + _redis_compression_config: Option, + _redis_type: RedisDataStruct, + ) -> Result<(), errors::RedisError> { self.conn .pool .set(key, value, Some(Expiration::EX(ttl)), None, false) .await .change_context(errors::RedisError::SetHashFailed) } + #[cfg(feature = "redis_compression")] + pub async fn setx( + &self, + key: &str, + value: &str, + ttl: i64, + redis_compression_config: Option, + redis_type: RedisDataStruct, + ) -> Result<(), errors::RedisError> { + let final_value = self.compress_string_with_config( + value, + key, + redis_compression_config.as_ref(), + redis_type, + ); + + // Convert Vec to RedisValue::Bytes for proper serialization + let redis_value = RedisValue::Bytes(final_value.into()); + + self.conn + .pool + .set::<(), _, _>(key, redis_value, Some(Expiration::EX(ttl)), None, false) + .await + .change_context(errors::RedisError::SetHashFailed)?; + + logger::debug!( + "REDIS_ZSTD_COMPRESS - REDIS_COMPRESSION - Successfully set key: {}", + key + ); + + Ok(()) + } pub async fn multi(&self, abort_on_error: bool, f: F) -> Result where R: FromRedis, @@ -174,4 +635,32 @@ impl RedisConnectionWrapper { .await .change_context(errors::RedisError::UnknownResult) } + + // Redis Hash Operations + pub async fn hget( + &self, + key: &str, + field: &str, + _type_name: &'static str, + ) -> Result, errors::RedisError> + where + T: serde::de::DeserializeOwned, + { + let result: Option = self + .conn + .pool + .hget(key, field) + .await + .change_context(errors::RedisError::GetFailed)?; + + match result { + Some(value_str) => { + let value: T = serde_json::from_str(&value_str) + .map_err(|_| errors::RedisError::UnknownResult) + .change_context(errors::RedisError::GetFailed)?; + Ok(Some(value)) + } + None => Ok(None), + } + } } diff --git a/src/redis/feature.rs b/src/redis/feature.rs index a7d526a6..b71bbf10 100644 --- a/src/redis/feature.rs +++ b/src/redis/feature.rs @@ -2,32 +2,34 @@ use crate::logger; use crate::redis::types::DimensionConf; use crate::redis::{cache::findByNameFromRedis, types::FeatureConf}; use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; // Converted functions // Original Haskell function: isFeatureEnabled -pub async fn isFeatureEnabled(f_name: String, mid: String, redis_name: String) -> bool { - isFeatureEnabledWithMaybeDBConf(None, f_name, mid, redis_name).await +pub async fn is_feature_enabled(f_name: String, mid: String, redis_name: String) -> bool { + is_feature_enabled_with_maybe_db_conf(None, f_name, mid, redis_name).await } // Original Haskell function: isFeatureEnabledWithMaybeDBConf -pub async fn isFeatureEnabledWithMaybeDBConf( - maybe_db_conf: Option, +pub async fn is_feature_enabled_with_maybe_db_conf( + _maybe_db_conf: Option, f_name: String, mid: String, _: String, ) -> bool { let maybe_conf = findByNameFromRedis::(f_name.clone()).await; - checkMerchantEnabled(maybe_conf, mid, f_name) + check_merchant_enabled(maybe_conf, mid, f_name) } // Original Haskell function: isFeatureEnabledByDimension -pub async fn isFeatureEnabledByDimension(f_name: String, dimension: String) -> bool { +pub async fn is_feature_enabled_by_dimension(f_name: String, dimension: String) -> bool { let maybe_conf = findByNameFromRedis::(f_name.clone()).await; - checkDimensionEnabled(maybe_conf, dimension, f_name) + check_dimension_enabled(maybe_conf, dimension, f_name) } // Original Haskell function: checkDimensionEnabled -pub fn checkDimensionEnabled( +pub fn check_dimension_enabled( dimension_conf: Option, dimension: String, key: String, @@ -54,12 +56,12 @@ pub fn checkDimensionEnabled( None => false, } }; - isDimensionConfigEnabled(conf, dimension, is_enabled, key) + is_dimension_config_enabled(conf, dimension, is_enabled, key) } } } -pub fn isDimensionConfigEnabled( +pub fn is_dimension_config_enabled( dimension_conf: DimensionConf, dimension: String, is_enabled: bool, @@ -80,7 +82,7 @@ pub fn isDimensionConfigEnabled( } // Original Haskell function: checkMerchantEnabled -pub fn checkMerchantEnabled(conf: Option, mid: String, key: String) -> bool { +pub fn check_merchant_enabled(conf: Option, mid: String, key: String) -> bool { match conf { None => false, Some(conf) => { @@ -118,3 +120,208 @@ pub fn roller(key: String, num: i32) -> bool { let random_int_v = rng.gen_range(1..=100); random_int_v <= num } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RedisCompressionConfig { + pub compEnabled: bool, + pub dictId: String, + pub compLevel: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RedisCompressionConfigCombined { + pub redisCompressionConfig: Option>, + pub isRedisCompEnabled: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RedisDataStruct { + STRING, + HASHMAP, + STREAM, + #[serde(rename = "STREAM_V2")] + StreamV2, +} + +impl RedisDataStruct { + pub fn as_str(&self) -> &'static str { + match self { + Self::STRING => "STRING", + Self::HASHMAP => "HASHMAP", + Self::STREAM => "STREAM", + Self::StreamV2 => "STREAM_V2", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RedisCompEnabledMerchant { + pub rc_merchant_id: String, + pub rc_rollout: u32, + pub enabled_dict_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RedisCompFeatureConf { + pub rc_enable_all: bool, + pub rc_enable_all_rollout: Option, + pub global_dict_id: String, + pub rc_disable_any: Option>, + pub explicit_enabled_merchants: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RedisCompressionCutover { + MultiDataStructCutover(HashMap), + StringDataStructCutover(RedisCompFeatureConf), +} + +pub async fn check_redis_comp_merchant_flag( + mid: String, +) -> Option> { + let mb_conf = findByNameFromRedis::( + "redis_compression_merchant_cutover".to_string(), + ) + .await; + + match mb_conf { + Some(RedisCompressionCutover::MultiDataStructCutover(ds_map)) => { + let mut final_map = HashMap::new(); + + for (data_struct_key, conf) in ds_map.iter() { + if let Some(dict_id) = + check_merchant_enabled_for_compression(Some(conf.clone()), &mid).await + { + let redis_comp_config = RedisCompressionConfig { + compEnabled: true, + dictId: dict_id, + compLevel: None, + }; + final_map.insert(data_struct_key.as_str().to_string(), redis_comp_config); + } + } + + if !final_map.is_empty() { + logger::info!( + "Redis compression config set for merchant {}: {:?}", + mid, + final_map + ); + Some(final_map) + } else { + None + } + } + Some(RedisCompressionCutover::StringDataStructCutover(conf)) => { + if let Some(dict_id) = check_merchant_enabled_for_compression(Some(conf), &mid).await { + let redis_comp_config = RedisCompressionConfig { + compEnabled: true, + dictId: dict_id, + compLevel: None, + }; + let mut final_map = HashMap::new(); + final_map.insert("STRING".to_string(), redis_comp_config); + + logger::info!( + "Redis compression config set for merchant {}: {:?}", + mid, + final_map + ); + Some(final_map) + } else { + None + } + } + None => None, + } +} + +async fn check_merchant_enabled_for_compression( + maybe_conf: Option, + mid: &str, +) -> Option { + match maybe_conf { + Some(conf) => { + // First check if merchant is explicitly enabled + if let Some(dict_id_explicit) = is_merchant_enabled_explicitly(&conf, mid).await { + return Some(dict_id_explicit); + } + + // Check global enable with rollout + let maybe_global_dict_id = if conf.rc_enable_all { + match conf.rc_enable_all_rollout { + Some(rollout) => { + // Generate random number for rollout decision + let mut rng = rand::thread_rng(); + let random_int_v: u32 = rng.gen_range(1..=100); + + if random_int_v <= rollout { + Some(conf.global_dict_id.clone()) + } else { + None + } + } + None => Some(conf.global_dict_id.clone()), + } + } else { + None + }; + + is_merchant_enabled_after_disable_check(&conf, mid, maybe_global_dict_id) + } + None => None, + } +} + +fn is_merchant_enabled_after_disable_check( + conf: &RedisCompFeatureConf, + mid: &str, + result: Option, +) -> Option { + match (result, &conf.rc_disable_any) { + (Some(dict_id), Some(disable_list)) => { + // Check if merchant is NOT in the disable list + let mid_lower = mid.to_lowercase(); + let is_disabled = disable_list + .iter() + .any(|disabled_mid| disabled_mid.to_lowercase() == mid_lower); + + if is_disabled { + None + } else { + Some(dict_id) + } + } + (res, _) => res, + } +} + +async fn is_merchant_enabled_explicitly(conf: &RedisCompFeatureConf, mid: &str) -> Option { + let list = &conf.explicit_enabled_merchants; + let mid_lower = mid.to_lowercase(); + + // Find the merchant configuration + let opt_m_conf = list + .iter() + .find(|m_conf| m_conf.rc_merchant_id.to_lowercase() == mid_lower); + + match opt_m_conf { + Some(m_conf) => { + // Generate random number for rollout decision + let mut rng = rand::thread_rng(); + let random_int_v: u32 = rng.gen_range(1..=100); + + if random_int_v <= m_conf.rc_rollout { + // Return the enabled dict ID if present, otherwise use the global dict ID + m_conf + .enabled_dict_id + .clone() + .or_else(|| Some(conf.global_dict_id.clone())) + } else { + None + } + } + None => None, + } +} diff --git a/src/redis/mem_cache.rs b/src/redis/mem_cache.rs new file mode 100644 index 00000000..ef2d513e --- /dev/null +++ b/src/redis/mem_cache.rs @@ -0,0 +1,172 @@ +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; + +// Global in-memory cache instance +pub static GLOBAL_CACHE: Lazy = Lazy::new(|| Registry::new(1000)); + +/// In-memory key–value cache with optional TTL expiration and simple eviction. +/// +/// ## Eviction Strategy +/// +/// The cache enforces a maximum capacity (`max_size`). +/// Eviction happens during `store()` when the cache is full **after removing expired entries**. +/// +/// The strategy works in two phases: +/// +/// 1. **Lazy Expiration Cleanup** +/// - Expired keys (based on `expires_at`) are removed during `store()` and `get()`. +/// - No background thread is used for cleanup — expiration is checked only on access. +/// +/// 2. **Size-Based Eviction** +/// - If the cache is still full after removing expired entries, the cache removes the +/// *oldest inserted key*. +/// (The implementation relies on the ordering of `HashMap::keys().next()` which effectively +/// removes an arbitrary older entry — simple FIFO-like eviction, not LRU.) + +#[derive(Debug)] +pub struct Registry { + data: Arc>>, + max_size: usize, +} + +#[derive(Debug, Clone)] +struct CacheEntry { + value: serde_json::Value, + expires_at: Option, +} + +impl Registry { + pub fn new(max_size: usize) -> Self { + Self { + data: Arc::new(RwLock::new(HashMap::new())), + max_size, + } + } + + pub fn get(&self, key: &str) -> Result + where + T: serde::de::DeserializeOwned, + { + { + let data = self + .data + .read() + .map_err(|e| format!("Read lock error: {}", e))?; + + if let Some(entry) = data.get(key) { + // Check if entry has expired + if let Some(expires_at) = entry.expires_at { + if Instant::now() > expires_at { + // Entry expired, need to remove it (drop read lock first) + drop(data); + } else { + // Entry is valid, return it + return serde_json::from_value(entry.value.clone()) + .map_err(|e| format!("Deserialization error: {}", e)); + } + } else { + // No expiration, return it + return serde_json::from_value(entry.value.clone()) + .map_err(|e| format!("Deserialization error: {}", e)); + } + } else { + return Err("Key not found".to_string()); + } + } + + // If we get here, the entry was expired and we need to remove it + let mut data = self + .data + .write() + .map_err(|e| format!("Write lock error: {}", e))?; + + // Double-check the entry is still there and expired + if let Some(entry) = data.get(key) { + if let Some(expires_at) = entry.expires_at { + if Instant::now() > expires_at { + data.remove(key); + return Err("Key expired".to_string()); + } + // If not expired anymore, return the value + return serde_json::from_value(entry.value.clone()) + .map_err(|e| format!("Deserialization error: {}", e)); + } + } + + Err("Key not found".to_string()) + } + + /// Inserts a value in the cache, applying TTL and eviction rules. + /// + /// - Expired entries are removed first. + /// - If capacity is still exceeded, the oldest remaining entry is evicted. + pub fn store(&self, key: String, value: T, ttl_seconds: Option) -> Result<(), String> + where + T: serde::Serialize, + { + let mut data = self + .data + .write() + .map_err(|e| format!("Write lock error: {}", e))?; + + // Remove expired entries and enforce max size + self.cleanup_expired(&mut data); + + if data.len() >= self.max_size { + // Remove oldest entry (simple eviction policy) + if let Some(oldest_key) = data.keys().next().cloned() { + data.remove(&oldest_key); + } + } + + let json_value = + serde_json::to_value(value).map_err(|e| format!("Serialization error: {}", e))?; + + let expires_at = ttl_seconds.map(|ttl| Instant::now() + Duration::from_secs(ttl)); + + data.insert( + key, + CacheEntry { + value: json_value, + expires_at, + }, + ); + + Ok(()) + } + + fn cleanup_expired(&self, data: &mut HashMap) { + let now = Instant::now(); + data.retain(|_, entry| { + if let Some(expires_at) = entry.expires_at { + now <= expires_at + } else { + true + } + }); + } + + pub fn remove(&self, key: &str) -> Result<(), String> { + let mut data = self + .data + .write() + .map_err(|e| format!("Write lock error: {}", e))?; + data.remove(key); + Ok(()) + } + + pub fn size(&self) -> usize { + self.data.read().unwrap().len() + } + + pub fn clear(&self) -> Result<(), String> { + let mut data = self + .data + .write() + .map_err(|e| format!("Write lock error: {}", e))?; + data.clear(); + Ok(()) + } +} diff --git a/src/redis/types.rs b/src/redis/types.rs index d9d55596..0f05b5f4 100644 --- a/src/redis/types.rs +++ b/src/redis/types.rs @@ -57,21 +57,13 @@ pub struct FeatureDimension { } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum DimensionType { - #[serde(rename = "JUSPAY_BANK_CODE")] - JUSPAY_BANK_CODE, - - #[serde(rename = "GATEWAY")] - GATEWAY, - - #[serde(rename = "CARD_BRAND")] - CARD_BRAND, - - #[serde(rename = "SCOF")] - SCOF, - - #[serde(rename = "FIDO")] - FIDO, + JuspayBankCode, + Gateway, + CardBrand, + Scof, + Fido, } pub trait ServiceConfigKey { diff --git a/src/routes.rs b/src/routes.rs index cee46491..484b138d 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,4 +1,6 @@ +pub mod hybrid_routing; // pub mod data; +pub mod analytics; pub mod decide_gateway; pub mod decision_gateway; pub mod health; diff --git a/src/routes/analytics.rs b/src/routes/analytics.rs new file mode 100644 index 00000000..918c152a --- /dev/null +++ b/src/routes/analytics.rs @@ -0,0 +1,227 @@ +use crate::analytics::{ + decisions as fetch_decisions, gateway_scores as fetch_gateway_scores, + log_summaries as fetch_log_summaries, overview as fetch_overview, parse_payment_audit_query, + parse_query, payment_audit as fetch_payment_audit, preview_trace as fetch_preview_trace, + routing_stats as fetch_routing_stats, +}; +use crate::custom_extractors::TenantStateResolver; +use crate::error; +use axum::extract::Query; +use axum::Json; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Debug, Clone, Deserialize)] +pub struct AnalyticsQueryParams { + pub merchant_id: Option, + pub scope: Option, + pub range: Option, + pub start_ms: Option, + pub end_ms: Option, + pub page: Option, + pub page_size: Option, + pub payment_method_type: Option, + pub payment_method: Option, + pub card_network: Option, + pub card_is_in: Option, + pub currency: Option, + pub country: Option, + pub auth_type: Option, + pub gateway: Option, + pub payment_id: Option, + pub request_id: Option, + pub route: Option, + pub status: Option, + pub event_type: Option, + pub error_code: Option, +} + +pub fn serve() -> axum::Router> { + axum::Router::>::new() + .route("/", axum::routing::get(overview)) + .route("/overview", axum::routing::get(overview)) + .route("/gateway-scores", axum::routing::get(gateway_scores)) + .route("/decisions", axum::routing::get(decisions)) + .route("/routing-stats", axum::routing::get(routing_stats)) + .route("/log-summaries", axum::routing::get(log_summaries)) + .route("/payment-audit", axum::routing::get(payment_audit)) + .route("/preview-trace", axum::routing::get(preview_trace)) +} + +pub async fn overview( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result, error::ContainerError> +{ + let query = parse_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_method_type, + params.payment_method, + params.card_network, + params.card_is_in, + params.currency, + params.country, + params.auth_type, + params.gateway, + ); + let response = fetch_overview(&state, &query).await?; + Ok(Json(response)) +} + +pub async fn gateway_scores( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result< + Json, + error::ContainerError, +> { + let query = parse_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_method_type, + params.payment_method, + params.card_network, + params.card_is_in, + params.currency, + params.country, + params.auth_type, + params.gateway, + ); + Ok(Json(fetch_gateway_scores(&state, &query).await?)) +} + +pub async fn decisions( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result, error::ContainerError> +{ + let query = parse_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_method_type, + params.payment_method, + params.card_network, + params.card_is_in, + params.currency, + params.country, + params.auth_type, + params.gateway, + ); + Ok(Json(fetch_decisions(&state, &query).await?)) +} + +pub async fn routing_stats( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result< + Json, + error::ContainerError, +> { + let query = parse_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_method_type, + params.payment_method, + params.card_network, + params.card_is_in, + params.currency, + params.country, + params.auth_type, + params.gateway, + ); + Ok(Json(fetch_routing_stats(&state, &query).await?)) +} + +pub async fn log_summaries( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result< + Json, + error::ContainerError, +> { + let query = parse_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_method_type, + params.payment_method, + params.card_network, + params.card_is_in, + params.currency, + params.country, + params.auth_type, + params.gateway, + ); + Ok(Json(fetch_log_summaries(&state, &query).await?)) +} + +pub async fn payment_audit( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result, error::ContainerError> { + let query = parse_payment_audit_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_id, + params.request_id, + params.gateway, + params.route, + params.status, + params.event_type, + params.error_code, + ); + Ok(Json(fetch_payment_audit(&state, &query).await?)) +} + +pub async fn preview_trace( + TenantStateResolver(state): TenantStateResolver, + Query(params): Query, +) -> Result, error::ContainerError> { + let query = parse_payment_audit_query( + params.merchant_id, + params.scope, + params.range, + params.start_ms, + params.end_ms, + params.page, + params.page_size, + params.payment_id, + params.request_id, + params.gateway, + params.route, + params.status, + params.event_type, + params.error_code, + ); + Ok(Json(fetch_preview_trace(&state, &query).await?)) +} diff --git a/src/routes/decide_gateway.rs b/src/routes/decide_gateway.rs index 8b79d23b..908afc02 100644 --- a/src/routes/decide_gateway.rs +++ b/src/routes/decide_gateway.rs @@ -1,6 +1,8 @@ +use std::time::Instant; + use crate::{ decider::gatewaydecider::{ - flow_new::deciderFullPayloadHSFunction, + flow_new::decider_full_payload_hs_function, types::{DecidedGateway, DomainDeciderRequestForApiCallV2, ErrorResponse, UnifiedError}, }, logger, metrics, @@ -35,6 +37,7 @@ impl IntoResponse for DecidedGateway { pub async fn decide_gateway( req: axum::http::Request, ) -> Result { + let cpu_start = Instant::now(); let timer = metrics::API_LATENCY_HISTOGRAM .with_label_values(&["decide_gateway"]) .start_timer(); @@ -42,7 +45,12 @@ pub async fn decide_gateway( .with_label_values(&["decide_gateway"]) .inc(); - let headers = req.headers(); + let headers = req.headers().clone(); + let x_request_id = headers + .get("x-request-id") + .and_then(|value| value.to_str().ok()) + .unwrap_or("unknown") + .to_string(); for (name, value) in headers.iter() { logger::debug!(tag = "DecideGateway", "Header: {}: {:?}", name, value); } @@ -53,6 +61,31 @@ pub async fn decide_gateway( } Err(e) => { logger::debug!(tag = "DecideGateway", "Error: {:?}", e); + crate::analytics::record_error_event( + "decide_gateway", + None, + None, + Some(x_request_id.clone()), + None, + None, + "400".to_string(), + "Error parsing request".to_string(), + serde_json::to_string(&serde_json::json!({ + "request_id": x_request_id, + "response": { + "status": "400", + "error_code": "400", + "error_message": "Error parsing request", + "error_info": { + "code": "INVALID_INPUT", + "user_message": "Invalid request params. Please verify your input.", + "developer_message": e.to_string(), + } + } + })) + .ok(), + Some("request_parse_failed".to_string()), + ); metrics::API_REQUEST_COUNTER .with_label_values(&["decide_gateway", "failure"]) .inc(); @@ -77,23 +110,125 @@ pub async fn decide_gateway( let api_decider_request: Result = serde_json::from_slice(&body); let result = match api_decider_request { - Ok(payload) => match deciderFullPayloadHSFunction(payload).await { - Ok(decided_gateway) => { - metrics::API_REQUEST_COUNTER - .with_label_values(&["decide_gateway", "success"]) - .inc(); - Ok(decided_gateway) - } - Err(e) => { - logger::debug!(tag = "DecideGateway", "Error: {:?}", e); - metrics::API_REQUEST_COUNTER - .with_label_values(&["decide_gateway", "failure"]) - .inc(); - Err(e) + Ok(payload) => { + crate::analytics::record_request_hit_event( + "decide_gateway", + Some(payload.merchant_id.clone()), + Some(payload.payment_id().to_string()), + Some(x_request_id.clone()), + ); + match decider_full_payload_hs_function(payload.clone(), cpu_start).await { + Ok(decided_gateway) => { + let routing_approach = serde_json::to_string(&decided_gateway.routing_approach) + .unwrap_or_else(|_| format!("{:?}", decided_gateway.routing_approach)) + .trim_matches('"') + .to_string(); + + crate::analytics::record_decision_event( + Some(payload.merchant_id.clone()), + Some(routing_approach), + Some(decided_gateway.decided_gateway.clone()), + Some("success".to_string()), + "decide_gateway", + decided_gateway.priority_logic_tag.clone(), + serde_json::to_string(&serde_json::json!({ + "request": &payload, + "response": &decided_gateway, + "score_context": decided_gateway.gateway_priority_map.clone(), + "selection_reason": { + "decided_gateway": decided_gateway.decided_gateway.clone(), + "routing_approach": decided_gateway.routing_approach.clone(), + "gateway_before_evaluation": decided_gateway.gateway_before_evaluation.clone(), + "priority_logic_tag": decided_gateway.priority_logic_tag.clone(), + "reset_approach": decided_gateway.reset_approach.clone(), + } + })) + .ok(), + Some(payload.payment_id().to_string()), + Some(x_request_id.clone()), + Some("gateway_decided".to_string()), + Some(payload.payment_method_type().to_string()), + Some(payload.payment_method().to_string()), + ); + metrics::API_REQUEST_COUNTER + .with_label_values(&["decide_gateway", "success"]) + .inc(); + Ok(decided_gateway) + } + Err(e) => { + logger::debug!(tag = "DecideGateway", "Error: {:?}", e); + crate::analytics::record_error_event( + "decide_gateway", + Some(payload.merchant_id.clone()), + Some(payload.payment_id().to_string()), + Some(x_request_id.clone()), + None, + e.routing_approach.clone().map(|approach| { + serde_json::to_string(&approach) + .unwrap_or_else(|_| format!("{:?}", approach)) + .trim_matches('"') + .to_string() + }), + e.error_code.clone(), + e.error_message.clone(), + serde_json::to_string(&serde_json::json!({ + "request_id": x_request_id, + "request": &payload, + "routing_approach": e.routing_approach.clone(), + "response": { + "status": e.status.clone(), + "error_code": e.error_code.clone(), + "error_message": e.error_message.clone(), + "priority_logic_tag": e.priority_logic_tag.clone(), + "routing_approach": e.routing_approach.clone(), + "filter_wise_gateways": e.filter_wise_gateways.clone(), + "priority_logic_output": e.priority_logic_output.clone(), + "is_dynamic_mga_enabled": e.is_dynamic_mga_enabled, + "error_info": { + "code": e.error_info.code.clone(), + "user_message": e.error_info.user_message.clone(), + "developer_message": e.error_info.developer_message.clone(), + } + } + })) + .ok(), + Some("request_failed".to_string()), + ); + metrics::API_REQUEST_COUNTER + .with_label_values(&["decide_gateway", "failure"]) + .inc(); + Err(e) + } } - }, + } Err(e) => { logger::debug!(tag = "DecideGateway", "Error: {:?}", e); + crate::analytics::record_error_event( + "decide_gateway", + None, + None, + Some(x_request_id.clone()), + None, + None, + "400".to_string(), + "Error parsing request".to_string(), + serde_json::to_string(&serde_json::json!({ + "request_id": x_request_id, + "raw_request": String::from_utf8_lossy(&body).to_string(), + "response": { + "status": "400", + "error_code": "400", + "error_message": "Error parsing request", + "error_info": { + "code": "INVALID_INPUT", + "user_message": "Invalid request params. Please verify your input.", + "developer_message": e.to_string(), + } + } + })) + .ok(), + Some("request_parse_failed".to_string()), + ); metrics::API_REQUEST_COUNTER .with_label_values(&["decide_gateway", "failure"]) .inc(); diff --git a/src/routes/decision_gateway.rs b/src/routes/decision_gateway.rs index 73eb8256..4a2b18c2 100644 --- a/src/routes/decision_gateway.rs +++ b/src/routes/decision_gateway.rs @@ -1,14 +1,20 @@ +use std::time::Instant; + use crate::decider::gatewaydecider::{ - flows::deciderFullPayloadHSFunction, + flows::decider_full_payload_hs_function, types::{DecidedGateway, DomainDeciderRequest, ErrorResponse, UnifiedError}, }; +use crate::logger; use crate::metrics::{API_LATENCY_HISTOGRAM, API_REQUEST_COUNTER, API_REQUEST_TOTAL_COUNTER}; -use crate::{logger, metrics}; +use crate::redis::feature::{ + check_redis_comp_merchant_flag, is_feature_enabled, RedisCompressionConfig, + RedisCompressionConfigCombined, +}; use axum::body::to_bytes; use axum::http::StatusCode; use axum::response::IntoResponse; -use cpu_time::ProcessTime; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; impl IntoResponse for DecidedGatewayResponse { fn into_response(self) -> axum::http::Response { @@ -36,6 +42,7 @@ impl IntoResponse for ErrorResponse { pub struct DecidedGatewayResponse { pub decided_gateway: DecidedGateway, pub filter_list: Vec<(String, Vec)>, + pub latency: Option, } #[axum::debug_handler] @@ -46,7 +53,7 @@ where DecidedGatewayResponse: IntoResponse, ErrorResponse: IntoResponse, { - let cpu_start = ProcessTime::now(); + let cpu_start = Instant::now(); let timer = API_LATENCY_HISTOGRAM .with_label_values(&["decision_gateway"]) .start_timer(); @@ -65,7 +72,11 @@ where let request_time = time::OffsetDateTime::now_utc() .format(&time::format_description::well_known::Rfc3339) .unwrap_or_else(|_| "unknown".to_string()); - let query_params = original_url.splitn(2, '?').nth(1).unwrap_or("").to_string(); + let query_params = original_url + .split_once('?') + .map(|x| x.1) + .unwrap_or("") + .to_string(); // Extract request headers as a JSON string // let req_headers = serde_json::to_string(&headers).unwrap_or("{}".to_string()); @@ -128,12 +139,39 @@ where match api_decider_request { Ok(payload) => { let merchant_id = payload.orderReference.merchantId.clone(); + let merchant_id_string = + crate::types::merchant::id::merchant_id_to_text(merchant_id.clone()); + + let redis_comp_config: Option> = + check_redis_comp_merchant_flag(merchant_id_string.clone()).await; + + let redis_comp_enabled_de = is_feature_enabled( + "REDIS_COMPRESSION_ENABLED_MERCHANT_DE".to_string(), + merchant_id_string.clone(), + "kv_redis".to_string(), + ) + .await; + + let redis_comp_config_final = Some(RedisCompressionConfigCombined { + redisCompressionConfig: redis_comp_config, + isRedisCompEnabled: redis_comp_enabled_de, + }); + let merchant_id_txt = crate::types::merchant::id::merchant_id_to_text(merchant_id); tracing::Span::current().record("merchant_id", merchant_id_txt.clone()); + tracing::Span::current().record("udf_txn_uuid", payload.txnDetail.txnUuid.clone()); + tracing::Span::current().record("txn_uuid", payload.txnDetail.txnUuid.clone()); + tracing::Span::current() + .record("udf_order_id", payload.orderReference.orderId.0.as_str()); + tracing::Span::current().record( + "is_audit_trail_log", + payload.shouldConsumeResult.unwrap_or(false), + ); jemalloc_ctl::epoch::advance().unwrap(); let allocated_before = jemalloc_ctl::stats::allocated::read().unwrap_or(0); - let result = deciderFullPayloadHSFunction(payload.clone()).await; + let result = + decider_full_payload_hs_function(payload.clone(), redis_comp_config_final).await; jemalloc_ctl::epoch::advance().unwrap(); let allocated_after = jemalloc_ctl::stats::allocated::read().unwrap_or(0); @@ -141,11 +179,12 @@ where let final_result = match result { Ok((decided_gateway, filter_list)) => { + let cpu_time = cpu_start.elapsed().as_millis() as u64; let response = DecidedGatewayResponse { decided_gateway, filter_list, + latency: Some(cpu_time), }; - // Serialize response body and headers for logging let res_body = serde_json::to_string(&response).unwrap_or("{}".to_string()); // let res_headers = r#"{"Content-Type": "application/json"}"#; @@ -166,7 +205,7 @@ where env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string()), action = "POST", - req_body = String::from_utf8_lossy(&body).to_string(), + req_body = format!("{:?}", payload), req_headers = format!("{:?}", headers), res_body = res_body, res_code = 200, @@ -183,6 +222,7 @@ where Err(e) => { let latency = start_time.elapsed().as_millis() as u64; let cpu_time = cpu_start.elapsed().as_millis() as u64; + logger::error!( url = original_url, method = "POST", diff --git a/src/routes/hybrid_routing.rs b/src/routes/hybrid_routing.rs new file mode 100644 index 00000000..587b3179 --- /dev/null +++ b/src/routes/hybrid_routing.rs @@ -0,0 +1,267 @@ +use crate::decider::gatewaydecider::flow_new::decider_full_payload_hs_function; +use crate::decider::gatewaydecider::types::DecidedGateway; +use crate::error::ContainerError; +use crate::euclid::ast::ConnectorInfo; +use crate::euclid::errors::EuclidErrors; +use crate::euclid::handlers::routing_rules::routing_evaluate; +use crate::metrics::{API_LATENCY_HISTOGRAM, API_REQUEST_COUNTER, API_REQUEST_TOTAL_COUNTER}; +use crate::types::hybrid_routing::HybridRoutingRequest; +use axum::{response::IntoResponse, Json}; +use serde::Serialize; +use std::time::Instant; + +fn to_json_value_or_invalid( + value: &T, + context: &str, +) -> Result> { + serde_json::to_value(value).map_err(|err| { + crate::logger::error!( + serialization_context = context, + "Failed to serialize hybrid routing response field: {}", + err + ); + EuclidErrors::FailedToSerializeJsonToString.into() + }) +} + +fn insert_serialized( + map: &mut serde_json::Map, + key: &str, + value: &T, + context: &str, +) -> Result<(), ContainerError> { + to_json_value_or_invalid(value, context).map(|json_value| { + map.insert(key.to_string(), json_value); + }) +} + +fn to_logged_success_response( + map: serde_json::Map, +) -> axum::response::Response { + let response_value = serde_json::Value::Object(map); + crate::logger::debug!(decision_engine_success_response = ?response_value); + Json(response_value).into_response() +} + +fn log_serializable_response(label: &str, value: &T) { + match serde_json::to_value(value) { + Ok(response_value) => { + crate::logger::debug!(decision_engine_response_label = label, decision_engine_response = ?response_value) + } + Err(err) => crate::logger::warn!( + decision_engine_response_label = label, + "Failed to serialize response for logging: {}", + err + ), + } +} + +/// Extracts ordered connector blobs from static routing output. +/// +/// Order is preserved intentionally because static routing can return +/// connector priority, and dynamic routing consumes this as candidate order. +fn extract_static_eligible_gateways( + response: &crate::euclid::types::RoutingEvaluateResponse, +) -> Vec { + response.eligible_connectors.clone() +} + +fn extract_gateway_names(connectors: &[ConnectorInfo]) -> Vec { + connectors + .iter() + .map(|connector| connector.gateway_name.clone()) + .collect::>() +} + +fn parse_dynamic_connector(connector_with_id: &str) -> ConnectorInfo { + match connector_with_id.split_once(':') { + Some((gateway_name, gateway_id)) => ConnectorInfo { + gateway_name: gateway_name.to_string(), + gateway_id: Some(gateway_id.to_string()), + }, + None => ConnectorInfo { + gateway_name: connector_with_id.to_string(), + gateway_id: None, + }, + } +} + +#[derive(Serialize)] +struct DynamicRoutingEnvelope { + status: &'static str, + decision: Option, + fallback_connectors: Option>, +} + +/// Stores the normalized client-facing connector field. +/// +/// Clients should rely on `evaluated_connectors` for connector consumption +/// instead of branching on static/dynamic payload shapes. +fn insert_evaluated_connectors( + map: &mut serde_json::Map, + connectors: &[ConnectorInfo], +) -> Result<(), ContainerError> { + insert_serialized( + map, + "evaluated_connectors", + &connectors.to_vec(), + "evaluated_connectors", + ) +} + +#[axum::debug_handler] +pub async fn hybrid_routing_evaluate( + Json(payload): Json, +) -> Result> { + let timer = API_LATENCY_HISTOGRAM + .with_label_values(&["hybrid_routing_evaluate"]) + .start_timer(); + API_REQUEST_TOTAL_COUNTER + .with_label_values(&["hybrid_routing_evaluate"]) + .inc(); + + let HybridRoutingRequest { + static_routing_request, + dynamic_routing_request, + } = payload; + + let is_empty_request = static_routing_request.is_none() && dynamic_routing_request.is_none(); + + let (static_routing_response, static_routing_error, static_fallback_gateways) = + match static_routing_request { + Some(req) => { + // Preserve static fallback connectors even when static evaluation fails, + // so dynamic can still run with a bounded candidate set. + let fallback_gateways = req.fallback_output.clone(); + + match routing_evaluate(Json(req)).await { + Ok(response) => (Some(response.0), None, fallback_gateways), + Err(err) => (None, Some(err), fallback_gateways), + } + } + None => (None, None, None), + }; + + // Prefer static evaluated connectors over static fallback connectors + // when auto-populating dynamic candidates. + let static_eligible_gateways = static_routing_response + .as_ref() + .map(extract_static_eligible_gateways); + + let dynamic_fallback_gateways = static_eligible_gateways + .clone() + .or(static_fallback_gateways); + + let dynamic_eval_result = match dynamic_routing_request { + Some(mut req) => { + // Request-provided dynamic list has precedence. + // Static-derived list is only used when request list is absent/empty. + let request_eligible_gateways = match req.eligible_gateway_list.take() { + Some(gateways) if gateways.is_empty() => None, + Some(gateways) => Some(gateways), + None => None, + }; + let fallback_eligible_gateways = dynamic_fallback_gateways + .clone() + .map(|connectors| extract_gateway_names(&connectors)); + req.eligible_gateway_list = request_eligible_gateways.or(fallback_eligible_gateways); + Some(decider_full_payload_hs_function(req, Instant::now()).await) + } + None => None, + }; + + let mut res = serde_json::Map::new(); + + let static_insert_result = match static_routing_response.as_ref() { + Some(static_response) => insert_serialized( + &mut res, + "static_routing", + static_response, + "static_routing", + ), + None => Ok(()), + }; + + let response_result = match ( + is_empty_request, + dynamic_eval_result, + dynamic_fallback_gateways, + static_eligible_gateways, + static_routing_error, + ) { + (true, _, _, _, _) => Err(EuclidErrors::InvalidRequest( + "At least one of static_routing_request or dynamic_routing_request must be provided." + .to_string(), + ) + .into()), + (false, Some(Ok(dynamic_ok)), _, _, _) => { + // Dynamic winner is the first-class output for normalized connector field. + let dynamic_connector = vec![parse_dynamic_connector(&dynamic_ok.decided_gateway)]; + let dynamic_payload = DynamicRoutingEnvelope { + status: "success", + decision: Some(dynamic_ok), + fallback_connectors: None, + }; + insert_serialized( + &mut res, + "dynamic_routing", + &dynamic_payload, + "dynamic_routing", + ) + .and_then(|_| insert_evaluated_connectors(&mut res, &dynamic_connector)) + .map(|_| (to_logged_success_response(res), "success")) + } + (false, Some(Err(_dynamic_err)), Some(dynamic_fallback), _, _) => { + // Graceful degradation: when dynamic fails but fallback connectors exist, + // return fallback connectors instead of hard-failing. + let dynamic_payload = DynamicRoutingEnvelope { + status: "fallback", + decision: None, + fallback_connectors: Some(dynamic_fallback.clone()), + }; + insert_serialized( + &mut res, + "dynamic_routing", + &dynamic_payload, + "dynamic_routing", + ) + .and_then(|_| insert_evaluated_connectors(&mut res, &dynamic_fallback)) + .map(|_| (to_logged_success_response(res), "success")) + } + (false, Some(Err(dynamic_err)), None, _, _) => { + log_serializable_response("dynamic_error_response", &dynamic_err); + Ok((dynamic_err.into_response(), "failure")) + } + (false, None, _, Some(static_gateways), _) => { + insert_evaluated_connectors(&mut res, &static_gateways) + .map(|_| (to_logged_success_response(res), "success")) + } + (false, None, Some(dynamic_fallback), None, _) => { + insert_evaluated_connectors(&mut res, &dynamic_fallback) + .map(|_| (to_logged_success_response(res), "success")) + } + (false, None, _, None, Some(static_err)) => { + crate::logger::debug!(decision_engine_response_label = "static_error_response", error = ?static_err); + Ok((static_err.into_response(), "failure")) + } + (false, None, _, None, None) => Ok((to_logged_success_response(res), "success")), + }; + + let final_result = static_insert_result.and(response_result); + let api_result = match final_result { + Ok((response, metric_status)) => { + API_REQUEST_COUNTER + .with_label_values(&["hybrid_routing_evaluate", metric_status]) + .inc(); + Ok(response) + } + Err(err) => { + API_REQUEST_COUNTER + .with_label_values(&["hybrid_routing_evaluate", "failure"]) + .inc(); + Err(err) + } + }; + timer.observe_duration(); + api_result +} diff --git a/src/routes/merchant_account_config.rs b/src/routes/merchant_account_config.rs index af7aedf3..acb6a93f 100644 --- a/src/routes/merchant_account_config.rs +++ b/src/routes/merchant_account_config.rs @@ -1,8 +1,23 @@ use crate::metrics::{API_LATENCY_HISTOGRAM, API_REQUEST_COUNTER, API_REQUEST_TOTAL_COUNTER}; use crate::types::merchant as ETM; -use crate::{error, logger, metrics, types}; +use crate::{error, logger}; use axum::{extract::Path, Json}; use error_stack::ResultExt; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MerchantAccountCreateResponse { + pub message: String, + pub merchant_id: String, + pub gateway_success_rate_based_decider_input: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MerchantAccountDeleteResponse { + pub message: String, + pub merchant_id: String, +} + #[axum::debug_handler] pub async fn get_merchant_config( Path(merchant_id): Path, @@ -49,7 +64,10 @@ pub async fn get_merchant_config( #[axum::debug_handler] pub async fn create_merchant_config( Json(payload): Json, -) -> Result, error::ContainerError> { +) -> Result< + Json, + error::ContainerError, +> { // Record total request count and start timer API_REQUEST_TOTAL_COUNTER .with_label_values(&["merchant_account_create"]) @@ -63,6 +81,10 @@ pub async fn create_merchant_config( payload ); + let merchant_id = payload.merchant_id.clone(); + let gateway_success_rate_based_decider_input = + payload.gateway_success_rate_based_decider_input.clone(); + let merchant_account = ETM::merchant_account::load_merchant_by_merchant_id(payload.merchant_id.clone()).await; @@ -84,7 +106,11 @@ pub async fn create_merchant_config( API_REQUEST_COUNTER .with_label_values(&["merchant_account_create", "success"]) .inc(); - Ok(Json("Merchant account created successfully".to_string())) + Ok(Json(MerchantAccountCreateResponse { + message: "Merchant account created successfully".to_string(), + merchant_id, + gateway_success_rate_based_decider_input, + })) } Err(e) => { API_REQUEST_COUNTER @@ -101,7 +127,10 @@ pub async fn create_merchant_config( #[axum::debug_handler] pub async fn delete_merchant_config( Path(merchant_id): Path, -) -> Result, error::ContainerError> { +) -> Result< + Json, + error::ContainerError, +> { // Record total request count and start timer API_REQUEST_TOTAL_COUNTER .with_label_values(&["merchant_account_delete"]) @@ -115,7 +144,7 @@ pub async fn delete_merchant_config( merchant_id ); - let result = ETM::merchant_account::delete_merchant_account(merchant_id) + let result = ETM::merchant_account::delete_merchant_account(merchant_id.clone()) .await .change_context(error::MerchantAccountConfigurationError::MerchantDeletionFailed); @@ -124,7 +153,10 @@ pub async fn delete_merchant_config( API_REQUEST_COUNTER .with_label_values(&["merchant_account_delete", "success"]) .inc(); - Ok(Json("Merchant account deleted successfully".to_string())) + Ok(Json(MerchantAccountDeleteResponse { + message: "Merchant account deleted successfully".to_string(), + merchant_id, + })) } Err(e) => { API_REQUEST_COUNTER diff --git a/src/routes/rule_configuration.rs b/src/routes/rule_configuration.rs index e558f574..3f8dbe69 100644 --- a/src/routes/rule_configuration.rs +++ b/src/routes/rule_configuration.rs @@ -3,11 +3,25 @@ use crate::types::merchant as ETM; use crate::{error, logger, types}; use axum::Json; use error_stack::ResultExt; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RuleConfigResponse { + pub message: String, + pub merchant_id: String, + pub config: types::routing_configuration::ConfigVariant, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RuleConfigDeleteResponse { + pub message: String, + pub merchant_id: String, +} #[axum::debug_handler] pub async fn create_rule_config( Json(payload): Json, -) -> Result, error::ContainerError> { +) -> Result, error::ContainerError> { let timer = API_LATENCY_HISTOGRAM .with_label_values(&["create_rule_config"]) .start_timer(); @@ -16,6 +30,9 @@ pub async fn create_rule_config( .inc(); logger::debug!("Received rule configuration: {:?}", payload); + let merchant_id = payload.merchant_id.clone(); + let config = payload.config.clone(); + let mid = payload.merchant_id.clone(); // Check if merchant exists @@ -31,9 +48,9 @@ pub async fn create_rule_config( } let result = match payload.config { - types::routing_configuration::ConfigVariant::SuccessRate(config) => { + types::routing_configuration::ConfigVariant::SuccessRate(success_config) => { let name = format!("SR_V3_INPUT_CONFIG_{}", mid); - let config = serde_json::to_string(&config) + let serialized_config = serde_json::to_string(&success_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; // Check if config already exists @@ -49,7 +66,7 @@ pub async fn create_rule_config( Err(error::RuleConfigurationError::ConfigurationAlreadyExists.into()) } None => { - match types::service_configuration::insert_config(name, Some(config)) + match types::service_configuration::insert_config(name, Some(serialized_config)) .await .change_context(error::RuleConfigurationError::StorageError) { @@ -57,9 +74,12 @@ pub async fn create_rule_config( API_REQUEST_COUNTER .with_label_values(&["sr_create_rule_config", "success"]) .inc(); - Ok(Json( - "Success Rate Configuration created successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Success Rate Configuration created successfully" + .to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -71,24 +91,28 @@ pub async fn create_rule_config( } } } - types::routing_configuration::ConfigVariant::Elimination(config) => { - let db_config = types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput::from_elimination_threshold(config.threshold); - let config = serde_json::to_string(&db_config) + types::routing_configuration::ConfigVariant::Elimination(elimination_config) => { + let db_config = types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput::from_elimination_threshold(elimination_config); + let serialized_config = serde_json::to_string(&db_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; - let result = - types::merchant::merchant_account::update_merchant_account(mid, Some(config)) - .await - .change_context(error::RuleConfigurationError::StorageError); + let result = types::merchant::merchant_account::update_merchant_account( + mid, + Some(serialized_config), + ) + .await + .change_context(error::RuleConfigurationError::StorageError); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["elimination_create_rule_config", "success"]) .inc(); - Ok(Json( - "Elimination Configuration created successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Elimination Configuration created successfully".to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -98,9 +122,9 @@ pub async fn create_rule_config( } } } - types::routing_configuration::ConfigVariant::DebitRouting(config) => { + types::routing_configuration::ConfigVariant::DebitRouting(debit_config) => { let name = format!("DEBIT_ROUTING_CONFIG_{}", mid); - let config = serde_json::to_string(&config) + let serialized_config = serde_json::to_string(&debit_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; // Check if config already exists @@ -116,7 +140,7 @@ pub async fn create_rule_config( Err(error::RuleConfigurationError::ConfigurationAlreadyExists.into()) } None => { - match types::service_configuration::insert_config(name, Some(config)) + match types::service_configuration::insert_config(name, Some(serialized_config)) .await .change_context(error::RuleConfigurationError::StorageError) { @@ -124,9 +148,12 @@ pub async fn create_rule_config( API_REQUEST_COUNTER .with_label_values(&["debit_routing_create_rule_config", "success"]) .inc(); - Ok(Json( - "Debit Routing Configuration created successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Debit Routing Configuration created successfully" + .to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -205,9 +232,10 @@ pub async fn get_rule_config( serde_json::from_str::< types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput, >(&account.gatewaySuccessRateBasedDeciderInput) - .map_err(|_| error::RuleConfigurationError::DeserializationError) + .map_err(|_| error::RuleConfigurationError::ConfigurationNotFound) .map(|config| types::routing_configuration::EliminationData { threshold: config.defaultEliminationThreshold, + txnLatency: config.txnLatency, }) }); @@ -274,7 +302,7 @@ pub async fn get_rule_config( #[axum::debug_handler] pub async fn update_rule_config( Json(payload): Json, -) -> Result, error::ContainerError> { +) -> Result, error::ContainerError> { let timer = API_LATENCY_HISTOGRAM .with_label_values(&["update_rule_config"]) .start_timer(); @@ -283,6 +311,9 @@ pub async fn update_rule_config( .inc(); logger::debug!("Received rule update configuration: {:?}", payload); + let merchant_id = payload.merchant_id.clone(); + let config = payload.config.clone(); + let mid = payload.merchant_id.clone(); ETM::merchant_account::load_merchant_by_merchant_id(mid.clone()) .await @@ -290,23 +321,25 @@ pub async fn update_rule_config( // Update DB call for updating the rule configuration let result = match payload.config { - types::routing_configuration::ConfigVariant::SuccessRate(config) => { + types::routing_configuration::ConfigVariant::SuccessRate(success_config) => { let name = format!("SR_V3_INPUT_CONFIG_{}", mid); - let config = serde_json::to_string(&config) + let serialized_config = serde_json::to_string(&success_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; - let result = types::service_configuration::update_config(name, Some(config)) + let result = types::service_configuration::update_config(name, Some(serialized_config)) .await - .change_context(error::RuleConfigurationError::StorageError); + .change_context(error::RuleConfigurationError::ConfigurationNotFound); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["sr_update_rule_config", "success"]) .inc(); - Ok(Json( - "Success Rate Configuration updated successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Success Rate Configuration updated successfully".to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -316,24 +349,28 @@ pub async fn update_rule_config( } } } - types::routing_configuration::ConfigVariant::Elimination(config) => { - let db_config = types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput::from_elimination_threshold(config.threshold); - let config = serde_json::to_string(&db_config) + types::routing_configuration::ConfigVariant::Elimination(elimination_config) => { + let db_config = types::gateway_routing_input::GatewaySuccessRateBasedRoutingInput::from_elimination_threshold(elimination_config); + let serialized_config = serde_json::to_string(&db_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; - let result = - types::merchant::merchant_account::update_merchant_account(mid, Some(config)) - .await - .change_context(error::RuleConfigurationError::StorageError); + let result = types::merchant::merchant_account::update_merchant_account( + mid, + Some(serialized_config), + ) + .await + .change_context(error::RuleConfigurationError::ConfigurationNotFound); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["elimination_update_rule_config", "success"]) .inc(); - Ok(Json( - "Elimination Configuration created successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Elimination Configuration updated successfully".to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -343,23 +380,25 @@ pub async fn update_rule_config( } } } - types::routing_configuration::ConfigVariant::DebitRouting(config) => { + types::routing_configuration::ConfigVariant::DebitRouting(debit_config) => { let name = format!("DEBIT_ROUTING_CONFIG_{}", mid); - let config = serde_json::to_string(&config) + let serialized_config = serde_json::to_string(&debit_config) .map_err(|_| error::RuleConfigurationError::StorageError)?; - let result = types::service_configuration::update_config(name, Some(config)) + let result = types::service_configuration::update_config(name, Some(serialized_config)) .await - .change_context(error::RuleConfigurationError::StorageError); + .change_context(error::RuleConfigurationError::ConfigurationNotFound); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["debit_routing_update_rule_config", "success"]) .inc(); - Ok(Json( - "Debit Routing Configuration updated successfully".to_string(), - )) + Ok(Json(RuleConfigResponse { + message: "Debit Routing Configuration updated successfully".to_string(), + merchant_id, + config, + })) } Err(e) => { API_REQUEST_COUNTER @@ -377,7 +416,7 @@ pub async fn update_rule_config( #[axum::debug_handler] pub async fn delete_rule_config( Json(payload): Json, -) -> Result, error::ContainerError> { +) -> Result, error::ContainerError> { let timer = API_LATENCY_HISTOGRAM .with_label_values(&["delete_rule_config"]) .start_timer(); @@ -387,7 +426,7 @@ pub async fn delete_rule_config( logger::debug!("Received rule delete request: {:?}", payload); let mid = payload.merchant_id.clone(); - ETM::merchant_account::load_merchant_by_merchant_id(mid.clone()) + let merchant_account = ETM::merchant_account::load_merchant_by_merchant_id(mid.clone()) .await .ok_or(error::RuleConfigurationError::MerchantNotFound)?; @@ -397,16 +436,17 @@ pub async fn delete_rule_config( let config_name = format!("SR_V3_INPUT_CONFIG_{}", mid); let result = types::service_configuration::delete_config(config_name) .await - .change_context(error::RuleConfigurationError::StorageError); + .change_context(error::RuleConfigurationError::ConfigurationNotFound); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["sr_delete_rule_config", "success"]) .inc(); - Ok(Json( - "Success Rate Configuration deleted successfully".to_string(), - )) + Ok(Json(RuleConfigDeleteResponse { + message: "Success Rate Configuration deleted successfully".to_string(), + merchant_id: mid, + })) } Err(e) => { API_REQUEST_COUNTER @@ -417,8 +457,18 @@ pub async fn delete_rule_config( } } types::routing_configuration::AlgorithmType::Elimination => { + if merchant_account + .gatewaySuccessRateBasedDeciderInput + .is_empty() + { + API_REQUEST_COUNTER + .with_label_values(&["elimination_delete_rule_config", "failure"]) + .inc(); + return Err(error::RuleConfigurationError::ConfigurationNotFound.into()); + } + let result = types::merchant::merchant_account::update_merchant_account( - mid, + mid.clone(), Some("".to_string()), ) // update to empty string .await @@ -429,9 +479,10 @@ pub async fn delete_rule_config( API_REQUEST_COUNTER .with_label_values(&["elimination_delete_rule_config", "success"]) .inc(); - Ok(Json( - "Elimination Configuration deleted successfully".to_string(), - )) + Ok(Json(RuleConfigDeleteResponse { + message: "Elimination Configuration deleted successfully".to_string(), + merchant_id: mid, + })) } Err(e) => { API_REQUEST_COUNTER @@ -445,16 +496,17 @@ pub async fn delete_rule_config( let config_name = format!("DEBIT_ROUTING_CONFIG_{}", mid); let result = types::service_configuration::delete_config(config_name) .await - .change_context(error::RuleConfigurationError::StorageError); + .change_context(error::RuleConfigurationError::ConfigurationNotFound); match result { Ok(_) => { API_REQUEST_COUNTER .with_label_values(&["debit_routing_delete_rule_config", "success"]) .inc(); - Ok(Json( - "Debit Routing Configuration deleted successfully".to_string(), - )) + Ok(Json(RuleConfigDeleteResponse { + message: "Debit Routing Configuration deleted successfully".to_string(), + merchant_id: mid, + })) } Err(e) => { API_REQUEST_COUNTER diff --git a/src/routes/update_gateway_score.rs b/src/routes/update_gateway_score.rs index b2405f62..95bf6441 100644 --- a/src/routes/update_gateway_score.rs +++ b/src/routes/update_gateway_score.rs @@ -31,6 +31,11 @@ use axum::extract::Json; pub async fn update_gateway_score( req: axum::http::Request, ) -> Result, ErrorResponse> { + let x_request_id = req + .headers() + .get("x-request-id") + .and_then(|value| value.to_str().ok()) + .map(str::to_string); let timer = API_LATENCY_HISTOGRAM .with_label_values(&["update_gateway_score"]) .start_timer(); @@ -50,6 +55,18 @@ pub async fn update_gateway_score( } Err(e) => { crate::logger::debug!(tag = "UpdateGatewayScore", "Error: {:?}", e); + crate::analytics::record_error_event( + "update_gateway_score", + None, + None, + None, + None, + x_request_id.clone(), + "400".to_string(), + "Error parsing request".to_string(), + Some("request body parse failure".to_string()), + Some("request_parse_failed".to_string()), + ); API_REQUEST_COUNTER .with_label_values(&["update_gateway_score", "failure"]) .inc(); @@ -75,21 +92,80 @@ pub async fn update_gateway_score( let update_score_request: Result = serde_json::from_slice(&body); match update_score_request { Ok(payload) => { - let result = check_and_update_gateway_score_(payload).await; + let merchant_id = payload.merchant_id.clone(); + let gateway = payload.gateway.clone(); + let payment_id = payload.payment_id.clone(); + crate::analytics::record_request_hit_event( + "update_gateway_score", + Some(merchant_id.clone()), + Some(payment_id.clone()), + x_request_id.clone(), + ); + let result = check_and_update_gateway_score_(payload.clone()).await; match result { - Ok(success) => { + Ok(_success) => { + let transaction_status = serde_json::to_string(&payload.status) + .unwrap_or_else(|_| format!("{:?}", payload.status)) + .trim_matches('"') + .to_string(); + let response = UpdateScoreResponse { + message: "Gateway score updated successfully".to_string(), + merchant_id: merchant_id.clone(), + gateway: gateway.clone(), + payment_id: payment_id.clone(), + }; + crate::analytics::record_gateway_update_event( + Some(merchant_id.clone()), + Some(gateway.clone()), + Some(transaction_status.clone()), + "update_gateway_score", + serde_json::to_string(&serde_json::json!({ + "request": { + "merchant_id": merchant_id, + "gateway": gateway, + "payment_id": payment_id, + "status": transaction_status, + "gateway_reference_id": payload.gateway_reference_id, + "enforce_dynamic_routing_failure": payload.enforce_dynamic_routing_failure, + "txn_latency": payload.txn_latency, + }, + "response": &response, + "selection_reason": { + "transaction_status": transaction_status, + "stage": "gateway score updated", + } + })) + .ok(), + Some(response.payment_id.clone()), + x_request_id.clone(), + Some("score_updated".to_string()), + ); API_REQUEST_COUNTER .with_label_values(&["update_gateway_score", "success"]) .inc(); timer.observe_duration(); - Ok(Json(UpdateScoreResponse { - message: "Success".to_string(), - })) + Ok(Json(response)) } Err(e) => { API_REQUEST_COUNTER .with_label_values(&["update_gateway_score", "failure"]) .inc(); + crate::analytics::record_error_event( + "update_gateway_score", + Some(merchant_id.clone()), + Some(payment_id.clone()), + x_request_id.clone(), + Some(gateway.clone()), + None, + e.error_code.clone(), + e.error_message.clone(), + serde_json::to_string(&serde_json::json!({ + "payment_id": payment_id, + "request_id": x_request_id, + })) + .ok(), + Some("score_update_failed".to_string()), + ); timer.observe_duration(); println!("Error: {:?}", e); Err(e) @@ -98,11 +174,23 @@ pub async fn update_gateway_score( } Err(e) => { crate::logger::debug!(tag = "UpdateScoreRequest", "Error: {:?}", e); + crate::analytics::record_error_event( + "update_gateway_score", + None, + None, + None, + None, + x_request_id.clone(), + "400".to_string(), + "Error parsing request".to_string(), + Some("request body parse failure".to_string()), + Some("request_parse_failed".to_string()), + ); API_REQUEST_COUNTER .with_label_values(&["update_gateway_score", "failure"]) .inc(); timer.observe_duration(); - return Err(ErrorResponse { + Err(ErrorResponse { status: "400".to_string(), error_code: "400".to_string(), error_message: "Error parsing request".to_string(), @@ -116,7 +204,7 @@ pub async fn update_gateway_score( }, priority_logic_output: None, is_dynamic_mga_enabled: false, - }); + }) } } } diff --git a/src/routes/update_score.rs b/src/routes/update_score.rs index e5137b0a..82622234 100644 --- a/src/routes/update_score.rs +++ b/src/routes/update_score.rs @@ -1,20 +1,30 @@ use crate::decider::gatewaydecider::types::{ErrorResponse, UnifiedError}; -use crate::feedback::gateway_scoring_service::check_and_update_gateway_score; +use crate::feedback::gateway_scoring_service::{ + check_and_update_gateway_score, invalid_request_error, +}; +use crate::logger; use crate::metrics::{API_LATENCY_HISTOGRAM, API_REQUEST_COUNTER, API_REQUEST_TOTAL_COUNTER}; -use crate::types::card::txn_card_info::TxnCardInfo; -use crate::types::txn_details::types::TxnDetail; -use crate::{logger, metrics}; +use crate::redis::feature::{ + check_redis_comp_merchant_flag, is_feature_enabled, RedisCompressionConfig, + RedisCompressionConfigCombined, +}; +use crate::types::card::txn_card_info::{convert_safe_to_txn_card_info, SafeTxnCardInfo}; +use crate::types::txn_details::types::{ + convert_safe_txn_detail_to_txn_detail, SafeTxnDetail, TransactionLatency, +}; use axum::body::to_bytes; use cpu_time::ProcessTime; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] struct UpdateScoreRequest { - txn_detail: TxnDetail, - txn_card_info: TxnCardInfo, + txn_detail: SafeTxnDetail, + txn_card_info: SafeTxnCardInfo, log_message: String, enforce_dynaic_routing_failure: Option, gateway_reference_id: Option, + txn_latency: Option, } #[axum::debug_handler] @@ -41,8 +51,12 @@ pub async fn update_score( let request_time = time::OffsetDateTime::now_utc() .format(&time::format_description::well_known::Rfc3339) .unwrap_or_else(|_| "unknown".to_string()); - let query_params = original_url.splitn(2, '?').nth(1).unwrap_or("").to_string(); - + let query_params = original_url + .split_once('?') + .map(|x| x.1) + .unwrap_or("") + .to_string(); + tracing::Span::current().record("is_audit_trail_log", "true"); // Buffer the body into memory let body_bytes = match to_bytes(req.into_body(), usize::MAX).await { Ok(bytes) => bytes, @@ -70,6 +84,24 @@ pub async fn update_score( // Log the error let latency = start_time.elapsed().as_millis() as u64; let cpu_time = cpu_start.elapsed().as_millis() as u64; + + crate::analytics::record_error_event( + "update_score", + None, + None, + None, + None, + Some(x_request_id.to_string()), + error_response.error_code.clone(), + error_response.error_message.clone(), + serde_json::to_string(&serde_json::json!({ + "request_id": x_request_id, + "query_params": query_params, + })) + .ok(), + Some("request_parse_failed".to_string()), + ); + logger::error!( url = original_url, method = "POST", @@ -126,6 +158,24 @@ pub async fn update_score( // Log the error let latency = start_time.elapsed().as_millis() as u64; let cpu_time = cpu_start.elapsed().as_millis() as u64; + + crate::analytics::record_error_event( + "update_score", + Some(merchant_id_txt.clone()), + None, + None, + None, + Some(x_request_id.to_string()), + error_response.error_code.clone(), + error_response.error_message.clone(), + serde_json::to_string(&serde_json::json!({ + "request_id": x_request_id, + "reason": "gateway_missing", + })) + .ok(), + Some("validation_failed".to_string()), + ); + logger::error!( url = original_url, method = "POST", @@ -155,21 +205,51 @@ pub async fn update_score( } // Process the request - let txn_detail = payload.txn_detail; - let txn_card_info = payload.txn_card_info; - let log_message = payload.log_message; + let txn_detail = match convert_safe_txn_detail_to_txn_detail(payload.txn_detail.clone()) + { + Ok(detail) => detail, + Err(e) => { + return Err(invalid_request_error("transaction details", &e)); + } + }; + let txn_card_info = match convert_safe_to_txn_card_info(payload.txn_card_info.clone()) { + Ok(card_info) => card_info, + Err(e) => { + return Err(invalid_request_error("transaction Card Info", &e)); + } + }; + + let log_message = payload.log_message.clone(); let enforce_failure = payload.enforce_dynaic_routing_failure.unwrap_or(false); - let gateway_reference_id = payload.gateway_reference_id; + let gateway_reference_id = payload.gateway_reference_id.clone(); + let txn_latency = payload.txn_latency.clone(); jemalloc_ctl::epoch::advance().unwrap(); let allocated_before = jemalloc_ctl::stats::allocated::read().unwrap_or(0); + let redis_comp_config: Option> = + check_redis_comp_merchant_flag(merchant_id_txt.clone()).await; + + let redis_comp_enabled_de = is_feature_enabled( + "REDIS_COMPRESSION_ENABLED_MERCHANT_DE".to_string(), + merchant_id_txt.clone(), + "kv_redis".to_string(), + ) + .await; + + let redis_comp_config_final = Some(RedisCompressionConfigCombined { + redisCompressionConfig: redis_comp_config, + isRedisCompEnabled: redis_comp_enabled_de, + }); + check_and_update_gateway_score( txn_detail, txn_card_info, log_message.as_str(), enforce_failure, gateway_reference_id, + txn_latency, + redis_comp_config_final, ) .await; @@ -192,7 +272,7 @@ pub async fn update_score( request_time = request_time, env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string()), action = "POST", - req_body = req_body, + req_body = format!("{:?}", payload.clone()), category = "INCOMING_API", req_headers = format!("{:?}", headers), "Successfully updated score" @@ -202,7 +282,7 @@ pub async fn update_score( .with_label_values(&["update_score", "success"]) .inc(); timer.observe_duration(); - return Ok("Success"); + Ok("Success") } Err(e) => { let error_response = ErrorResponse { @@ -249,7 +329,7 @@ pub async fn update_score( .with_label_values(&["update_score", "failure"]) .inc(); timer.observe_duration(); - return Err(error_response); + Err(error_response) } } } diff --git a/src/storage.rs b/src/storage.rs index 1770e559..f7154edc 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,13 +1,18 @@ -use crate::{ - config::Database, - config::PgDatabase, - error::{self, ContainerError}, - logger, -}; +use crate::error::{self, ContainerError}; + +#[cfg(feature = "mysql")] +use crate::config::Database; +#[cfg(feature = "postgres")] +use crate::config::PgDatabase; + +#[cfg(feature = "mysql")] +use crate::logger; use crate::generics::StorageResult; use bb8::PooledConnection; +#[cfg(feature = "mysql")] use std::time::Duration; +#[cfg(feature = "mysql")] use tokio::time; #[cfg(feature = "mysql")] @@ -16,18 +21,12 @@ use diesel::MysqlConnection; use diesel::PgConnection; #[cfg(feature = "mysql")] use diesel_async::{ - pooled_connection::{ - self, - deadpool::{Object, Pool}, - }, + pooled_connection::{self, deadpool::Pool}, AsyncMysqlConnection, }; #[cfg(feature = "postgres")] use diesel_async::{ - pooled_connection::{ - self, - deadpool::{Object, Pool}, - }, + pooled_connection::{self, deadpool::Pool}, AsyncPgConnection, }; @@ -68,12 +67,6 @@ pub type MysqlPoolConn = async_bb8_diesel::Connection; #[cfg(feature = "mysql")] pub type MysqlPool = bb8::Pool; -#[cfg(feature = "postgres")] -type DeadPoolConnType = Object; - -#[cfg(feature = "mysql")] -type DeadPoolConnType = Object; - #[cfg(feature = "postgres")] impl Storage { /// Create a new storage interface from configuration @@ -96,13 +89,13 @@ impl Storage { pooled_connection::AsyncDieselConnectionManager::::new(database_url); let pool = Pool::builder(config); - let pool = match database.pg_pool_size { + let _pool = match database.pg_pool_size { Some(value) => pool.max_size(value), None => pool, }; let pool = diesel_make_pg_pool(database, schema, false).await?; - return Ok(Self { pg_pool: pool }); + Ok(Self { pg_pool: pool }) } pub async fn get_conn( &self, @@ -110,7 +103,7 @@ impl Storage { { match self.pg_pool.get().await { Ok(conn) => Ok(conn), - Err(err) => Err(crate::generics::MeshError::DatabaseConnectionError), + Err(_err) => Err(crate::generics::MeshError::DatabaseConnectionError), } } } @@ -138,13 +131,13 @@ impl Storage { ); let pool = Pool::builder(config); - let pool = match database.pool_size { + let _pool = match database.pool_size { Some(value) => pool.max_size(value), None => pool, }; let pool = diesel_make_mysql_pool(database, schema, false).await?; - return Ok(Self { pg_pool: pool }); + Ok(Self { pg_pool: pool }) } /// Get connection from database pool for accessing data @@ -155,7 +148,7 @@ impl Storage { let timeout_duration = Duration::from_secs(10); match time::timeout(timeout_duration, self.pg_pool.get()).await { Ok(Ok(conn)) => { - logger::info!( + logger::debug!( action = "DB_CONNECTION_SUCCESS", "connection to db successful" ); @@ -191,7 +184,7 @@ pub(crate) trait TestInterface { pub async fn diesel_make_pg_pool( database: &PgDatabase, schema: &str, - test_transaction: bool, + _test_transaction: bool, ) -> error_stack::Result { let database_url = format!( "postgres://{}:{}@{}:{}/{}?application_name={}&options=-c search_path%3D{}", @@ -218,7 +211,7 @@ pub async fn diesel_make_pg_pool( pub async fn diesel_make_mysql_pool( database: &Database, schema: &str, - test_transaction: bool, + _test_transaction: bool, ) -> error_stack::Result { let database_url = format!( "mysql://{}:{}@{}:{}/{}?application_name={}&options=-c search_path%3D{}", diff --git a/src/storage/schema.rs b/src/storage/schema.rs index c021cb28..df87f83b 100644 --- a/src/storage/schema.rs +++ b/src/storage/schema.rs @@ -520,6 +520,56 @@ diesel::table! { } } +diesel::table! { + analytics_event (id) { + id -> Int4, + #[max_length = 64] + event_type -> Varchar, + #[max_length = 255] + merchant_id -> Nullable, + #[max_length = 255] + payment_id -> Nullable, + #[max_length = 255] + request_id -> Nullable, + #[max_length = 255] + payment_method_type -> Nullable, + #[max_length = 255] + payment_method -> Nullable, + #[max_length = 255] + card_network -> Nullable, + #[max_length = 255] + card_is_in -> Nullable, + #[max_length = 64] + currency -> Nullable, + #[max_length = 64] + country -> Nullable, + #[max_length = 64] + auth_type -> Nullable, + #[max_length = 255] + gateway -> Nullable, + #[max_length = 128] + event_stage -> Nullable, + #[max_length = 128] + routing_approach -> Nullable, + #[max_length = 255] + rule_name -> Nullable, + #[max_length = 64] + status -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + score_value -> Nullable, + sigma_factor -> Nullable, + average_latency -> Nullable, + tp99_latency -> Nullable, + transaction_count -> Nullable, + #[max_length = 128] + route -> Nullable, + details -> Nullable, + created_at_ms -> Bigint, + } +} + diesel::allow_tables_to_appear_in_same_query!( card_brand_routes, card_info, @@ -541,6 +591,7 @@ diesel::allow_tables_to_appear_in_same_query!( merchant_gateway_payment_method_flow, merchant_iframe_preferences, merchant_priority_logic, + analytics_event, payment_method, service_configuration, tenant_config, diff --git a/src/storage/schema_pg.rs b/src/storage/schema_pg.rs index 10307faa..804052cc 100644 --- a/src/storage/schema_pg.rs +++ b/src/storage/schema_pg.rs @@ -1,5 +1,55 @@ // @generated automatically by Diesel CLI. +diesel::table! { + analytics_event (id) { + id -> Int4, + #[max_length = 64] + event_type -> Varchar, + #[max_length = 255] + merchant_id -> Nullable, + #[max_length = 255] + payment_method_type -> Nullable, + #[max_length = 255] + payment_method -> Nullable, + #[max_length = 255] + gateway -> Nullable, + #[max_length = 128] + routing_approach -> Nullable, + #[max_length = 255] + rule_name -> Nullable, + #[max_length = 64] + status -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + score_value -> Nullable, + sigma_factor -> Nullable, + average_latency -> Nullable, + tp99_latency -> Nullable, + transaction_count -> Nullable, + #[max_length = 128] + route -> Nullable, + details -> Nullable, + created_at_ms -> Int8, + #[max_length = 255] + payment_id -> Nullable, + #[max_length = 255] + request_id -> Nullable, + #[max_length = 128] + event_stage -> Nullable, + #[max_length = 255] + card_network -> Nullable, + #[max_length = 255] + card_is_in -> Nullable, + #[max_length = 64] + currency -> Nullable, + #[max_length = 64] + country -> Nullable, + #[max_length = 64] + auth_type -> Nullable, + } +} + diesel::table! { card_brand_routes (id) { id -> Int8, @@ -514,6 +564,7 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( + analytics_event, card_brand_routes, card_info, co_badged_cards_info_test, diff --git a/src/storage/types.rs b/src/storage/types.rs index a41f6727..6350c616 100644 --- a/src/storage/types.rs +++ b/src/storage/types.rs @@ -2,18 +2,22 @@ use crate::decider::gatewaydecider::{self, types}; use crate::decider::network_decider; use crate::error; use crate::utils::CustomResult; +#[cfg(feature = "mysql")] use diesel::sql_types::Bit; +#[cfg(feature = "postgres")] use diesel::sql_types::Bool; #[cfg(feature = "mysql")] use super::schema; #[cfg(feature = "postgres")] use super::schema_pg; +use crate::decider::gatewaydecider::utils::mask_secret_option; #[cfg(feature = "mysql")] use diesel::mysql::Mysql; #[cfg(feature = "postgres")] use diesel::pg::Pg; use diesel::serialize::{IsNull, Output}; +#[cfg(feature = "mysql")] use diesel::sql_types::Binary; use diesel::*; use diesel::{ @@ -21,6 +25,7 @@ use diesel::{ Queryable, Selectable, }; use error_stack::ResultExt; +use masking::Secret; use serde::Serialize; use serde::{self, Deserialize}; use std::io::Write; @@ -66,7 +71,6 @@ pub struct EmiBankCode { #[derive(Debug, Clone, Identifiable, Queryable, Serialize, Selectable)] #[cfg_attr(feature = "mysql", diesel(table_name = schema::feature))] #[cfg_attr(feature = "postgres", diesel(table_name = schema_pg::feature))] - pub struct Feature { #[cfg(feature = "mysql")] pub id: i64, @@ -382,10 +386,10 @@ pub struct BitBool(pub bool); impl ToSql for BitBool { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> diesel::serialize::Result { match *self { - BitBool(true) => { + Self(true) => { out.write_all(&[1u8])?; } - BitBool(false) => { + Self(false) => { out.write_all(&[0u8])?; } } @@ -406,8 +410,8 @@ impl FromSql for BitBool { impl ToSql for BitBool { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result { match *self { - BitBool(true) => out.write_all(&[1])?, - BitBool(false) => out.write_all(&[0])?, + Self(true) => out.write_all(&[1])?, + Self(false) => out.write_all(&[0])?, } Ok(IsNull::No) } @@ -417,8 +421,8 @@ impl ToSql for BitBool { impl FromSql for BitBool { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { match bytes.as_bytes().first() { - Some(&1) => Ok(BitBool(true)), - _ => Ok(BitBool(false)), + Some(&1) => Ok(Self(true)), + _ => Ok(Self(false)), } } } @@ -432,10 +436,10 @@ pub struct BitBoolWrite(pub bool); impl ToSql for BitBoolWrite { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> diesel::serialize::Result { match *self { - BitBoolWrite(true) => { + Self(true) => { out.write_all(&[1u8])?; } - BitBoolWrite(false) => { + Self(false) => { out.write_all(&[0u8])?; } } @@ -457,8 +461,8 @@ impl FromSql for BitBoolWrite { impl ToSql for BitBoolWrite { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result { match *self { - BitBoolWrite(true) => out.write_all(&[1])?, - BitBoolWrite(false) => out.write_all(&[0])?, + Self(true) => out.write_all(&[1])?, + Self(false) => out.write_all(&[0])?, } Ok(IsNull::No) } @@ -469,8 +473,8 @@ impl FromSql for BitBoolWrite { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { // Should be Pg match bytes.as_bytes().first() { - Some(&1) => Ok(BitBoolWrite(true)), - _ => Ok(BitBoolWrite(false)), + Some(&1) => Ok(Self(true)), + _ => Ok(Self(false)), } } } @@ -590,7 +594,7 @@ pub struct TokenBinInfo { pub last_updated: Option, } -#[derive(Debug, Clone, Identifiable, Queryable)] +#[derive(Debug, Clone, Identifiable, Queryable, Serialize)] #[cfg_attr(feature = "mysql", diesel(table_name = schema::txn_card_info))] #[cfg_attr(feature = "postgres", diesel(table_name = schema_pg::txn_card_info))] pub struct TxnCardInfo { @@ -605,7 +609,8 @@ pub struct TxnCardInfo { pub date_created: Option, pub payment_method_type: Option, pub payment_method: Option, - pub payment_source: Option, + #[serde(serialize_with = "mask_secret_option")] + pub payment_source: Option>, pub auth_type: Option, pub partition_key: Option, } diff --git a/src/types.rs b/src/types.rs index 47f6d310..292651e6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,6 +13,7 @@ pub mod gateway_outage; pub mod gateway_payment_flow; pub mod gateway_payment_method_flow; pub mod gateway_routing_input; +pub mod hybrid_routing; pub mod isin_routes; pub mod merchant; pub mod merchant_config; diff --git a/src/types/bank_code.rs b/src/types/bank_code.rs index 655c7513..69f58b52 100644 --- a/src/types/bank_code.rs +++ b/src/types/bank_code.rs @@ -50,7 +50,7 @@ pub async fn find_bank_code(bank_code: String) -> Option { .await { Ok(db_record) => parse_juspay_bank_code(db_record).ok(), - Err(e) => None, + Err(_e) => None, } } diff --git a/src/types/card/isin.rs b/src/types/card/isin.rs index bd0c236d..b6237192 100644 --- a/src/types/card/isin.rs +++ b/src/types/card/isin.rs @@ -1,6 +1,5 @@ -#[macro_use] -use serde::{Deserialize, Serialize, Serializer}; use crate::error::ApiError; +use serde::{Deserialize, Serialize, Serializer}; use std::convert::TryFrom; use std::option::Option; use std::string::String; diff --git a/src/types/card/txn_card_info.rs b/src/types/card/txn_card_info.rs index 951f7d9b..3385ca13 100644 --- a/src/types/card/txn_card_info.rs +++ b/src/types/card/txn_card_info.rs @@ -1,101 +1,89 @@ use crate::error::ApiError; use crate::types::card::card_type::CardType; -use crate::utils::StringExt; -use masking::Secret; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Deserializer, Serialize}; use time::{OffsetDateTime, PrimitiveDateTime}; // use crate::types::transaction::id::TransactionId; // use crate::types::txn_details::types::TxnDetailId; // use juspay::extra::parsing::{Step, lift_either, lift_pure, ParsingErrorType}; // use juspay::extra::secret::{Secret, SecretContext}; +use crate::decider::gatewaydecider::utils::mask_secret_option; use std::fmt::Debug; use std::option::Option; use std::string::String; #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum AuthType { - #[serde(rename = "ATMPIN")] - ATMPIN, - #[serde(rename = "THREE_DS")] - THREE_DS, - #[serde(rename = "THREE_DS_2")] - THREE_DS_2, - #[serde(rename = "OTP")] - OTP, - #[serde(rename = "OBO_OTP")] - OBO_OTP, - #[serde(rename = "VIES")] - VIES, - #[serde(rename = "NO_THREE_DS")] - NO_THREE_DS, - #[serde(rename = "NETWORK_TOKEN")] - NETWORK_TOKEN, - #[serde(rename = "MOTO")] - MOTO, - #[serde(rename = "FIDO")] - FIDO, - #[serde(rename = "CTP")] - CTP, + Atmpin, + ThreeDs, + ThreeDs2, + Otp, + OboOtp, + Vies, + NoThreeDs, + NetworkToken, + Moto, + Fido, + Ctp, } impl std::fmt::Display for AuthType { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::ATMPIN => write!(f, "ATMPIN"), - Self::THREE_DS => write!(f, "THREE_DS"), - Self::THREE_DS_2 => write!(f, "THREE_DS_2"), - Self::OTP => write!(f, "OTP"), - Self::OBO_OTP => write!(f, "OBO_OTP"), - Self::VIES => write!(f, "VIES"), - Self::NO_THREE_DS => write!(f, "NO_THREE_DS"), - Self::NETWORK_TOKEN => write!(f, "NETWORK_TOKEN"), - Self::MOTO => write!(f, "MOTO"), - Self::FIDO => write!(f, "FIDO"), - Self::CTP => write!(f, "CTP"), + Self::Atmpin => write!(f, "ATMPIN"), + Self::ThreeDs => write!(f, "THREE_DS"), + Self::ThreeDs2 => write!(f, "THREE_DS_2"), + Self::Otp => write!(f, "OTP"), + Self::OboOtp => write!(f, "OBO_OTP"), + Self::Vies => write!(f, "VIES"), + Self::NoThreeDs => write!(f, "NO_THREE_DS"), + Self::NetworkToken => write!(f, "NETWORK_TOKEN"), + Self::Moto => write!(f, "MOTO"), + Self::Fido => write!(f, "FIDO"), + Self::Ctp => write!(f, "CTP"), } } } pub fn text_to_auth_type(ctx: &str) -> Result { match ctx { - "ATMPIN" => Ok(AuthType::ATMPIN), - "THREE_DS" => Ok(AuthType::THREE_DS), - "THREE_DS_2" => Ok(AuthType::THREE_DS_2), - "OTP" => Ok(AuthType::OTP), - "OBO_OTP" => Ok(AuthType::OBO_OTP), - "VIES" => Ok(AuthType::VIES), - "NO_THREE_DS" => Ok(AuthType::NO_THREE_DS), - "NETWORK_TOKEN" => Ok(AuthType::NETWORK_TOKEN), - "MOTO" => Ok(AuthType::MOTO), - "FIDO" => Ok(AuthType::FIDO), - "CTP" => Ok(AuthType::CTP), + "ATMPIN" => Ok(AuthType::Atmpin), + "THREE_DS" => Ok(AuthType::ThreeDs), + "THREE_DS_2" => Ok(AuthType::ThreeDs2), + "OTP" => Ok(AuthType::Otp), + "OBO_OTP" => Ok(AuthType::OboOtp), + "VIES" => Ok(AuthType::Vies), + "NO_THREE_DS" => Ok(AuthType::NoThreeDs), + "NETWORK_TOKEN" => Ok(AuthType::NetworkToken), + "MOTO" => Ok(AuthType::Moto), + "FIDO" => Ok(AuthType::Fido), + "CTP" => Ok(AuthType::Ctp), _ => Err(ApiError::ParsingError("Invalid Auth Type")), } } pub fn auth_type_to_text(ctx: &AuthType) -> String { match ctx { - AuthType::ATMPIN => "ATMPIN".to_string(), - AuthType::THREE_DS => "THREE_DS".to_string(), - AuthType::THREE_DS_2 => "THREE_DS_2".to_string(), - AuthType::OTP => "OTP".to_string(), - AuthType::OBO_OTP => "OBO_OTP".to_string(), - AuthType::VIES => "VIES".to_string(), - AuthType::NO_THREE_DS => "NO_THREE_DS".to_string(), - AuthType::NETWORK_TOKEN => "NETWORK_TOKEN".to_string(), - AuthType::MOTO => "MOTO".to_string(), - AuthType::FIDO => "FIDO".to_string(), - AuthType::CTP => "CTP".to_string(), + AuthType::Atmpin => "ATMPIN".to_string(), + AuthType::ThreeDs => "THREE_DS".to_string(), + AuthType::ThreeDs2 => "THREE_DS_2".to_string(), + AuthType::Otp => "OTP".to_string(), + AuthType::OboOtp => "OBO_OTP".to_string(), + AuthType::Vies => "VIES".to_string(), + AuthType::NoThreeDs => "NO_THREE_DS".to_string(), + AuthType::NetworkToken => "NETWORK_TOKEN".to_string(), + AuthType::Moto => "MOTO".to_string(), + AuthType::Fido => "FIDO".to_string(), + AuthType::Ctp => "CTP".to_string(), } } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum EMIType { - #[serde(rename = "NO_COST_EMI")] - NO_COST_EMI, - #[serde(rename = "LOW_COST_EMI")] - LOW_COST_EMI, - #[serde(rename = "STANDARD_EMI")] - STANDARD_EMI, + NoCostEmi, + LowCostEmi, + StandardEmi, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -165,10 +153,82 @@ pub struct TxnCardInfo { #[serde(rename = "paymentMethod")] pub paymentMethod: String, #[serde(rename = "paymentSource")] - pub paymentSource: Option, + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, #[serde(rename = "authType")] pub authType: Option, #[serde(rename = "partitionKey")] #[serde(deserialize_with = "deserialize_optional_primitive_datetime")] pub partitionKey: Option, } + +impl TxnCardInfo { + pub fn get_payment_source_last(&self) -> Option { + self.paymentSource.as_ref().and_then(|ps| { + // use `expose_secret()` instead of peek() + let ps_str = ps.peek(); + ps_str.split('@').next_back().map(|s| s.to_string()) + }) + } + + pub fn get_payment_source(&self) -> Option { + self.paymentSource.clone().map(|s| s.peek().to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SafeTxnCardInfo { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "cardIsin")] + pub card_isin: Option, + #[serde(rename = "cardIssuerBankName")] + pub cardIssuerBankName: Option, + #[serde(rename = "cardSwitchProvider")] + pub cardSwitchProvider: Option>, + #[serde(rename = "cardType")] + pub card_type: Option, + #[serde(rename = "nameOnCard")] + pub nameOnCard: Option>, + #[serde(with = "time::serde::iso8601")] + #[serde(rename = "dateCreated")] + pub dateCreated: OffsetDateTime, + #[serde(rename = "paymentMethodType")] + pub paymentMethodType: String, + #[serde(rename = "paymentMethod")] + pub paymentMethod: String, + #[serde(rename = "paymentSource")] + #[serde(serialize_with = "mask_secret_option")] + pub paymentSource: Option>, + #[serde(rename = "authType")] + pub authType: Option, + #[serde(rename = "partitionKey")] + #[serde(deserialize_with = "deserialize_optional_primitive_datetime")] + pub partitionKey: Option, +} + +pub fn convert_safe_to_txn_card_info( + safe_info: SafeTxnCardInfo, +) -> Result { + let id_i64 = safe_info + .id + .parse::() + .map_err(|_| crate::error::ApiError::ParsingError("id"))?; + + Ok(TxnCardInfo { + id: TxnCardInfoPId(id_i64), + card_isin: safe_info.card_isin, + cardIssuerBankName: safe_info.cardIssuerBankName, + cardSwitchProvider: safe_info.cardSwitchProvider, + card_type: safe_info.card_type, + nameOnCard: safe_info.nameOnCard, + dateCreated: safe_info.dateCreated, + paymentMethodType: safe_info.paymentMethodType, + paymentMethod: safe_info.paymentMethod, + paymentSource: safe_info.paymentSource, + authType: safe_info + .authType + .and_then(|auth| text_to_auth_type(&auth).ok()), + partitionKey: safe_info.partitionKey, + }) +} diff --git a/src/types/country/country_iso.rs b/src/types/country/country_iso.rs index d8f9911f..ad37ef8e 100644 --- a/src/types/country/country_iso.rs +++ b/src/types/country/country_iso.rs @@ -4,12 +4,12 @@ use std::hash::Hash; use std::string::String; #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Copy)] pub enum CountryISO { - AIA, AFG, - AGO, + AIA, ALA, ALB, AND, + AGO, ARE, ARG, ARM, @@ -207,218 +207,221 @@ pub fn country_iso_to_text(country_iso: CountryISO) -> String { pub fn text_db_to_country_iso(text: &str) -> Result { match text { - "AIA" => Ok(CountryISO::AIA), "AFG" => Ok(CountryISO::AFG), - "ASM" => Ok(CountryISO::ASM), - "ALB" => Ok(CountryISO::ALB), + "AIA" => Ok(CountryISO::AIA), "ALA" => Ok(CountryISO::ALA), - "DZA" => Ok(CountryISO::DZA), + "ALB" => Ok(CountryISO::ALB), "AND" => Ok(CountryISO::AND), "AGO" => Ok(CountryISO::AGO), + "ARE" => Ok(CountryISO::ARE), "ARG" => Ok(CountryISO::ARG), "ARM" => Ok(CountryISO::ARM), + "ASM" => Ok(CountryISO::ASM), + "ATG" => Ok(CountryISO::ATG), "AUS" => Ok(CountryISO::AUS), "AUT" => Ok(CountryISO::AUT), "AZE" => Ok(CountryISO::AZE), - "BHS" => Ok(CountryISO::BHS), - "BHR" => Ok(CountryISO::BHR), + "BDI" => Ok(CountryISO::BDI), + "BEL" => Ok(CountryISO::BEL), + "BEN" => Ok(CountryISO::BEN), + "BFA" => Ok(CountryISO::BFA), "BGD" => Ok(CountryISO::BGD), - "BRB" => Ok(CountryISO::BRB), + "BGR" => Ok(CountryISO::BGR), + "BHR" => Ok(CountryISO::BHR), + "BHS" => Ok(CountryISO::BHS), + "BIH" => Ok(CountryISO::BIH), "BLR" => Ok(CountryISO::BLR), - "BEL" => Ok(CountryISO::BEL), "BLZ" => Ok(CountryISO::BLZ), - "BEN" => Ok(CountryISO::BEN), - "BTN" => Ok(CountryISO::BTN), "BOL" => Ok(CountryISO::BOL), - "BIH" => Ok(CountryISO::BIH), - "BWA" => Ok(CountryISO::BWA), "BRA" => Ok(CountryISO::BRA), + "BRB" => Ok(CountryISO::BRB), "BRN" => Ok(CountryISO::BRN), - "BGR" => Ok(CountryISO::BGR), - "BFA" => Ok(CountryISO::BFA), - "BDI" => Ok(CountryISO::BDI), - "CPV" => Ok(CountryISO::CPV), - "KHM" => Ok(CountryISO::KHM), - "CMR" => Ok(CountryISO::CMR), - "CAN" => Ok(CountryISO::CAN), + "BTN" => Ok(CountryISO::BTN), + "BWA" => Ok(CountryISO::BWA), "CAF" => Ok(CountryISO::CAF), - "TCD" => Ok(CountryISO::TCD), + "CAN" => Ok(CountryISO::CAN), + "CHE" => Ok(CountryISO::CHE), "CHL" => Ok(CountryISO::CHL), "CHN" => Ok(CountryISO::CHN), + "CMR" => Ok(CountryISO::CMR), + "COD" => Ok(CountryISO::COD), + "COG" => Ok(CountryISO::COG), "COL" => Ok(CountryISO::COL), "COM" => Ok(CountryISO::COM), - "COG" => Ok(CountryISO::COG), + "CPV" => Ok(CountryISO::CPV), "CRI" => Ok(CountryISO::CRI), - "HRV" => Ok(CountryISO::HRV), "CUB" => Ok(CountryISO::CUB), "CYP" => Ok(CountryISO::CYP), "CZE" => Ok(CountryISO::CZE), - "COD" => Ok(CountryISO::COD), - "DNK" => Ok(CountryISO::DNK), + "DEU" => Ok(CountryISO::DEU), "DJI" => Ok(CountryISO::DJI), "DMA" => Ok(CountryISO::DMA), + "DNK" => Ok(CountryISO::DNK), "DOM" => Ok(CountryISO::DOM), + "DZA" => Ok(CountryISO::DZA), "ECU" => Ok(CountryISO::ECU), "EGY" => Ok(CountryISO::EGY), - "SLV" => Ok(CountryISO::SLV), - "GNQ" => Ok(CountryISO::GNQ), "ERI" => Ok(CountryISO::ERI), + "ESP" => Ok(CountryISO::ESP), "EST" => Ok(CountryISO::EST), - "SWZ" => Ok(CountryISO::SWZ), "ETH" => Ok(CountryISO::ETH), - "FJI" => Ok(CountryISO::FJI), "FIN" => Ok(CountryISO::FIN), + "FJI" => Ok(CountryISO::FJI), "FRA" => Ok(CountryISO::FRA), + "FSM" => Ok(CountryISO::FSM), "GAB" => Ok(CountryISO::GAB), - "GMB" => Ok(CountryISO::GMB), + "GBR" => Ok(CountryISO::GBR), "GEO" => Ok(CountryISO::GEO), - "DEU" => Ok(CountryISO::DEU), "GHA" => Ok(CountryISO::GHA), + "GIN" => Ok(CountryISO::GIN), + "GMB" => Ok(CountryISO::GMB), + "GNB" => Ok(CountryISO::GNB), + "GNQ" => Ok(CountryISO::GNQ), "GRC" => Ok(CountryISO::GRC), "GRD" => Ok(CountryISO::GRD), "GTM" => Ok(CountryISO::GTM), - "GIN" => Ok(CountryISO::GIN), - "GNB" => Ok(CountryISO::GNB), "GUY" => Ok(CountryISO::GUY), "HKG" => Ok(CountryISO::HKG), - "HTI" => Ok(CountryISO::HTI), "HND" => Ok(CountryISO::HND), + "HRV" => Ok(CountryISO::HRV), + "HTI" => Ok(CountryISO::HTI), "HUN" => Ok(CountryISO::HUN), - "ISL" => Ok(CountryISO::ISL), - "IND" => Ok(CountryISO::IND), "IDN" => Ok(CountryISO::IDN), + "IND" => Ok(CountryISO::IND), + "IRL" => Ok(CountryISO::IRL), "IRN" => Ok(CountryISO::IRN), "IRQ" => Ok(CountryISO::IRQ), - "IRL" => Ok(CountryISO::IRL), + "ISL" => Ok(CountryISO::ISL), "ISR" => Ok(CountryISO::ISR), "ITA" => Ok(CountryISO::ITA), "JAM" => Ok(CountryISO::JAM), - "JPN" => Ok(CountryISO::JPN), "JOR" => Ok(CountryISO::JOR), + "JPN" => Ok(CountryISO::JPN), "KAZ" => Ok(CountryISO::KAZ), "KEN" => Ok(CountryISO::KEN), + "KGZ" => Ok(CountryISO::KGZ), + "KHM" => Ok(CountryISO::KHM), "KIR" => Ok(CountryISO::KIR), + "KNA" => Ok(CountryISO::KNA), + "KOR" => Ok(CountryISO::KOR), "KWT" => Ok(CountryISO::KWT), - "KGZ" => Ok(CountryISO::KGZ), "LAO" => Ok(CountryISO::LAO), - "LVA" => Ok(CountryISO::LVA), "LBN" => Ok(CountryISO::LBN), - "LSO" => Ok(CountryISO::LSO), "LBR" => Ok(CountryISO::LBR), "LBY" => Ok(CountryISO::LBY), + "LCA" => Ok(CountryISO::LCA), "LIE" => Ok(CountryISO::LIE), + "LKA" => Ok(CountryISO::LKA), + "LSO" => Ok(CountryISO::LSO), "LTU" => Ok(CountryISO::LTU), "LUX" => Ok(CountryISO::LUX), + "LVA" => Ok(CountryISO::LVA), + "MAR" => Ok(CountryISO::MAR), + "MCO" => Ok(CountryISO::MCO), + "MDA" => Ok(CountryISO::MDA), "MDG" => Ok(CountryISO::MDG), - "MWI" => Ok(CountryISO::MWI), - "MYS" => Ok(CountryISO::MYS), "MDV" => Ok(CountryISO::MDV), + "MEX" => Ok(CountryISO::MEX), + "MHL" => Ok(CountryISO::MHL), + "MKD" => Ok(CountryISO::MKD), "MLI" => Ok(CountryISO::MLI), "MLT" => Ok(CountryISO::MLT), - "MHL" => Ok(CountryISO::MHL), - "MRT" => Ok(CountryISO::MRT), - "MUS" => Ok(CountryISO::MUS), - "MEX" => Ok(CountryISO::MEX), - "FSM" => Ok(CountryISO::FSM), - "MDA" => Ok(CountryISO::MDA), - "MCO" => Ok(CountryISO::MCO), + "MMR" => Ok(CountryISO::MMR), "MNG" => Ok(CountryISO::MNG), "MNE" => Ok(CountryISO::MNE), - "MAR" => Ok(CountryISO::MAR), "MOZ" => Ok(CountryISO::MOZ), - "MMR" => Ok(CountryISO::MMR), + "MRT" => Ok(CountryISO::MRT), + "MUS" => Ok(CountryISO::MUS), + "MWI" => Ok(CountryISO::MWI), + "MYS" => Ok(CountryISO::MYS), "NAM" => Ok(CountryISO::NAM), - "NRU" => Ok(CountryISO::NRU), - "NPL" => Ok(CountryISO::NPL), - "NLD" => Ok(CountryISO::NLD), - "NZL" => Ok(CountryISO::NZL), - "NIC" => Ok(CountryISO::NIC), "NER" => Ok(CountryISO::NER), "NGA" => Ok(CountryISO::NGA), - "MKD" => Ok(CountryISO::MKD), + "NIC" => Ok(CountryISO::NIC), + "NLD" => Ok(CountryISO::NLD), "NOR" => Ok(CountryISO::NOR), + "NPL" => Ok(CountryISO::NPL), + "NRU" => Ok(CountryISO::NRU), + "NZL" => Ok(CountryISO::NZL), "OMN" => Ok(CountryISO::OMN), "PAK" => Ok(CountryISO::PAK), - "PLW" => Ok(CountryISO::PLW), "PAN" => Ok(CountryISO::PAN), - "PNG" => Ok(CountryISO::PNG), - "PRY" => Ok(CountryISO::PRY), "PER" => Ok(CountryISO::PER), "PHL" => Ok(CountryISO::PHL), + "PLW" => Ok(CountryISO::PLW), + "PNG" => Ok(CountryISO::PNG), "POL" => Ok(CountryISO::POL), "PRT" => Ok(CountryISO::PRT), + "PRY" => Ok(CountryISO::PRY), "QAT" => Ok(CountryISO::QAT), - "KOR" => Ok(CountryISO::KOR), "ROU" => Ok(CountryISO::ROU), "RUS" => Ok(CountryISO::RUS), "RWA" => Ok(CountryISO::RWA), - "KNA" => Ok(CountryISO::KNA), - "LCA" => Ok(CountryISO::LCA), - "VCT" => Ok(CountryISO::VCT), - "WSM" => Ok(CountryISO::WSM), - "SMR" => Ok(CountryISO::SMR), - "STP" => Ok(CountryISO::STP), "SAU" => Ok(CountryISO::SAU), + "SDN" => Ok(CountryISO::SDN), "SEN" => Ok(CountryISO::SEN), - "SRB" => Ok(CountryISO::SRB), - "SYC" => Ok(CountryISO::SYC), - "SLE" => Ok(CountryISO::SLE), "SGP" => Ok(CountryISO::SGP), - "SVK" => Ok(CountryISO::SVK), - "SVN" => Ok(CountryISO::SVN), "SLB" => Ok(CountryISO::SLB), + "SLE" => Ok(CountryISO::SLE), + "SLV" => Ok(CountryISO::SLV), + "SMR" => Ok(CountryISO::SMR), "SOM" => Ok(CountryISO::SOM), - "ZAF" => Ok(CountryISO::ZAF), + "SRB" => Ok(CountryISO::SRB), "SSD" => Ok(CountryISO::SSD), - "ESP" => Ok(CountryISO::ESP), - "LKA" => Ok(CountryISO::LKA), - "SDN" => Ok(CountryISO::SDN), + "STP" => Ok(CountryISO::STP), "SUR" => Ok(CountryISO::SUR), + "SVK" => Ok(CountryISO::SVK), + "SVN" => Ok(CountryISO::SVN), "SWE" => Ok(CountryISO::SWE), - "CHE" => Ok(CountryISO::CHE), + "SWZ" => Ok(CountryISO::SWZ), + "SYC" => Ok(CountryISO::SYC), "SYR" => Ok(CountryISO::SYR), - "TJK" => Ok(CountryISO::TJK), - "TZA" => Ok(CountryISO::TZA), + "TCD" => Ok(CountryISO::TCD), + "TGO" => Ok(CountryISO::TGO), "THA" => Ok(CountryISO::THA), + "TJK" => Ok(CountryISO::TJK), + "TKM" => Ok(CountryISO::TKM), "TLS" => Ok(CountryISO::TLS), - "TGO" => Ok(CountryISO::TGO), "TON" => Ok(CountryISO::TON), "TTO" => Ok(CountryISO::TTO), "TUN" => Ok(CountryISO::TUN), "TUR" => Ok(CountryISO::TUR), - "TKM" => Ok(CountryISO::TKM), "TUV" => Ok(CountryISO::TUV), + "TZA" => Ok(CountryISO::TZA), "UGA" => Ok(CountryISO::UGA), "UKR" => Ok(CountryISO::UKR), - "ARE" => Ok(CountryISO::ARE), - "GBR" => Ok(CountryISO::GBR), - "USA" => Ok(CountryISO::USA), "URY" => Ok(CountryISO::URY), + "USA" => Ok(CountryISO::USA), "UZB" => Ok(CountryISO::UZB), - "VUT" => Ok(CountryISO::VUT), "VEN" => Ok(CountryISO::VEN), + "VCT" => Ok(CountryISO::VCT), "VNM" => Ok(CountryISO::VNM), + "VUT" => Ok(CountryISO::VUT), + "WSM" => Ok(CountryISO::WSM), "YEM" => Ok(CountryISO::YEM), + "ZAF" => Ok(CountryISO::ZAF), "ZMB" => Ok(CountryISO::ZMB), "ZWE" => Ok(CountryISO::ZWE), - "ATG" => Ok(CountryISO::ATG), _ => Err(ApiError::ParsingError("Invalid Country ISO")), } } -#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, strum::Display, Copy)] pub enum CountryISO2 { AD, AE, AF, + AG, AI, AL, AM, AO, + AQ, AR, AS, AT, AU, + AW, AX, AZ, BA, @@ -430,20 +433,26 @@ pub enum CountryISO2 { BH, BI, BJ, + BL, + BM, BN, BO, + BQ, BR, BS, BT, + BV, BW, BY, BZ, CA, + CC, CD, CF, CG, CH, CI, + CK, CL, CM, CN, @@ -451,6 +460,8 @@ pub enum CountryISO2 { CR, CU, CV, + CW, + CX, CY, CZ, DE, @@ -461,27 +472,38 @@ pub enum CountryISO2 { DZ, EC, EE, + EH, EG, ER, ES, ET, FI, FJ, + FK, FM, + FO, FR, GA, GB, GD, GE, + GF, + GG, GH, + GI, + GL, GM, GN, + GP, GQ, GR, + GS, GT, + GU, GW, GY, HK, + HM, HN, HR, HT, @@ -489,11 +511,14 @@ pub enum CountryISO2 { ID, IE, IL, + IM, IN, + IO, IQ, IR, IS, IT, + JE, JM, JO, JP, @@ -506,6 +531,7 @@ pub enum CountryISO2 { KP, KR, KW, + KY, KZ, LA, LB, @@ -522,6 +548,369 @@ pub enum CountryISO2 { MC, MD, ME, + MF, MG, MH, + MK, + ML, + MM, + MN, + MO, + MP, + MQ, + MR, + MS, + MT, + MU, + MV, + MW, + MX, + MY, + MZ, + NA, + NC, + NE, + NF, + NG, + NI, + NL, + NO, + NP, + NR, + NU, + NZ, + OM, + PA, + PE, + PF, + PG, + PH, + PK, + PL, + PM, + PN, + PR, + PS, + PT, + PW, + PY, + QA, + RE, + RO, + RS, + RU, + RW, + SA, + SB, + SC, + SD, + SE, + SG, + SH, + SI, + SJ, + SK, + SL, + SM, + SN, + SO, + SR, + SS, + ST, + SV, + SX, + SY, + SZ, + TC, + TD, + TF, + TG, + TH, + TJ, + TK, + TL, + TM, + TN, + TO, + TR, + TT, + TV, + TW, + TZ, + UA, + UG, + UM, + US, + UY, + UZ, + VA, + VC, + VE, + VG, + VI, + VN, + VU, + WF, + WS, + YE, + YT, + ZA, + ZM, + ZW, +} + +impl CountryISO2 { + pub fn text_to_country(text: &str) -> Result { + match text { + "AD" => Ok(Self::AD), + "AE" => Ok(Self::AE), + "AF" => Ok(Self::AF), + "AG" => Ok(Self::AG), + "AI" => Ok(Self::AI), + "AL" => Ok(Self::AL), + "AM" => Ok(Self::AM), + "AO" => Ok(Self::AO), + "AQ" => Ok(Self::AQ), + "AR" => Ok(Self::AR), + "AS" => Ok(Self::AS), + "AT" => Ok(Self::AT), + "AU" => Ok(Self::AU), + "AW" => Ok(Self::AW), + "AX" => Ok(Self::AX), + "AZ" => Ok(Self::AZ), + "BA" => Ok(Self::BA), + "BB" => Ok(Self::BB), + "BD" => Ok(Self::BD), + "BE" => Ok(Self::BE), + "BF" => Ok(Self::BF), + "BG" => Ok(Self::BG), + "BH" => Ok(Self::BH), + "BI" => Ok(Self::BI), + "BJ" => Ok(Self::BJ), + "BL" => Ok(Self::BL), + "BM" => Ok(Self::BM), + "BN" => Ok(Self::BN), + "BO" => Ok(Self::BO), + "BQ" => Ok(Self::BQ), + "BR" => Ok(Self::BR), + "BS" => Ok(Self::BS), + "BT" => Ok(Self::BT), + "BV" => Ok(Self::BV), + "BW" => Ok(Self::BW), + "BY" => Ok(Self::BY), + "BZ" => Ok(Self::BZ), + "CA" => Ok(Self::CA), + "CC" => Ok(Self::CC), + "CD" => Ok(Self::CD), + "CF" => Ok(Self::CF), + "CG" => Ok(Self::CG), + "CH" => Ok(Self::CH), + "CI" => Ok(Self::CI), + "CK" => Ok(Self::CK), + "CL" => Ok(Self::CL), + "CM" => Ok(Self::CM), + "CN" => Ok(Self::CN), + "CO" => Ok(Self::CO), + "CR" => Ok(Self::CR), + "CU" => Ok(Self::CU), + "CV" => Ok(Self::CV), + "CW" => Ok(Self::CW), + "CX" => Ok(Self::CX), + "CY" => Ok(Self::CY), + "CZ" => Ok(Self::CZ), + "DE" => Ok(Self::DE), + "DJ" => Ok(Self::DJ), + "DK" => Ok(Self::DK), + "DM" => Ok(Self::DM), + "DO" => Ok(Self::DO), + "DZ" => Ok(Self::DZ), + "EC" => Ok(Self::EC), + "EE" => Ok(Self::EE), + "EH" => Ok(Self::EH), + "EG" => Ok(Self::EG), + "ER" => Ok(Self::ER), + "ES" => Ok(Self::ES), + "ET" => Ok(Self::ET), + "FI" => Ok(Self::FI), + "FJ" => Ok(Self::FJ), + "FK" => Ok(Self::FK), + "FM" => Ok(Self::FM), + "FO" => Ok(Self::FO), + "FR" => Ok(Self::FR), + "GA" => Ok(Self::GA), + "GB" => Ok(Self::GB), + "GD" => Ok(Self::GD), + "GE" => Ok(Self::GE), + "GF" => Ok(Self::GF), + "GG" => Ok(Self::GG), + "GH" => Ok(Self::GH), + "GI" => Ok(Self::GI), + "GL" => Ok(Self::GL), + "GM" => Ok(Self::GM), + "GN" => Ok(Self::GN), + "GP" => Ok(Self::GP), + "GQ" => Ok(Self::GQ), + "GR" => Ok(Self::GR), + "GS" => Ok(Self::GS), + "GT" => Ok(Self::GT), + "GU" => Ok(Self::GU), + "GW" => Ok(Self::GW), + "GY" => Ok(Self::GY), + "HK" => Ok(Self::HK), + "HM" => Ok(Self::HM), + "HN" => Ok(Self::HN), + "HR" => Ok(Self::HR), + "HT" => Ok(Self::HT), + "HU" => Ok(Self::HU), + "ID" => Ok(Self::ID), + "IE" => Ok(Self::IE), + "IL" => Ok(Self::IL), + "IM" => Ok(Self::IM), + "IN" => Ok(Self::IN), + "IO" => Ok(Self::IO), + "IQ" => Ok(Self::IQ), + "IR" => Ok(Self::IR), + "IS" => Ok(Self::IS), + "IT" => Ok(Self::IT), + "JE" => Ok(Self::JE), + "JM" => Ok(Self::JM), + "JO" => Ok(Self::JO), + "JP" => Ok(Self::JP), + "KE" => Ok(Self::KE), + "KG" => Ok(Self::KG), + "KH" => Ok(Self::KH), + "KI" => Ok(Self::KI), + "KM" => Ok(Self::KM), + "KN" => Ok(Self::KN), + "KP" => Ok(Self::KP), + "KR" => Ok(Self::KR), + "KW" => Ok(Self::KW), + "KY" => Ok(Self::KY), + "KZ" => Ok(Self::KZ), + "LA" => Ok(Self::LA), + "LB" => Ok(Self::LB), + "LC" => Ok(Self::LC), + "LI" => Ok(Self::LI), + "LK" => Ok(Self::LK), + "LR" => Ok(Self::LR), + "LS" => Ok(Self::LS), + "LT" => Ok(Self::LT), + "LU" => Ok(Self::LU), + "LV" => Ok(Self::LV), + "LY" => Ok(Self::LY), + "MA" => Ok(Self::MA), + "MC" => Ok(Self::MC), + "MD" => Ok(Self::MD), + "ME" => Ok(Self::ME), + "MF" => Ok(Self::MF), + "MG" => Ok(Self::MG), + "MH" => Ok(Self::MH), + "MK" => Ok(Self::MK), + "ML" => Ok(Self::ML), + "MM" => Ok(Self::MM), + "MN" => Ok(Self::MN), + "MO" => Ok(Self::MO), + "MP" => Ok(Self::MP), + "MQ" => Ok(Self::MQ), + "MR" => Ok(Self::MR), + "MS" => Ok(Self::MS), + "MT" => Ok(Self::MT), + "MU" => Ok(Self::MU), + "MV" => Ok(Self::MV), + "MW" => Ok(Self::MW), + "MX" => Ok(Self::MX), + "MY" => Ok(Self::MY), + "MZ" => Ok(Self::MZ), + "NA" => Ok(Self::NA), + "NC" => Ok(Self::NC), + "NE" => Ok(Self::NE), + "NF" => Ok(Self::NF), + "NG" => Ok(Self::NG), + "NI" => Ok(Self::NI), + "NL" => Ok(Self::NL), + "NO" => Ok(Self::NO), + "NP" => Ok(Self::NP), + "NR" => Ok(Self::NR), + "NU" => Ok(Self::NU), + "NZ" => Ok(Self::NZ), + "OM" => Ok(Self::OM), + "PA" => Ok(Self::PA), + "PE" => Ok(Self::PE), + "PF" => Ok(Self::PF), + "PG" => Ok(Self::PG), + "PH" => Ok(Self::PH), + "PK" => Ok(Self::PK), + "PL" => Ok(Self::PL), + "PM" => Ok(Self::PM), + "PN" => Ok(Self::PN), + "PR" => Ok(Self::PR), + "PS" => Ok(Self::PS), + "PT" => Ok(Self::PT), + "PW" => Ok(Self::PW), + "PY" => Ok(Self::PY), + "QA" => Ok(Self::QA), + "RE" => Ok(Self::RE), + "RO" => Ok(Self::RO), + "RS" => Ok(Self::RS), + "RU" => Ok(Self::RU), + "RW" => Ok(Self::RW), + "SA" => Ok(Self::SA), + "SB" => Ok(Self::SB), + "SC" => Ok(Self::SC), + "SD" => Ok(Self::SD), + "SE" => Ok(Self::SE), + "SG" => Ok(Self::SG), + "SH" => Ok(Self::SH), + "SI" => Ok(Self::SI), + "SJ" => Ok(Self::SJ), + "SK" => Ok(Self::SK), + "SL" => Ok(Self::SL), + "SM" => Ok(Self::SM), + "SN" => Ok(Self::SN), + "SO" => Ok(Self::SO), + "SR" => Ok(Self::SR), + "SS" => Ok(Self::SS), + "ST" => Ok(Self::ST), + "SV" => Ok(Self::SV), + "SX" => Ok(Self::SX), + "SY" => Ok(Self::SY), + "SZ" => Ok(Self::SZ), + "TC" => Ok(Self::TC), + "TD" => Ok(Self::TD), + "TF" => Ok(Self::TF), + "TG" => Ok(Self::TG), + "TH" => Ok(Self::TH), + "TJ" => Ok(Self::TJ), + "TK" => Ok(Self::TK), + "TL" => Ok(Self::TL), + "TM" => Ok(Self::TM), + "TN" => Ok(Self::TN), + "TO" => Ok(Self::TO), + "TR" => Ok(Self::TR), + "TT" => Ok(Self::TT), + "TV" => Ok(Self::TV), + "TZ" => Ok(Self::TZ), + "UA" => Ok(Self::UA), + "UG" => Ok(Self::UG), + "UM" => Ok(Self::UM), + "US" => Ok(Self::US), + "UY" => Ok(Self::UY), + "UZ" => Ok(Self::UZ), + "VA" => Ok(Self::VA), + "VC" => Ok(Self::VC), + "VE" => Ok(Self::VE), + "VG" => Ok(Self::VG), + "VI" => Ok(Self::VI), + "VN" => Ok(Self::VN), + "VU" => Ok(Self::VU), + "WF" => Ok(Self::WF), + "WS" => Ok(Self::WS), + "YE" => Ok(Self::YE), + "YT" => Ok(Self::YT), + "ZA" => Ok(Self::ZA), + "ZM" => Ok(Self::ZM), + "ZW" => Ok(Self::ZW), + _ => Err(ApiError::ParsingError("Invalid Country ISO2")), + } + } } diff --git a/src/types/feature.rs b/src/types/feature.rs index f3ce71ca..411018c1 100644 --- a/src/types/feature.rs +++ b/src/types/feature.rs @@ -1,7 +1,7 @@ use crate::app::get_tenant_app_state; // use db::eulermeshimpl::mesh_config; // use db::mesh::internal::*; -use crate::storage::types::{BitBool, Feature as DBFeature}; +use crate::storage::types::Feature as DBFeature; // use types::utils::dbconfig::get_euler_db_conf; use crate::types::merchant::id::{merchant_id_to_text, to_merchant_id, MerchantId}; // use juspay::extra::parsing::{Parsed, Step, around, lift_pure, mandated, parse_field, project}; diff --git a/src/types/gateway.rs b/src/types/gateway.rs index 82aba145..d6b6fc62 100644 --- a/src/types/gateway.rs +++ b/src/types/gateway.rs @@ -9,207 +9,208 @@ use crate::error::ApiError; #[derive( Debug, Clone, PartialOrd, Ord, PartialEq, Serialize, Deserialize, Eq, Hash, strum::Display, )] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Gateway { - ADYEN, - AFTERPAY, - AIRPAY, - PAYPHI, - AIRTELMONEY, - AIRWALLEX, - AMAZONPAY, - AMEX, - AMPS, - ATOM, - AUTHORIZEDOTNET, - AXIS, - AXIS_BIZ, - AXIS_UPI, - AXISNB, - BAJAJFINSERV, - BHARATX, - BILLDESK, - BLAZEPAY, - BRAINTREE, - BOKU, - CAMSPAY, - CAPILLARY, - CAPITALFLOAT, - CAREEMPAY, - CASH, - CASHTOCODE, - CCAVENUE, - CCAVENUE_V2, - CHECKOUT, - CIELO, - CITI, - CITRUS, - CRED, - CYBERSOURCE, - CYBS_CHASE, - DIGIO, - DIRECT_BANK_EMI, - DLOCAL, - DUMMY, - EARLYSALARY, - EASEBUZZ, - EBANX, - EBS, - EBS_V3, - EPAYLATER, - EXPRESSPG, - FACILITAPAY, - FASPAY, - FEDERAL_BIZ, - FDCONNECT, - FIB, - FINBOX, - FLEXMONEY, - FLUTTERWAVE, - FREECHARGE, - FREECHARGEPG, - FREECHARGE_V2, - FSS_ATM_PIN, - FSS_ATM_PIN_V2, - FSSPAY, - GOCASHFREE, - GOKWIK, - GOOGLEPAY, - GPAY_IMF, - GYFTR, - HDFC, - HDFC_DC_EMI, - HDFC_CC_EMI, - HDFC_EBS_VAS, - HDFC_FLEXIPAY, - HDFC_IVR, - HDFCBANK_SMARTGATEWAY, - HDFC_UPI, - HDFCNB, - HSBC, - HSBC_UPI, - HYPERPAY, - HYPERPG, - HYPER_PG, - IATAPAY, - ICICI, - ICICI_UPI, - ICICINB, - ICICIPAYLATER, - IFG, - INDUS_UPI, - INSPIRE_NETZ, - INSTACRED, - INDUS_PAYU, - IPG, - ITAU, - ITZCASH, - JIOMONEY, - JIOPAY, - KBANK, - KLARNA, - KOTAK, - KOTAK_BIZ, - KOTAK_UPI, - LAZYPAY, - LINEPAY, - LOANTAP, - LOTUSPAY, - LOYLTYREWARDZ, - LSP, - LSP_ETB, - LYRA, - M2P, - MERCADOPAGO, - MERCHANT_CONTAINER, - MIDTRANS, - MIGS, - MOBIKWIK, - MOBIKWIKZIP, - MORPHEUS, - MPESA, - MPGS, - MSWIPE, - MYFATOORAH, - TAPPAY, - TAZAPAY, - PAYMOB, - FAWRYPAY, - NAVITAIRE, - NBBL, - MOSAMBEE, - NETCETERA, - NMI, - NOON, - NONE, - NUPAY, - OLAMONEY, - OLAPOSTPAID, - ONEPAY, - OPNPAYMENTS, - PAGALEVE, - PAGSEGURO, - PAYAMIGO, - PAYFORT, - PAYGLOCAL, - PAYLATER, - PAYPAL, - PAYTM, - PAYTM_UPI, - PAYTM_V2, - PAYU, - PAYZAPP, - PHONEPE, - PINELABS, - PINELABS_OFFLINE, - POSTPAY, - QWIKCILVER, - RAZORPAY, - RBL, - RBL_BIZ, - SBI, - SBI_UPI, - SBIBUDDY, - SBIePAY, - SEZZLE, - SHOPSE, - SIKA_SIMPL, - SIMPL, - SNAPMINT, - SODEXO, - STRIPE, - TABBY, - TATA_PA, - TATA_UPI, - TATANEU, - TATAPAY, - TATAPAYLATER, - TAP, - TPSL, - TPSL_SI, - TRIPLEA, - TRUSTPAY, - TWID, - TWID_V2, - TWOC_TWOP, - VIJAYA_UPI, - VOLT, - VISA_PBW, - WALLET_CONTAINER, - WORLDPAY, - XENDIT, - YES_BIZ, - YESBANK_UPI, - YPP, - ZAAKPAY, - ZESTMONEY, - DOKU, - ZAINCASH, - FASTPAY, - PAY10, - PINELABS_ONLINE, - WIBMO, - NDPS, - DEFAULT, + Adyen, + Afterpay, + Airpay, + Payphi, + Airtelmoney, + Airwallex, + Amazonpay, + Amex, + Amps, + Atom, + Authorizedotnet, + Axis, + AxisBiz, + AxisUpi, + Axisnb, + Bajajfinserv, + Bharatx, + Billdesk, + Blazepay, + Braintree, + Boku, + Camspay, + Capillary, + Capitalfloat, + Careempay, + Cash, + Cashtocode, + Ccavenue, + CcavenueV2, + Checkout, + Cielo, + Citi, + Citrus, + Cred, + Cybersource, + CybsChase, + Digio, + DirectBankEmi, + Dlocal, + Dummy, + Earlysalary, + Easebuzz, + Ebanx, + Ebs, + EbsV3, + Epaylater, + Expresspg, + Facilitatpay, + FasPay, + FederalBiz, + Fdconnect, + Fib, + Finbox, + Flexmoney, + Flutterwave, + Freecharge, + Freechargepg, + FreechargeV2, + FssAtmPin, + FssAtmPinV2, + Fsspay, + Gocashfree, + Gokwik, + Googlepay, + GpayImf, + Gyftr, + Hdfc, + HdfcDcEmi, + HdfcCcEmi, + HdfcEbsVas, + HdfcFlexipay, + HdfcIvr, + HdfcbankSmartgateway, + HdfcUpi, + Hdfcnb, + Hsbc, + HsbcUpi, + Hyperpay, + Hyperpg, + HyperPg, + Iatapay, + Icici, + IciciUpi, + Icicinb, + Icicipaylater, + Ifg, + IndusUpi, + InspireNetz, + Instacred, + IndusPayu, + Ipg, + Itau, + Itzcash, + Jiomoney, + Jiopay, + Kbank, + Klarna, + Kotak, + KotakBiz, + KotakUpi, + Lazypay, + Linepay, + Loantap, + Lotuspay, + Loyltyrewardz, + Lsp, + LspEtb, + Lyra, + M2p, + Mercadopago, + MerchantContainer, + Midtrans, + Migs, + Mobikwik, + Mobikwikzip, + Morpheus, + Mpesa, + Mpgs, + Mswipe, + Myfatoorah, + Tappay, + Tazapay, + Paymob, + Fawrypay, + Navitaire, + Nbbl, + Mosambee, + Netcetera, + Nmi, + Noon, + None, + Nupay, + Olamoney, + Olapostpaid, + Onepay, + Opnpayments, + Pagaleve, + Pagseguo, + Payamigo, + Payfort, + Payglocal, + Paylater, + Paypal, + Paytm, + PaytmUpi, + PaytmV2, + Payu, + Payzapp, + Phonepe, + Pinelabs, + PinelabsOffline, + Postpay, + Qwikcilver, + Razorpay, + Rbl, + RblBiz, + Sbi, + SbiUpi, + Sbibuddy, + Sbiepay, + Sezzle, + Shopse, + SikaSimpl, + Simpl, + Snapmint, + Sodexo, + Stripe, + Tabby, + TataPa, + TataUpi, + Tatanue, + Tatapay, + Tatapaylater, + Tap, + Tpsl, + TpslSi, + Triplea, + Trustpay, + Twid, + TwidV2, + TwocTwop, + VijayaUpi, + Volt, + VisaPbw, + WalletContainer, + Worldpay, + Xendit, + YesBiz, + YesbankUpi, + Ypp, + Zaakpay, + Zestmoney, + Doku, + Zaincash, + Fastpay, + Pay10, + PinelabsOnline, + Wibmo, + Ndps, + Default, } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -236,206 +237,206 @@ pub fn gateway_any_to_text(gateway_any: &GatewayAny) -> Option { pub fn text_to_gateway(text: &str) -> Result { match text { - "ADYEN" => Ok(Gateway::ADYEN), - "AFTERPAY" => Ok(Gateway::AFTERPAY), - "AIRPAY" => Ok(Gateway::AIRPAY), - "PAYPHI" => Ok(Gateway::PAYPHI), - "AIRTELMONEY" => Ok(Gateway::AIRTELMONEY), - "AIRWALLEX" => Ok(Gateway::AIRWALLEX), - "AMAZONPAY" => Ok(Gateway::AMAZONPAY), - "AMEX" => Ok(Gateway::AMEX), - "AMPS" => Ok(Gateway::AMPS), - "ATOM" => Ok(Gateway::ATOM), - "AUTHORIZEDOTNET" => Ok(Gateway::AUTHORIZEDOTNET), - "AXIS_BIZ" => Ok(Gateway::AXIS_BIZ), - "AXIS_UPI" => Ok(Gateway::AXIS_UPI), - "AXIS" => Ok(Gateway::AXIS), - "AXISNB" => Ok(Gateway::AXISNB), - "BAJAJFINSERV" => Ok(Gateway::BAJAJFINSERV), - "BHARATX" => Ok(Gateway::BHARATX), - "BILLDESK" => Ok(Gateway::BILLDESK), - "BLAZEPAY" => Ok(Gateway::BLAZEPAY), - "BRAINTREE" => Ok(Gateway::BRAINTREE), - "BOKU" => Ok(Gateway::BOKU), - "CAMSPAY" => Ok(Gateway::CAMSPAY), - "CAPILLARY" => Ok(Gateway::CAPILLARY), - "CAPITALFLOAT" => Ok(Gateway::CAPITALFLOAT), - "CAREEMPAY" => Ok(Gateway::CAREEMPAY), - "CASH" => Ok(Gateway::CASH), - "CASHTOCODE" => Ok(Gateway::CASHTOCODE), - "CCAVENUE_V2" => Ok(Gateway::CCAVENUE_V2), - "CCAVENUE" => Ok(Gateway::CCAVENUE), - "CHECKOUT" => Ok(Gateway::CHECKOUT), - "CIELO" => Ok(Gateway::CIELO), - "CITI" => Ok(Gateway::CITI), - "CITRUS" => Ok(Gateway::CITRUS), - "CRED" => Ok(Gateway::CRED), - "CYBERSOURCE" => Ok(Gateway::CYBERSOURCE), - "CYBS_CHASE" => Ok(Gateway::CYBS_CHASE), - "DIGIO" => Ok(Gateway::DIGIO), - "DLOCAL" => Ok(Gateway::DLOCAL), - "DUMMY" => Ok(Gateway::DUMMY), - "EARLYSALARY" => Ok(Gateway::EARLYSALARY), - "EASEBUZZ" => Ok(Gateway::EASEBUZZ), - "EBANX" => Ok(Gateway::EBANX), - "EBS_V3" => Ok(Gateway::EBS_V3), - "EBS" => Ok(Gateway::EBS), - "EPAYLATER" => Ok(Gateway::EPAYLATER), - "EXPRESSPG" => Ok(Gateway::EXPRESSPG), - "FACILITAPAY" => Ok(Gateway::FACILITAPAY), - "FASPAY" => Ok(Gateway::FASPAY), - "FASTPAY" => Ok(Gateway::FASTPAY), - "FEDERAL_BIZ" => Ok(Gateway::FEDERAL_BIZ), - "FDCONNECT" => Ok(Gateway::FDCONNECT), - "FIB" => Ok(Gateway::FIB), - "FINBOX" => Ok(Gateway::FINBOX), - "FLEXMONEY" => Ok(Gateway::FLEXMONEY), - "FLUTTERWAVE" => Ok(Gateway::FLUTTERWAVE), - "FREECHARGE_V2" => Ok(Gateway::FREECHARGE_V2), - "FREECHARGEPG" => Ok(Gateway::FREECHARGEPG), - "FREECHARGE" => Ok(Gateway::FREECHARGE), - "FSS_ATM_PIN_V2" => Ok(Gateway::FSS_ATM_PIN_V2), - "FSS_ATM_PIN" => Ok(Gateway::FSS_ATM_PIN), - "FSSPAY" => Ok(Gateway::FSSPAY), - "GOCASHFREE" => Ok(Gateway::GOCASHFREE), - "GOKWIK" => Ok(Gateway::GOKWIK), - "GOOGLEPAY" => Ok(Gateway::GOOGLEPAY), - "GPAY_IMF" => Ok(Gateway::GPAY_IMF), - "GYFTR" => Ok(Gateway::GYFTR), - "HDFC_DC_EMI" => Ok(Gateway::HDFC_DC_EMI), - "HDFC_CC_EMI" => Ok(Gateway::HDFC_CC_EMI), - "HDFC_EBS_VAS" => Ok(Gateway::HDFC_EBS_VAS), - "HDFC_FLEXIPAY" => Ok(Gateway::HDFC_FLEXIPAY), - "HDFC_IVR" => Ok(Gateway::HDFC_IVR), - "HDFCBANK_SMARTGATEWAY" => Ok(Gateway::HDFCBANK_SMARTGATEWAY), - "HDFC_UPI" => Ok(Gateway::HDFC_UPI), - "HDFC" => Ok(Gateway::HDFC), - "HDFCNB" => Ok(Gateway::HDFCNB), - "HSBC" => Ok(Gateway::HSBC), - "HSBC_UPI" => Ok(Gateway::HSBC_UPI), - "HYPERPG" => Ok(Gateway::HYPERPG), - "HYPERPAY" => Ok(Gateway::HYPERPAY), - "HYPER_PG" => Ok(Gateway::HYPER_PG), - "IATAPAY" => Ok(Gateway::IATAPAY), - "ICICI_UPI" => Ok(Gateway::ICICI_UPI), - "ICICI" => Ok(Gateway::ICICI), - "ICICINB" => Ok(Gateway::ICICINB), - "ICICIPAYLATER" => Ok(Gateway::ICICIPAYLATER), - "IFG" => Ok(Gateway::IFG), - "INDUS_UPI" => Ok(Gateway::INDUS_UPI), - "INSPIRE_NETZ" => Ok(Gateway::INSPIRE_NETZ), - "INSTACRED" => Ok(Gateway::INSTACRED), - "INDUS_PAYU" => Ok(Gateway::INDUS_PAYU), - "IPG" => Ok(Gateway::IPG), - "ITAU" => Ok(Gateway::ITAU), - "ITZCASH" => Ok(Gateway::ITZCASH), - "JIOMONEY" => Ok(Gateway::JIOMONEY), - "JIOPAY" => Ok(Gateway::JIOPAY), - "KBANK" => Ok(Gateway::KBANK), - "KLARNA" => Ok(Gateway::KLARNA), - "KOTAK_UPI" => Ok(Gateway::KOTAK_UPI), - "KOTAK" => Ok(Gateway::KOTAK), - "KOTAK_BIZ" => Ok(Gateway::KOTAK_BIZ), - "LAZYPAY" => Ok(Gateway::LAZYPAY), - "LINEPAY" => Ok(Gateway::LINEPAY), - "LOANTAP" => Ok(Gateway::LOANTAP), - "LOTUSPAY" => Ok(Gateway::LOTUSPAY), - "LOYLTYREWARDZ" => Ok(Gateway::LOYLTYREWARDZ), - "LSP" => Ok(Gateway::LSP), - "LSP_ETB" => Ok(Gateway::LSP_ETB), - "LYRA" => Ok(Gateway::LYRA), - "M2P" => Ok(Gateway::M2P), - "MERCADOPAGO" => Ok(Gateway::MERCADOPAGO), - "MERCHANT_CONTAINER" => Ok(Gateway::MERCHANT_CONTAINER), - "MIDTRANS" => Ok(Gateway::MIDTRANS), - "MIGS" => Ok(Gateway::MIGS), - "MOBIKWIK" => Ok(Gateway::MOBIKWIK), - "MOBIKWIKZIP" => Ok(Gateway::MOBIKWIKZIP), - "MORPHEUS" => Ok(Gateway::MORPHEUS), - "MPESA" => Ok(Gateway::MPESA), - "MPGS" => Ok(Gateway::MPGS), - "MSWIPE" => Ok(Gateway::MSWIPE), - "MYFATOORAH" => Ok(Gateway::MYFATOORAH), - "TAPPAY" => Ok(Gateway::TAPPAY), - "TAZAPAY" => Ok(Gateway::TAZAPAY), - "PAYMOB" => Ok(Gateway::PAYMOB), - "FAWRYPAY" => Ok(Gateway::FAWRYPAY), - "NAVITAIRE" => Ok(Gateway::NAVITAIRE), - "NBBL" => Ok(Gateway::NBBL), - "MOSAMBEE" => Ok(Gateway::MOSAMBEE), - "NETCETERA" => Ok(Gateway::NETCETERA), - "NMI" => Ok(Gateway::NMI), - "NOON" => Ok(Gateway::NOON), - "NONE" => Ok(Gateway::NONE), - "NUPAY" => Ok(Gateway::NUPAY), - "OLAMONEY" => Ok(Gateway::OLAMONEY), - "OLAPOSTPAID" => Ok(Gateway::OLAPOSTPAID), - "ONEPAY" => Ok(Gateway::ONEPAY), - "OPNPAYMENTS" => Ok(Gateway::OPNPAYMENTS), - "PAGALEVE" => Ok(Gateway::PAGALEVE), - "PAGSEGURO" => Ok(Gateway::PAGSEGURO), - "PAYAMIGO" => Ok(Gateway::PAYAMIGO), - "PAYFORT" => Ok(Gateway::PAYFORT), - "PAYGLOCAL" => Ok(Gateway::PAYGLOCAL), - "PAYLATER" => Ok(Gateway::PAYLATER), - "PAYPAL" => Ok(Gateway::PAYPAL), - "PAYTM_UPI" => Ok(Gateway::PAYTM_UPI), - "PAYTM_V2" => Ok(Gateway::PAYTM_V2), - "PAYTM" => Ok(Gateway::PAYTM), - "PAYU" => Ok(Gateway::PAYU), - "PAYZAPP" => Ok(Gateway::PAYZAPP), - "PHONEPE" => Ok(Gateway::PHONEPE), - "PINELABS" => Ok(Gateway::PINELABS), - "PINELABS_OFFLINE" => Ok(Gateway::PINELABS_OFFLINE), - "POSTPAY" => Ok(Gateway::POSTPAY), - "QWIKCILVER" => Ok(Gateway::QWIKCILVER), - "RAZORPAY" => Ok(Gateway::RAZORPAY), - "RBL_BIZ" => Ok(Gateway::RBL_BIZ), - "RBL" => Ok(Gateway::RBL), - "SBI_UPI" => Ok(Gateway::SBI_UPI), - "SBI" => Ok(Gateway::SBI), - "SBIBUDDY" => Ok(Gateway::SBIBUDDY), - "SBIePAY" => Ok(Gateway::SBIePAY), - "SEZZLE" => Ok(Gateway::SEZZLE), - "SHOPSE" => Ok(Gateway::SHOPSE), - "SIKA_SIMPL" => Ok(Gateway::SIKA_SIMPL), - "SIMPL" => Ok(Gateway::SIMPL), - "SNAPMINT" => Ok(Gateway::SNAPMINT), - "SODEXO" => Ok(Gateway::SODEXO), - "STRIPE" => Ok(Gateway::STRIPE), - "TABBY" => Ok(Gateway::TABBY), - "TATA_PA" => Ok(Gateway::TATA_PA), - "TATA_UPI" => Ok(Gateway::TATA_UPI), - "TATANEU" => Ok(Gateway::TATANEU), - "TATAPAY" => Ok(Gateway::TATAPAY), - "TATAPAYLATER" => Ok(Gateway::TATAPAYLATER), - "TAP" => Ok(Gateway::TAP), - "TPSL_SI" => Ok(Gateway::TPSL_SI), - "TPSL" => Ok(Gateway::TPSL), - "TRIPLEA" => Ok(Gateway::TRIPLEA), - "TRUSTPAY" => Ok(Gateway::TRUSTPAY), - "TWID" => Ok(Gateway::TWID), - "TWID_V2" => Ok(Gateway::TWID_V2), - "TWOC_TWOP" => Ok(Gateway::TWOC_TWOP), - "VIJAYA_UPI" => Ok(Gateway::VIJAYA_UPI), - "VISA_PBW" => Ok(Gateway::VISA_PBW), - "VOLT" => Ok(Gateway::VOLT), - "WALLET_CONTAINER" => Ok(Gateway::WALLET_CONTAINER), - "WORLDPAY" => Ok(Gateway::WORLDPAY), - "XENDIT" => Ok(Gateway::XENDIT), - "YES_BIZ" => Ok(Gateway::YES_BIZ), - "YESBANK_UPI" => Ok(Gateway::YESBANK_UPI), - "YPP" => Ok(Gateway::YPP), - "ZAAKPAY" => Ok(Gateway::ZAAKPAY), - "ZESTMONEY" => Ok(Gateway::ZESTMONEY), - "DOKU" => Ok(Gateway::DOKU), - "DIRECT_BANK_EMI" => Ok(Gateway::DIRECT_BANK_EMI), - "ZAINCASH" => Ok(Gateway::ZAINCASH), - "PAY10" => Ok(Gateway::PAY10), - "PINELABS_ONLINE" => Ok(Gateway::PINELABS_ONLINE), - "WIBMO" => Ok(Gateway::WIBMO), - "NDPS" => Ok(Gateway::NDPS), - "DEFAULT" => Ok(Gateway::DEFAULT), + "ADYEN" => Ok(Gateway::Adyen), + "AFTERPAY" => Ok(Gateway::Afterpay), + "AIRPAY" => Ok(Gateway::Airpay), + "PAYPHI" => Ok(Gateway::Payphi), + "AIRTELMONEY" => Ok(Gateway::Airtelmoney), + "AIRWALLEX" => Ok(Gateway::Airwallex), + "AMAZONPAY" => Ok(Gateway::Amazonpay), + "AMEX" => Ok(Gateway::Amex), + "AMPS" => Ok(Gateway::Amps), + "ATOM" => Ok(Gateway::Atom), + "AUTHORIZEDOTNET" => Ok(Gateway::Authorizedotnet), + "AXIS_BIZ" => Ok(Gateway::AxisBiz), + "AXIS_UPI" => Ok(Gateway::AxisUpi), + "AXIS" => Ok(Gateway::Axis), + "AXISNB" => Ok(Gateway::Axisnb), + "BAJAJFINSERV" => Ok(Gateway::Bajajfinserv), + "BHARATX" => Ok(Gateway::Bharatx), + "BILLDESK" => Ok(Gateway::Billdesk), + "BLAZEPAY" => Ok(Gateway::Blazepay), + "BRAINTREE" => Ok(Gateway::Braintree), + "BOKU" => Ok(Gateway::Boku), + "CAMSPAY" => Ok(Gateway::Camspay), + "CAPILLARY" => Ok(Gateway::Capillary), + "CAPITALFLOAT" => Ok(Gateway::Capitalfloat), + "CAREEMPAY" => Ok(Gateway::Careempay), + "CASH" => Ok(Gateway::Cash), + "CASHTOCODE" => Ok(Gateway::Cashtocode), + "CCAVENUE_V2" => Ok(Gateway::CcavenueV2), + "CCAVENUE" => Ok(Gateway::Ccavenue), + "CHECKOUT" => Ok(Gateway::Checkout), + "CIELO" => Ok(Gateway::Cielo), + "CITI" => Ok(Gateway::Citi), + "CITRUS" => Ok(Gateway::Citrus), + "CRED" => Ok(Gateway::Cred), + "CYBERSOURCE" => Ok(Gateway::Cybersource), + "CYBS_CHASE" => Ok(Gateway::CybsChase), + "DIGIO" => Ok(Gateway::Digio), + "DLOCAL" => Ok(Gateway::Dlocal), + "DUMMY" => Ok(Gateway::Dummy), + "EARLYSALARY" => Ok(Gateway::Earlysalary), + "EASEBUZZ" => Ok(Gateway::Easebuzz), + "EBANX" => Ok(Gateway::Ebanx), + "EBS_V3" => Ok(Gateway::EbsV3), + "EBS" => Ok(Gateway::Ebs), + "EPAYLATER" => Ok(Gateway::Epaylater), + "EXPRESSPG" => Ok(Gateway::Expresspg), + "FACILITAPAY" => Ok(Gateway::Facilitatpay), + "FASPAY" => Ok(Gateway::FasPay), + "FASTPAY" => Ok(Gateway::Fastpay), + "FEDERAL_BIZ" => Ok(Gateway::FederalBiz), + "FDCONNECT" => Ok(Gateway::Fdconnect), + "FIB" => Ok(Gateway::Fib), + "FINBOX" => Ok(Gateway::Finbox), + "FLEXMONEY" => Ok(Gateway::Flexmoney), + "FLUTTERWAVE" => Ok(Gateway::Flutterwave), + "FREECHARGE_V2" => Ok(Gateway::FreechargeV2), + "FREECHARGEPG" => Ok(Gateway::Freechargepg), + "FREECHARGE" => Ok(Gateway::Freecharge), + "FSS_ATM_PIN_V2" => Ok(Gateway::FssAtmPinV2), + "FSS_ATM_PIN" => Ok(Gateway::FssAtmPin), + "FSSPAY" => Ok(Gateway::Fsspay), + "GOCASHFREE" => Ok(Gateway::Gocashfree), + "GOKWIK" => Ok(Gateway::Gokwik), + "GOOGLEPAY" => Ok(Gateway::Googlepay), + "GPAY_IMF" => Ok(Gateway::GpayImf), + "GYFTR" => Ok(Gateway::Gyftr), + "HDFC_DC_EMI" => Ok(Gateway::HdfcDcEmi), + "HDFC_CC_EMI" => Ok(Gateway::HdfcCcEmi), + "HDFC_EBS_VAS" => Ok(Gateway::HdfcEbsVas), + "HDFC_FLEXIPAY" => Ok(Gateway::HdfcFlexipay), + "HDFC_IVR" => Ok(Gateway::HdfcIvr), + "HDFCBANK_SMARTGATEWAY" => Ok(Gateway::HdfcbankSmartgateway), + "HDFC_UPI" => Ok(Gateway::HdfcUpi), + "HDFC" => Ok(Gateway::Hdfc), + "HDFCNB" => Ok(Gateway::Hdfcnb), + "HSBC" => Ok(Gateway::Hsbc), + "HSBC_UPI" => Ok(Gateway::HsbcUpi), + "HYPERPG" => Ok(Gateway::Hyperpg), + "HYPERPAY" => Ok(Gateway::Hyperpay), + "HYPER_PG" => Ok(Gateway::HyperPg), + "IATAPAY" => Ok(Gateway::Iatapay), + "ICICI_UPI" => Ok(Gateway::IciciUpi), + "ICICI" => Ok(Gateway::Icici), + "ICICINB" => Ok(Gateway::Icicinb), + "ICICIPAYLATER" => Ok(Gateway::Icicipaylater), + "IFG" => Ok(Gateway::Ifg), + "INDUS_UPI" => Ok(Gateway::IndusUpi), + "INSPIRE_NETZ" => Ok(Gateway::InspireNetz), + "INSTACRED" => Ok(Gateway::Instacred), + "INDUS_PAYU" => Ok(Gateway::IndusPayu), + "IPG" => Ok(Gateway::Ipg), + "ITAU" => Ok(Gateway::Itau), + "ITZCASH" => Ok(Gateway::Itzcash), + "JIOMONEY" => Ok(Gateway::Jiomoney), + "JIOPAY" => Ok(Gateway::Jiopay), + "KBANK" => Ok(Gateway::Kbank), + "KLARNA" => Ok(Gateway::Klarna), + "KOTAK_UPI" => Ok(Gateway::KotakUpi), + "KOTAK" => Ok(Gateway::Kotak), + "KOTAK_BIZ" => Ok(Gateway::KotakBiz), + "LAZYPAY" => Ok(Gateway::Lazypay), + "LINEPAY" => Ok(Gateway::Linepay), + "LOANTAP" => Ok(Gateway::Loantap), + "LOTUSPAY" => Ok(Gateway::Lotuspay), + "LOYLTYREWARDZ" => Ok(Gateway::Loyltyrewardz), + "LSP" => Ok(Gateway::Lsp), + "LSP_ETB" => Ok(Gateway::LspEtb), + "LYRA" => Ok(Gateway::Lyra), + "M2P" => Ok(Gateway::M2p), + "MERCADOPAGO" => Ok(Gateway::Mercadopago), + "MERCHANT_CONTAINER" => Ok(Gateway::MerchantContainer), + "MIDTRANS" => Ok(Gateway::Midtrans), + "MIGS" => Ok(Gateway::Migs), + "MOBIKWIK" => Ok(Gateway::Mobikwik), + "MOBIKWIKZIP" => Ok(Gateway::Mobikwikzip), + "MORPHEUS" => Ok(Gateway::Morpheus), + "MPESA" => Ok(Gateway::Mpesa), + "MPGS" => Ok(Gateway::Mpgs), + "MSWIPE" => Ok(Gateway::Mswipe), + "MYFATOORAH" => Ok(Gateway::Myfatoorah), + "TAPPAY" => Ok(Gateway::Tappay), + "TAZAPAY" => Ok(Gateway::Tazapay), + "PAYMOB" => Ok(Gateway::Paymob), + "FAWRYPAY" => Ok(Gateway::Fawrypay), + "NAVITAIRE" => Ok(Gateway::Navitaire), + "NBBL" => Ok(Gateway::Nbbl), + "MOSAMBEE" => Ok(Gateway::Mosambee), + "NETCETERA" => Ok(Gateway::Netcetera), + "NMI" => Ok(Gateway::Nmi), + "NOON" => Ok(Gateway::Noon), + "NONE" => Ok(Gateway::None), + "NUPAY" => Ok(Gateway::Nupay), + "OLAMONEY" => Ok(Gateway::Olamoney), + "OLAPOSTPAID" => Ok(Gateway::Olapostpaid), + "ONEPAY" => Ok(Gateway::Onepay), + "OPNPAYMENTS" => Ok(Gateway::Opnpayments), + "PAGALEVE" => Ok(Gateway::Pagaleve), + "PAGSEGURO" => Ok(Gateway::Pagseguo), + "PAYAMIGO" => Ok(Gateway::Payamigo), + "PAYFORT" => Ok(Gateway::Payfort), + "PAYGLOCAL" => Ok(Gateway::Payglocal), + "PAYLATER" => Ok(Gateway::Paylater), + "PAYPAL" => Ok(Gateway::Paypal), + "PAYTM_UPI" => Ok(Gateway::PaytmUpi), + "PAYTM_V2" => Ok(Gateway::PaytmV2), + "PAYTM" => Ok(Gateway::Paytm), + "PAYU" => Ok(Gateway::Payu), + "PAYZAPP" => Ok(Gateway::Payzapp), + "PHONEPE" => Ok(Gateway::Phonepe), + "PINELABS" => Ok(Gateway::Pinelabs), + "PINELABS_OFFLINE" => Ok(Gateway::PinelabsOffline), + "POSTPAY" => Ok(Gateway::Postpay), + "QWIKCILVER" => Ok(Gateway::Qwikcilver), + "RAZORPAY" => Ok(Gateway::Razorpay), + "RBL_BIZ" => Ok(Gateway::RblBiz), + "RBL" => Ok(Gateway::Rbl), + "SBI_UPI" => Ok(Gateway::SbiUpi), + "SBI" => Ok(Gateway::Sbi), + "SBIBUDDY" => Ok(Gateway::Sbibuddy), + "SBIePAY" => Ok(Gateway::Sbiepay), + "SEZZLE" => Ok(Gateway::Sezzle), + "SHOPSE" => Ok(Gateway::Shopse), + "SIKA_SIMPL" => Ok(Gateway::SikaSimpl), + "SIMPL" => Ok(Gateway::Simpl), + "SNAPMINT" => Ok(Gateway::Snapmint), + "SODEXO" => Ok(Gateway::Sodexo), + "STRIPE" => Ok(Gateway::Stripe), + "TABBY" => Ok(Gateway::Tabby), + "TATA_PA" => Ok(Gateway::TataPa), + "TATA_UPI" => Ok(Gateway::TataUpi), + "TATANEU" => Ok(Gateway::Tatanue), + "TATAPAY" => Ok(Gateway::Tatapay), + "TATAPAYLATER" => Ok(Gateway::Tatapaylater), + "TAP" => Ok(Gateway::Tap), + "TPSL_SI" => Ok(Gateway::TpslSi), + "TPSL" => Ok(Gateway::Tpsl), + "TRIPLEA" => Ok(Gateway::Triplea), + "TRUSTPAY" => Ok(Gateway::Trustpay), + "TWID" => Ok(Gateway::Twid), + "TWID_V2" => Ok(Gateway::TwidV2), + "TWOC_TWOP" => Ok(Gateway::TwocTwop), + "VIJAYA_UPI" => Ok(Gateway::VijayaUpi), + "VISA_PBW" => Ok(Gateway::VisaPbw), + "VOLT" => Ok(Gateway::Volt), + "WALLET_CONTAINER" => Ok(Gateway::WalletContainer), + "WORLDPAY" => Ok(Gateway::Worldpay), + "XENDIT" => Ok(Gateway::Xendit), + "YES_BIZ" => Ok(Gateway::YesBiz), + "YESBANK_UPI" => Ok(Gateway::YesbankUpi), + "YPP" => Ok(Gateway::Ypp), + "ZAAKPAY" => Ok(Gateway::Zaakpay), + "ZESTMONEY" => Ok(Gateway::Zestmoney), + "DOKU" => Ok(Gateway::Doku), + "DIRECT_BANK_EMI" => Ok(Gateway::DirectBankEmi), + "ZAINCASH" => Ok(Gateway::Zaincash), + "PAY10" => Ok(Gateway::Pay10), + "PINELABS_ONLINE" => Ok(Gateway::PinelabsOnline), + "WIBMO" => Ok(Gateway::Wibmo), + "NDPS" => Ok(Gateway::Ndps), + "DEFAULT" => Ok(Gateway::Default), _ => Err(ApiError::ParsingError("Invalid Gateway")), } } diff --git a/src/types/gateway_bank_emi_support.rs b/src/types/gateway_bank_emi_support.rs index 21e7c9fb..b0d94a6e 100644 --- a/src/types/gateway_bank_emi_support.rs +++ b/src/types/gateway_bank_emi_support.rs @@ -63,7 +63,7 @@ impl TryFrom for GatewayBankEmiSupport { } } -pub async fn getGatewayBankEmiSupportDB( +pub async fn get_gateway_bank_emi_support_db( emi_bank: String, t_gws: Vec, scp: String, @@ -103,13 +103,13 @@ pub async fn getGatewayBankEmiSupportDB( query } -pub async fn getGatewayBankEmiSupport( +pub async fn get_gateway_bank_emi_support( emi_bank: String, gws: Vec, scp: String, ) -> Vec { // Call the DB function and handle results - match getGatewayBankEmiSupportDB(emi_bank, gws, scp).await { + match get_gateway_bank_emi_support_db(emi_bank, gws, scp).await { Ok(db_results) => db_results .into_iter() .filter_map(|db_record| GatewayBankEmiSupport::try_from(db_record).ok()) @@ -120,7 +120,7 @@ pub async fn getGatewayBankEmiSupport( // #TOD implement db calls -// pub async fn getGatewayBankEmiSupportDB( +// pub async fn get_gateway_bank_emi_support_db( // emi_bank: String, // gws: Vec, // scp: String, diff --git a/src/types/gateway_bank_emi_support_v2.rs b/src/types/gateway_bank_emi_support_v2.rs index 088f4b83..46e3fd56 100644 --- a/src/types/gateway_bank_emi_support_v2.rs +++ b/src/types/gateway_bank_emi_support_v2.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use crate::app::get_tenant_app_state; use crate::error::ApiError; use crate::storage::types::GatewayBankEmiSupportV2 as DBGatewayBankEmiSupportV2; -use std::i64; use std::string::String; use std::vec::Vec; // use types::utils::dbconfig::getEulerDbConf; @@ -59,7 +58,7 @@ impl TryFrom for GatewayBankEmiSupportV2 { } } -pub async fn getGatewayBankEmiSupportV2DB( +pub async fn get_gateway_bank_emi_support_v2_db( jbc_id: i64, gateway_strings: Vec, scp: String, @@ -86,7 +85,7 @@ pub async fn getGatewayBankEmiSupportV2DB( } // Domain-level function with error handling and conversion -pub async fn getGatewayBankEmiSupportV2( +pub async fn get_gateway_bank_emi_support_v2( jbc_id: i64, gws: Vec, scp: String, @@ -94,7 +93,7 @@ pub async fn getGatewayBankEmiSupportV2( ten: i32, ) -> Vec { // Call the DB function and handle results - match getGatewayBankEmiSupportV2DB(jbc_id, gws, scp, ct, ten).await { + match get_gateway_bank_emi_support_v2_db(jbc_id, gws, scp, ct, ten).await { Ok(db_results) => db_results .into_iter() .filter_map(|db_record| GatewayBankEmiSupportV2::try_from(db_record).ok()) diff --git a/src/types/gateway_card_info.rs b/src/types/gateway_card_info.rs index 8f3909dc..430c9e52 100644 --- a/src/types/gateway_card_info.rs +++ b/src/types/gateway_card_info.rs @@ -1,12 +1,10 @@ use crate::app::get_tenant_app_state; use crate::logger; -use diesel::sql_types::Bool; use serde::{Deserialize, Serialize}; // use db::euler_mesh_impl::mesh_config; // use db::mesh::internal; use crate::storage::types::{BitBool, GatewayCardInfo as DBGatewayCardInfo}; use crate::types::bank_code::{to_bank_code_id, BankCodeId}; -use crate::types::gateway::GatewayAny; // use juspay::extra::parsing::{ // Parsed, Step, ParsingErrorType, ParsingErrorType::UnexpectedTextValue, around, lift_either, // lift_pure, mandated, non_negative, parse_field, project, @@ -15,7 +13,6 @@ use crate::types::gateway::GatewayAny; // use types::utils::dbconfig::get_euler_db_conf; // use eulerhs::language::MonadFlow; use crate::error::ApiError; -use std::clone; use std::cmp::PartialEq; use std::fmt::Debug; use std::fmt::Display; @@ -28,7 +25,6 @@ use crate::storage::schema::gateway_card_info::dsl; #[cfg(feature = "postgres")] use crate::storage::schema_pg::gateway_card_info::dsl; use diesel::associations::HasTable; -use diesel::dsl::sql; use diesel::*; // use super::payment::payment_method::text_to_payment_method_type; @@ -119,15 +115,13 @@ impl TryFrom for GatewayCardInfo { gateway: db_gci.gateway, cardIssuerBankName: db_gci.card_issuer_bank_name, authType: db_gci.auth_type, - juspayBankCodeId: db_gci.juspay_bank_code_id.map(|id| to_bank_code_id(id)), + juspayBankCodeId: db_gci.juspay_bank_code_id.map(to_bank_code_id), disabled: db_gci.disabled.map(|f| f.0), validationType: db_gci .validation_type - .map(|validation_type| text_to_validation_type(validation_type)) + .map(text_to_validation_type) .transpose()?, - paymentMethodType: db_gci - .payment_method_type - .map(|payment_method_type| payment_method_type), + paymentMethodType: db_gci.payment_method_type, }) } } @@ -158,7 +152,7 @@ pub async fn get_enabled_gateway_card_info_for_gateways( ) -> Vec { // Early return if both input lists are empty if card_bins.is_empty() && gateways.is_empty() { - logger::info!( + logger::debug!( tag = "get_enabled_gateway_card_info_for_gateways", action = "get_enabled_gateway_card_info_for_gateways", "card_bins and gateways are empty" @@ -168,10 +162,9 @@ pub async fn get_enabled_gateway_card_info_for_gateways( let app_state = get_tenant_app_state().await; // Convert gateways to strings - let gateway_strings: Vec> = - gateways.clone().into_iter().map(|g| Some(g)).collect(); + let gateway_strings: Vec> = gateways.clone().into_iter().map(Some).collect(); - logger::info!( + logger::debug!( tag = "get_enabled_gateway_card_info_for_gateways", action = "get_enabled_gateway_card_info_for_gateways", "gateway_strings: {:?}, gateways: {:?}, card_bins: {:?}", @@ -194,7 +187,7 @@ pub async fn get_enabled_gateway_card_info_for_gateways( .await { Ok(db_results) => { - logger::info!( + logger::debug!( tag = "get_enabled_gateway_card_info_for_gateways", action = "get_enabled_gateway_card_info_for_gateways", "db_results: {:?}", @@ -206,7 +199,7 @@ pub async fn get_enabled_gateway_card_info_for_gateways( .collect() } Err(e) => { - logger::info!( + logger::debug!( tag = "get_enabled_gateway_card_info_for_gateways", action = "get_enabled_gateway_card_info_for_gateways", "Error fetching data from DB: {:?}", diff --git a/src/types/gateway_payment_method_flow.rs b/src/types/gateway_payment_method_flow.rs index 8df505ca..082245c4 100644 --- a/src/types/gateway_payment_method_flow.rs +++ b/src/types/gateway_payment_method_flow.rs @@ -112,11 +112,7 @@ impl TryFrom for GatewayPaymentMethodFlowF { }; // Convert optional payment method type - let payment_method_type = if let Some(pmt) = db.payment_method_type { - Some(pmt) - } else { - None - }; + let payment_method_type = db.payment_method_type; // Construct the GatewayPaymentMethodFlow instance Ok(Self { diff --git a/src/types/gateway_routing_input.rs b/src/types/gateway_routing_input.rs index c8040ae2..f60e9b5f 100644 --- a/src/types/gateway_routing_input.rs +++ b/src/types/gateway_routing_input.rs @@ -1,29 +1,28 @@ -use crate::types::merchant::id::MerchantId; +use crate::error; +use crate::types::{merchant::id::MerchantId, routing_configuration}; +use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use std::option::Option; use std::string::String; use std::vec::Vec; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum EliminationLevel { - #[serde(rename = "GATEWAY")] - GATEWAY, - #[serde(rename = "PAYMENT_METHOD_TYPE")] - PAYMENT_METHOD_TYPE, - #[serde(rename = "PAYMENT_METHOD")] - PAYMENT_METHOD, - #[serde(rename = "NONE")] - NONE, - #[serde(rename = "FORCED_PAYMENT_METHOD")] - FORCED_PAYMENT_METHOD, + Gateway, + PaymentMethodType, + PaymentMethod, + None, + ForcedPaymentMethod, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum SelectionLevel { #[serde(rename = "PAYMENT_MODE")] - SL_PAYMENT_MODE, + SlPaymentMode, #[serde(rename = "PAYMENT_METHOD")] - SL_PAYMENT_METHOD, + SlPaymentMethod, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -118,14 +117,16 @@ pub struct GatewaySuccessRateBasedRoutingInput { pub defaultGatewayLevelEliminationThreshold: Option, #[serde(rename = "defaultEliminationV2SuccessRate")] pub defaultEliminationV2SuccessRate: Option, + #[serde(rename = "txnLatency")] + pub txnLatency: Option, } impl GatewaySuccessRateBasedRoutingInput { - pub fn from_elimination_threshold(elimination_threshold: f64) -> Self { + pub fn from_elimination_threshold(config: routing_configuration::EliminationData) -> Self { Self { gatewayWiseInputs: None, - defaultEliminationThreshold: elimination_threshold, - defaultEliminationLevel: EliminationLevel::PAYMENT_METHOD, + defaultEliminationThreshold: config.threshold, + defaultEliminationLevel: EliminationLevel::PaymentMethod, defaultSelectionLevel: None, enabledPaymentMethodTypes: vec![], eliminationV2SuccessRateInputs: None, @@ -138,8 +139,14 @@ impl GatewaySuccessRateBasedRoutingInput { defaultGlobalSoftTxnResetCount: None, defaultGatewayLevelEliminationThreshold: None, defaultEliminationV2SuccessRate: None, + txnLatency: config.txnLatency, } } + pub fn from_str(input: &str) -> error_stack::Result { + serde_json::from_str(input) + .change_context(error::RuleConfigurationError::DeserializationError) + .attach_printable_lazy(|| format!("Unable to parse Input from string: {:?}", input)) + } } impl Default for GatewaySuccessRateBasedRoutingInput { @@ -147,7 +154,7 @@ impl Default for GatewaySuccessRateBasedRoutingInput { Self { gatewayWiseInputs: None, defaultEliminationThreshold: 0.0, - defaultEliminationLevel: EliminationLevel::PAYMENT_METHOD, + defaultEliminationLevel: EliminationLevel::PaymentMethod, defaultSelectionLevel: None, enabledPaymentMethodTypes: vec![], eliminationV2SuccessRateInputs: None, @@ -160,6 +167,7 @@ impl Default for GatewaySuccessRateBasedRoutingInput { defaultGlobalSoftTxnResetCount: None, defaultGatewayLevelEliminationThreshold: None, defaultEliminationV2SuccessRate: None, + txnLatency: None, } } } diff --git a/src/types/hybrid_routing.rs b/src/types/hybrid_routing.rs new file mode 100644 index 00000000..b2cd9c0b --- /dev/null +++ b/src/types/hybrid_routing.rs @@ -0,0 +1,9 @@ +use crate::decider::gatewaydecider::types::DomainDeciderRequestForApiCallV2; +use crate::euclid::types::RoutingRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HybridRoutingRequest { + pub static_routing_request: Option, + pub dynamic_routing_request: Option, +} diff --git a/src/types/merchant/merchant_account.rs b/src/types/merchant/merchant_account.rs index 46c96767..8208a9ad 100644 --- a/src/types/merchant/merchant_account.rs +++ b/src/types/merchant/merchant_account.rs @@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize}; // use db::mesh::internal::*; use crate::error::ApiError; use crate::storage::types::{ - BitBool, BitBoolWrite, MerchantAccount as DBMerchantAccount, MerchantAccountNew, - MerchantAccountUpdate, + BitBoolWrite, MerchantAccount as DBMerchantAccount, MerchantAccountNew, MerchantAccountUpdate, }; // use types::utils::dbconfig::get_euler_db_conf; // use types::locker::id::{LockerId, to_locker_id}; @@ -36,7 +35,7 @@ pub struct EnableTokenization { pub enable_issuer_tokenization: bool, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct MerchantAccount { // #[serde(rename = "id")] pub id: MerchantPId, @@ -78,6 +77,39 @@ pub struct MerchantAccount { pub merchantCategoryCode: Option, } +impl Debug for MerchantAccount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MerchantAccount") + .field("id", &self.id) + .field("merchantId", &self.merchantId) + .field("country", &self.country) + .field( + "gatewayDecidedByHealthEnabled", + &self.gatewayDecidedByHealthEnabled, + ) + .field("gatewayPriority", &self.gatewayPriority) + .field("gatewayPriorityLogic", &self.gatewayPriorityLogic) + .field("useCodeForGatewayPriority", &self.useCodeForGatewayPriority) + .field("internalHashKey", &"[REDACTED]") + .field("userId", &self.userId) + .field("secondaryMerchantAccountId", &"[REDACTED]") + .field( + "enableGatewayReferenceIdBasedRouting", + &self.enableGatewayReferenceIdBasedRouting, + ) + .field( + "gatewaySuccessRateBasedDeciderInput", + &self.gatewaySuccessRateBasedDeciderInput, + ) + .field("internalMetadata", &"[REDACTED]") + .field("installmentEnabled", &self.installmentEnabled) + .field("tenantAccountId", &"[REDACTED]") + .field("priorityLogicConfig", &self.priorityLogicConfig) + .field("merchantCategoryCode", &self.merchantCategoryCode) + .finish() + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct MerchantAccountCreateRequest { pub merchant_id: String, @@ -136,9 +168,7 @@ impl TryFrom for MerchantAccount { useCodeForGatewayPriority: value.use_code_for_gateway_priority.0, internalHashKey: value.internal_hash_key, userId: value.user_id, - secondaryMerchantAccountId: value - .secondary_merchant_account_id - .map(|mid| to_merchant_pid(mid)), + secondaryMerchantAccountId: value.secondary_merchant_account_id.map(to_merchant_pid), enableGatewayReferenceIdBasedRouting: value .enable_gateway_reference_id_based_routing .map(|f| f.0), @@ -233,7 +263,7 @@ pub async fn delete_merchant_account( let conn = &app_state.db.get_conn().await?; // Use Diesel's query builder with multiple conditions crate::generics::generic_delete::<::Table, _>( - &conn, + conn, dsl::merchant_id.eq(merchant_id), ) .await?; @@ -255,7 +285,7 @@ pub async fn update_merchant_account( ::Table, MerchantAccountUpdate, _, - >(&conn, dsl::merchant_id.eq(merchant_id), values) + >(conn, dsl::merchant_id.eq(merchant_id), values) .await?; Ok(()) } diff --git a/src/types/merchant/merchant_gateway_account.rs b/src/types/merchant/merchant_gateway_account.rs index 5ea69799..b67f4264 100644 --- a/src/types/merchant/merchant_gateway_account.rs +++ b/src/types/merchant/merchant_gateway_account.rs @@ -12,7 +12,6 @@ use crate::types::merchant::id::{to_merchant_id, MerchantId}; // use juspay::extra::secret::{Secret, SecretContext}; // use eulerhs::extra::combinators::to_domain_all; // use eulerhs::language::MonadFlow; -use std::i64; use std::option::Option; use std::string::String; use std::vec::Vec; @@ -54,7 +53,7 @@ pub fn to_supported_payment_flows( ) -> Result { match serde_json::from_str::(&supported_payment_flows) { Ok(res) => Ok(res), - Err(_) => Err(ApiError::ParsingError("Inavlid Supported Payment Flowws")), + Err(_) => Err(ApiError::ParsingError("Invalid Supported Payment Flows")), } } @@ -127,10 +126,10 @@ impl TryFrom for MerchantGatewayAccount { paymentMethods: value.payment_methods, supported_payment_flows: value .supported_payment_flows - .map(|flows| to_supported_payment_flows(flows)) + .map(to_supported_payment_flows) .transpose()?, disabled: value.disabled.map(|f| f.0), - referenceId: value.reference_id.map(|id| to_mga_reference_id(id)), + referenceId: value.reference_id.map(to_mga_reference_id), supportedCurrencies: value.supported_currencies, gatewayIdentifier: value.gateway_identifier, gatewayType: value.gateway_type, @@ -250,7 +249,7 @@ pub async fn getEnabledMgasByMerchantIdAndRefId( .filter_map(|db_record| MerchantGatewayAccount::try_from(db_record).ok()) .collect(), Err(err) => { - crate::logger::info!("Error in getEnabledMgasByMerchantIdAndRefId: {:?}", err); + crate::logger::debug!("Error in getEnabledMgasByMerchantIdAndRefId: {:?}", err); Vec::new() // Silently handle any errors by returning empty vec } } diff --git a/src/types/merchant/merchant_iframe_preferences.rs b/src/types/merchant/merchant_iframe_preferences.rs index 23df262e..dcdd4506 100644 --- a/src/types/merchant/merchant_iframe_preferences.rs +++ b/src/types/merchant/merchant_iframe_preferences.rs @@ -10,7 +10,6 @@ use crate::types::merchant::id::{to_merchant_id, MerchantId}; // use juspay::extra::parsing::{Parsed, Step, defaulting, lift_pure, mandated, non_negative, parse_field, project}; // use eulerhs::extra::combinators::to_domain_all; // use eulerhs::language::MonadFlow; -use std::i64; use std::option::Option; use std::string::String; // use named::Named; @@ -48,7 +47,7 @@ pub struct MerchantIframePreferences { impl From for MerchantIframePreferences { fn from(value: DBMerchantIframePreferences) -> Self { - MerchantIframePreferences { + Self { id: to_merchant_iframe_prefs_pid(value.id), merchantId: to_merchant_id(value.merchant_id), dynamicSwitchingEnabled: value.dynamic_switching_enabled.unwrap_or(BitBool(false)).0, diff --git a/src/types/merchant_config/types.rs b/src/types/merchant_config/types.rs index ff8c791b..ee020bac 100644 --- a/src/types/merchant_config/types.rs +++ b/src/types/merchant_config/types.rs @@ -10,9 +10,10 @@ pub struct MerchantConfigPId(pub String); pub struct ConfigName(pub String); #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ConfigCategory { - PAYMENT_FLOW, - GENERAL_CONFIG, + PaymentFlow, + GeneralConfig, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -23,7 +24,7 @@ pub enum ConfigStatus { pub struct TenantConfigValueType { pub status: ConfigStatus, - pub configValue: Option>, + pub configValue: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -49,8 +50,8 @@ pub fn to_merchant_config_pid(id: String) -> MerchantConfigPId { pub fn config_category_to_text(category: ConfigCategory) -> String { match category { - ConfigCategory::PAYMENT_FLOW => "PAYMENT_FLOW".to_string(), - ConfigCategory::GENERAL_CONFIG => "GENERAL_CONFIG".to_string(), + ConfigCategory::PaymentFlow => "PAYMENT_FLOW".to_string(), + ConfigCategory::GeneralConfig => "GENERAL_CONFIG".to_string(), } } @@ -60,8 +61,8 @@ pub fn to_config_name(name: &str) -> ConfigName { pub fn to_config_category(category: &str) -> Result { match category { - "PAYMENT_FLOW" => Ok(ConfigCategory::PAYMENT_FLOW), - "GENERAL_CONFIG" => Ok(ConfigCategory::GENERAL_CONFIG), + "PAYMENT_FLOW" => Ok(ConfigCategory::PaymentFlow), + "GENERAL_CONFIG" => Ok(ConfigCategory::GeneralConfig), _ => Err(ApiError::ParsingError("Invalid ConfigCategory")), } } @@ -85,8 +86,8 @@ pub fn config_status_to_text(status: ConfigStatus) -> String { impl Display for ConfigCategory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PAYMENT_FLOW => write!(f, "PAYMENT_FLOW"), - Self::GENERAL_CONFIG => write!(f, "GENERAL_CONFIG"), + Self::PaymentFlow => write!(f, "PAYMENT_FLOW"), + Self::GeneralConfig => write!(f, "GENERAL_CONFIG"), } } } diff --git a/src/types/merchant_gateway_account_sub_info.rs b/src/types/merchant_gateway_account_sub_info.rs index 7cc1c914..2df2fd51 100644 --- a/src/types/merchant_gateway_account_sub_info.rs +++ b/src/types/merchant_gateway_account_sub_info.rs @@ -23,6 +23,7 @@ use crate::storage::schema::merchant_gateway_account_sub_info::dsl; use crate::storage::schema_pg::merchant_gateway_account_sub_info::dsl; use diesel::associations::HasTable; use diesel::*; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Clone)] pub struct MerchantGatewayAccountSubInfo { @@ -44,9 +45,10 @@ pub fn to_mgasi_pid(id: i64) -> MgasiPId { MgasiPId { mgasiPId: id } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum SubInfoType { - SPLIT_SETTLEMENT, + SplitSettlement, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -57,7 +59,7 @@ pub enum SubIdType { pub fn text_to_sub_info_type(ctx: String) -> Result { match ctx.as_str() { - "SPLIT_SETTLEMENT" => Ok(SubInfoType::SPLIT_SETTLEMENT), + "SPLIT_SETTLEMENT" => Ok(SubInfoType::SplitSettlement), _ => Err(ApiError::ParsingError("Invalid Sub Info Type")), } } diff --git a/src/types/merchant_gateway_card_info.rs b/src/types/merchant_gateway_card_info.rs index 8d9ca7f8..a784dde3 100644 --- a/src/types/merchant_gateway_card_info.rs +++ b/src/types/merchant_gateway_card_info.rs @@ -87,10 +87,7 @@ pub async fn find_all_mgcis_by_macc_and_gci_p_id( ) -> Vec { // Call the database function and handle results match find_all_mgcis_by_macc_and_gci_p_id_db(&m_pid, &gci_ids).await { - Ok(db_results) => db_results - .into_iter() - .filter_map(|db_record| MerchantGatewayCardInfo::try_from(db_record).ok()) - .collect(), + Ok(db_results) => db_results.into_iter().map(From::from).collect(), Err(_) => Vec::new(), // Silently handle any errors by returning empty vec } } diff --git a/src/types/merchant_gateway_payment_method_flow.rs b/src/types/merchant_gateway_payment_method_flow.rs index 178fef4b..b7ad7135 100644 --- a/src/types/merchant_gateway_payment_method_flow.rs +++ b/src/types/merchant_gateway_payment_method_flow.rs @@ -168,10 +168,7 @@ pub async fn get_all_mgpmf_by_mga_id_and_gpmf_ids( ) .await { - Ok(db_results) => db_results - .into_iter() - .filter_map(|db_record| MerchantGatewayPaymentMethodFlow::try_from(db_record).ok()) - .collect(), + Ok(db_results) => db_results.into_iter().map(From::from).collect(), Err(_) => Vec::new(), // Silently handle any errors by returning empty vec } } diff --git a/src/types/merchant_priority_logic.rs b/src/types/merchant_priority_logic.rs index aa67d534..39e097bc 100644 --- a/src/types/merchant_priority_logic.rs +++ b/src/types/merchant_priority_logic.rs @@ -7,8 +7,6 @@ use crate::storage::types::MerchantPriorityLogic as DBMerchantPriorityLogic; use diesel::associations::HasTable; use diesel::*; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use std::error::Error; use std::option::Option; use std::string::String; use std::vec::Vec; @@ -105,10 +103,7 @@ pub async fn find_all_priority_logic_by_merchant_pid(mpid: i64) -> Vec(&app_state.db, dsl::merchant_account_id.eq(mpid)) .await { - Ok(db_results) => db_results - .into_iter() - .filter_map(|db_record| MerchantPriorityLogic::try_from(db_record).ok()) - .collect(), + Ok(db_results) => db_results.into_iter().map(From::from).collect(), Err(_) => Vec::new(), // Silently handle any errors by returning an empty vector } } diff --git a/src/types/order.rs b/src/types/order.rs index c1e6ed49..da2574e2 100644 --- a/src/types/order.rs +++ b/src/types/order.rs @@ -139,6 +139,8 @@ pub enum OrderType { VanPayment, #[serde(rename = "MOTO_PAYMENT")] MotoPayment, + #[serde(rename = "UNKNOWN")] + Unknown, } impl OrderType { @@ -153,6 +155,7 @@ impl OrderType { Self::TpvMandatePayment => "TPV_MANDATE_PAYMENT".to_string(), Self::VanPayment => "VAN_PAYMENT".to_string(), Self::MotoPayment => "MOTO_PAYMENT".to_string(), + Self::Unknown => "UNKNOWN".to_string(), } } @@ -186,6 +189,7 @@ impl OrderType { TxnObjectType::PartialVoid => Self::OrderPayment, TxnObjectType::VanPayment => Self::VanPayment, TxnObjectType::MotoPayment => Self::MotoPayment, + _ => Self::Unknown, } } } diff --git a/src/types/order_metadata_v2.rs b/src/types/order_metadata_v2.rs index c58dc859..e62eea72 100644 --- a/src/types/order_metadata_v2.rs +++ b/src/types/order_metadata_v2.rs @@ -31,8 +31,8 @@ where } #[serde_as] -#[serde(rename_all = "camelCase")] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct OrderMetadataV2 { pub id: OrderMetadataV2PId, #[serde(with = "time::serde::iso8601")] diff --git a/src/types/payment/payment_method.rs b/src/types/payment/payment_method.rs index a92385bf..3bd6ba27 100644 --- a/src/types/payment/payment_method.rs +++ b/src/types/payment/payment_method.rs @@ -46,69 +46,57 @@ pub struct PaymentMethod { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PaymentMethodSubType { - #[serde(rename = "WALLET")] - WALLET, - #[serde(rename = "CF_BNPL")] - CF_BNPL, - #[serde(rename = "GIFT_CARD")] - GIFT_CARD, - #[serde(rename = "CF_EMI")] - CF_EMI, - #[serde(rename = "REAL_TIME")] - REAL_TIME, - #[serde(rename = "CF_POD")] - CF_POD, - #[serde(rename = "REWARD")] - REWARD, - #[serde(rename = "VAN")] - VAN, - #[serde(rename = "STORE")] - STORE, - #[serde(rename = "POS")] - POS, - #[serde(rename = "CF_LSP")] - CF_LSP, - #[serde(rename = "FPX")] - FPX, - #[serde(rename = "UNKNOWN")] - UNKNOWN, + Wallet, + CfBnpl, + GiftCard, + CfEmi, + RealTime, + CfPod, + Reward, + Van, + Store, + Pos, + CfLsp, + Fpx, + Unknown, } impl PaymentMethodSubType { pub fn to_text(&self) -> &'static str { match self { - Self::WALLET => "WALLET", - Self::CF_BNPL => "CF_BNPL", - Self::GIFT_CARD => "GIFT_CARD", - Self::CF_EMI => "CF_EMI", - Self::REAL_TIME => "REAL_TIME", - Self::CF_POD => "CF_POD", - Self::REWARD => "REWARD", - Self::VAN => "VAN", - Self::STORE => "STORE", - Self::POS => "POS", - Self::CF_LSP => "CF_LSP", - Self::FPX => "FPX", - Self::UNKNOWN => "UNKNOWN", + Self::Wallet => "WALLET", + Self::CfBnpl => "CF_BNPL", + Self::GiftCard => "GIFT_CARD", + Self::CfEmi => "CF_EMI", + Self::RealTime => "REAL_TIME", + Self::CfPod => "CF_POD", + Self::Reward => "REWARD", + Self::Van => "VAN", + Self::Store => "STORE", + Self::Pos => "POS", + Self::CfLsp => "CF_LSP", + Self::Fpx => "FPX", + Self::Unknown => "UNKNOWN", } } pub fn from_text(ctx: &str) -> Result { match ctx { - "WALLET" => Ok(Self::WALLET), - "CF_BNPL" => Ok(Self::CF_BNPL), - "GIFT_CARD" => Ok(Self::GIFT_CARD), - "CF_EMI" => Ok(Self::CF_EMI), - "REAL_TIME" => Ok(Self::REAL_TIME), - "CF_POD" => Ok(Self::CF_POD), - "REWARD" => Ok(Self::REWARD), - "VAN" => Ok(Self::VAN), - "STORE" => Ok(Self::STORE), - "POS" => Ok(Self::POS), - "CF_LSP" => Ok(Self::CF_LSP), - "FPX" => Ok(Self::FPX), - "UNKNOWN" => Ok(Self::UNKNOWN), + "WALLET" => Ok(Self::Wallet), + "CF_BNPL" => Ok(Self::CfBnpl), + "GIFT_CARD" => Ok(Self::GiftCard), + "CF_EMI" => Ok(Self::CfEmi), + "REAL_TIME" => Ok(Self::RealTime), + "CF_POD" => Ok(Self::CfPod), + "REWARD" => Ok(Self::Reward), + "VAN" => Ok(Self::Van), + "STORE" => Ok(Self::Store), + "POS" => Ok(Self::Pos), + "CF_LSP" => Ok(Self::CfLsp), + "FPX" => Ok(Self::Fpx), + "UNKNOWN" => Ok(Self::Unknown), _ => Err(ApiError::ParsingError("Invalid Payment Method Sub Type")), } } diff --git a/src/types/payment_flow.rs b/src/types/payment_flow.rs index df01244c..84c46b25 100644 --- a/src/types/payment_flow.rs +++ b/src/types/payment_flow.rs @@ -3,462 +3,463 @@ use serde::{Deserialize, Serialize}; use crate::error::ApiError; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PaymentFlow { - CARD_3DS, - CARD_3DS2, - CARD_DOTP, - CARD_MOTO, - CARD_NO_3DS, - CARD_VIES, - ZERO_AUTH, - CARD_TOKENIZATION, - CVVLESS, - DIRECT_DEBIT, - EMANDATE, - EMI, - INAPP_DEBIT, - MANDATE, - PARTIAL_CAPTURE, - PARTIAL_VOID, - PARTIAL_PAYMENT, - PREAUTH, - SDKLESS_INTENT, - SPLIT_PAYMENT, - SPLIT_SETTLEMENT, - TOPUP, - TPV, - VISA_CHECKOUT, - OUTAGE, - SR_BASED_ROUTING, - ELIMINATION_BASED_ROUTING, - PL_BASED_ROUTING, - MANDATE_WORKFLOW, - ALTID, - SURCHARGE, - OFFER, - CAPTCHA, - PAYMENT_COLLECTION_LINK, - AUTO_REFUND, - PAYMENT_LINK, - PAYMENT_FORM, - RISK_CHECK, - DYNAMIC_CURRENCY_CONVERSION, - PART_PAYMENT, - STANDALONE_AUTHENTICATION, - STANDALONE_AUTHORIZATION, - STANDALONE_CAPTURE, - AUTHN_AUTHZ, - AUTHZ_CAPTURE, - REDIRECT_DEBIT, - LINK_AND_DEBIT, - NEW_CARD, - INSTANT_REFUND, - ASYNC, - DOTP, - MERCHANT_MANAGED_DEBIT, - ADDRESS_VERIFICATION, - FRICTIONLESS_3DS, - TA_FILE, - FIDO, - REFUND, - CTP, - ONE_TIME_PAYMENT, - REVERSE_PENNY_DROP, - ON_DEMAND_SPLIT_SETTLEMENT, - CREDIT_CARD_ON_UPI, - DECIDER_FALLBACK_DOTP_TO_3DS, - DECIDER_FALLBACK_NO_3DS_TO_3DS, - PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS, - PG_FAILURE_FALLBACK_DOTP_TO_3DS, - TOKENIZATION_CONSENT_FALLBACK_DOTP_TO_3DS, - CUSTOMER_FALLBACK_DOTP_TO_3DS, - AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS, - FRM_PREFERENCE_TO_NO_3DS, - MERCHANT_FALLBACK_3DS2_TO_3DS, - MERCHANT_FALLBACK_FIDO_TO_3DS, - ORDER_PREFERENCE_FALLBACK_NO_3DS_TO_3DS, - MERCHANT_PREFERENCE_FALLBACK_NO_3DS_TO_3DS, - ORDER_PREFERENCE_TO_NO_3DS, - MUTUAL_FUND, - CROSS_BORDER_PAYMENT, - APPLEPAY_TOKEN_DECRYPTION_FLOW, - ONE_TIME_MANDATE, - SINGLE_BLOCK_MULTIPLE_DEBIT, - SILENT_RETRY, - WALLET_TOPUP, - NETWORK_TOKEN_CREATED, - ISSUER_TOKEN_CREATED, - LOCKER_TOKEN_CREATED, - SODEXO_TOKEN_CREATED, - NETWORK_TOKEN_USED, - ISSUER_TOKEN_USED, - LOCKER_TOKEN_USED, - SODEXO_TOKEN_USED, - PAYU_TOKEN_USED, - MANDATE_REGISTER, - MANDATE_REGISTER_DEBIT, - MANDATE_PAYMENT, - EMANDATE_REGISTER, - EMANDATE_REGISTER_DEBIT, - EMANDATE_PAYMENT, - SI_HUB, - TPV_EMANDATE, - COLLECT, - INTENT, - INAPP, - QR, - PUSH_PAY, - NO_COST_EMI, - LOW_COST_EMI, - STANDARD_EMI, - STANDARD_EMI_SPLIT, - INTERNAL_NO_COST_EMI, - INTERNAL_LOW_COST_EMI, - INTERNAL_NO_COST_EMI_SPLIT, - INTERNAL_LOW_COST_EMI_SPLIT, - DIRECT_BANK_EMI, - PG_EMI, - AUTO_DISBURSEMENT, - AUTO_USER_REGISTRATION, - BANK_INSTANT_REFUND, - MANDATE_PREDEBIT_NOTIFICATION_DISABLEMENT, - ORDER_AMOUNT_AS_SUBVENTION_AMOUNT, - ORDER_ID_AS_RECON_ID, - PASS_USER_TOKEN_TO_GATEWAY, - S2S_FLOW, - SPLIT_SETTLE_ONLY, - SUBSCRIPTION_ONLY, - TPV_ONLY, - TXN_UUID_AS_TR, - UPI_INTENT_REGISTRATION, - V2_INTEGRATION, - V2_LINK_AND_PAY, - VPOS2, - PAYMENT_PAGE, - PP_QUICKPAY, - PP_RETRY, - INAPP_NEW_PAY, - INAPP_REPEAT_PAY, + Card3ds, + Card3ds2, + CardDotp, + CardMoto, + CardNo3ds, + CardVies, + ZeroAuth, + CardTokenization, + Cvvless, + DirectDebit, + Emandate, + Emi, + InappDebit, + Mandate, + PartialCapture, + PartialVoid, + PartialPayment, + Preauth, + SdklessIntent, + SplitPayment, + SplitSettlement, + Topup, + Tpv, + VisaCheckout, + Outage, + SrBasedRouting, + EliminationBasedRouting, + PlBasedRouting, + MandateWorkflow, + Altid, + Surcharge, + Offer, + Captcha, + PaymentCollectionLink, + AutoRefund, + PaymentLink, + PaymentForm, + RiskCheck, + DynamicCurrencyConversion, + PartPayment, + StandaloneAuthentication, + StandaloneAuthorization, + StandaloneCapture, + AuthnAuthz, + AuthzCapture, + RedirectDebit, + LinkAndDebit, + NewCard, + InstantRefund, + Async, + Dotp, + MerchantManagedDebit, + AddressVerification, + Frictionless3ds, + TaFile, + Fido, + Refund, + Ctp, + OneTimePayment, + ReversePennyDrop, + OnDemandSplitSettlement, + CreditCardOnUpi, + DeciderFallbackDotpTo3ds, + DeciderFallbackNo3dsTo3ds, + PaymentChannelFallbackDotpTo3ds, + PgFailureFallbackDotpTo3ds, + TokenizationConsentFallbackDotpTo3ds, + CustomerFallbackDotpTo3ds, + AuthProviderFallback3ds2To3ds, + FrmPreferenceToNo3ds, + MerchantFallback3ds2To3ds, + MerchantFallbackFidoTo3ds, + OrderPreferenceFallbackNo3dsTo3ds, + MerchantPreferenceFallbackNo3dsTo3ds, + OrderPreferenceToNo3ds, + MutualFund, + CrossBorderPayment, + ApplepayTokenDecryptionFlow, + OneTimeMandate, + SingleBlockMultipleDebit, + SilentRetry, + WalletTopup, + NetworkTokenCreated, + IssuerTokenCreated, + LockerTokenCreated, + SodexoTokenCreated, + NetworkTokenUsed, + IssuerTokenUsed, + LockerTokenUsed, + SodexoTokenUsed, + PayuTokenUsed, + MandateRegister, + MandateRegisterDebit, + MandatePayment, + EmandateRegister, + EmandateRegisterDebit, + EmandatePayment, + SiHub, + TpvEmandate, + Collect, + Intent, + Inapp, + Qr, + PushPay, + NoCostEmi, + LowCostEmi, + StandardEmi, + StandardEmiSplit, + InternalNoCostEmi, + InternalLowCostEmi, + InternalNoCostEmiSplit, + InternalLowCostEmiSplit, + DirectBankEmi, + PgEmi, + AutoDisbursement, + AutoUserRegistration, + BankInstantRefund, + MandatePredebitNotificationDisablement, + OrderAmountAsSubventionAmount, + OrderIdAsReconId, + PassUserTokenToGateway, + S2sFlow, + SplitSettleOnly, + SubscriptionOnly, + TpvOnly, + TxnUuidAsTr, + UpiIntentRegistration, + V2Integration, + V2LinkAndPay, + Vpos2, + PaymentPage, + PpQuickpay, + PpRetry, + InappNewPay, + InappRepeatPay, + PixAutomaticRedirect, } pub fn payment_flows_to_text(payment_flow: &PaymentFlow) -> String { match payment_flow { - PaymentFlow::CARD_3DS => "CARD_3DS".to_string(), - PaymentFlow::CARD_3DS2 => "CARD_3DS2".to_string(), - PaymentFlow::FRICTIONLESS_3DS => "FRICTIONLESS_3DS".to_string(), - PaymentFlow::CARD_DOTP => "CARD_DOTP".to_string(), - PaymentFlow::CARD_MOTO => "CARD_MOTO".to_string(), - PaymentFlow::CARD_NO_3DS => "CARD_NO_3DS".to_string(), - PaymentFlow::CARD_VIES => "CARD_VIES".to_string(), - PaymentFlow::ZERO_AUTH => "ZERO_AUTH".to_string(), - PaymentFlow::CARD_TOKENIZATION => "CARD_TOKENIZATION".to_string(), - PaymentFlow::CVVLESS => "CVVLESS".to_string(), - PaymentFlow::DIRECT_DEBIT => "DIRECT_DEBIT".to_string(), - PaymentFlow::EMANDATE => "EMANDATE".to_string(), - PaymentFlow::EMI => "EMI".to_string(), - PaymentFlow::INAPP_DEBIT => "INAPP_DEBIT".to_string(), - PaymentFlow::MANDATE => "MANDATE".to_string(), - PaymentFlow::PARTIAL_CAPTURE => "PARTIAL_CAPTURE".to_string(), - PaymentFlow::PARTIAL_VOID => "PARTIAL_VOID".to_string(), - PaymentFlow::PARTIAL_PAYMENT => "PARTIAL_PAYMENT".to_string(), - PaymentFlow::PREAUTH => "PREAUTH".to_string(), - PaymentFlow::SDKLESS_INTENT => "SDKLESS_INTENT".to_string(), - PaymentFlow::SPLIT_PAYMENT => "SPLIT_PAYMENT".to_string(), - PaymentFlow::SPLIT_SETTLEMENT => "SPLIT_SETTLEMENT".to_string(), - PaymentFlow::WALLET_TOPUP => "WALLET_TOPUP".to_string(), - PaymentFlow::TPV => "TPV".to_string(), - PaymentFlow::VISA_CHECKOUT => "VISA_CHECKOUT".to_string(), - PaymentFlow::AUTO_DISBURSEMENT => "AUTO_DISBURSEMENT".to_string(), - PaymentFlow::AUTO_USER_REGISTRATION => "AUTO_USER_REGISTRATION".to_string(), - PaymentFlow::BANK_INSTANT_REFUND => "BANK_INSTANT_REFUND".to_string(), - PaymentFlow::MANDATE_PREDEBIT_NOTIFICATION_DISABLEMENT => { + PaymentFlow::Card3ds => "CARD_3DS".to_string(), + PaymentFlow::Card3ds2 => "CARD_3DS2".to_string(), + PaymentFlow::Frictionless3ds => "FRICTIONLESS_3DS".to_string(), + PaymentFlow::CardDotp => "CARD_DOTP".to_string(), + PaymentFlow::CardMoto => "CARD_MOTO".to_string(), + PaymentFlow::CardNo3ds => "CARD_NO_3DS".to_string(), + PaymentFlow::CardVies => "CARD_VIES".to_string(), + PaymentFlow::ZeroAuth => "ZERO_AUTH".to_string(), + PaymentFlow::CardTokenization => "CARD_TOKENIZATION".to_string(), + PaymentFlow::Cvvless => "CVVLESS".to_string(), + PaymentFlow::DirectDebit => "DIRECT_DEBIT".to_string(), + PaymentFlow::Emandate => "EMANDATE".to_string(), + PaymentFlow::Emi => "EMI".to_string(), + PaymentFlow::InappDebit => "INAPP_DEBIT".to_string(), + PaymentFlow::Mandate => "MANDATE".to_string(), + PaymentFlow::PartialCapture => "PARTIAL_CAPTURE".to_string(), + PaymentFlow::PartialVoid => "PARTIAL_VOID".to_string(), + PaymentFlow::PartialPayment => "PARTIAL_PAYMENT".to_string(), + PaymentFlow::Preauth => "PREAUTH".to_string(), + PaymentFlow::SdklessIntent => "SDKLESS_INTENT".to_string(), + PaymentFlow::SplitPayment => "SPLIT_PAYMENT".to_string(), + PaymentFlow::SplitSettlement => "SPLIT_SETTLEMENT".to_string(), + PaymentFlow::WalletTopup => "WALLET_TOPUP".to_string(), + PaymentFlow::Tpv => "TPV".to_string(), + PaymentFlow::VisaCheckout => "VISA_CHECKOUT".to_string(), + PaymentFlow::AutoDisbursement => "AUTO_DISBURSEMENT".to_string(), + PaymentFlow::AutoUserRegistration => "AUTO_USER_REGISTRATION".to_string(), + PaymentFlow::BankInstantRefund => "BANK_INSTANT_REFUND".to_string(), + PaymentFlow::MandatePredebitNotificationDisablement => { "MANDATE_PREDEBIT_NOTIFICATION_DISABLEMENT".to_string() } - PaymentFlow::ORDER_AMOUNT_AS_SUBVENTION_AMOUNT => { + PaymentFlow::OrderAmountAsSubventionAmount => { "ORDER_AMOUNT_AS_SUBVENTION_AMOUNT".to_string() } - PaymentFlow::ORDER_ID_AS_RECON_ID => "ORDER_ID_AS_RECON_ID".to_string(), - PaymentFlow::PASS_USER_TOKEN_TO_GATEWAY => "PASS_USER_TOKEN_TO_GATEWAY".to_string(), - PaymentFlow::S2S_FLOW => "S2S_FLOW".to_string(), - PaymentFlow::SPLIT_SETTLE_ONLY => "SPLIT_SETTLE_ONLY".to_string(), - PaymentFlow::SUBSCRIPTION_ONLY => "SUBSCRIPTION_ONLY".to_string(), - PaymentFlow::TPV_ONLY => "TPV_ONLY".to_string(), - PaymentFlow::TXN_UUID_AS_TR => "TXN_UUID_AS_TR".to_string(), - PaymentFlow::UPI_INTENT_REGISTRATION => "UPI_INTENT_REGISTRATION".to_string(), - PaymentFlow::V2_INTEGRATION => "V2_INTEGRATION".to_string(), - PaymentFlow::V2_LINK_AND_PAY => "V2_LINK_AND_PAY".to_string(), - PaymentFlow::VPOS2 => "VPOS2".to_string(), - PaymentFlow::OUTAGE => "OUTAGE".to_string(), - PaymentFlow::SR_BASED_ROUTING => "SR_BASED_ROUTING".to_string(), - PaymentFlow::ELIMINATION_BASED_ROUTING => "ELIMINATION_BASED_ROUTING".to_string(), - PaymentFlow::PL_BASED_ROUTING => "PL_BASED_ROUTING".to_string(), - PaymentFlow::MANDATE_WORKFLOW => "MANDATE_WORKFLOW".to_string(), - PaymentFlow::ALTID => "ALTID".to_string(), - PaymentFlow::SURCHARGE => "SURCHARGE".to_string(), - PaymentFlow::OFFER => "OFFER".to_string(), - PaymentFlow::CAPTCHA => "CAPTCHA".to_string(), - PaymentFlow::PAYMENT_COLLECTION_LINK => "PAYMENT_COLLECTION_LINK".to_string(), - PaymentFlow::AUTO_REFUND => "AUTO_REFUND".to_string(), - PaymentFlow::PAYMENT_LINK => "PAYMENT_LINK".to_string(), - PaymentFlow::PAYMENT_FORM => "PAYMENT_FORM".to_string(), - PaymentFlow::RISK_CHECK => "RISK_CHECK".to_string(), - PaymentFlow::DYNAMIC_CURRENCY_CONVERSION => "DYNAMIC_CURRENCY_CONVERSION".to_string(), - PaymentFlow::PART_PAYMENT => "PART_PAYMENT".to_string(), - PaymentFlow::STANDALONE_AUTHENTICATION => "STANDALONE_AUTHENTICATION".to_string(), - PaymentFlow::STANDALONE_AUTHORIZATION => "STANDALONE_AUTHORIZATION".to_string(), - PaymentFlow::STANDALONE_CAPTURE => "STANDALONE_CAPTURE".to_string(), - PaymentFlow::AUTHN_AUTHZ => "AUTHN_AUTHZ".to_string(), - PaymentFlow::AUTHZ_CAPTURE => "AUTHZ_CAPTURE".to_string(), - PaymentFlow::REDIRECT_DEBIT => "REDIRECT_DEBIT".to_string(), - PaymentFlow::LINK_AND_DEBIT => "LINK_AND_DEBIT".to_string(), - PaymentFlow::NEW_CARD => "NEW_CARD".to_string(), - PaymentFlow::NETWORK_TOKEN_CREATED => "NETWORK_TOKEN_CREATED".to_string(), - PaymentFlow::ISSUER_TOKEN_CREATED => "ISSUER_TOKEN_CREATED".to_string(), - PaymentFlow::LOCKER_TOKEN_CREATED => "LOCKER_TOKEN_CREATED".to_string(), - PaymentFlow::SODEXO_TOKEN_CREATED => "SODEXO_TOKEN_CREATED".to_string(), - PaymentFlow::NETWORK_TOKEN_USED => "NETWORK_TOKEN_USED".to_string(), - PaymentFlow::ISSUER_TOKEN_USED => "ISSUER_TOKEN_USED".to_string(), - PaymentFlow::LOCKER_TOKEN_USED => "LOCKER_TOKEN_USED".to_string(), - PaymentFlow::SODEXO_TOKEN_USED => "SODEXO_TOKEN_USED".to_string(), - PaymentFlow::PAYU_TOKEN_USED => "PAYU_TOKEN_USED".to_string(), - PaymentFlow::MANDATE_REGISTER => "MANDATE_REGISTER".to_string(), - PaymentFlow::MANDATE_REGISTER_DEBIT => "MANDATE_REGISTER_DEBIT".to_string(), - PaymentFlow::MANDATE_PAYMENT => "MANDATE_PAYMENT".to_string(), - PaymentFlow::EMANDATE_REGISTER => "EMANDATE_REGISTER".to_string(), - PaymentFlow::EMANDATE_REGISTER_DEBIT => "EMANDATE_REGISTER_DEBIT".to_string(), - PaymentFlow::EMANDATE_PAYMENT => "EMANDATE_PAYMENT".to_string(), - PaymentFlow::SI_HUB => "SI_HUB".to_string(), - PaymentFlow::TPV_EMANDATE => "TPV_EMANDATE".to_string(), - PaymentFlow::COLLECT => "COLLECT".to_string(), - PaymentFlow::INTENT => "INTENT".to_string(), - PaymentFlow::INAPP => "INAPP".to_string(), - PaymentFlow::QR => "QR".to_string(), - PaymentFlow::PUSH_PAY => "PUSH_PAY".to_string(), - PaymentFlow::INSTANT_REFUND => "INSTANT_REFUND".to_string(), - PaymentFlow::ASYNC => "ASYNC".to_string(), - PaymentFlow::DOTP => "DOTP".to_string(), - PaymentFlow::MERCHANT_MANAGED_DEBIT => "MERCHANT_MANAGED_DEBIT".to_string(), - PaymentFlow::ADDRESS_VERIFICATION => "ADDRESS_VERIFICATION".to_string(), - PaymentFlow::TA_FILE => "TA_FILE".to_string(), - PaymentFlow::PAYMENT_PAGE => "PAYMENT_PAGE".to_string(), - PaymentFlow::PP_QUICKPAY => "PP_QUICKPAY".to_string(), - PaymentFlow::PP_RETRY => "PP_RETRY".to_string(), - PaymentFlow::INAPP_NEW_PAY => "INAPP_NEW_PAY".to_string(), - PaymentFlow::INAPP_REPEAT_PAY => "INAPP_REPEAT_PAY".to_string(), - PaymentFlow::PG_EMI => "PG_EMI".to_string(), - PaymentFlow::SILENT_RETRY => "SILENT_RETRY".to_string(), - PaymentFlow::FIDO => "FIDO".to_string(), - PaymentFlow::REFUND => "REFUND".to_string(), - PaymentFlow::CTP => "CTP".to_string(), - PaymentFlow::ONE_TIME_PAYMENT => "ONE_TIME_PAYMENT".to_string(), - PaymentFlow::NO_COST_EMI => "NO_COST_EMI".to_string(), - PaymentFlow::LOW_COST_EMI => "LOW_COST_EMI".to_string(), - PaymentFlow::STANDARD_EMI => "STANDARD_EMI".to_string(), - PaymentFlow::STANDARD_EMI_SPLIT => "STANDARD_EMI_SPLIT".to_string(), - PaymentFlow::INTERNAL_NO_COST_EMI => "INTERNAL_NO_COST_EMI".to_string(), - PaymentFlow::INTERNAL_LOW_COST_EMI => "INTERNAL_LOW_COST_EMI".to_string(), - PaymentFlow::INTERNAL_NO_COST_EMI_SPLIT => "INTERNAL_NO_COST_EMI_SPLIT".to_string(), - PaymentFlow::INTERNAL_LOW_COST_EMI_SPLIT => "INTERNAL_LOW_COST_EMI_SPLIT".to_string(), - PaymentFlow::DIRECT_BANK_EMI => "DIRECT_BANK_EMI".to_string(), - PaymentFlow::REVERSE_PENNY_DROP => "REVERSE_PENNY_DROP".to_string(), - PaymentFlow::TOPUP => "TOPUP".to_string(), - PaymentFlow::ON_DEMAND_SPLIT_SETTLEMENT => "ON_DEMAND_SPLIT_SETTLEMENT".to_string(), - PaymentFlow::CREDIT_CARD_ON_UPI => "CREDIT_CARD_ON_UPI".to_string(), - PaymentFlow::DECIDER_FALLBACK_DOTP_TO_3DS => "DECIDER_FALLBACK_DOTP_TO_3DS".to_string(), - PaymentFlow::DECIDER_FALLBACK_NO_3DS_TO_3DS => "DECIDER_FALLBACK_NO_3DS_TO_3DS".to_string(), - PaymentFlow::PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS => { + PaymentFlow::OrderIdAsReconId => "ORDER_ID_AS_RECON_ID".to_string(), + PaymentFlow::PassUserTokenToGateway => "PASS_USER_TOKEN_TO_GATEWAY".to_string(), + PaymentFlow::S2sFlow => "S2S_FLOW".to_string(), + PaymentFlow::SplitSettleOnly => "SPLIT_SETTLE_ONLY".to_string(), + PaymentFlow::SubscriptionOnly => "SUBSCRIPTION_ONLY".to_string(), + PaymentFlow::TpvOnly => "TPV_ONLY".to_string(), + PaymentFlow::TxnUuidAsTr => "TXN_UUID_AS_TR".to_string(), + PaymentFlow::UpiIntentRegistration => "UPI_INTENT_REGISTRATION".to_string(), + PaymentFlow::V2Integration => "V2_INTEGRATION".to_string(), + PaymentFlow::V2LinkAndPay => "V2_LINK_AND_PAY".to_string(), + PaymentFlow::Vpos2 => "VPOS2".to_string(), + PaymentFlow::Outage => "OUTAGE".to_string(), + PaymentFlow::SrBasedRouting => "SR_BASED_ROUTING".to_string(), + PaymentFlow::EliminationBasedRouting => "ELIMINATION_BASED_ROUTING".to_string(), + PaymentFlow::PlBasedRouting => "PL_BASED_ROUTING".to_string(), + PaymentFlow::MandateWorkflow => "MANDATE_WORKFLOW".to_string(), + PaymentFlow::Altid => "ALTID".to_string(), + PaymentFlow::Surcharge => "SURCHARGE".to_string(), + PaymentFlow::Offer => "OFFER".to_string(), + PaymentFlow::Captcha => "CAPTCHA".to_string(), + PaymentFlow::PaymentCollectionLink => "PAYMENT_COLLECTION_LINK".to_string(), + PaymentFlow::AutoRefund => "AUTO_REFUND".to_string(), + PaymentFlow::PaymentLink => "PAYMENT_LINK".to_string(), + PaymentFlow::PaymentForm => "PAYMENT_FORM".to_string(), + PaymentFlow::RiskCheck => "RISK_CHECK".to_string(), + PaymentFlow::DynamicCurrencyConversion => "DYNAMIC_CURRENCY_CONVERSION".to_string(), + PaymentFlow::PartPayment => "PART_PAYMENT".to_string(), + PaymentFlow::StandaloneAuthentication => "STANDALONE_AUTHENTICATION".to_string(), + PaymentFlow::StandaloneAuthorization => "STANDALONE_AUTHORIZATION".to_string(), + PaymentFlow::StandaloneCapture => "STANDALONE_CAPTURE".to_string(), + PaymentFlow::AuthnAuthz => "AUTHN_AUTHZ".to_string(), + PaymentFlow::AuthzCapture => "AUTHZ_CAPTURE".to_string(), + PaymentFlow::RedirectDebit => "REDIRECT_DEBIT".to_string(), + PaymentFlow::LinkAndDebit => "LINK_AND_DEBIT".to_string(), + PaymentFlow::NewCard => "NEW_CARD".to_string(), + PaymentFlow::NetworkTokenCreated => "NETWORK_TOKEN_CREATED".to_string(), + PaymentFlow::IssuerTokenCreated => "ISSUER_TOKEN_CREATED".to_string(), + PaymentFlow::LockerTokenCreated => "LOCKER_TOKEN_CREATED".to_string(), + PaymentFlow::SodexoTokenCreated => "SODEXO_TOKEN_CREATED".to_string(), + PaymentFlow::NetworkTokenUsed => "NETWORK_TOKEN_USED".to_string(), + PaymentFlow::IssuerTokenUsed => "ISSUER_TOKEN_USED".to_string(), + PaymentFlow::LockerTokenUsed => "LOCKER_TOKEN_USED".to_string(), + PaymentFlow::SodexoTokenUsed => "SODEXO_TOKEN_USED".to_string(), + PaymentFlow::PayuTokenUsed => "PAYU_TOKEN_USED".to_string(), + PaymentFlow::MandateRegister => "MANDATE_REGISTER".to_string(), + PaymentFlow::MandateRegisterDebit => "MANDATE_REGISTER_DEBIT".to_string(), + PaymentFlow::MandatePayment => "MANDATE_PAYMENT".to_string(), + PaymentFlow::EmandateRegister => "EMANDATE_REGISTER".to_string(), + PaymentFlow::EmandateRegisterDebit => "EMANDATE_REGISTER_DEBIT".to_string(), + PaymentFlow::EmandatePayment => "EMANDATE_PAYMENT".to_string(), + PaymentFlow::SiHub => "SI_HUB".to_string(), + PaymentFlow::TpvEmandate => "TPV_EMANDATE".to_string(), + PaymentFlow::Collect => "COLLECT".to_string(), + PaymentFlow::Intent => "INTENT".to_string(), + PaymentFlow::Inapp => "INAPP".to_string(), + PaymentFlow::Qr => "QR".to_string(), + PaymentFlow::PushPay => "PUSH_PAY".to_string(), + PaymentFlow::InstantRefund => "INSTANT_REFUND".to_string(), + PaymentFlow::Async => "ASYNC".to_string(), + PaymentFlow::Dotp => "DOTP".to_string(), + PaymentFlow::MerchantManagedDebit => "MERCHANT_MANAGED_DEBIT".to_string(), + PaymentFlow::AddressVerification => "ADDRESS_VERIFICATION".to_string(), + PaymentFlow::TaFile => "TA_FILE".to_string(), + PaymentFlow::PaymentPage => "PAYMENT_PAGE".to_string(), + PaymentFlow::PpQuickpay => "PP_QUICKPAY".to_string(), + PaymentFlow::PpRetry => "PP_RETRY".to_string(), + PaymentFlow::InappNewPay => "INAPP_NEW_PAY".to_string(), + PaymentFlow::InappRepeatPay => "INAPP_REPEAT_PAY".to_string(), + PaymentFlow::PgEmi => "PG_EMI".to_string(), + PaymentFlow::SilentRetry => "SILENT_RETRY".to_string(), + PaymentFlow::Fido => "FIDO".to_string(), + PaymentFlow::Refund => "REFUND".to_string(), + PaymentFlow::Ctp => "CTP".to_string(), + PaymentFlow::OneTimePayment => "ONE_TIME_PAYMENT".to_string(), + PaymentFlow::NoCostEmi => "NO_COST_EMI".to_string(), + PaymentFlow::LowCostEmi => "LOW_COST_EMI".to_string(), + PaymentFlow::StandardEmi => "STANDARD_EMI".to_string(), + PaymentFlow::StandardEmiSplit => "STANDARD_EMI_SPLIT".to_string(), + PaymentFlow::InternalNoCostEmi => "INTERNAL_NO_COST_EMI".to_string(), + PaymentFlow::InternalLowCostEmi => "INTERNAL_LOW_COST_EMI".to_string(), + PaymentFlow::InternalNoCostEmiSplit => "INTERNAL_NO_COST_EMI_SPLIT".to_string(), + PaymentFlow::InternalLowCostEmiSplit => "INTERNAL_LOW_COST_EMI_SPLIT".to_string(), + PaymentFlow::DirectBankEmi => "DIRECT_BANK_EMI".to_string(), + PaymentFlow::ReversePennyDrop => "REVERSE_PENNY_DROP".to_string(), + PaymentFlow::Topup => "TOPUP".to_string(), + PaymentFlow::OnDemandSplitSettlement => "ON_DEMAND_SPLIT_SETTLEMENT".to_string(), + PaymentFlow::CreditCardOnUpi => "CREDIT_CARD_ON_UPI".to_string(), + PaymentFlow::DeciderFallbackDotpTo3ds => "DECIDER_FALLBACK_DOTP_TO_3DS".to_string(), + PaymentFlow::DeciderFallbackNo3dsTo3ds => "DECIDER_FALLBACK_NO_3DS_TO_3DS".to_string(), + PaymentFlow::PaymentChannelFallbackDotpTo3ds => { "PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS".to_string() } - PaymentFlow::PG_FAILURE_FALLBACK_DOTP_TO_3DS => { - "PG_FAILURE_FALLBACK_DOTP_TO_3DS".to_string() - } - PaymentFlow::TOKENIZATION_CONSENT_FALLBACK_DOTP_TO_3DS => { + PaymentFlow::PgFailureFallbackDotpTo3ds => "PG_FAILURE_FALLBACK_DOTP_TO_3DS".to_string(), + PaymentFlow::TokenizationConsentFallbackDotpTo3ds => { "TOKENIZATION_CONSENT_FALLBACK_DOTP_TO_3DS".to_string() } - PaymentFlow::CUSTOMER_FALLBACK_DOTP_TO_3DS => "CUSTOMER_FALLBACK_DOTP_TO_3DS".to_string(), - PaymentFlow::AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS => { + PaymentFlow::CustomerFallbackDotpTo3ds => "CUSTOMER_FALLBACK_DOTP_TO_3DS".to_string(), + PaymentFlow::AuthProviderFallback3ds2To3ds => { "AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS".to_string() } - PaymentFlow::FRM_PREFERENCE_TO_NO_3DS => "FRM_PREFERENCE_TO_NO_3DS".to_string(), - PaymentFlow::MERCHANT_FALLBACK_3DS2_TO_3DS => "MERCHANT_FALLBACK_3DS2_TO_3DS".to_string(), - PaymentFlow::MERCHANT_FALLBACK_FIDO_TO_3DS => "MERCHANT_FALLBACK_FIDO_TO_3DS".to_string(), - PaymentFlow::ORDER_PREFERENCE_FALLBACK_NO_3DS_TO_3DS => { + PaymentFlow::FrmPreferenceToNo3ds => "FRM_PREFERENCE_TO_NO_3DS".to_string(), + PaymentFlow::MerchantFallback3ds2To3ds => "MERCHANT_FALLBACK_3DS2_TO_3DS".to_string(), + PaymentFlow::MerchantFallbackFidoTo3ds => "MERCHANT_FALLBACK_FIDO_TO_3DS".to_string(), + PaymentFlow::OrderPreferenceFallbackNo3dsTo3ds => { "ORDER_PREFERENCE_FALLBACK_NO_3DS_TO_3DS".to_string() } - PaymentFlow::MERCHANT_PREFERENCE_FALLBACK_NO_3DS_TO_3DS => { + PaymentFlow::MerchantPreferenceFallbackNo3dsTo3ds => { "MERCHANT_PREFERENCE_FALLBACK_NO_3DS_TO_3DS".to_string() } - PaymentFlow::ORDER_PREFERENCE_TO_NO_3DS => "ORDER_PREFERENCE_TO_NO_3DS".to_string(), - PaymentFlow::MUTUAL_FUND => "MUTUAL_FUND".to_string(), - PaymentFlow::CROSS_BORDER_PAYMENT => "CROSS_BORDER_PAYMENT".to_string(), - PaymentFlow::APPLEPAY_TOKEN_DECRYPTION_FLOW => "APPLEPAY_TOKEN_DECRYPTION_FLOW".to_string(), - PaymentFlow::ONE_TIME_MANDATE => "ONE_TIME_MANDATE".to_string(), - PaymentFlow::SINGLE_BLOCK_MULTIPLE_DEBIT => "SINGLE_BLOCK_MULTIPLE_DEBIT".to_string(), + PaymentFlow::OrderPreferenceToNo3ds => "ORDER_PREFERENCE_TO_NO_3DS".to_string(), + PaymentFlow::MutualFund => "MUTUAL_FUND".to_string(), + PaymentFlow::CrossBorderPayment => "CROSS_BORDER_PAYMENT".to_string(), + PaymentFlow::ApplepayTokenDecryptionFlow => "APPLEPAY_TOKEN_DECRYPTION_FLOW".to_string(), + PaymentFlow::OneTimeMandate => "ONE_TIME_MANDATE".to_string(), + PaymentFlow::SingleBlockMultipleDebit => "SINGLE_BLOCK_MULTIPLE_DEBIT".to_string(), + PaymentFlow::PixAutomaticRedirect => "PIX_AUTOMATIC_REDIRECT".to_string(), } } pub fn text_to_payment_flows(text: String) -> Result { match text.as_str() { - "CARD_3DS" => Ok(PaymentFlow::CARD_3DS), - "CARD_3DS2" => Ok(PaymentFlow::CARD_3DS2), - "FRICTIONLESS_3DS" => Ok(PaymentFlow::FRICTIONLESS_3DS), - "CARD_DOTP" => Ok(PaymentFlow::CARD_DOTP), - "CARD_MOTO" => Ok(PaymentFlow::CARD_MOTO), - "CARD_NO_3DS" => Ok(PaymentFlow::CARD_NO_3DS), - "CARD_VIES" => Ok(PaymentFlow::CARD_VIES), - "ZERO_AUTH" => Ok(PaymentFlow::ZERO_AUTH), - "CARD_TOKENIZATION" => Ok(PaymentFlow::CARD_TOKENIZATION), - "CVVLESS" => Ok(PaymentFlow::CVVLESS), - "DIRECT_DEBIT" => Ok(PaymentFlow::DIRECT_DEBIT), - "EMANDATE" => Ok(PaymentFlow::EMANDATE), - "EMI" => Ok(PaymentFlow::EMI), - "INAPP_DEBIT" => Ok(PaymentFlow::INAPP_DEBIT), - "MANDATE" => Ok(PaymentFlow::MANDATE), - "PARTIAL_CAPTURE" => Ok(PaymentFlow::PARTIAL_CAPTURE), - "PARTIAL_VOID" => Ok(PaymentFlow::PARTIAL_VOID), - "PARTIAL_PAYMENT" => Ok(PaymentFlow::PARTIAL_PAYMENT), - "PREAUTH" => Ok(PaymentFlow::PREAUTH), - "SDKLESS_INTENT" => Ok(PaymentFlow::SDKLESS_INTENT), - "SPLIT_PAYMENT" => Ok(PaymentFlow::SPLIT_PAYMENT), - "SPLIT_SETTLEMENT" => Ok(PaymentFlow::SPLIT_SETTLEMENT), - "WALLET_TOPUP" => Ok(PaymentFlow::WALLET_TOPUP), - "TPV" => Ok(PaymentFlow::TPV), - "VISA_CHECKOUT" => Ok(PaymentFlow::VISA_CHECKOUT), - "AUTO_DISBURSEMENT" => Ok(PaymentFlow::AUTO_DISBURSEMENT), - "AUTO_USER_REGISTRATION" => Ok(PaymentFlow::AUTO_USER_REGISTRATION), - "BANK_INSTANT_REFUND" => Ok(PaymentFlow::BANK_INSTANT_REFUND), + "CARD_3DS" => Ok(PaymentFlow::Card3ds), + "CARD_3DS2" => Ok(PaymentFlow::Card3ds2), + "FRICTIONLESS_3DS" => Ok(PaymentFlow::Frictionless3ds), + "CARD_DOTP" => Ok(PaymentFlow::CardDotp), + "CARD_MOTO" => Ok(PaymentFlow::CardMoto), + "CARD_NO_3DS" => Ok(PaymentFlow::CardNo3ds), + "CARD_VIES" => Ok(PaymentFlow::CardVies), + "ZERO_AUTH" => Ok(PaymentFlow::ZeroAuth), + "CARD_TOKENIZATION" => Ok(PaymentFlow::CardTokenization), + "CVVLESS" => Ok(PaymentFlow::Cvvless), + "DIRECT_DEBIT" => Ok(PaymentFlow::DirectDebit), + "EMANDATE" => Ok(PaymentFlow::Emandate), + "EMI" => Ok(PaymentFlow::Emi), + "INAPP_DEBIT" => Ok(PaymentFlow::InappDebit), + "MANDATE" => Ok(PaymentFlow::Mandate), + "PARTIAL_CAPTURE" => Ok(PaymentFlow::PartialCapture), + "PARTIAL_VOID" => Ok(PaymentFlow::PartialVoid), + "PARTIAL_PAYMENT" => Ok(PaymentFlow::PartialPayment), + "PREAUTH" => Ok(PaymentFlow::Preauth), + "SDKLESS_INTENT" => Ok(PaymentFlow::SdklessIntent), + "SPLIT_PAYMENT" => Ok(PaymentFlow::SplitPayment), + "SPLIT_SETTLEMENT" => Ok(PaymentFlow::SplitSettlement), + "WALLET_TOPUP" => Ok(PaymentFlow::WalletTopup), + "TPV" => Ok(PaymentFlow::Tpv), + "VISA_CHECKOUT" => Ok(PaymentFlow::VisaCheckout), + "AUTO_DISBURSEMENT" => Ok(PaymentFlow::AutoDisbursement), + "AUTO_USER_REGISTRATION" => Ok(PaymentFlow::AutoUserRegistration), + "BANK_INSTANT_REFUND" => Ok(PaymentFlow::BankInstantRefund), "MANDATE_PREDEBIT_NOTIFICATION_DISABLEMENT" => { - Ok(PaymentFlow::MANDATE_PREDEBIT_NOTIFICATION_DISABLEMENT) - } - "ORDER_AMOUNT_AS_SUBVENTION_AMOUNT" => Ok(PaymentFlow::ORDER_AMOUNT_AS_SUBVENTION_AMOUNT), - "ORDER_ID_AS_RECON_ID" => Ok(PaymentFlow::ORDER_ID_AS_RECON_ID), - "PASS_USER_TOKEN_TO_GATEWAY" => Ok(PaymentFlow::PASS_USER_TOKEN_TO_GATEWAY), - "S2S_FLOW" => Ok(PaymentFlow::S2S_FLOW), - "SPLIT_SETTLE_ONLY" => Ok(PaymentFlow::SPLIT_SETTLE_ONLY), - "SUBSCRIPTION_ONLY" => Ok(PaymentFlow::SUBSCRIPTION_ONLY), - "TPV_ONLY" => Ok(PaymentFlow::TPV_ONLY), - "TXN_UUID_AS_TR" => Ok(PaymentFlow::TXN_UUID_AS_TR), - "UPI_INTENT_REGISTRATION" => Ok(PaymentFlow::UPI_INTENT_REGISTRATION), - "V2_INTEGRATION" => Ok(PaymentFlow::V2_INTEGRATION), - "V2_LINK_AND_PAY" => Ok(PaymentFlow::V2_LINK_AND_PAY), - "VPOS2" => Ok(PaymentFlow::VPOS2), - "OUTAGE" => Ok(PaymentFlow::OUTAGE), - "SR_BASED_ROUTING" => Ok(PaymentFlow::SR_BASED_ROUTING), - "ELIMINATION_BASED_ROUTING" => Ok(PaymentFlow::ELIMINATION_BASED_ROUTING), - "PL_BASED_ROUTING" => Ok(PaymentFlow::PL_BASED_ROUTING), - "MANDATE_WORKFLOW" => Ok(PaymentFlow::MANDATE_WORKFLOW), - "ALTID" => Ok(PaymentFlow::ALTID), - "SURCHARGE" => Ok(PaymentFlow::SURCHARGE), - "OFFER" => Ok(PaymentFlow::OFFER), - "CAPTCHA" => Ok(PaymentFlow::CAPTCHA), - "PAYMENT_COLLECTION_LINK" => Ok(PaymentFlow::PAYMENT_COLLECTION_LINK), - "AUTO_REFUND" => Ok(PaymentFlow::AUTO_REFUND), - "PAYMENT_LINK" => Ok(PaymentFlow::PAYMENT_LINK), - "PAYMENT_FORM" => Ok(PaymentFlow::PAYMENT_FORM), - "RISK_CHECK" => Ok(PaymentFlow::RISK_CHECK), - "DYNAMIC_CURRENCY_CONVERSION" => Ok(PaymentFlow::DYNAMIC_CURRENCY_CONVERSION), - "PART_PAYMENT" => Ok(PaymentFlow::PART_PAYMENT), - "STANDALONE_AUTHENTICATION" => Ok(PaymentFlow::STANDALONE_AUTHENTICATION), - "STANDALONE_AUTHORIZATION" => Ok(PaymentFlow::STANDALONE_AUTHORIZATION), - "STANDALONE_CAPTURE" => Ok(PaymentFlow::STANDALONE_CAPTURE), - "AUTHN_AUTHZ" => Ok(PaymentFlow::AUTHN_AUTHZ), - "AUTHZ_CAPTURE" => Ok(PaymentFlow::AUTHZ_CAPTURE), - "REDIRECT_DEBIT" => Ok(PaymentFlow::REDIRECT_DEBIT), - "LINK_AND_DEBIT" => Ok(PaymentFlow::LINK_AND_DEBIT), - "NEW_CARD" => Ok(PaymentFlow::NEW_CARD), - "NETWORK_TOKEN_CREATED" => Ok(PaymentFlow::NETWORK_TOKEN_CREATED), - "ISSUER_TOKEN_CREATED" => Ok(PaymentFlow::ISSUER_TOKEN_CREATED), - "LOCKER_TOKEN_CREATED" => Ok(PaymentFlow::LOCKER_TOKEN_CREATED), - "SODEXO_TOKEN_CREATED" => Ok(PaymentFlow::SODEXO_TOKEN_CREATED), - "NETWORK_TOKEN_USED" => Ok(PaymentFlow::NETWORK_TOKEN_USED), - "ISSUER_TOKEN_USED" => Ok(PaymentFlow::ISSUER_TOKEN_USED), - "LOCKER_TOKEN_USED" => Ok(PaymentFlow::LOCKER_TOKEN_USED), - "SODEXO_TOKEN_USED" => Ok(PaymentFlow::SODEXO_TOKEN_USED), - "PAYU_TOKEN_USED" => Ok(PaymentFlow::PAYU_TOKEN_USED), - "MANDATE_REGISTER" => Ok(PaymentFlow::MANDATE_REGISTER), - "MANDATE_REGISTER_DEBIT" => Ok(PaymentFlow::MANDATE_REGISTER_DEBIT), - "MANDATE_PAYMENT" => Ok(PaymentFlow::MANDATE_PAYMENT), - "EMANDATE_REGISTER" => Ok(PaymentFlow::EMANDATE_REGISTER), - "EMANDATE_REGISTER_DEBIT" => Ok(PaymentFlow::EMANDATE_REGISTER_DEBIT), - "EMANDATE_PAYMENT" => Ok(PaymentFlow::EMANDATE_PAYMENT), - "SI_HUB" => Ok(PaymentFlow::SI_HUB), - "TPV_EMANDATE" => Ok(PaymentFlow::TPV_EMANDATE), - "COLLECT" => Ok(PaymentFlow::COLLECT), - "INTENT" => Ok(PaymentFlow::INTENT), - "INAPP" => Ok(PaymentFlow::INAPP), - "QR" => Ok(PaymentFlow::QR), - "PUSH_PAY" => Ok(PaymentFlow::PUSH_PAY), - "INSTANT_REFUND" => Ok(PaymentFlow::INSTANT_REFUND), - "ASYNC" => Ok(PaymentFlow::ASYNC), - "DOTP" => Ok(PaymentFlow::DOTP), - "MERCHANT_MANAGED_DEBIT" => Ok(PaymentFlow::MERCHANT_MANAGED_DEBIT), - "ADDRESS_VERIFICATION" => Ok(PaymentFlow::ADDRESS_VERIFICATION), - "TA_FILE" => Ok(PaymentFlow::TA_FILE), - "PAYMENT_PAGE" => Ok(PaymentFlow::PAYMENT_PAGE), - "PP_QUICKPAY" => Ok(PaymentFlow::PP_QUICKPAY), - "PP_RETRY" => Ok(PaymentFlow::PP_RETRY), - "INAPP_NEW_PAY" => Ok(PaymentFlow::INAPP_NEW_PAY), - "INAPP_REPEAT_PAY" => Ok(PaymentFlow::INAPP_REPEAT_PAY), - "PG_EMI" => Ok(PaymentFlow::PG_EMI), - "SILENT_RETRY" => Ok(PaymentFlow::SILENT_RETRY), - "FIDO" => Ok(PaymentFlow::FIDO), - "REFUND" => Ok(PaymentFlow::REFUND), - "CTP" => Ok(PaymentFlow::CTP), - "ONE_TIME_PAYMENT" => Ok(PaymentFlow::ONE_TIME_PAYMENT), - "NO_COST_EMI" => Ok(PaymentFlow::NO_COST_EMI), - "LOW_COST_EMI" => Ok(PaymentFlow::LOW_COST_EMI), - "STANDARD_EMI" => Ok(PaymentFlow::STANDARD_EMI), - "STANDARD_EMI_SPLIT" => Ok(PaymentFlow::STANDARD_EMI_SPLIT), - "INTERNAL_NO_COST_EMI" => Ok(PaymentFlow::INTERNAL_NO_COST_EMI), - "INTERNAL_LOW_COST_EMI" => Ok(PaymentFlow::INTERNAL_LOW_COST_EMI), - "INTERNAL_NO_COST_EMI_SPLIT" => Ok(PaymentFlow::INTERNAL_NO_COST_EMI_SPLIT), - "INTERNAL_LOW_COST_EMI_SPLIT" => Ok(PaymentFlow::INTERNAL_LOW_COST_EMI_SPLIT), - "DIRECT_BANK_EMI" => Ok(PaymentFlow::DIRECT_BANK_EMI), - "REVERSE_PENNY_DROP" => Ok(PaymentFlow::REVERSE_PENNY_DROP), - "TOPUP" => Ok(PaymentFlow::TOPUP), - "ON_DEMAND_SPLIT_SETTLEMENT" => Ok(PaymentFlow::ON_DEMAND_SPLIT_SETTLEMENT), - "CREDIT_CARD_ON_UPI" => Ok(PaymentFlow::CREDIT_CARD_ON_UPI), - "DECIDER_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::DECIDER_FALLBACK_DOTP_TO_3DS), - "DECIDER_FALLBACK_NO_3DS_TO_3DS" => Ok(PaymentFlow::DECIDER_FALLBACK_NO_3DS_TO_3DS), - "PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS" => { - Ok(PaymentFlow::PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS) + Ok(PaymentFlow::MandatePredebitNotificationDisablement) } - "PG_FAILURE_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::PG_FAILURE_FALLBACK_DOTP_TO_3DS), + "ORDER_AMOUNT_AS_SUBVENTION_AMOUNT" => Ok(PaymentFlow::OrderAmountAsSubventionAmount), + "ORDER_ID_AS_RECON_ID" => Ok(PaymentFlow::OrderIdAsReconId), + "PASS_USER_TOKEN_TO_GATEWAY" => Ok(PaymentFlow::PassUserTokenToGateway), + "S2S_FLOW" => Ok(PaymentFlow::S2sFlow), + "SPLIT_SETTLE_ONLY" => Ok(PaymentFlow::SplitSettleOnly), + "SUBSCRIPTION_ONLY" => Ok(PaymentFlow::SubscriptionOnly), + "TPV_ONLY" => Ok(PaymentFlow::TpvOnly), + "TXN_UUID_AS_TR" => Ok(PaymentFlow::TxnUuidAsTr), + "UPI_INTENT_REGISTRATION" => Ok(PaymentFlow::UpiIntentRegistration), + "V2_INTEGRATION" => Ok(PaymentFlow::V2Integration), + "V2_LINK_AND_PAY" => Ok(PaymentFlow::V2LinkAndPay), + "VPOS2" => Ok(PaymentFlow::Vpos2), + "OUTAGE" => Ok(PaymentFlow::Outage), + "SR_BASED_ROUTING" => Ok(PaymentFlow::SrBasedRouting), + "ELIMINATION_BASED_ROUTING" => Ok(PaymentFlow::EliminationBasedRouting), + "PL_BASED_ROUTING" => Ok(PaymentFlow::PlBasedRouting), + "MANDATE_WORKFLOW" => Ok(PaymentFlow::MandateWorkflow), + "ALTID" => Ok(PaymentFlow::Altid), + "SURCHARGE" => Ok(PaymentFlow::Surcharge), + "OFFER" => Ok(PaymentFlow::Offer), + "CAPTCHA" => Ok(PaymentFlow::Captcha), + "PAYMENT_COLLECTION_LINK" => Ok(PaymentFlow::PaymentCollectionLink), + "AUTO_REFUND" => Ok(PaymentFlow::AutoRefund), + "PAYMENT_LINK" => Ok(PaymentFlow::PaymentLink), + "PAYMENT_FORM" => Ok(PaymentFlow::PaymentForm), + "RISK_CHECK" => Ok(PaymentFlow::RiskCheck), + "DYNAMIC_CURRENCY_CONVERSION" => Ok(PaymentFlow::DynamicCurrencyConversion), + "PART_PAYMENT" => Ok(PaymentFlow::PartPayment), + "STANDALONE_AUTHENTICATION" => Ok(PaymentFlow::StandaloneAuthentication), + "STANDALONE_AUTHORIZATION" => Ok(PaymentFlow::StandaloneAuthorization), + "STANDALONE_CAPTURE" => Ok(PaymentFlow::StandaloneCapture), + "AUTHN_AUTHZ" => Ok(PaymentFlow::AuthnAuthz), + "AUTHZ_CAPTURE" => Ok(PaymentFlow::AuthzCapture), + "REDIRECT_DEBIT" => Ok(PaymentFlow::RedirectDebit), + "LINK_AND_DEBIT" => Ok(PaymentFlow::LinkAndDebit), + "NEW_CARD" => Ok(PaymentFlow::NewCard), + "NETWORK_TOKEN_CREATED" => Ok(PaymentFlow::NetworkTokenCreated), + "ISSUER_TOKEN_CREATED" => Ok(PaymentFlow::IssuerTokenCreated), + "LOCKER_TOKEN_CREATED" => Ok(PaymentFlow::LockerTokenCreated), + "SODEXO_TOKEN_CREATED" => Ok(PaymentFlow::SodexoTokenCreated), + "NETWORK_TOKEN_USED" => Ok(PaymentFlow::NetworkTokenUsed), + "ISSUER_TOKEN_USED" => Ok(PaymentFlow::IssuerTokenUsed), + "LOCKER_TOKEN_USED" => Ok(PaymentFlow::LockerTokenUsed), + "SODEXO_TOKEN_USED" => Ok(PaymentFlow::SodexoTokenUsed), + "PAYU_TOKEN_USED" => Ok(PaymentFlow::PayuTokenUsed), + "MANDATE_REGISTER" => Ok(PaymentFlow::MandateRegister), + "MANDATE_REGISTER_DEBIT" => Ok(PaymentFlow::MandateRegisterDebit), + "MANDATE_PAYMENT" => Ok(PaymentFlow::MandatePayment), + "EMANDATE_REGISTER" => Ok(PaymentFlow::EmandateRegister), + "EMANDATE_REGISTER_DEBIT" => Ok(PaymentFlow::EmandateRegisterDebit), + "EMANDATE_PAYMENT" => Ok(PaymentFlow::EmandatePayment), + "SI_HUB" => Ok(PaymentFlow::SiHub), + "TPV_EMANDATE" => Ok(PaymentFlow::TpvEmandate), + "COLLECT" => Ok(PaymentFlow::Collect), + "INTENT" => Ok(PaymentFlow::Intent), + "INAPP" => Ok(PaymentFlow::Inapp), + "QR" => Ok(PaymentFlow::Qr), + "PUSH_PAY" => Ok(PaymentFlow::PushPay), + "INSTANT_REFUND" => Ok(PaymentFlow::InstantRefund), + "ASYNC" => Ok(PaymentFlow::Async), + "DOTP" => Ok(PaymentFlow::Dotp), + "MERCHANT_MANAGED_DEBIT" => Ok(PaymentFlow::MerchantManagedDebit), + "ADDRESS_VERIFICATION" => Ok(PaymentFlow::AddressVerification), + "TA_FILE" => Ok(PaymentFlow::TaFile), + "PAYMENT_PAGE" => Ok(PaymentFlow::PaymentPage), + "PP_QUICKPAY" => Ok(PaymentFlow::PpQuickpay), + "PP_RETRY" => Ok(PaymentFlow::PpRetry), + "INAPP_NEW_PAY" => Ok(PaymentFlow::InappNewPay), + "INAPP_REPEAT_PAY" => Ok(PaymentFlow::InappRepeatPay), + "PG_EMI" => Ok(PaymentFlow::PgEmi), + "SILENT_RETRY" => Ok(PaymentFlow::SilentRetry), + "FIDO" => Ok(PaymentFlow::Fido), + "REFUND" => Ok(PaymentFlow::Refund), + "CTP" => Ok(PaymentFlow::Ctp), + "ONE_TIME_PAYMENT" => Ok(PaymentFlow::OneTimePayment), + "NO_COST_EMI" => Ok(PaymentFlow::NoCostEmi), + "LOW_COST_EMI" => Ok(PaymentFlow::LowCostEmi), + "STANDARD_EMI" => Ok(PaymentFlow::StandardEmi), + "STANDARD_EMI_SPLIT" => Ok(PaymentFlow::StandardEmiSplit), + "INTERNAL_NO_COST_EMI" => Ok(PaymentFlow::InternalNoCostEmi), + "INTERNAL_LOW_COST_EMI" => Ok(PaymentFlow::InternalLowCostEmi), + "INTERNAL_NO_COST_EMI_SPLIT" => Ok(PaymentFlow::InternalNoCostEmiSplit), + "INTERNAL_LOW_COST_EMI_SPLIT" => Ok(PaymentFlow::InternalLowCostEmiSplit), + "DIRECT_BANK_EMI" => Ok(PaymentFlow::DirectBankEmi), + "REVERSE_PENNY_DROP" => Ok(PaymentFlow::ReversePennyDrop), + "TOPUP" => Ok(PaymentFlow::Topup), + "ON_DEMAND_SPLIT_SETTLEMENT" => Ok(PaymentFlow::OnDemandSplitSettlement), + "CREDIT_CARD_ON_UPI" => Ok(PaymentFlow::CreditCardOnUpi), + "DECIDER_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::DeciderFallbackDotpTo3ds), + "DECIDER_FALLBACK_NO_3DS_TO_3DS" => Ok(PaymentFlow::DeciderFallbackNo3dsTo3ds), + "PAYMENT_CHANNEL_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::PaymentChannelFallbackDotpTo3ds), + "PG_FAILURE_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::PgFailureFallbackDotpTo3ds), "TOKENIZATION_CONSENT_FALLBACK_DOTP_TO_3DS" => { - Ok(PaymentFlow::TOKENIZATION_CONSENT_FALLBACK_DOTP_TO_3DS) + Ok(PaymentFlow::TokenizationConsentFallbackDotpTo3ds) } - "CUSTOMER_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::CUSTOMER_FALLBACK_DOTP_TO_3DS), - "AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS" => Ok(PaymentFlow::AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS), - "FRM_PREFERENCE_TO_NO_3DS" => Ok(PaymentFlow::FRM_PREFERENCE_TO_NO_3DS), - "MERCHANT_FALLBACK_3DS2_TO_3DS" => Ok(PaymentFlow::MERCHANT_FALLBACK_3DS2_TO_3DS), - "MERCHANT_FALLBACK_FIDO_TO_3DS" => Ok(PaymentFlow::MERCHANT_FALLBACK_FIDO_TO_3DS), + "CUSTOMER_FALLBACK_DOTP_TO_3DS" => Ok(PaymentFlow::CustomerFallbackDotpTo3ds), + "AUTH_PROVIDER_FALLBACK_3DS2_TO_3DS" => Ok(PaymentFlow::AuthProviderFallback3ds2To3ds), + "FRM_PREFERENCE_TO_NO_3DS" => Ok(PaymentFlow::FrmPreferenceToNo3ds), + "MERCHANT_FALLBACK_3DS2_TO_3DS" => Ok(PaymentFlow::MerchantFallback3ds2To3ds), + "MERCHANT_FALLBACK_FIDO_TO_3DS" => Ok(PaymentFlow::MerchantFallbackFidoTo3ds), "ORDER_PREFERENCE_FALLBACK_NO_3DS_TO_3DS" => { - Ok(PaymentFlow::ORDER_PREFERENCE_FALLBACK_NO_3DS_TO_3DS) + Ok(PaymentFlow::OrderPreferenceFallbackNo3dsTo3ds) } "MERCHANT_PREFERENCE_FALLBACK_NO_3DS_TO_3DS" => { - Ok(PaymentFlow::MERCHANT_PREFERENCE_FALLBACK_NO_3DS_TO_3DS) + Ok(PaymentFlow::MerchantPreferenceFallbackNo3dsTo3ds) } - "ORDER_PREFERENCE_TO_NO_3DS" => Ok(PaymentFlow::ORDER_PREFERENCE_TO_NO_3DS), - "MUTUAL_FUND" => Ok(PaymentFlow::MUTUAL_FUND), - "CROSS_BORDER_PAYMENT" => Ok(PaymentFlow::CROSS_BORDER_PAYMENT), - "APPLEPAY_TOKEN_DECRYPTION_FLOW" => Ok(PaymentFlow::APPLEPAY_TOKEN_DECRYPTION_FLOW), - "ONE_TIME_MANDATE" => Ok(PaymentFlow::ONE_TIME_MANDATE), - "SINGLE_BLOCK_MULTIPLE_DEBIT" => Ok(PaymentFlow::SINGLE_BLOCK_MULTIPLE_DEBIT), + "ORDER_PREFERENCE_TO_NO_3DS" => Ok(PaymentFlow::OrderPreferenceToNo3ds), + "MUTUAL_FUND" => Ok(PaymentFlow::MutualFund), + "CROSS_BORDER_PAYMENT" => Ok(PaymentFlow::CrossBorderPayment), + "APPLEPAY_TOKEN_DECRYPTION_FLOW" => Ok(PaymentFlow::ApplepayTokenDecryptionFlow), + "ONE_TIME_MANDATE" => Ok(PaymentFlow::OneTimeMandate), + "SINGLE_BLOCK_MULTIPLE_DEBIT" => Ok(PaymentFlow::SingleBlockMultipleDebit), + "PIX_AUTOMATIC_REDIRECT" => Ok(PaymentFlow::PixAutomaticRedirect), _ => Err(ApiError::ParsingError("Invalid Payment Flow")), } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Purpose { - AFFORDABILITY, - COMPLIANT, - INTERNAL_CONFIG, - SR_IMPROVEMENT, - UEX_IMPROVEMENT, - PAYMENT, - SECURITY, - OPTIMIZATION, - NO_CODE_PAYMENT, + Affordability, + Compliant, + InternalConfig, + SrImprovement, + UexImprovement, + Payment, + Security, + Optimization, + NoCodePayment, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] @@ -468,14 +469,15 @@ pub enum Category { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ControlLevel { - GATEWAY, - MERCHANT, - MERCHANT_GATEWAY, - TENANT, - TENANT_GATEWAY, - TRACKING_ONLY, - MERCHANT_GATEWAY_ACCOUNT, + Gateway, + Merchant, + MerchantGateway, + Tenant, + TenantGateway, + TrackingOnly, + MerchantGatewayAccount, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] @@ -494,9 +496,10 @@ pub enum MicroPaymentFlowType { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum UiAccessMode { - READ_ONLY, - HIDDEN, + ReadOnly, + Hidden, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/types/routing_configuration.rs b/src/types/routing_configuration.rs index fd1da63a..45e71cb3 100644 --- a/src/types/routing_configuration.rs +++ b/src/types/routing_configuration.rs @@ -20,7 +20,7 @@ pub enum AlgorithmType { DebitRouting, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "data")] #[serde(rename_all = "camelCase")] pub enum ConfigVariant { @@ -29,7 +29,7 @@ pub enum ConfigVariant { DebitRouting(DebitRoutingData), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SuccessRateData { pub default_latency_threshold: Option, @@ -41,7 +41,7 @@ pub struct SuccessRateData { pub sub_level_input_config: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SRSubLevelInputConfig { pub payment_method_type: Option, @@ -54,22 +54,30 @@ pub struct SRSubLevelInputConfig { pub gateway_extra_score: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GatewayWiseExtraScore { pub gateway_name: String, pub gateway_sigma_factor: f64, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EliminationData { pub threshold: f64, + pub txnLatency: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DebitRoutingData { pub merchant_category_code: String, pub acquirer_country: String, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionLatencyThreshold { + /// To have a hard threshold for latency in millis, which is used to filter out gateways that exceed this threshold. + pub gatewayLatency: Option, +} diff --git a/src/types/service_configuration.rs b/src/types/service_configuration.rs index 0b51c77b..d7120609 100644 --- a/src/types/service_configuration.rs +++ b/src/types/service_configuration.rs @@ -61,7 +61,7 @@ pub async fn update_config( ::Table, ServiceConfigurationUpdate, _, - >(&conn, dsl::name.eq(name), values) + >(conn, dsl::name.eq(name), values) .await?; Ok(()) @@ -77,7 +77,7 @@ pub async fn delete_config(name: String) -> Result<(), crate::generics::MeshErro .map_err(|_| crate::generics::MeshError::DatabaseConnectionError)?; // Use Diesel's query builder with multiple conditions crate::generics::generic_delete::<::Table, _>( - &conn, + conn, dsl::name.eq(name), ) .await?; diff --git a/src/types/tenant/tenant_config.rs b/src/types/tenant/tenant_config.rs index 3e5e9d45..4aed2d66 100644 --- a/src/types/tenant/tenant_config.rs +++ b/src/types/tenant/tenant_config.rs @@ -22,38 +22,38 @@ pub fn tenant_config_id_to_text(id: TenantConfigId) -> String { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ModuleName { #[serde(rename = "MERCHANT_ACCOUNT")] - MERCHANT_ACCOUNT, + MerchantAccount, #[serde(rename = "SURCHARGE_LOGIC")] - SURCHARGE_LOGIC, + SurchargeLogic, #[serde(rename = "PRIORITY_LOGIC")] - PRIORITY_LOGIC, + PriorityLogic, #[serde(rename = "MERCHANT_CONFIG")] - MERCHANT_CONFIG, + MerchantConfig, #[serde(rename = "TENANT_FEATURE")] - TENANT_FEATURE, + TenantFeature, #[serde(rename = "TENANT_ACCOUNT")] - TENANT_ACCOUNT, + TenantAccount, } pub fn module_name_to_text(module_name: &ModuleName) -> String { match module_name { - ModuleName::MERCHANT_ACCOUNT => "MERCHANT_ACCOUNT".to_string(), - ModuleName::SURCHARGE_LOGIC => "SURCHARGE_LOGIC".to_string(), - ModuleName::PRIORITY_LOGIC => "PRIORITY_LOGIC".to_string(), - ModuleName::MERCHANT_CONFIG => "MERCHANT_CONFIG".to_string(), - ModuleName::TENANT_FEATURE => "TENANT_FEATURE".to_string(), - ModuleName::TENANT_ACCOUNT => "TENANT_ACCOUNT".to_string(), + ModuleName::MerchantAccount => "MERCHANT_ACCOUNT".to_string(), + ModuleName::SurchargeLogic => "SURCHARGE_LOGIC".to_string(), + ModuleName::PriorityLogic => "PRIORITY_LOGIC".to_string(), + ModuleName::MerchantConfig => "MERCHANT_CONFIG".to_string(), + ModuleName::TenantFeature => "TENANT_FEATURE".to_string(), + ModuleName::TenantAccount => "TENANT_ACCOUNT".to_string(), } } pub fn text_to_module_name(module_name: String) -> Result { match module_name.as_str() { - "MERCHANT_ACCOUNT" => Ok(ModuleName::MERCHANT_ACCOUNT), - "SURCHARGE_LOGIC" => Ok(ModuleName::SURCHARGE_LOGIC), - "PRIORITY_LOGIC" => Ok(ModuleName::PRIORITY_LOGIC), - "MERCHANT_CONFIG" => Ok(ModuleName::MERCHANT_CONFIG), - "TENANT_FEATURE" => Ok(ModuleName::TENANT_FEATURE), - "TENANT_ACCOUNT" => Ok(ModuleName::TENANT_ACCOUNT), + "MERCHANT_ACCOUNT" => Ok(ModuleName::MerchantAccount), + "SURCHARGE_LOGIC" => Ok(ModuleName::SurchargeLogic), + "PRIORITY_LOGIC" => Ok(ModuleName::PriorityLogic), + "MERCHANT_CONFIG" => Ok(ModuleName::MerchantConfig), + "TENANT_FEATURE" => Ok(ModuleName::TenantFeature), + "TENANT_ACCOUNT" => Ok(ModuleName::TenantAccount), _ => Err(ApiError::ParsingError("Invalid Module Name")), } } @@ -112,6 +112,22 @@ pub fn text_to_filter_dimension(filter_dimension: String) -> Result { + pub status: TenantConfigStatus, + #[serde(rename = "configValue")] + pub config_value: Option, +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ConfigStatus { #[serde(rename = "ACTIVE")] diff --git a/src/types/tenant/tenant_config_filter.rs b/src/types/tenant/tenant_config_filter.rs index 6f199520..1ec1ba9c 100644 --- a/src/types/tenant/tenant_config_filter.rs +++ b/src/types/tenant/tenant_config_filter.rs @@ -1,7 +1,7 @@ #[cfg(feature = "mysql")] -use crate::storage::schema::tenant_config_filter::{dimension_value, dsl, filter_group_id}; +use crate::storage::schema::tenant_config_filter::dsl; #[cfg(feature = "postgres")] -use crate::storage::schema_pg::tenant_config_filter::{dimension_value, dsl, filter_group_id}; +use crate::storage::schema_pg::tenant_config_filter::dsl; use crate::{ app::get_tenant_app_state, error::ApiError, storage::types::TenantConfigFilter as DBTenantConfigFilter, @@ -44,7 +44,7 @@ impl TryFrom for TenantConfigFilter { type Error = ApiError; fn try_from(db_tenant_config_filter: DBTenantConfigFilter) -> Result { - Ok(TenantConfigFilter { + Ok(Self { id: text_to_tenant_config_filter_id(db_tenant_config_filter.id), filterGroupId: db_tenant_config_filter.filter_group_id, dimensionValue: db_tenant_config_filter.dimension_value, @@ -56,7 +56,7 @@ impl TryFrom for TenantConfigFilter { pub async fn get_tenant_config_filter_by_group_id_and_dimension_value( group_id: String, - dimension_valu: String, + dimension_value: String, ) -> Option { let app_state = get_tenant_app_state().await; @@ -69,7 +69,7 @@ pub async fn get_tenant_config_filter_by_group_id_and_dimension_value( &app_state.db, dsl::filter_group_id .eq(group_id) - .and(dsl::dimension_value.eq(dimension_valu)), + .and(dsl::dimension_value.eq(dimension_value)), ) .await { diff --git a/src/types/token_bin_info.rs b/src/types/token_bin_info.rs index f904104a..19a9134a 100644 --- a/src/types/token_bin_info.rs +++ b/src/types/token_bin_info.rs @@ -69,9 +69,7 @@ pub async fn getAllTokenBinInfoByTokenBins(token_bins: Vec) -> Vec db_results.into_iter() - .filter_map(|db_record| TokenBinInfo::try_from(db_record).ok()) - .collect(), + Ok(db_results) => db_results.into_iter().map(From::from).collect(), Err(_) => Vec::new(), // Silently handle any errors by returning an empty vec } } diff --git a/src/types/txn_details/types.rs b/src/types/txn_details/types.rs index a91674a4..e190e233 100644 --- a/src/types/txn_details/types.rs +++ b/src/types/txn_details/types.rs @@ -13,13 +13,14 @@ use time::Time; // use db::mesh::internal; // use crate::storage::internal::primd_id_to_int; // use types::utils::dbconfig::get_euler_db_conf; +use crate::types::country::country_iso::CountryISO2; use crate::types::currency::Currency; use crate::types::merchant::id::MerchantId; use crate::types::merchant::merchant_gateway_account::MerchantGwAccId; use crate::types::money::internal::Money; use crate::types::order::id::OrderId; // use juspay::extra::parsing::{Parsed, ParsingErrorType, Step, around, defaulting, lift_either, lift_pure, mandated, non_empty_text, non_negative, parse_field, project, to_utc}; -use crate::types::source_object_id::SourceObjectId; +use crate::types::source_object_id::{to_source_object_id, SourceObjectId}; // use juspay::extra::nonemptytext::NonEmptyText; use crate::types::transaction::id::TransactionId; // use eulerhs::extra::combinators::to_domain_all; @@ -77,6 +78,8 @@ pub enum TxnObjectType { VanPayment, #[serde(rename = "MOTO_PAYMENT")] MotoPayment, + #[serde(rename = "UNKNOWN")] + Unknown, } impl fmt::Display for TxnObjectType { @@ -99,6 +102,7 @@ impl fmt::Display for TxnObjectType { Self::PartialVoid => "PARTIAL_VOID", Self::VanPayment => "VAN_PAYMENT", Self::MotoPayment => "MOTO_PAYMENT", + Self::Unknown => "UNKNOWN", } ) } @@ -121,6 +125,7 @@ impl TxnObjectType { "PARTIAL_VOID" => Some(Self::PartialVoid), "VAN_PAYMENT" => Some(Self::VanPayment), "MOTO_PAYMENT" => Some(Self::MotoPayment), + "UNKNOWN" => Some(Self::Unknown), _ => None, } } @@ -141,6 +146,7 @@ impl TxnObjectType { Self::PartialVoid => "PARTIAL_VOID", Self::VanPayment => "VAN_PAYMENT", Self::MotoPayment => "MOTO_PAYMENT", + Self::Unknown => "UNKNOWN", } } } @@ -485,6 +491,115 @@ where .transpose() } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SafeTxnDetail { + #[serde(rename = "id")] + pub id: Option, + #[serde(rename = "orderId")] + pub orderId: OrderId, + pub status: String, + #[serde(rename = "txnId")] + pub txnId: TransactionId, + #[serde(rename = "type")] + pub txnType: String, + #[serde(with = "time::serde::iso8601::option", rename = "dateCreated")] + pub dateCreated: Option, + #[serde(rename = "addToLocker")] + pub addToLocker: Option, + #[serde(rename = "merchantId")] + pub merchantId: MerchantId, + pub gateway: Option, + #[serde(rename = "expressCheckout")] + pub expressCheckout: Option, + #[serde(rename = "isEmi")] + pub isEmi: Option, + #[serde(rename = "emiBank")] + pub emiBank: Option, + #[serde(rename = "emiTenure")] + pub emiTenure: Option, + #[serde(rename = "txnUuid")] + pub txnUuid: String, + #[serde(rename = "merchantGatewayAccountId")] + pub merchantGatewayAccountId: Option, + #[serde(rename = "txnAmount")] + pub txnAmount: Option, + #[serde(rename = "txnObjectType")] + pub txnObjectType: Option, + #[serde(rename = "sourceObject")] + pub sourceObject: Option, + #[serde(rename = "sourceObjectId")] + pub sourceObjectId: Option, + pub currency: Option, + #[serde(rename = "netAmount")] + pub netAmount: Option, + #[serde(rename = "surchargeAmount")] + pub surchargeAmount: Option, + #[serde(rename = "taxAmount")] + pub taxAmount: Option, + #[serde(rename = "offerDeductionAmount")] + pub offerDeductionAmount: Option, + pub metadata: Option>, + #[serde(rename = "internalMetadata")] + pub internalMetadata: Option>, + #[serde(rename = "internalTrackingInfo")] + pub internalTrackingInfo: Option, + #[serde( + deserialize_with = "deserialize_optional_primitive_datetime", + rename = "partitionKey" + )] + pub partitionKey: Option, + pub txnAmountBreakup: Option>, +} + +pub fn convert_safe_txn_detail_to_txn_detail( + safe_detail: SafeTxnDetail, +) -> Result { + Ok(TxnDetail { + id: safe_detail + .id + .and_then(|s| s.parse::().ok()) + .map(to_txn_detail_id) + .unwrap(), + dateCreated: safe_detail + .dateCreated + .unwrap_or_else(OffsetDateTime::now_utc), + orderId: safe_detail.orderId, + status: TxnStatus::from_text(safe_detail.status).unwrap_or(TxnStatus::Failure), + txnId: safe_detail.txnId, + txnType: Some(safe_detail.txnType), + addToLocker: safe_detail.addToLocker, + merchantId: safe_detail.merchantId, + gateway: safe_detail.gateway, + expressCheckout: safe_detail.expressCheckout, + isEmi: safe_detail.isEmi, + emiBank: safe_detail.emiBank, + emiTenure: safe_detail.emiTenure.map(|t| t as i32), + txnUuid: safe_detail.txnUuid, + merchantGatewayAccountId: safe_detail + .merchantGatewayAccountId + .map(|id| MerchantGwAccId { + merchantGwAccId: id, + }), + netAmount: safe_detail.netAmount, + txnAmount: safe_detail.txnAmount, + txnObjectType: safe_detail.txnObjectType.and_then(TxnObjectType::from_text), + sourceObject: safe_detail.sourceObject, + sourceObjectId: safe_detail.sourceObjectId.map(to_source_object_id), + currency: safe_detail + .currency + .and_then(|s| Currency::text_to_curr(&s).ok()) + .ok_or(crate::error::ApiError::MissingRequiredField("currency"))?, + country: None, + surchargeAmount: safe_detail.surchargeAmount, + taxAmount: safe_detail.taxAmount, + internalMetadata: safe_detail.internalMetadata, + metadata: safe_detail.metadata, + offerDeductionAmount: safe_detail.offerDeductionAmount, + internalTrackingInfo: safe_detail.internalTrackingInfo, + partitionKey: safe_detail.partitionKey, + txnAmountBreakup: safe_detail.txnAmountBreakup, + }) +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TxnDetail { #[serde(rename = "id")] @@ -499,17 +614,17 @@ pub struct TxnDetail { #[serde(rename = "txnId")] pub txnId: TransactionId, #[serde(rename = "txnType")] - pub txnType: String, + pub txnType: Option, #[serde(rename = "addToLocker")] - pub addToLocker: bool, + pub addToLocker: Option, #[serde(rename = "merchantId")] pub merchantId: MerchantId, #[serde(rename = "gateway")] pub gateway: Option, #[serde(rename = "expressCheckout")] - pub expressCheckout: bool, + pub expressCheckout: Option, #[serde(rename = "isEmi")] - pub isEmi: bool, + pub isEmi: Option, #[serde(rename = "emiBank")] pub emiBank: Option, #[serde(rename = "emiTenure")] @@ -519,25 +634,27 @@ pub struct TxnDetail { #[serde(rename = "merchantGatewayAccountId")] pub merchantGatewayAccountId: Option, #[serde(rename = "netAmount")] - pub netAmount: Money, + pub netAmount: Option, #[serde(rename = "txnAmount")] - pub txnAmount: Money, + pub txnAmount: Option, #[serde(rename = "txnObjectType")] - pub txnObjectType: TxnObjectType, + pub txnObjectType: Option, #[serde(rename = "sourceObject")] pub sourceObject: Option, #[serde(rename = "sourceObjectId")] pub sourceObjectId: Option, #[serde(rename = "currency")] pub currency: Currency, + #[serde(rename = "country")] + pub country: Option, #[serde(rename = "surchargeAmount")] pub surchargeAmount: Option, #[serde(rename = "taxAmount")] pub taxAmount: Option, #[serde(rename = "internalMetadata")] - pub internalMetadata: Option, + pub internalMetadata: Option>, #[serde(rename = "metadata")] - pub metadata: Option, + pub metadata: Option>, #[serde(rename = "offerDeductionAmount")] pub offerDeductionAmount: Option, #[serde(rename = "internalTrackingInfo")] @@ -600,11 +717,18 @@ pub enum ChargeMethod { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ChargeName { - BASE, - SURCHARGE, - TAX_ON_SURCHARGE, - OFFER, - ADD_ON, - GATEWAY_ADJUSTMENT, + Base, + Surcharge, + TaxOnSurcharge, + Offer, + AddOn, + GatewayAdjustment, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TransactionLatency { + #[serde(rename = "gatewayLatency")] + pub gateway_latency: Option, } diff --git a/src/types/txn_offer.rs b/src/types/txn_offer.rs index 9e117a8f..affd0d98 100644 --- a/src/types/txn_offer.rs +++ b/src/types/txn_offer.rs @@ -75,10 +75,7 @@ pub async fn getOffersDB( pub async fn getOffers(txn_id: &TxnDetailId) -> Vec { // Call the database function and handle results match getOffersDB(txn_id).await { - Ok(db_results) => db_results - .into_iter() - .filter_map(|db_record| TxnOffer::try_from(db_record).ok()) - .collect(), + Ok(db_results) => db_results.into_iter().map(From::from).collect(), Err(_) => Vec::new(), // Silently handle any errors by returning empty vec } } diff --git a/src/types/txn_offer_detail.rs b/src/types/txn_offer_detail.rs index e03a6552..8481c7a7 100644 --- a/src/types/txn_offer_detail.rs +++ b/src/types/txn_offer_detail.rs @@ -1,17 +1,39 @@ use serde::{Deserialize, Serialize}; use std::option::Option; use std::string::String; -use std::time::SystemTime; -use time::PrimitiveDateTime; +use time::{OffsetDateTime, PrimitiveDateTime}; // use chrono::NaiveDateTime; // use data::text::Text; // use juspay::extra::nonemptytext::NonEmptyText; -use crate::types::txn_details::types::TxnDetailId; +use crate::types::txn_details::types::{deserialize_optional_primitive_datetime, TxnDetailId}; +use serde::{de, ser}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TxnOfferDetailId { - #[serde(rename = "txnOfferDetailId")] - pub txnOfferDetailId: String, +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TxnOfferDetailId(String); + +impl TxnOfferDetailId { + pub fn new(s: String) -> Result { + Ok(Self(s)) + } +} + +impl<'de> Deserialize<'de> for TxnOfferDetailId { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::new(s).map_err(de::Error::custom) + } +} + +impl Serialize for TxnOfferDetailId { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.0) + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -25,14 +47,17 @@ pub struct TxnOfferDetail { #[serde(rename = "status")] pub status: TxnOfferDetailStatus, #[serde(rename = "dateCreated")] - pub dateCreated: Option, + #[serde(with = "time::serde::iso8601::option")] + pub dateCreated: Option, #[serde(rename = "lastUpdated")] - pub lastUpdated: Option, + #[serde(with = "time::serde::iso8601::option")] + pub lastUpdated: Option, #[serde(rename = "gatewayInfo")] pub gatewayInfo: Option, #[serde(rename = "internalMetadata")] pub internalMetadata: Option, #[serde(rename = "partitionKey")] + #[serde(deserialize_with = "deserialize_optional_primitive_datetime")] pub partitionKey: Option, } diff --git a/src/types/txn_offer_info.rs b/src/types/txn_offer_info.rs index fd9d6c59..2873e14d 100644 --- a/src/types/txn_offer_info.rs +++ b/src/types/txn_offer_info.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; -use crate::decider::gatewaydecider::types::Gateway; - use super::money::internal::Money; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] @@ -20,11 +18,13 @@ pub enum OfferStatus { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum OfferType { - CASHBACK, - VOUCHER, - DISCOUNT, - REWARD_POINT, + Cashback, + VCoucher, + Discount, + #[serde(rename = "REWARD_POINT")] + RewardPoint, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/utils.rs b/src/utils.rs index edad49e0..9663dea2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,5 @@ -use crate::logger::storage::Storage; use crate::{ - decider::gatewaydecider::runner::ResponseBody, error::ApiClientError, logger, storage::consts, + decider::gatewaydecider::runner::ResponseBody, error::ApiClientError, storage::consts, }; use axum::{body::Body, extract::Request}; use error_stack::ResultExt; @@ -11,7 +10,6 @@ use reqwest::{ Client, }; use serde::Deserialize; -use serde_json::Value; use std::time::{SystemTime, UNIX_EPOCH}; /// Date-time utilities. @@ -26,7 +24,7 @@ pub mod date_time { } pub fn record_fields_from_header(request: &Request) -> tracing::Span { - let span = tracing::debug_span!( + let span = tracing::info_span!( "request", method = %request.method(), uri = %request.uri(), @@ -35,6 +33,7 @@ pub fn record_fields_from_header(request: &Request) -> tracing::Span { udf_order_id = tracing::field::Empty, udf_customer_id = tracing::field::Empty, udf_txn_uuid = tracing::field::Empty, + txn_uuid = tracing::field::Empty, "x-request-id" = tracing::field::Empty, "x-global-request-id" = tracing::field::Empty, merchant_id = tracing::field::Empty, @@ -68,7 +67,7 @@ pub fn record_fields_from_header(request: &Request) -> tracing::Span { record_field(consts::X_SESSION_ID, "sdk_session_span"); record_field(consts::X_CELL_SELECTOR, "cell_selector"); record_field(consts::X_ART_RECORDING, "is_art_enabled"); - span.record("is_audit_trail_log", "true"); + span.record("is_audit_trail_log", "false"); span.record("schema_version", "V2"); span.record("tenant_name", "JUSPAY"); span.record("tenant_id", "JUSPAY"); @@ -226,7 +225,7 @@ pub async fn call_api(url: &str, body: &serde_json::Value) -> Result f64 { +pub fn generate_random_number(_tag: String, range: (f64, f64)) -> f64 { let (min, max) = range; // Create a random number generator diff --git a/website/dist/assets/index-BcfEJAiY.js b/website/dist/assets/index-BcfEJAiY.js new file mode 100644 index 00000000..d96595a4 --- /dev/null +++ b/website/dist/assets/index-BcfEJAiY.js @@ -0,0 +1,348 @@ +var fR=Object.defineProperty;var pR=(e,t,r)=>t in e?fR(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var u1=(e,t,r)=>pR(e,typeof t!="symbol"?t+"":t,r);function hR(e,t){for(var r=0;rn[a]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(a){if(a.ep)return;a.ep=!0;const i=r(a);fetch(a.href,i)}})();var Id=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function dt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var MO={exports:{}},Dh={},DO={exports:{}},Je={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var dd=Symbol.for("react.element"),mR=Symbol.for("react.portal"),vR=Symbol.for("react.fragment"),yR=Symbol.for("react.strict_mode"),gR=Symbol.for("react.profiler"),xR=Symbol.for("react.provider"),bR=Symbol.for("react.context"),wR=Symbol.for("react.forward_ref"),_R=Symbol.for("react.suspense"),SR=Symbol.for("react.memo"),jR=Symbol.for("react.lazy"),c1=Symbol.iterator;function kR(e){return e===null||typeof e!="object"?null:(e=c1&&e[c1]||e["@@iterator"],typeof e=="function"?e:null)}var LO={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},FO=Object.assign,zO={};function Cl(e,t,r){this.props=e,this.context=t,this.refs=zO,this.updater=r||LO}Cl.prototype.isReactComponent={};Cl.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Cl.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function BO(){}BO.prototype=Cl.prototype;function F0(e,t,r){this.props=e,this.context=t,this.refs=zO,this.updater=r||LO}var z0=F0.prototype=new BO;z0.constructor=F0;FO(z0,Cl.prototype);z0.isPureReactComponent=!0;var d1=Array.isArray,UO=Object.prototype.hasOwnProperty,B0={current:null},VO={key:!0,ref:!0,__self:!0,__source:!0};function WO(e,t,r){var n,a={},i=null,o=null;if(t!=null)for(n in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(i=""+t.key),t)UO.call(t,n)&&!VO.hasOwnProperty(n)&&(a[n]=t[n]);var s=arguments.length-2;if(s===1)a.children=r;else if(1>>1,K=D[q];if(0>>1;qa(de,G))fea(Ie,de)?(D[q]=Ie,D[fe]=G,q=fe):(D[q]=de,D[Z]=G,q=Z);else if(fea(Ie,G))D[q]=Ie,D[fe]=G,q=fe;else break e}}return U}function a(D,U){var G=D.sortIndex-U.sortIndex;return G!==0?G:D.id-U.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var l=[],c=[],f=1,d=null,p=3,h=!1,x=!1,v=!1,y=typeof setTimeout=="function"?setTimeout:null,g=typeof clearTimeout=="function"?clearTimeout:null,m=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function w(D){for(var U=r(c);U!==null;){if(U.callback===null)n(c);else if(U.startTime<=D)n(c),U.sortIndex=U.expirationTime,t(l,U);else break;U=r(c)}}function S(D){if(v=!1,w(D),!x)if(r(l)!==null)x=!0,V(b);else{var U=r(c);U!==null&&I(S,U.startTime-D)}}function b(D,U){x=!1,v&&(v=!1,g(O),O=-1),h=!0;var G=p;try{for(w(U),d=r(l);d!==null&&(!(d.expirationTime>U)||D&&!R());){var q=d.callback;if(typeof q=="function"){d.callback=null,p=d.priorityLevel;var K=q(d.expirationTime<=U);U=e.unstable_now(),typeof K=="function"?d.callback=K:d===r(l)&&n(l),w(U)}else n(l);d=r(l)}if(d!==null)var te=!0;else{var Z=r(c);Z!==null&&I(S,Z.startTime-U),te=!1}return te}finally{d=null,p=G,h=!1}}var _=!1,k=null,O=-1,N=5,T=-1;function R(){return!(e.unstable_now()-TD||125q?(D.sortIndex=G,t(c,D),r(l)===null&&D===r(c)&&(v?(g(O),O=-1):v=!0,I(S,G-q))):(D.sortIndex=K,t(l,D),x||h||(x=!0,V(b))),D},e.unstable_shouldYield=R,e.unstable_wrapCallback=function(D){var U=p;return function(){var G=p;p=U;try{return D.apply(this,arguments)}finally{p=G}}}})(XO);KO.exports=XO;var DR=KO.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var LR=j,gn=DR;function ie(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),zy=Object.prototype.hasOwnProperty,FR=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p1={},h1={};function zR(e){return zy.call(h1,e)?!0:zy.call(p1,e)?!1:FR.test(e)?h1[e]=!0:(p1[e]=!0,!1)}function BR(e,t,r,n){if(r!==null&&r.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return n?!1:r!==null?!r.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function UR(e,t,r,n){if(t===null||typeof t>"u"||BR(e,t,r,n))return!0;if(n)return!1;if(r!==null)switch(r.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Vr(e,t,r,n,a,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=a,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var Sr={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Sr[e]=new Vr(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Sr[t]=new Vr(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Sr[e]=new Vr(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Sr[e]=new Vr(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Sr[e]=new Vr(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Sr[e]=new Vr(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Sr[e]=new Vr(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Sr[e]=new Vr(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Sr[e]=new Vr(e,5,!1,e.toLowerCase(),null,!1,!1)});var V0=/[\-:]([a-z])/g;function W0(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(V0,W0);Sr[t]=new Vr(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(V0,W0);Sr[t]=new Vr(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(V0,W0);Sr[t]=new Vr(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Sr[e]=new Vr(e,1,!1,e.toLowerCase(),null,!1,!1)});Sr.xlinkHref=new Vr("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Sr[e]=new Vr(e,1,!1,e.toLowerCase(),null,!0,!0)});function H0(e,t,r,n){var a=Sr.hasOwnProperty(t)?Sr[t]:null;(a!==null?a.type!==0:n||!(2s||a[o]!==i[s]){var l=` +`+a[o].replace(" at new "," at ");return e.displayName&&l.includes("")&&(l=l.replace("",e.displayName)),l}while(1<=o&&0<=s);break}}}finally{tv=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?Su(e):""}function VR(e){switch(e.tag){case 5:return Su(e.type);case 16:return Su("Lazy");case 13:return Su("Suspense");case 19:return Su("SuspenseList");case 0:case 2:case 15:return e=rv(e.type,!1),e;case 11:return e=rv(e.type.render,!1),e;case 1:return e=rv(e.type,!0),e;default:return""}}function Wy(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case ms:return"Fragment";case hs:return"Portal";case By:return"Profiler";case G0:return"StrictMode";case Uy:return"Suspense";case Vy:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case QO:return(e.displayName||"Context")+".Consumer";case ZO:return(e._context.displayName||"Context")+".Provider";case q0:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case K0:return t=e.displayName||null,t!==null?t:Wy(e.type)||"Memo";case hi:t=e._payload,e=e._init;try{return Wy(e(t))}catch{}}return null}function WR(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Wy(t);case 8:return t===G0?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function zi(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function eA(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function HR(e){var t=eA(e)?"checked":"value",r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),n=""+e[t];if(!e.hasOwnProperty(t)&&typeof r<"u"&&typeof r.get=="function"&&typeof r.set=="function"){var a=r.get,i=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return a.call(this)},set:function(o){n=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(o){n=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Ld(e){e._valueTracker||(e._valueTracker=HR(e))}function tA(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=eA(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function Xf(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Hy(e,t){var r=t.checked;return It({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r??e._wrapperState.initialChecked})}function v1(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=zi(t.value!=null?t.value:r),e._wrapperState={initialChecked:n,initialValue:r,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function rA(e,t){t=t.checked,t!=null&&H0(e,"checked",t,!1)}function Gy(e,t){rA(e,t);var r=zi(t.value),n=t.type;if(r!=null)n==="number"?(r===0&&e.value===""||e.value!=r)&&(e.value=""+r):e.value!==""+r&&(e.value=""+r);else if(n==="submit"||n==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?qy(e,t.type,r):t.hasOwnProperty("defaultValue")&&qy(e,t.type,zi(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function y1(e,t,r){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var n=t.type;if(!(n!=="submit"&&n!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,r||t===e.value||(e.value=t),e.defaultValue=t}r=e.name,r!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,r!==""&&(e.name=r)}function qy(e,t,r){(t!=="number"||Xf(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}var ju=Array.isArray;function Rs(e,t,r,n){if(e=e.options,t){t={};for(var a=0;a"+t.valueOf().toString()+"",t=Fd.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function oc(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var Du={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},GR=["Webkit","ms","Moz","O"];Object.keys(Du).forEach(function(e){GR.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Du[t]=Du[e]})});function oA(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||Du.hasOwnProperty(e)&&Du[e]?(""+t).trim():t+"px"}function sA(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,a=oA(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,a):e[r]=a}}var qR=It({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Yy(e,t){if(t){if(qR[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(ie(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(ie(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(ie(61))}if(t.style!=null&&typeof t.style!="object")throw Error(ie(62))}}function Zy(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Qy=null;function X0(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Jy=null,Is=null,Ms=null;function b1(e){if(e=hd(e)){if(typeof Jy!="function")throw Error(ie(280));var t=e.stateNode;t&&(t=Uh(t),Jy(e.stateNode,e.type,t))}}function lA(e){Is?Ms?Ms.push(e):Ms=[e]:Is=e}function uA(){if(Is){var e=Is,t=Ms;if(Ms=Is=null,b1(e),t)for(e=0;e>>=0,e===0?32:31-(aI(e)/iI|0)|0}var zd=64,Bd=4194304;function ku(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Jf(e,t){var r=e.pendingLanes;if(r===0)return 0;var n=0,a=e.suspendedLanes,i=e.pingedLanes,o=r&268435455;if(o!==0){var s=o&~a;s!==0?n=ku(s):(i&=o,i!==0&&(n=ku(i)))}else o=r&~a,o!==0?n=ku(o):i!==0&&(n=ku(i));if(n===0)return 0;if(t!==0&&t!==n&&!(t&a)&&(a=n&-n,i=t&-t,a>=i||a===16&&(i&4194240)!==0))return t;if(n&4&&(n|=r&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=n;0r;r++)t.push(e);return t}function fd(e,t,r){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-ea(t),e[t]=r}function uI(e,t){var r=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var n=e.eventTimes;for(e=e.expirationTimes;0=Fu),E1=" ",P1=!1;function EA(e,t){switch(e){case"keyup":return DI.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function PA(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var vs=!1;function FI(e,t){switch(e){case"compositionend":return PA(t);case"keypress":return t.which!==32?null:(P1=!0,E1);case"textInput":return e=t.data,e===E1&&P1?null:e;default:return null}}function zI(e,t){if(vs)return e==="compositionend"||!nb&&EA(e,t)?(e=AA(),If=eb=Si=null,vs=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=R1(r)}}function RA(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?RA(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function IA(){for(var e=window,t=Xf();t instanceof e.HTMLIFrameElement;){try{var r=typeof t.contentWindow.location.href=="string"}catch{r=!1}if(r)e=t.contentWindow;else break;t=Xf(e.document)}return t}function ab(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function XI(e){var t=IA(),r=e.focusedElem,n=e.selectionRange;if(t!==r&&r&&r.ownerDocument&&RA(r.ownerDocument.documentElement,r)){if(n!==null&&ab(r)){if(t=n.start,e=n.end,e===void 0&&(e=t),"selectionStart"in r)r.selectionStart=t,r.selectionEnd=Math.min(e,r.value.length);else if(e=(t=r.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var a=r.textContent.length,i=Math.min(n.start,a);n=n.end===void 0?i:Math.min(n.end,a),!e.extend&&i>n&&(a=n,n=i,i=a),a=I1(r,i);var o=I1(r,n);a&&o&&(e.rangeCount!==1||e.anchorNode!==a.node||e.anchorOffset!==a.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(a.node,a.offset),e.removeAllRanges(),i>n?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=r;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;r=document.documentMode,ys=null,ig=null,Bu=null,og=!1;function M1(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;og||ys==null||ys!==Xf(n)||(n=ys,"selectionStart"in n&&ab(n)?n={start:n.selectionStart,end:n.selectionEnd}:(n=(n.ownerDocument&&n.ownerDocument.defaultView||window).getSelection(),n={anchorNode:n.anchorNode,anchorOffset:n.anchorOffset,focusNode:n.focusNode,focusOffset:n.focusOffset}),Bu&&fc(Bu,n)||(Bu=n,n=rp(ig,"onSelect"),0bs||(e.current=fg[bs],fg[bs]=null,bs--)}function wt(e,t){bs++,fg[bs]=e.current,e.current=t}var Bi={},Tr=qi(Bi),Jr=qi(!1),Co=Bi;function Ks(e,t){var r=e.type.contextTypes;if(!r)return Bi;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var a={},i;for(i in r)a[i]=t[i];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function en(e){return e=e.childContextTypes,e!=null}function ap(){Pt(Jr),Pt(Tr)}function V1(e,t,r){if(Tr.current!==Bi)throw Error(ie(168));wt(Tr,t),wt(Jr,r)}function WA(e,t,r){var n=e.stateNode;if(t=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var a in n)if(!(a in t))throw Error(ie(108,WR(e)||"Unknown",a));return It({},r,n)}function ip(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Bi,Co=Tr.current,wt(Tr,e),wt(Jr,Jr.current),!0}function W1(e,t,r){var n=e.stateNode;if(!n)throw Error(ie(169));r?(e=WA(e,t,Co),n.__reactInternalMemoizedMergedChildContext=e,Pt(Jr),Pt(Tr),wt(Tr,e)):Pt(Jr),wt(Jr,r)}var Da=null,Vh=!1,vv=!1;function HA(e){Da===null?Da=[e]:Da.push(e)}function sM(e){Vh=!0,HA(e)}function Ki(){if(!vv&&Da!==null){vv=!0;var e=0,t=ft;try{var r=Da;for(ft=1;e>=o,a-=o,Fa=1<<32-ea(t)+a|r<O?(N=k,k=null):N=k.sibling;var T=p(g,k,w[O],S);if(T===null){k===null&&(k=N);break}e&&k&&T.alternate===null&&t(g,k),m=i(T,m,O),_===null?b=T:_.sibling=T,_=T,k=N}if(O===w.length)return r(g,k),Ct&&lo(g,O),b;if(k===null){for(;OO?(N=k,k=null):N=k.sibling;var R=p(g,k,T.value,S);if(R===null){k===null&&(k=N);break}e&&k&&R.alternate===null&&t(g,k),m=i(R,m,O),_===null?b=R:_.sibling=R,_=R,k=N}if(T.done)return r(g,k),Ct&&lo(g,O),b;if(k===null){for(;!T.done;O++,T=w.next())T=d(g,T.value,S),T!==null&&(m=i(T,m,O),_===null?b=T:_.sibling=T,_=T);return Ct&&lo(g,O),b}for(k=n(g,k);!T.done;O++,T=w.next())T=h(k,g,O,T.value,S),T!==null&&(e&&T.alternate!==null&&k.delete(T.key===null?O:T.key),m=i(T,m,O),_===null?b=T:_.sibling=T,_=T);return e&&k.forEach(function(A){return t(g,A)}),Ct&&lo(g,O),b}function y(g,m,w,S){if(typeof w=="object"&&w!==null&&w.type===ms&&w.key===null&&(w=w.props.children),typeof w=="object"&&w!==null){switch(w.$$typeof){case Dd:e:{for(var b=w.key,_=m;_!==null;){if(_.key===b){if(b=w.type,b===ms){if(_.tag===7){r(g,_.sibling),m=a(_,w.props.children),m.return=g,g=m;break e}}else if(_.elementType===b||typeof b=="object"&&b!==null&&b.$$typeof===hi&&q1(b)===_.type){r(g,_.sibling),m=a(_,w.props),m.ref=au(g,_,w),m.return=g,g=m;break e}r(g,_);break}else t(g,_);_=_.sibling}w.type===ms?(m=Oo(w.props.children,g.mode,S,w.key),m.return=g,g=m):(S=Vf(w.type,w.key,w.props,null,g.mode,S),S.ref=au(g,m,w),S.return=g,g=S)}return o(g);case hs:e:{for(_=w.key;m!==null;){if(m.key===_)if(m.tag===4&&m.stateNode.containerInfo===w.containerInfo&&m.stateNode.implementation===w.implementation){r(g,m.sibling),m=a(m,w.children||[]),m.return=g,g=m;break e}else{r(g,m);break}else t(g,m);m=m.sibling}m=jv(w,g.mode,S),m.return=g,g=m}return o(g);case hi:return _=w._init,y(g,m,_(w._payload),S)}if(ju(w))return x(g,m,w,S);if(Jl(w))return v(g,m,w,S);Kd(g,w)}return typeof w=="string"&&w!==""||typeof w=="number"?(w=""+w,m!==null&&m.tag===6?(r(g,m.sibling),m=a(m,w),m.return=g,g=m):(r(g,m),m=Sv(w,g.mode,S),m.return=g,g=m),o(g)):r(g,m)}return y}var Ys=XA(!0),YA=XA(!1),lp=qi(null),up=null,Ss=null,lb=null;function ub(){lb=Ss=up=null}function cb(e){var t=lp.current;Pt(lp),e._currentValue=t}function mg(e,t,r){for(;e!==null;){var n=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,n!==null&&(n.childLanes|=t)):n!==null&&(n.childLanes&t)!==t&&(n.childLanes|=t),e===r)break;e=e.return}}function Ls(e,t){up=e,lb=Ss=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Yr=!0),e.firstContext=null)}function Mn(e){var t=e._currentValue;if(lb!==e)if(e={context:e,memoizedValue:t,next:null},Ss===null){if(up===null)throw Error(ie(308));Ss=e,up.dependencies={lanes:0,firstContext:e}}else Ss=Ss.next=e;return t}var yo=null;function db(e){yo===null?yo=[e]:yo.push(e)}function ZA(e,t,r,n){var a=t.interleaved;return a===null?(r.next=r,db(t)):(r.next=a.next,a.next=r),t.interleaved=r,Za(e,n)}function Za(e,t){e.lanes|=t;var r=e.alternate;for(r!==null&&(r.lanes|=t),r=e,e=e.return;e!==null;)e.childLanes|=t,r=e.alternate,r!==null&&(r.childLanes|=t),r=e,e=e.return;return r.tag===3?r.stateNode:null}var mi=!1;function fb(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function QA(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ga(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function $i(e,t,r){var n=e.updateQueue;if(n===null)return null;if(n=n.shared,at&2){var a=n.pending;return a===null?t.next=t:(t.next=a.next,a.next=t),n.pending=t,Za(e,r)}return a=n.interleaved,a===null?(t.next=t,db(n)):(t.next=a.next,a.next=t),n.interleaved=t,Za(e,r)}function Df(e,t,r){if(t=t.updateQueue,t!==null&&(t=t.shared,(r&4194240)!==0)){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,Z0(e,r)}}function K1(e,t){var r=e.updateQueue,n=e.alternate;if(n!==null&&(n=n.updateQueue,r===n)){var a=null,i=null;if(r=r.firstBaseUpdate,r!==null){do{var o={eventTime:r.eventTime,lane:r.lane,tag:r.tag,payload:r.payload,callback:r.callback,next:null};i===null?a=i=o:i=i.next=o,r=r.next}while(r!==null);i===null?a=i=t:i=i.next=t}else a=i=t;r={baseState:n.baseState,firstBaseUpdate:a,lastBaseUpdate:i,shared:n.shared,effects:n.effects},e.updateQueue=r;return}e=r.lastBaseUpdate,e===null?r.firstBaseUpdate=t:e.next=t,r.lastBaseUpdate=t}function cp(e,t,r,n){var a=e.updateQueue;mi=!1;var i=a.firstBaseUpdate,o=a.lastBaseUpdate,s=a.shared.pending;if(s!==null){a.shared.pending=null;var l=s,c=l.next;l.next=null,o===null?i=c:o.next=c,o=l;var f=e.alternate;f!==null&&(f=f.updateQueue,s=f.lastBaseUpdate,s!==o&&(s===null?f.firstBaseUpdate=c:s.next=c,f.lastBaseUpdate=l))}if(i!==null){var d=a.baseState;o=0,f=c=l=null,s=i;do{var p=s.lane,h=s.eventTime;if((n&p)===p){f!==null&&(f=f.next={eventTime:h,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});e:{var x=e,v=s;switch(p=t,h=r,v.tag){case 1:if(x=v.payload,typeof x=="function"){d=x.call(h,d,p);break e}d=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=v.payload,p=typeof x=="function"?x.call(h,d,p):x,p==null)break e;d=It({},d,p);break e;case 2:mi=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,p=a.effects,p===null?a.effects=[s]:p.push(s))}else h={eventTime:h,lane:p,tag:s.tag,payload:s.payload,callback:s.callback,next:null},f===null?(c=f=h,l=d):f=f.next=h,o|=p;if(s=s.next,s===null){if(s=a.shared.pending,s===null)break;p=s,s=p.next,p.next=null,a.lastBaseUpdate=p,a.shared.pending=null}}while(!0);if(f===null&&(l=d),a.baseState=l,a.firstBaseUpdate=c,a.lastBaseUpdate=f,t=a.shared.interleaved,t!==null){a=t;do o|=a.lane,a=a.next;while(a!==t)}else i===null&&(a.shared.lanes=0);Ro|=o,e.lanes=o,e.memoizedState=d}}function X1(e,t,r){if(e=t.effects,t.effects=null,e!==null)for(t=0;tr?r:4,e(!0);var n=gv.transition;gv.transition={};try{e(!1),t()}finally{ft=r,gv.transition=n}}function mN(){return Dn().memoizedState}function dM(e,t,r){var n=Ii(e);if(r={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null},vN(e))yN(t,r);else if(r=ZA(e,t,r,n),r!==null){var a=Br();ta(r,e,n,a),gN(r,t,n)}}function fM(e,t,r){var n=Ii(e),a={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null};if(vN(e))yN(t,a);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var o=t.lastRenderedState,s=i(o,r);if(a.hasEagerState=!0,a.eagerState=s,na(s,o)){var l=t.interleaved;l===null?(a.next=a,db(t)):(a.next=l.next,l.next=a),t.interleaved=a;return}}catch{}finally{}r=ZA(e,t,a,n),r!==null&&(a=Br(),ta(r,e,n,a),gN(r,t,n))}}function vN(e){var t=e.alternate;return e===Rt||t!==null&&t===Rt}function yN(e,t){Uu=fp=!0;var r=e.pending;r===null?t.next=t:(t.next=r.next,r.next=t),e.pending=t}function gN(e,t,r){if(r&4194240){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,Z0(e,r)}}var pp={readContext:Mn,useCallback:kr,useContext:kr,useEffect:kr,useImperativeHandle:kr,useInsertionEffect:kr,useLayoutEffect:kr,useMemo:kr,useReducer:kr,useRef:kr,useState:kr,useDebugValue:kr,useDeferredValue:kr,useTransition:kr,useMutableSource:kr,useSyncExternalStore:kr,useId:kr,unstable_isNewReconciler:!1},pM={readContext:Mn,useCallback:function(e,t){return ua().memoizedState=[e,t===void 0?null:t],e},useContext:Mn,useEffect:Z1,useImperativeHandle:function(e,t,r){return r=r!=null?r.concat([e]):null,Ff(4194308,4,cN.bind(null,t,e),r)},useLayoutEffect:function(e,t){return Ff(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ff(4,2,e,t)},useMemo:function(e,t){var r=ua();return t=t===void 0?null:t,e=e(),r.memoizedState=[e,t],e},useReducer:function(e,t,r){var n=ua();return t=r!==void 0?r(t):t,n.memoizedState=n.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},n.queue=e,e=e.dispatch=dM.bind(null,Rt,e),[n.memoizedState,e]},useRef:function(e){var t=ua();return e={current:e},t.memoizedState=e},useState:Y1,useDebugValue:bb,useDeferredValue:function(e){return ua().memoizedState=e},useTransition:function(){var e=Y1(!1),t=e[0];return e=cM.bind(null,e[1]),ua().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,r){var n=Rt,a=ua();if(Ct){if(r===void 0)throw Error(ie(407));r=r()}else{if(r=t(),vr===null)throw Error(ie(349));$o&30||rN(n,t,r)}a.memoizedState=r;var i={value:r,getSnapshot:t};return a.queue=i,Z1(aN.bind(null,n,i,e),[e]),n.flags|=2048,bc(9,nN.bind(null,n,i,r,t),void 0,null),r},useId:function(){var e=ua(),t=vr.identifierPrefix;if(Ct){var r=za,n=Fa;r=(n&~(1<<32-ea(n)-1)).toString(32)+r,t=":"+t+"R"+r,r=gc++,0<\/script>",e=e.removeChild(e.firstChild)):typeof n.is=="string"?e=o.createElement(r,{is:n.is}):(e=o.createElement(r),r==="select"&&(o=e,n.multiple?o.multiple=!0:n.size&&(o.size=n.size))):e=o.createElementNS(e,r),e[da]=t,e[mc]=n,NN(e,t,!1,!1),t.stateNode=e;e:{switch(o=Zy(r,n),r){case"dialog":kt("cancel",e),kt("close",e),a=n;break;case"iframe":case"object":case"embed":kt("load",e),a=n;break;case"video":case"audio":for(a=0;aJs&&(t.flags|=128,n=!0,iu(i,!1),t.lanes=4194304)}else{if(!n)if(e=dp(o),e!==null){if(t.flags|=128,n=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),iu(i,!0),i.tail===null&&i.tailMode==="hidden"&&!o.alternate&&!Ct)return Or(t),null}else 2*Wt()-i.renderingStartTime>Js&&r!==1073741824&&(t.flags|=128,n=!0,iu(i,!1),t.lanes=4194304);i.isBackwards?(o.sibling=t.child,t.child=o):(r=i.last,r!==null?r.sibling=o:t.child=o,i.last=o)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Wt(),t.sibling=null,r=$t.current,wt($t,n?r&1|2:r&1),t):(Or(t),null);case 22:case 23:return Ob(),n=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==n&&(t.flags|=8192),n&&t.mode&1?un&1073741824&&(Or(t),t.subtreeFlags&6&&(t.flags|=8192)):Or(t),null;case 24:return null;case 25:return null}throw Error(ie(156,t.tag))}function wM(e,t){switch(ob(t),t.tag){case 1:return en(t.type)&&ap(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zs(),Pt(Jr),Pt(Tr),mb(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return hb(t),null;case 13:if(Pt($t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(ie(340));Xs()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Pt($t),null;case 4:return Zs(),null;case 10:return cb(t.type._context),null;case 22:case 23:return Ob(),null;case 24:return null;default:return null}}var Yd=!1,Er=!1,_M=typeof WeakSet=="function"?WeakSet:Set,ye=null;function js(e,t){var r=e.ref;if(r!==null)if(typeof r=="function")try{r(null)}catch(n){Ft(e,t,n)}else r.current=null}function jg(e,t,r){try{r()}catch(n){Ft(e,t,n)}}var l_=!1;function SM(e,t){if(sg=ep,e=IA(),ab(e)){if("selectionStart"in e)var r={start:e.selectionStart,end:e.selectionEnd};else e:{r=(r=e.ownerDocument)&&r.defaultView||window;var n=r.getSelection&&r.getSelection();if(n&&n.rangeCount!==0){r=n.anchorNode;var a=n.anchorOffset,i=n.focusNode;n=n.focusOffset;try{r.nodeType,i.nodeType}catch{r=null;break e}var o=0,s=-1,l=-1,c=0,f=0,d=e,p=null;t:for(;;){for(var h;d!==r||a!==0&&d.nodeType!==3||(s=o+a),d!==i||n!==0&&d.nodeType!==3||(l=o+n),d.nodeType===3&&(o+=d.nodeValue.length),(h=d.firstChild)!==null;)p=d,d=h;for(;;){if(d===e)break t;if(p===r&&++c===a&&(s=o),p===i&&++f===n&&(l=o),(h=d.nextSibling)!==null)break;d=p,p=d.parentNode}d=h}r=s===-1||l===-1?null:{start:s,end:l}}else r=null}r=r||{start:0,end:0}}else r=null;for(lg={focusedElem:e,selectionRange:r},ep=!1,ye=t;ye!==null;)if(t=ye,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,ye=e;else for(;ye!==null;){t=ye;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var v=x.memoizedProps,y=x.memoizedState,g=t.stateNode,m=g.getSnapshotBeforeUpdate(t.elementType===t.type?v:Gn(t.type,v),y);g.__reactInternalSnapshotBeforeUpdate=m}break;case 3:var w=t.stateNode.containerInfo;w.nodeType===1?w.textContent="":w.nodeType===9&&w.documentElement&&w.removeChild(w.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(ie(163))}}catch(S){Ft(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,ye=e;break}ye=t.return}return x=l_,l_=!1,x}function Vu(e,t,r){var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var a=n=n.next;do{if((a.tag&e)===e){var i=a.destroy;a.destroy=void 0,i!==void 0&&jg(t,r,i)}a=a.next}while(a!==n)}}function Gh(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var r=t=t.next;do{if((r.tag&e)===e){var n=r.create;r.destroy=n()}r=r.next}while(r!==t)}}function kg(e){var t=e.ref;if(t!==null){var r=e.stateNode;switch(e.tag){case 5:e=r;break;default:e=r}typeof t=="function"?t(e):t.current=e}}function CN(e){var t=e.alternate;t!==null&&(e.alternate=null,CN(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[da],delete t[mc],delete t[dg],delete t[iM],delete t[oM])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function TN(e){return e.tag===5||e.tag===3||e.tag===4}function u_(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||TN(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Og(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.nodeType===8?r.parentNode.insertBefore(e,t):r.insertBefore(e,t):(r.nodeType===8?(t=r.parentNode,t.insertBefore(e,r)):(t=r,t.appendChild(e)),r=r._reactRootContainer,r!=null||t.onclick!==null||(t.onclick=np));else if(n!==4&&(e=e.child,e!==null))for(Og(e,t,r),e=e.sibling;e!==null;)Og(e,t,r),e=e.sibling}function Ag(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(Ag(e,t,r),e=e.sibling;e!==null;)Ag(e,t,r),e=e.sibling}var br=null,qn=!1;function di(e,t,r){for(r=r.child;r!==null;)$N(e,t,r),r=r.sibling}function $N(e,t,r){if(ma&&typeof ma.onCommitFiberUnmount=="function")try{ma.onCommitFiberUnmount(Lh,r)}catch{}switch(r.tag){case 5:Er||js(r,t);case 6:var n=br,a=qn;br=null,di(e,t,r),br=n,qn=a,br!==null&&(qn?(e=br,r=r.stateNode,e.nodeType===8?e.parentNode.removeChild(r):e.removeChild(r)):br.removeChild(r.stateNode));break;case 18:br!==null&&(qn?(e=br,r=r.stateNode,e.nodeType===8?mv(e.parentNode,r):e.nodeType===1&&mv(e,r),cc(e)):mv(br,r.stateNode));break;case 4:n=br,a=qn,br=r.stateNode.containerInfo,qn=!0,di(e,t,r),br=n,qn=a;break;case 0:case 11:case 14:case 15:if(!Er&&(n=r.updateQueue,n!==null&&(n=n.lastEffect,n!==null))){a=n=n.next;do{var i=a,o=i.destroy;i=i.tag,o!==void 0&&(i&2||i&4)&&jg(r,t,o),a=a.next}while(a!==n)}di(e,t,r);break;case 1:if(!Er&&(js(r,t),n=r.stateNode,typeof n.componentWillUnmount=="function"))try{n.props=r.memoizedProps,n.state=r.memoizedState,n.componentWillUnmount()}catch(s){Ft(r,t,s)}di(e,t,r);break;case 21:di(e,t,r);break;case 22:r.mode&1?(Er=(n=Er)||r.memoizedState!==null,di(e,t,r),Er=n):di(e,t,r);break;default:di(e,t,r)}}function c_(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var r=e.stateNode;r===null&&(r=e.stateNode=new _M),t.forEach(function(n){var a=TM.bind(null,e,n);r.has(n)||(r.add(n),n.then(a,a))})}}function Vn(e,t){var r=t.deletions;if(r!==null)for(var n=0;na&&(a=o),n&=~i}if(n=a,n=Wt()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*kM(n/1960))-n,10e?16:e,ji===null)var n=!1;else{if(e=ji,ji=null,vp=0,at&6)throw Error(ie(331));var a=at;for(at|=4,ye=e.current;ye!==null;){var i=ye,o=i.child;if(ye.flags&16){var s=i.deletions;if(s!==null){for(var l=0;lWt()-jb?ko(e,0):Sb|=r),tn(e,t)}function BN(e,t){t===0&&(e.mode&1?(t=Bd,Bd<<=1,!(Bd&130023424)&&(Bd=4194304)):t=1);var r=Br();e=Za(e,t),e!==null&&(fd(e,t,r),tn(e,r))}function CM(e){var t=e.memoizedState,r=0;t!==null&&(r=t.retryLane),BN(e,r)}function TM(e,t){var r=0;switch(e.tag){case 13:var n=e.stateNode,a=e.memoizedState;a!==null&&(r=a.retryLane);break;case 19:n=e.stateNode;break;default:throw Error(ie(314))}n!==null&&n.delete(t),BN(e,r)}var UN;UN=function(e,t,r){if(e!==null)if(e.memoizedProps!==t.pendingProps||Jr.current)Yr=!0;else{if(!(e.lanes&r)&&!(t.flags&128))return Yr=!1,xM(e,t,r);Yr=!!(e.flags&131072)}else Yr=!1,Ct&&t.flags&1048576&&GA(t,sp,t.index);switch(t.lanes=0,t.tag){case 2:var n=t.type;zf(e,t),e=t.pendingProps;var a=Ks(t,Tr.current);Ls(t,r),a=yb(null,t,n,e,a,r);var i=gb();return t.flags|=1,typeof a=="object"&&a!==null&&typeof a.render=="function"&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,en(n)?(i=!0,ip(t)):i=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,fb(t),a.updater=Hh,t.stateNode=a,a._reactInternals=t,yg(t,n,e,r),t=bg(null,t,n,!0,i,r)):(t.tag=0,Ct&&i&&ib(t),Mr(null,t,a,r),t=t.child),t;case 16:n=t.elementType;e:{switch(zf(e,t),e=t.pendingProps,a=n._init,n=a(n._payload),t.type=n,a=t.tag=RM(n),e=Gn(n,e),a){case 0:t=xg(null,t,n,e,r);break e;case 1:t=i_(null,t,n,e,r);break e;case 11:t=n_(null,t,n,e,r);break e;case 14:t=a_(null,t,n,Gn(n.type,e),r);break e}throw Error(ie(306,n,""))}return t;case 0:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Gn(n,a),xg(e,t,n,a,r);case 1:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Gn(n,a),i_(e,t,n,a,r);case 3:e:{if(kN(t),e===null)throw Error(ie(387));n=t.pendingProps,i=t.memoizedState,a=i.element,QA(e,t),cp(t,n,null,r);var o=t.memoizedState;if(n=o.element,i.isDehydrated)if(i={element:n,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){a=Qs(Error(ie(423)),t),t=o_(e,t,n,r,a);break e}else if(n!==a){a=Qs(Error(ie(424)),t),t=o_(e,t,n,r,a);break e}else for(fn=Ti(t.stateNode.containerInfo.firstChild),mn=t,Ct=!0,Yn=null,r=YA(t,null,n,r),t.child=r;r;)r.flags=r.flags&-3|4096,r=r.sibling;else{if(Xs(),n===a){t=Qa(e,t,r);break e}Mr(e,t,n,r)}t=t.child}return t;case 5:return JA(t),e===null&&hg(t),n=t.type,a=t.pendingProps,i=e!==null?e.memoizedProps:null,o=a.children,ug(n,a)?o=null:i!==null&&ug(n,i)&&(t.flags|=32),jN(e,t),Mr(e,t,o,r),t.child;case 6:return e===null&&hg(t),null;case 13:return ON(e,t,r);case 4:return pb(t,t.stateNode.containerInfo),n=t.pendingProps,e===null?t.child=Ys(t,null,n,r):Mr(e,t,n,r),t.child;case 11:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Gn(n,a),n_(e,t,n,a,r);case 7:return Mr(e,t,t.pendingProps,r),t.child;case 8:return Mr(e,t,t.pendingProps.children,r),t.child;case 12:return Mr(e,t,t.pendingProps.children,r),t.child;case 10:e:{if(n=t.type._context,a=t.pendingProps,i=t.memoizedProps,o=a.value,wt(lp,n._currentValue),n._currentValue=o,i!==null)if(na(i.value,o)){if(i.children===a.children&&!Jr.current){t=Qa(e,t,r);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var s=i.dependencies;if(s!==null){o=i.child;for(var l=s.firstContext;l!==null;){if(l.context===n){if(i.tag===1){l=Ga(-1,r&-r),l.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var f=c.pending;f===null?l.next=l:(l.next=f.next,f.next=l),c.pending=l}}i.lanes|=r,l=i.alternate,l!==null&&(l.lanes|=r),mg(i.return,r,t),s.lanes|=r;break}l=l.next}}else if(i.tag===10)o=i.type===t.type?null:i.child;else if(i.tag===18){if(o=i.return,o===null)throw Error(ie(341));o.lanes|=r,s=o.alternate,s!==null&&(s.lanes|=r),mg(o,r,t),o=i.sibling}else o=i.child;if(o!==null)o.return=i;else for(o=i;o!==null;){if(o===t){o=null;break}if(i=o.sibling,i!==null){i.return=o.return,o=i;break}o=o.return}i=o}Mr(e,t,a.children,r),t=t.child}return t;case 9:return a=t.type,n=t.pendingProps.children,Ls(t,r),a=Mn(a),n=n(a),t.flags|=1,Mr(e,t,n,r),t.child;case 14:return n=t.type,a=Gn(n,t.pendingProps),a=Gn(n.type,a),a_(e,t,n,a,r);case 15:return _N(e,t,t.type,t.pendingProps,r);case 17:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Gn(n,a),zf(e,t),t.tag=1,en(n)?(e=!0,ip(t)):e=!1,Ls(t,r),xN(t,n,a),yg(t,n,a,r),bg(null,t,n,!0,e,r);case 19:return AN(e,t,r);case 22:return SN(e,t,r)}throw Error(ie(156,t.tag))};function VN(e,t){return vA(e,t)}function $M(e,t,r,n){this.tag=e,this.key=r,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=n,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function $n(e,t,r,n){return new $M(e,t,r,n)}function Nb(e){return e=e.prototype,!(!e||!e.isReactComponent)}function RM(e){if(typeof e=="function")return Nb(e)?1:0;if(e!=null){if(e=e.$$typeof,e===q0)return 11;if(e===K0)return 14}return 2}function Mi(e,t){var r=e.alternate;return r===null?(r=$n(e.tag,t,e.key,e.mode),r.elementType=e.elementType,r.type=e.type,r.stateNode=e.stateNode,r.alternate=e,e.alternate=r):(r.pendingProps=t,r.type=e.type,r.flags=0,r.subtreeFlags=0,r.deletions=null),r.flags=e.flags&14680064,r.childLanes=e.childLanes,r.lanes=e.lanes,r.child=e.child,r.memoizedProps=e.memoizedProps,r.memoizedState=e.memoizedState,r.updateQueue=e.updateQueue,t=e.dependencies,r.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},r.sibling=e.sibling,r.index=e.index,r.ref=e.ref,r}function Vf(e,t,r,n,a,i){var o=2;if(n=e,typeof e=="function")Nb(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case ms:return Oo(r.children,a,i,t);case G0:o=8,a|=8;break;case By:return e=$n(12,r,t,a|2),e.elementType=By,e.lanes=i,e;case Uy:return e=$n(13,r,t,a),e.elementType=Uy,e.lanes=i,e;case Vy:return e=$n(19,r,t,a),e.elementType=Vy,e.lanes=i,e;case JO:return Kh(r,a,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ZO:o=10;break e;case QO:o=9;break e;case q0:o=11;break e;case K0:o=14;break e;case hi:o=16,n=null;break e}throw Error(ie(130,e==null?e:typeof e,""))}return t=$n(o,r,t,a),t.elementType=e,t.type=n,t.lanes=i,t}function Oo(e,t,r,n){return e=$n(7,e,n,t),e.lanes=r,e}function Kh(e,t,r,n){return e=$n(22,e,n,t),e.elementType=JO,e.lanes=r,e.stateNode={isHidden:!1},e}function Sv(e,t,r){return e=$n(6,e,null,t),e.lanes=r,e}function jv(e,t,r){return t=$n(4,e.children!==null?e.children:[],e.key,t),t.lanes=r,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function IM(e,t,r,n,a){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=av(0),this.expirationTimes=av(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=av(0),this.identifierPrefix=n,this.onRecoverableError=a,this.mutableSourceEagerHydrationData=null}function Eb(e,t,r,n,a,i,o,s,l){return e=new IM(e,t,r,s,l),t===1?(t=1,i===!0&&(t|=8)):t=0,i=$n(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:n,isDehydrated:r,cache:null,transitions:null,pendingSuspenseBoundaries:null},fb(i),e}function MM(e,t,r){var n=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(qN)}catch(e){console.error(e)}}qN(),qO.exports=bn;var Os=qO.exports,g_=Os;Fy.createRoot=g_.createRoot,Fy.hydrateRoot=g_.hydrateRoot;/** + * @remix-run/router v1.23.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function _c(){return _c=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function $b(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function UM(){return Math.random().toString(36).substr(2,8)}function b_(e,t){return{usr:e.state,key:e.key,idx:t}}function Tg(e,t,r,n){return r===void 0&&(r=null),_c({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?Rl(t):t,{state:r,key:t&&t.key||n||UM()})}function KN(e){let{pathname:t="/",search:r="",hash:n=""}=e;return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function Rl(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substr(r),e=e.substr(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substr(n),e=e.substr(0,n)),e&&(t.pathname=e)}return t}function VM(e,t,r,n){n===void 0&&(n={});let{window:a=document.defaultView,v5Compat:i=!1}=n,o=a.history,s=ki.Pop,l=null,c=f();c==null&&(c=0,o.replaceState(_c({},o.state,{idx:c}),""));function f(){return(o.state||{idx:null}).idx}function d(){s=ki.Pop;let y=f(),g=y==null?null:y-c;c=y,l&&l({action:s,location:v.location,delta:g})}function p(y,g){s=ki.Push;let m=Tg(v.location,y,g);c=f()+1;let w=b_(m,c),S=v.createHref(m);try{o.pushState(w,"",S)}catch(b){if(b instanceof DOMException&&b.name==="DataCloneError")throw b;a.location.assign(S)}i&&l&&l({action:s,location:v.location,delta:1})}function h(y,g){s=ki.Replace;let m=Tg(v.location,y,g);c=f();let w=b_(m,c),S=v.createHref(m);o.replaceState(w,"",S),i&&l&&l({action:s,location:v.location,delta:0})}function x(y){let g=a.location.origin!=="null"?a.location.origin:a.location.href,m=typeof y=="string"?y:KN(y);return m=m.replace(/ $/,"%20"),Zt(g,"No window.location.(origin|href) available to create URL for href: "+m),new URL(m,g)}let v={get action(){return s},get location(){return e(a,o)},listen(y){if(l)throw new Error("A history only accepts one active listener");return a.addEventListener(x_,d),l=y,()=>{a.removeEventListener(x_,d),l=null}},createHref(y){return t(a,y)},createURL:x,encodeLocation(y){let g=x(y);return{pathname:g.pathname,search:g.search,hash:g.hash}},push:p,replace:h,go(y){return o.go(y)}};return v}var w_;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(w_||(w_={}));function WM(e,t,r){return r===void 0&&(r="/"),HM(e,t,r)}function HM(e,t,r,n){let a=typeof t=="string"?Rl(t):t,i=ZN(a.pathname||"/",r);if(i==null)return null;let o=XN(e);GM(o);let s=null;for(let l=0;s==null&&l{let l={relativePath:s===void 0?i.path||"":s,caseSensitive:i.caseSensitive===!0,childrenIndex:o,route:i};l.relativePath.startsWith("/")&&(Zt(l.relativePath.startsWith(n),'Absolute route path "'+l.relativePath+'" nested under path '+('"'+n+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),l.relativePath=l.relativePath.slice(n.length));let c=Ao([n,l.relativePath]),f=r.concat(l);i.children&&i.children.length>0&&(Zt(i.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+c+'".')),XN(i.children,t,f,c)),!(i.path==null&&!i.index)&&t.push({path:c,score:JM(c,i.index),routesMeta:f})};return e.forEach((i,o)=>{var s;if(i.path===""||!((s=i.path)!=null&&s.includes("?")))a(i,o);else for(let l of YN(i.path))a(i,o,l)}),t}function YN(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,a=r.endsWith("?"),i=r.replace(/\?$/,"");if(n.length===0)return a?[i,""]:[i];let o=YN(n.join("/")),s=[];return s.push(...o.map(l=>l===""?i:[i,l].join("/"))),a&&s.push(...o),s.map(l=>e.startsWith("/")&&l===""?"/":l)}function GM(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:eD(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}const qM=/^:[\w-]+$/,KM=3,XM=2,YM=1,ZM=10,QM=-2,__=e=>e==="*";function JM(e,t){let r=e.split("/"),n=r.length;return r.some(__)&&(n+=QM),t&&(n+=XM),r.filter(a=>!__(a)).reduce((a,i)=>a+(qM.test(i)?KM:i===""?YM:ZM),n)}function eD(e,t){return e.length===t.length&&e.slice(0,-1).every((n,a)=>n===t[a])?e[e.length-1]-t[t.length-1]:0}function tD(e,t,r){let{routesMeta:n}=e,a={},i="/",o=[];for(let s=0;s{let{paramName:p,isOptional:h}=f;if(p==="*"){let v=s[d]||"";o=i.slice(0,i.length-v.length).replace(/(.)\/+$/,"$1")}const x=s[d];return h&&!x?c[p]=void 0:c[p]=(x||"").replace(/%2F/g,"/"),c},{}),pathname:i,pathnameBase:o,pattern:e}}function nD(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!0),$b(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let n=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,s,l)=>(n.push({paramName:s,isOptional:l!=null}),l?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(n.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,t?void 0:"i"),n]}function aD(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return $b(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function ZN(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}const iD=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,oD=e=>iD.test(e);function sD(e,t){t===void 0&&(t="/");let{pathname:r,search:n="",hash:a=""}=typeof e=="string"?Rl(e):e,i;if(r)if(oD(r))i=r;else{if(r.includes("//")){let o=r;r=r.replace(/\/\/+/g,"/"),$b(!1,"Pathnames cannot have embedded double slashes - normalizing "+(o+" -> "+r))}r.startsWith("/")?i=S_(r.substring(1),"/"):i=S_(r,t)}else i=t;return{pathname:i,search:cD(n),hash:dD(a)}}function S_(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(a=>{a===".."?r.length>1&&r.pop():a!=="."&&r.push(a)}),r.length>1?r.join("/"):"/"}function kv(e,t,r,n){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(n)+"]. Please separate it out to the ")+("`to."+r+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function lD(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function QN(e,t){let r=lD(e);return t?r.map((n,a)=>a===r.length-1?n.pathname:n.pathnameBase):r.map(n=>n.pathnameBase)}function JN(e,t,r,n){n===void 0&&(n=!1);let a;typeof e=="string"?a=Rl(e):(a=_c({},e),Zt(!a.pathname||!a.pathname.includes("?"),kv("?","pathname","search",a)),Zt(!a.pathname||!a.pathname.includes("#"),kv("#","pathname","hash",a)),Zt(!a.search||!a.search.includes("#"),kv("#","search","hash",a)));let i=e===""||a.pathname==="",o=i?"/":a.pathname,s;if(o==null)s=r;else{let d=t.length-1;if(!n&&o.startsWith("..")){let p=o.split("/");for(;p[0]==="..";)p.shift(),d-=1;a.pathname=p.join("/")}s=d>=0?t[d]:"/"}let l=sD(a,s),c=o&&o!=="/"&&o.endsWith("/"),f=(i||o===".")&&r.endsWith("/");return!l.pathname.endsWith("/")&&(c||f)&&(l.pathname+="/"),l}const Ao=e=>e.join("/").replace(/\/\/+/g,"/"),uD=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),cD=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,dD=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function fD(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const eE=["post","put","patch","delete"];new Set(eE);const pD=["get",...eE];new Set(pD);/** + * React Router v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Sc(){return Sc=Object.assign?Object.assign.bind():function(e){for(var t=1;t{s.current=!0}),j.useCallback(function(c,f){if(f===void 0&&(f={}),!s.current)return;if(typeof c=="number"){n.go(c);return}let d=JN(c,JSON.parse(o),i,f.relative==="path");e==null&&t!=="/"&&(d.pathname=d.pathname==="/"?t:Ao([t,d.pathname])),(f.replace?n.replace:n.push)(d,f.state,f)},[t,n,o,i,e])}const vD=j.createContext(null);function yD(e){let t=j.useContext(Xi).outlet;return t&&j.createElement(vD.Provider,{value:e},t)}function gD(e,t){return xD(e,t)}function xD(e,t,r,n){yd()||Zt(!1);let{navigator:a}=j.useContext(vd),{matches:i}=j.useContext(Xi),o=i[i.length-1],s=o?o.params:{};o&&o.pathname;let l=o?o.pathnameBase:"/";o&&o.route;let c=gd(),f;if(t){var d;let y=typeof t=="string"?Rl(t):t;l==="/"||(d=y.pathname)!=null&&d.startsWith(l)||Zt(!1),f=y}else f=c;let p=f.pathname||"/",h=p;if(l!=="/"){let y=l.replace(/^\//,"").split("/");h="/"+p.replace(/^\//,"").split("/").slice(y.length).join("/")}let x=WM(e,{pathname:h}),v=jD(x&&x.map(y=>Object.assign({},y,{params:Object.assign({},s,y.params),pathname:Ao([l,a.encodeLocation?a.encodeLocation(y.pathname).pathname:y.pathname]),pathnameBase:y.pathnameBase==="/"?l:Ao([l,a.encodeLocation?a.encodeLocation(y.pathnameBase).pathname:y.pathnameBase])})),i,r,n);return t&&v?j.createElement(Jh.Provider,{value:{location:Sc({pathname:"/",search:"",hash:"",state:null,key:"default"},f),navigationType:ki.Pop}},v):v}function bD(){let e=ND(),t=fD(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,a={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return j.createElement(j.Fragment,null,j.createElement("h2",null,"Unexpected Application Error!"),j.createElement("h3",{style:{fontStyle:"italic"}},t),r?j.createElement("pre",{style:a},r):null,null)}const wD=j.createElement(bD,null);class _D extends j.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){console.error("React Router caught the following error during render",t,r)}render(){return this.state.error!==void 0?j.createElement(Xi.Provider,{value:this.props.routeContext},j.createElement(tE.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function SD(e){let{routeContext:t,match:r,children:n}=e,a=j.useContext(Rb);return a&&a.static&&a.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(a.staticContext._deepestRenderedBoundaryId=r.route.id),j.createElement(Xi.Provider,{value:t},n)}function jD(e,t,r,n){var a;if(t===void 0&&(t=[]),r===void 0&&(r=null),n===void 0&&(n=null),e==null){var i;if(!r)return null;if(r.errors)e=r.matches;else if((i=n)!=null&&i.v7_partialHydration&&t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let o=e,s=(a=r)==null?void 0:a.errors;if(s!=null){let f=o.findIndex(d=>d.route.id&&(s==null?void 0:s[d.route.id])!==void 0);f>=0||Zt(!1),o=o.slice(0,Math.min(o.length,f+1))}let l=!1,c=-1;if(r&&n&&n.v7_partialHydration)for(let f=0;f=0?o=o.slice(0,c+1):o=[o[0]];break}}}return o.reduceRight((f,d,p)=>{let h,x=!1,v=null,y=null;r&&(h=s&&d.route.id?s[d.route.id]:void 0,v=d.route.errorElement||wD,l&&(c<0&&p===0?(PD("route-fallback"),x=!0,y=null):c===p&&(x=!0,y=d.route.hydrateFallbackElement||null)));let g=t.concat(o.slice(0,p+1)),m=()=>{let w;return h?w=v:x?w=y:d.route.Component?w=j.createElement(d.route.Component,null):d.route.element?w=d.route.element:w=f,j.createElement(SD,{match:d,routeContext:{outlet:f,matches:g,isDataRoute:r!=null},children:w})};return r&&(d.route.ErrorBoundary||d.route.errorElement||p===0)?j.createElement(_D,{location:r.location,revalidation:r.revalidation,component:v,error:h,children:m(),routeContext:{outlet:null,matches:g,isDataRoute:!0}}):m()},null)}var nE=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(nE||{}),aE=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(aE||{});function kD(e){let t=j.useContext(Rb);return t||Zt(!1),t}function OD(e){let t=j.useContext(hD);return t||Zt(!1),t}function AD(e){let t=j.useContext(Xi);return t||Zt(!1),t}function iE(e){let t=AD(),r=t.matches[t.matches.length-1];return r.route.id||Zt(!1),r.route.id}function ND(){var e;let t=j.useContext(tE),r=OD(),n=iE();return t!==void 0?t:(e=r.errors)==null?void 0:e[n]}function ED(){let{router:e}=kD(nE.UseNavigateStable),t=iE(aE.UseNavigateStable),r=j.useRef(!1);return rE(()=>{r.current=!0}),j.useCallback(function(a,i){i===void 0&&(i={}),r.current&&(typeof a=="number"?e.navigate(a):e.navigate(a,Sc({fromRouteId:t},i)))},[e,t])}const j_={};function PD(e,t,r){j_[e]||(j_[e]=!0)}function CD(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function TD(e){let{to:t,replace:r,state:n,relative:a}=e;yd()||Zt(!1);let{future:i,static:o}=j.useContext(vd),{matches:s}=j.useContext(Xi),{pathname:l}=gd(),c=xd(),f=JN(t,QN(s,i.v7_relativeSplatPath),l,a==="path"),d=JSON.stringify(f);return j.useEffect(()=>c(JSON.parse(d),{replace:r,state:n,relative:a}),[c,d,a,r,n]),null}function $D(e){return yD(e.context)}function On(e){Zt(!1)}function RD(e){let{basename:t="/",children:r=null,location:n,navigationType:a=ki.Pop,navigator:i,static:o=!1,future:s}=e;yd()&&Zt(!1);let l=t.replace(/^\/*/,"/"),c=j.useMemo(()=>({basename:l,navigator:i,static:o,future:Sc({v7_relativeSplatPath:!1},s)}),[l,s,i,o]);typeof n=="string"&&(n=Rl(n));let{pathname:f="/",search:d="",hash:p="",state:h=null,key:x="default"}=n,v=j.useMemo(()=>{let y=ZN(f,l);return y==null?null:{location:{pathname:y,search:d,hash:p,state:h,key:x},navigationType:a}},[l,f,d,p,h,x,a]);return v==null?null:j.createElement(vd.Provider,{value:c},j.createElement(Jh.Provider,{children:r,value:v}))}function ID(e){let{children:t,location:r}=e;return gD($g(t),r)}new Promise(()=>{});function $g(e,t){t===void 0&&(t=[]);let r=[];return j.Children.forEach(e,(n,a)=>{if(!j.isValidElement(n))return;let i=[...t,a];if(n.type===j.Fragment){r.push.apply(r,$g(n.props.children,i));return}n.type!==On&&Zt(!1),!n.props.index||!n.props.children||Zt(!1);let o={id:n.props.id||i.join("-"),caseSensitive:n.props.caseSensitive,element:n.props.element,Component:n.props.Component,index:n.props.index,path:n.props.path,loader:n.props.loader,action:n.props.action,errorElement:n.props.errorElement,ErrorBoundary:n.props.ErrorBoundary,hasErrorBoundary:n.props.ErrorBoundary!=null||n.props.errorElement!=null,shouldRevalidate:n.props.shouldRevalidate,handle:n.props.handle,lazy:n.props.lazy};n.props.children&&(o.children=$g(n.props.children,i)),r.push(o)}),r}/** + * React Router DOM v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Rg(e){return e===void 0&&(e=""),new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,r)=>{let n=e[r];return t.concat(Array.isArray(n)?n.map(a=>[r,a]):[[r,n]])},[]))}function MD(e,t){let r=Rg(e);return t&&t.forEach((n,a)=>{r.has(a)||t.getAll(a).forEach(i=>{r.append(a,i)})}),r}const DD="6";try{window.__reactRouterVersion=DD}catch{}const LD="startTransition",k_=PR[LD];function FD(e){let{basename:t,children:r,future:n,window:a}=e,i=j.useRef();i.current==null&&(i.current=BM({window:a,v5Compat:!0}));let o=i.current,[s,l]=j.useState({action:o.action,location:o.location}),{v7_startTransition:c}=n||{},f=j.useCallback(d=>{c&&k_?k_(()=>l(d)):l(d)},[l,c]);return j.useLayoutEffect(()=>o.listen(f),[o,f]),j.useEffect(()=>CD(n),[n]),j.createElement(RD,{basename:t,children:r,location:s.location,navigationType:s.action,navigator:o,future:n})}var O_;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(O_||(O_={}));var A_;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(A_||(A_={}));function zD(e){let t=j.useRef(Rg(e)),r=j.useRef(!1),n=gd(),a=j.useMemo(()=>MD(n.search,r.current?null:t.current),[n.search]),i=xd(),o=j.useCallback((s,l)=>{const c=Rg(typeof s=="function"?s(a):s);r.current=!0,i("?"+c,l)},[i,a]);return[a,o]}/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var BD={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const UD=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const lt=(e,t)=>{const r=j.forwardRef(({color:n="currentColor",size:a=24,strokeWidth:i=2,absoluteStrokeWidth:o,className:s="",children:l,...c},f)=>j.createElement("svg",{ref:f,...BD,width:a,height:a,stroke:n,strokeWidth:o?Number(i)*24/Number(a):i,className:["lucide",`lucide-${UD(e)}`,s].join(" "),...c},[...t.map(([d,p])=>j.createElement(d,p)),...Array.isArray(l)?l:[l]]));return r.displayName=`${e}`,r};/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zs=lt("Activity",[["path",{d:"M22 12h-4l-3 9L9 3l-3 9H2",key:"d5dnw9"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const oE=lt("ArrowRight",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xp=lt("BarChart3",[["path",{d:"M3 3v18h18",key:"1s2lah"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const VD=lt("BookOpen",[["path",{d:"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z",key:"vv98re"}],["path",{d:"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z",key:"1cyq3y"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const WD=lt("Building2",[["path",{d:"M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z",key:"1b4qmf"}],["path",{d:"M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2",key:"i71pzd"}],["path",{d:"M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2",key:"10jefs"}],["path",{d:"M10 6h4",key:"1itunk"}],["path",{d:"M10 10h4",key:"tcdvrf"}],["path",{d:"M10 14h4",key:"kelpxr"}],["path",{d:"M10 18h4",key:"1ulq68"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Au=lt("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Nu=lt("ChevronUp",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const HD=lt("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GD=lt("CircleCheck",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qD=lt("CircleX",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const N_=lt("Clock3",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16.5 12",key:"1aq6pp"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ov=lt("Code",[["polyline",{points:"16 18 22 12 16 6",key:"z7tu5w"}],["polyline",{points:"8 6 2 12 8 18",key:"1eg1df"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KD=lt("CreditCard",[["rect",{width:"20",height:"14",x:"2",y:"5",rx:"2",key:"ynyp8z"}],["line",{x1:"2",x2:"22",y1:"10",y2:"10",key:"1b3vmo"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const em=lt("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Gu=lt("GitBranch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const XD=lt("GripVertical",[["circle",{cx:"9",cy:"12",r:"1",key:"1vctgf"}],["circle",{cx:"9",cy:"5",r:"1",key:"hp0tcf"}],["circle",{cx:"9",cy:"19",r:"1",key:"fkjjf6"}],["circle",{cx:"15",cy:"12",r:"1",key:"1tmaij"}],["circle",{cx:"15",cy:"5",r:"1",key:"19l28e"}],["circle",{cx:"15",cy:"19",r:"1",key:"f4zoj3"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YD=lt("Layers",[["path",{d:"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z",key:"8b97xw"}],["path",{d:"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65",key:"dd6zsq"}],["path",{d:"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65",key:"ep9fru"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ZD=lt("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const QD=lt("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const JD=lt("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const sE=lt("Network",[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bp=lt("PieChart",[["path",{d:"M21.21 15.89A10 10 0 1 1 8 2.83",key:"k2fpak"}],["path",{d:"M22 12A10 10 0 0 0 12 2v10z",key:"1rfc4y"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Jd=lt("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ui=lt("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Av=lt("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const e3=lt("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const t3=lt("ShieldCheck",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const r3=lt("Sparkles",[["path",{d:"m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z",key:"17u4zn"}],["path",{d:"M5 3v4",key:"bklmnn"}],["path",{d:"M19 17v4",key:"iiml17"}],["path",{d:"M3 5h4",key:"nem4j1"}],["path",{d:"M17 19h4",key:"lbex7p"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const n3=lt("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ja=lt("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const lE=lt("TrendingUp",[["polyline",{points:"22 7 13.5 15.5 8.5 10.5 2 17",key:"126l90"}],["polyline",{points:"16 7 22 7 22 13",key:"kwv8wd"}]]);/** + * @license lucide-react v0.363.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const E_=lt("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);function a3(){const e=gd(),[t,r]=j.useState(null),n=t??e.pathname,a="/dashboard/";return j.useLayoutEffect(()=>{if(!t)return;(e.pathname===t||e.pathname.startsWith(`${t}/`))&&r(null)},[e.pathname,t]),u.jsxs("aside",{className:"relative z-20 flex h-screen w-64 shrink-0 flex-col border-r border-slate-200 bg-white transition-colors duration-300 dark:border-[#22262f] dark:bg-[#06080d]",children:[u.jsx("div",{className:"flex h-[78px] shrink-0 items-center border-b border-slate-200 px-6 transition-colors duration-300 dark:border-[#22262f]",children:u.jsxs("div",{className:"flex items-center",children:[u.jsx("img",{src:`${a}logo/decision-engine-light.svg`,alt:"Juspay Decision Engine",className:"h-11 w-auto dark:hidden"}),u.jsx("img",{src:`${a}logo/decision-engine-dark.svg`,alt:"Juspay Decision Engine",className:"hidden h-11 w-auto dark:block"})]})}),u.jsxs("nav",{className:"flex-1 space-y-1 overflow-y-auto px-4 py-8",children:[u.jsx(Ca,{to:"/",icon:ZD,end:!0,selectedPath:n,onNavigate:r,children:"Overview"}),u.jsx(Ca,{to:"/decisions",icon:e3,selectedPath:n,onNavigate:r,children:"Decision Explorer"}),u.jsx(Ca,{to:"/analytics",icon:xp,selectedPath:n,onNavigate:r,children:"Analytics"}),u.jsx(Ca,{to:"/audit",icon:zs,selectedPath:n,onNavigate:r,children:"Decision Audit"}),u.jsx("div",{className:"flex items-center gap-2 px-3 pb-3 pt-8",children:u.jsx("span",{className:"text-[11px] font-bold uppercase tracking-widest text-slate-400 dark:text-[#6d768a]",children:"Routing"})}),u.jsx(Ca,{to:"/routing",icon:Gu,end:!0,selectedPath:n,onNavigate:r,children:"Routing Hub"}),u.jsx(Ca,{to:"/routing/sr",icon:lE,indent:!0,selectedPath:n,onNavigate:r,children:"Auth-Rate Based"}),u.jsx(Ca,{to:"/routing/rules",icon:VD,indent:!0,selectedPath:n,onNavigate:r,children:"Rule-Based"}),u.jsx(Ca,{to:"/routing/volume",icon:bp,indent:!0,selectedPath:n,onNavigate:r,children:"Volume Split"}),u.jsx(Ca,{to:"/routing/debit",icon:sE,indent:!0,selectedPath:n,onNavigate:r,children:"Debit Routing"})]}),u.jsx("div",{className:"border-t border-slate-200 bg-white px-6 py-5 transition-colors duration-300 dark:border-[#22262f] dark:bg-[#0a0d12]",children:u.jsx("span",{className:"text-[11px] font-medium tracking-wide text-slate-500 dark:text-[#7d879b]",children:"v1.4"})})]})}function Ca({to:e,icon:t,children:r,end:n,indent:a,selectedPath:i,onNavigate:o}){const s=xd(),l=n?i===e:i===e||i.startsWith(`${e}/`);return u.jsxs("button",{type:"button","aria-current":l?"page":void 0,onMouseDown:c=>{c.detail>0&&c.preventDefault()},onClick:c=>{document.activeElement instanceof HTMLElement&&document.activeElement.blur(),o==null||o(e),c.currentTarget.blur(),s(e)},className:`group relative flex w-full appearance-none items-center gap-3 rounded-[16px] border-0 px-4 py-3 text-[14px] font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]/40 focus-visible:ring-offset-0 ${a?"pl-8":""} ${l?"bg-transparent text-slate-950 dark:text-white":"bg-transparent text-slate-500 hover:bg-slate-900/[0.025] hover:text-slate-900 dark:text-[#8d96aa] dark:hover:bg-white/[0.035] dark:hover:text-white"}`,children:[u.jsx("span",{"aria-hidden":"true",className:`absolute left-1 top-1/2 h-7 w-[3px] -translate-y-1/2 rounded-full transition-all duration-150 ${l?"bg-brand-600 opacity-100 dark:bg-sky-300":"opacity-0"}`}),u.jsx(t,{size:18,className:`transition-colors duration-200 ${l?"text-brand-600 dark:text-sky-300":"text-slate-400 group-hover:text-slate-700 dark:text-[#697387] dark:group-hover:text-white"}`,strokeWidth:l?2.5:2}),u.jsx("span",{className:"flex-1 text-left",children:r})]})}const i3={},P_=e=>{let t;const r=new Set,n=(f,d)=>{const p=typeof f=="function"?f(t):f;if(!Object.is(p,t)){const h=t;t=d??(typeof p!="object"||p===null)?p:Object.assign({},t,p),r.forEach(x=>x(t,h))}},a=()=>t,l={setState:n,getState:a,getInitialState:()=>c,subscribe:f=>(r.add(f),()=>r.delete(f)),destroy:()=>{(i3?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},c=t=e(n,a,l);return l},o3=e=>e?P_(e):P_;var uE={exports:{}},cE={},dE={exports:{}},fE={};/** + * @license React + * use-sync-external-store-shim.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var el=j;function s3(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var l3=typeof Object.is=="function"?Object.is:s3,u3=el.useState,c3=el.useEffect,d3=el.useLayoutEffect,f3=el.useDebugValue;function p3(e,t){var r=t(),n=u3({inst:{value:r,getSnapshot:t}}),a=n[0].inst,i=n[1];return d3(function(){a.value=r,a.getSnapshot=t,Nv(a)&&i({inst:a})},[e,r,t]),c3(function(){return Nv(a)&&i({inst:a}),e(function(){Nv(a)&&i({inst:a})})},[e]),f3(r),r}function Nv(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!l3(e,r)}catch{return!0}}function h3(e,t){return t()}var m3=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?h3:p3;fE.useSyncExternalStore=el.useSyncExternalStore!==void 0?el.useSyncExternalStore:m3;dE.exports=fE;var Ig=dE.exports;/** + * @license React + * use-sync-external-store-shim/with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var tm=j,v3=Ig;function y3(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var g3=typeof Object.is=="function"?Object.is:y3,x3=v3.useSyncExternalStore,b3=tm.useRef,w3=tm.useEffect,_3=tm.useMemo,S3=tm.useDebugValue;cE.useSyncExternalStoreWithSelector=function(e,t,r,n,a){var i=b3(null);if(i.current===null){var o={hasValue:!1,value:null};i.current=o}else o=i.current;i=_3(function(){function l(h){if(!c){if(c=!0,f=h,h=n(h),a!==void 0&&o.hasValue){var x=o.value;if(a(x,h))return d=x}return d=h}if(x=d,g3(f,h))return x;var v=n(h);return a!==void 0&&a(x,v)?(f=h,x):(f=h,d=v)}var c=!1,f,d,p=r===void 0?null:r;return[function(){return l(t())},p===null?void 0:function(){return l(p())}]},[t,r,n,a]);var s=x3(e,i[0],i[1]);return w3(function(){o.hasValue=!0,o.value=s},[s]),S3(s),s};uE.exports=cE;var j3=uE.exports;const k3=dt(j3),pE={},{useDebugValue:O3}=E,{useSyncExternalStoreWithSelector:A3}=k3;let C_=!1;const N3=e=>e;function E3(e,t=N3,r){(pE?"production":void 0)!=="production"&&r&&!C_&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),C_=!0);const n=A3(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return O3(n),n}const P3=e=>{(pE?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?o3(e):e,r=(n,a)=>E3(t,n,a);return Object.assign(r,t),r},C3=e=>P3,T3={};function $3(e,t){let r;try{r=e()}catch{return}return{getItem:a=>{var i;const o=l=>l===null?null:JSON.parse(l,void 0),s=(i=r.getItem(a))!=null?i:null;return s instanceof Promise?s.then(o):o(s)},setItem:(a,i)=>r.setItem(a,JSON.stringify(i,void 0)),removeItem:a=>r.removeItem(a)}}const jc=e=>t=>{try{const r=e(t);return r instanceof Promise?r:{then(n){return jc(n)(r)},catch(n){return this}}}catch(r){return{then(n){return this},catch(n){return jc(n)(r)}}}},R3=(e,t)=>(r,n,a)=>{let i={getStorage:()=>localStorage,serialize:JSON.stringify,deserialize:JSON.parse,partialize:y=>y,version:0,merge:(y,g)=>({...g,...y}),...t},o=!1;const s=new Set,l=new Set;let c;try{c=i.getStorage()}catch{}if(!c)return e((...y)=>{console.warn(`[zustand persist middleware] Unable to update item '${i.name}', the given storage is currently unavailable.`),r(...y)},n,a);const f=jc(i.serialize),d=()=>{const y=i.partialize({...n()});let g;const m=f({state:y,version:i.version}).then(w=>c.setItem(i.name,w)).catch(w=>{g=w});if(g)throw g;return m},p=a.setState;a.setState=(y,g)=>{p(y,g),d()};const h=e((...y)=>{r(...y),d()},n,a);let x;const v=()=>{var y;if(!c)return;o=!1,s.forEach(m=>m(n()));const g=((y=i.onRehydrateStorage)==null?void 0:y.call(i,n()))||void 0;return jc(c.getItem.bind(c))(i.name).then(m=>{if(m)return i.deserialize(m)}).then(m=>{if(m)if(typeof m.version=="number"&&m.version!==i.version){if(i.migrate)return i.migrate(m.state,m.version);console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return m.state}).then(m=>{var w;return x=i.merge(m,(w=n())!=null?w:h),r(x,!0),d()}).then(()=>{g==null||g(x,void 0),o=!0,l.forEach(m=>m(x))}).catch(m=>{g==null||g(void 0,m)})};return a.persist={setOptions:y=>{i={...i,...y},y.getStorage&&(c=y.getStorage())},clearStorage:()=>{c==null||c.removeItem(i.name)},getOptions:()=>i,rehydrate:()=>v(),hasHydrated:()=>o,onHydrate:y=>(s.add(y),()=>{s.delete(y)}),onFinishHydration:y=>(l.add(y),()=>{l.delete(y)})},v(),x||h},I3=(e,t)=>(r,n,a)=>{let i={storage:$3(()=>localStorage),partialize:v=>v,version:0,merge:(v,y)=>({...y,...v}),...t},o=!1;const s=new Set,l=new Set;let c=i.storage;if(!c)return e((...v)=>{console.warn(`[zustand persist middleware] Unable to update item '${i.name}', the given storage is currently unavailable.`),r(...v)},n,a);const f=()=>{const v=i.partialize({...n()});return c.setItem(i.name,{state:v,version:i.version})},d=a.setState;a.setState=(v,y)=>{d(v,y),f()};const p=e((...v)=>{r(...v),f()},n,a);a.getInitialState=()=>p;let h;const x=()=>{var v,y;if(!c)return;o=!1,s.forEach(m=>{var w;return m((w=n())!=null?w:p)});const g=((y=i.onRehydrateStorage)==null?void 0:y.call(i,(v=n())!=null?v:p))||void 0;return jc(c.getItem.bind(c))(i.name).then(m=>{if(m)if(typeof m.version=="number"&&m.version!==i.version){if(i.migrate)return[!0,i.migrate(m.state,m.version)];console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return[!1,m.state];return[!1,void 0]}).then(m=>{var w;const[S,b]=m;if(h=i.merge(b,(w=n())!=null?w:p),r(h,!0),S)return f()}).then(()=>{g==null||g(h,void 0),h=n(),o=!0,l.forEach(m=>m(h))}).catch(m=>{g==null||g(void 0,m)})};return a.persist={setOptions:v=>{i={...i,...v},v.storage&&(c=v.storage)},clearStorage:()=>{c==null||c.removeItem(i.name)},getOptions:()=>i,rehydrate:()=>x(),hasHydrated:()=>o,onHydrate:v=>(s.add(v),()=>{s.delete(v)}),onFinishHydration:v=>(l.add(v),()=>{l.delete(v)})},i.skipHydration||x(),h||p},M3=(e,t)=>"getStorage"in t||"serialize"in t||"deserialize"in t?((T3?"production":void 0)!=="production"&&console.warn("[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead."),R3(e,t)):I3(e,t),D3=M3,ka=C3()(D3(e=>({merchantId:"",setMerchantId:t=>{console.log(` +[STORE] Merchant ID changed: "${t}"`),e({merchantId:t})}}),{name:"merchant-store"})),L3="public";function F3(e,t,r){console.log(` +`+"=".repeat(80)),console.log(`[API REQUEST] ${new Date().toISOString()}`),console.log(`Method: ${e}`),console.log(`Path: ${t}`),r!==void 0&&console.log("Body:",JSON.stringify(r,null,2)),console.log("=".repeat(80))}function z3(e,t,r,n){console.log(` +`+"-".repeat(80)),console.log(`[API RESPONSE] ${new Date().toISOString()}`),console.log(`Path: ${e}`),console.log(`Status: ${t} ${r}`),console.log("Response Body:",n),console.log("-".repeat(80)+` +`)}function T_(e,t){console.log(` +`+"!".repeat(80)),console.log(`[API ERROR] ${new Date().toISOString()}`),console.log(`Path: ${e}`),t instanceof Error?(console.log("Error:",t.message),console.log("Stack:",t.stack)):console.log("Error:",t),console.log("!".repeat(80)+` +`)}async function hE(e,t){const r=(t==null?void 0:t.method)||"GET",n=t!=null&&t.body?JSON.parse(t.body):void 0;F3(r,e,n);try{const a=await fetch(e,{headers:{"Content-Type":"application/json","x-tenant-id":L3,...t==null?void 0:t.headers},...t}),i=await a.text();let o;try{const s=JSON.parse(i);o=JSON.stringify(s,null,2)}catch{o=i}if(z3(e,a.status,a.statusText,o),!a.ok){const s=new Error(`API error ${a.status}: ${i}`);throw T_(e,s),s}return i.trim()?JSON.parse(i):void 0}catch(a){throw T_(e,a),a}}async function Et(e,t){return hE(e,{method:"POST",body:t!==void 0?JSON.stringify(t):void 0})}async function pn(e){return hE(e)}function B3(){const{merchantId:e,setMerchantId:t}=ka(),[r,n]=j.useState(e),[a,i]=j.useState(!1),[o,s]=j.useState(()=>localStorage.getItem("theme")==="dark");j.useEffect(()=>{const c=window.document.documentElement;o?(c.classList.add("dark"),localStorage.setItem("theme","dark")):(c.classList.remove("dark"),localStorage.setItem("theme","light"))},[o]);async function l(){const c=r.trim();if(c){t(c),i(!0);try{await Et("/merchant-account/create",{merchant_id:c,gateway_success_rate_based_decider_input:null})}catch{}finally{i(!1)}}}return u.jsxs("header",{className:"relative z-10 flex h-[78px] shrink-0 items-center justify-between border-b border-slate-200 bg-white px-6 transition-colors duration-300 dark:border-[#22262f] dark:bg-[#06080d] md:px-8",children:[u.jsx("div",{}),u.jsxs("div",{className:"flex items-center gap-6",children:[u.jsxs("div",{className:"relative",children:[u.jsx("input",{value:r,onChange:c=>n(c.target.value),onKeyDown:c=>c.key==="Enter"&&l(),placeholder:"Set Merchant ID",className:"w-72 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 shadow-[0_6px_20px_-16px_rgba(15,23,42,0.18)] transition-all placeholder-slate-400 focus:outline-none focus:border-[#3b82f6]/30 dark:border-[#22262f] dark:bg-[#11141b] dark:text-white dark:placeholder-[#6c7486] dark:shadow-none"}),u.jsx("button",{onClick:l,disabled:a,className:"absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-2 text-slate-400 transition-colors hover:text-slate-700 dark:text-[#7a8397] dark:hover:text-[#dbe7ff]",children:a?u.jsx(QD,{size:16,className:"animate-spin"}):u.jsx(oE,{size:16})})]}),e&&u.jsxs("div",{className:"ml-2 flex items-center gap-2 border-l border-slate-200 pl-6 transition-colors duration-300 dark:border-[#22262f]",children:[u.jsx(WD,{size:16,className:"text-brand-500 dark:text-sky-300"}),u.jsx("span",{className:"text-sm font-medium text-slate-800 dark:text-white",children:e})]}),u.jsx("button",{onClick:()=>s(!o),className:"rounded-full border border-slate-200 bg-white p-2.5 text-slate-600 shadow-[0_6px_20px_-16px_rgba(15,23,42,0.18)] transition-colors duration-200 hover:bg-slate-50 hover:text-slate-900 dark:border-[#22262f] dark:bg-[#11141b] dark:text-[#aeb6c7] dark:shadow-none dark:hover:bg-[#171b23] dark:hover:text-white","aria-label":"Toggle theme",children:o?u.jsx(n3,{size:18}):u.jsx(JD,{size:18})})]})]})}function U3(){return u.jsxs("div",{className:"relative flex h-screen overflow-hidden bg-[#ffffff] text-slate-900 transition-colors duration-300 dark:bg-[#030507] dark:text-white",children:[u.jsx("div",{className:"pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(59,130,246,0.05),_transparent_22%),radial-gradient(circle_at_top_right,_rgba(14,165,233,0.04),_transparent_20%),linear-gradient(180deg,_rgba(255,255,255,1),_rgba(255,255,255,1))] dark:bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.06),_transparent_22%),linear-gradient(180deg,_rgba(3,5,7,1),_rgba(5,8,12,1))]"}),u.jsx("div",{className:"aurora-top"}),u.jsx(a3,{}),u.jsxs("div",{className:"flex-1 flex flex-col overflow-hidden relative z-10",children:[u.jsx(B3,{}),u.jsx("main",{className:"relative flex-1 overflow-y-auto p-6 md:p-8",children:u.jsx($D,{})})]})]})}const mE=0,vE=1,yE=2,$_=3;var R_=Object.prototype.hasOwnProperty;function Mg(e,t){var r,n;if(e===t)return!0;if(e&&t&&(r=e.constructor)===t.constructor){if(r===Date)return e.getTime()===t.getTime();if(r===RegExp)return e.toString()===t.toString();if(r===Array){if((n=e.length)===t.length)for(;n--&&Mg(e[n],t[n]););return n===-1}if(!r||typeof e=="object"){n=0;for(r in e)if(R_.call(e,r)&&++n&&!R_.call(t,r)||!(r in t)||!Mg(e[r],t[r]))return!1;return Object.keys(t).length===n}}return e!==e&&t!==t}const La=new WeakMap,Ba=()=>{},Pr=Ba(),Dg=Object,et=e=>e===Pr,fa=e=>typeof e=="function",Vi=(e,t)=>({...e,...t}),gE=e=>fa(e.then),Ev={},ef={},Ib="undefined",bd=typeof window!=Ib,Lg=typeof document!=Ib,V3=bd&&"Deno"in window,W3=()=>bd&&typeof window.requestAnimationFrame!=Ib,xE=(e,t)=>{const r=La.get(e);return[()=>!et(t)&&e.get(t)||Ev,n=>{if(!et(t)){const a=e.get(t);t in ef||(ef[t]=a),r[5](t,Vi(a,n),a||Ev)}},r[6],()=>!et(t)&&t in ef?ef[t]:!et(t)&&e.get(t)||Ev]};let Fg=!0;const H3=()=>Fg,[zg,Bg]=bd&&window.addEventListener?[window.addEventListener.bind(window),window.removeEventListener.bind(window)]:[Ba,Ba],G3=()=>{const e=Lg&&document.visibilityState;return et(e)||e!=="hidden"},q3=e=>(Lg&&document.addEventListener("visibilitychange",e),zg("focus",e),()=>{Lg&&document.removeEventListener("visibilitychange",e),Bg("focus",e)}),K3=e=>{const t=()=>{Fg=!0,e()},r=()=>{Fg=!1};return zg("online",t),zg("offline",r),()=>{Bg("online",t),Bg("offline",r)}},X3={isOnline:H3,isVisible:G3},Y3={initFocus:q3,initReconnect:K3},I_=!E.useId,Bs=!bd||V3,Z3=e=>W3()?window.requestAnimationFrame(e):setTimeout(e,1),Pv=Bs?j.useEffect:j.useLayoutEffect,Cv=typeof navigator<"u"&&navigator.connection,M_=!Bs&&Cv&&(["slow-2g","2g"].includes(Cv.effectiveType)||Cv.saveData),tf=new WeakMap,Q3=e=>Dg.prototype.toString.call(e),Tv=(e,t)=>e===`[object ${t}]`;let J3=0;const Ug=e=>{const t=typeof e,r=Q3(e),n=Tv(r,"Date"),a=Tv(r,"RegExp"),i=Tv(r,"Object");let o,s;if(Dg(e)===e&&!n&&!a){if(o=tf.get(e),o)return o;if(o=++J3+"~",tf.set(e,o),Array.isArray(e)){for(o="@",s=0;s{if(fa(e))try{e=e()}catch{e=""}const t=e;return e=typeof e=="string"?e:(Array.isArray(e)?e.length:e)?Ug(e):"",[e,t]};let eL=0;const Vg=()=>++eL;async function bE(...e){const[t,r,n,a]=e,i=Vi({populateCache:!0,throwOnError:!0},typeof a=="boolean"?{revalidate:a}:a||{});let o=i.populateCache;const s=i.rollbackOnError;let l=i.optimisticData;const c=p=>typeof s=="function"?s(p):s!==!1,f=i.throwOnError;if(fa(r)){const p=r,h=[],x=t.keys();for(const v of x)!/^\$(inf|sub)\$/.test(v)&&p(t.get(v)._k)&&h.push(v);return Promise.all(h.map(d))}return d(r);async function d(p){const[h]=Mb(p);if(!h)return;const[x,v]=xE(t,h),[y,g,m,w]=La.get(t),S=()=>{const M=y[h];return(fa(i.revalidate)?i.revalidate(x().data,p):i.revalidate!==!1)&&(delete m[h],delete w[h],M&&M[0])?M[0](yE).then(()=>x().data):x().data};if(e.length<3)return S();let b=n,_,k=!1;const O=Vg();g[h]=[O,0];const N=!et(l),T=x(),R=T.data,A=T._c,C=et(A)?R:A;if(N&&(l=fa(l)?l(C,R):l,v({data:l,_c:C})),fa(b))try{b=b(C)}catch(M){_=M,k=!0}if(b&&gE(b))if(b=await b.catch(M=>{_=M,k=!0}),O!==g[h][0]){if(k)throw _;return b}else k&&N&&c(_)&&(o=!0,v({data:C,_c:Pr}));if(o&&!k)if(fa(o)){const M=o(b,C);v({data:M,error:Pr,_c:Pr})}else v({data:b,error:Pr,_c:Pr});if(g[h][1]=Vg(),Promise.resolve(S()).then(()=>{v({_c:Pr})}),k){if(f)throw _;return}return b}}const D_=(e,t)=>{for(const r in e)e[r][0]&&e[r][0](t)},tL=(e,t)=>{if(!La.has(e)){const r=Vi(Y3,t),n=Object.create(null),a=bE.bind(Pr,e);let i=Ba;const o=Object.create(null),s=(f,d)=>{const p=o[f]||[];return o[f]=p,p.push(d),()=>p.splice(p.indexOf(d),1)},l=(f,d,p)=>{e.set(f,d);const h=o[f];if(h)for(const x of h)x(d,p)},c=()=>{if(!La.has(e)&&(La.set(e,[n,Object.create(null),Object.create(null),Object.create(null),a,l,s]),!Bs)){const f=r.initFocus(setTimeout.bind(Pr,D_.bind(Pr,n,mE))),d=r.initReconnect(setTimeout.bind(Pr,D_.bind(Pr,n,vE)));i=()=>{f&&f(),d&&d(),La.delete(e)}}};return c(),[e,a,c,i]}return[e,La.get(e)[4]]},rL=(e,t,r,n,a)=>{const i=r.errorRetryCount,o=a.retryCount,s=~~((Math.random()+.5)*(1<<(o<8?o:8)))*r.errorRetryInterval;!et(i)&&o>i||setTimeout(n,s,a)},nL=Mg,[wE,aL]=tL(new Map),iL=Vi({onLoadingSlow:Ba,onSuccess:Ba,onError:Ba,onErrorRetry:rL,onDiscarded:Ba,revalidateOnFocus:!0,revalidateOnReconnect:!0,revalidateIfStale:!0,shouldRetryOnError:!0,errorRetryInterval:M_?1e4:5e3,focusThrottleInterval:5*1e3,dedupingInterval:2*1e3,loadingTimeout:M_?5e3:3e3,compare:nL,isPaused:()=>!1,cache:wE,mutate:aL,fallback:{}},X3),oL=(e,t)=>{const r=Vi(e,t);if(t){const{use:n,fallback:a}=e,{use:i,fallback:o}=t;n&&i&&(r.use=n.concat(i)),a&&o&&(r.fallback=Vi(a,o))}return r},sL=j.createContext({}),lL="$inf$",_E=bd&&window.__SWR_DEVTOOLS_USE__,uL=_E?window.__SWR_DEVTOOLS_USE__:[],cL=()=>{_E&&(window.__SWR_DEVTOOLS_REACT__=E)},dL=e=>fa(e[1])?[e[0],e[1],e[2]||{}]:[e[0],null,(e[1]===null?e[2]:e[1])||{}],SE=()=>{const e=j.useContext(sL);return j.useMemo(()=>Vi(iL,e),[e])},fL=e=>(t,r,n)=>e(t,r&&((...i)=>{const[o]=Mb(t),[,,,s]=La.get(wE);if(o.startsWith(lL))return r(...i);const l=s[o];return et(l)?r(...i):(delete s[o],l)}),n),pL=uL.concat(fL),hL=e=>function(...r){const n=SE(),[a,i,o]=dL(r),s=oL(n,o);let l=e;const{use:c}=s,f=(c||[]).concat(pL);for(let d=f.length;d--;)l=f[d](l);return l(a,i||s.fetcher||null,s)},mL=(e,t,r)=>{const n=t[e]||(t[e]=[]);return n.push(r),()=>{const a=n.indexOf(r);a>=0&&(n[a]=n[n.length-1],n.pop())}};cL();const $v=E.use||(e=>{switch(e.status){case"pending":throw e;case"fulfilled":return e.value;case"rejected":throw e.reason;default:throw e.status="pending",e.then(t=>{e.status="fulfilled",e.value=t},t=>{e.status="rejected",e.reason=t}),e}}),Rv={dedupe:!0},L_=Promise.resolve(Pr),vL=()=>Ba,yL=(e,t,r)=>{const{cache:n,compare:a,suspense:i,fallbackData:o,revalidateOnMount:s,revalidateIfStale:l,refreshInterval:c,refreshWhenHidden:f,refreshWhenOffline:d,keepPreviousData:p,strictServerPrefetchWarning:h}=r,[x,v,y,g]=La.get(n),[m,w]=Mb(e),S=j.useRef(!1),b=j.useRef(!1),_=j.useRef(m),k=j.useRef(t),O=j.useRef(r),N=()=>O.current,T=()=>N().isVisible()&&N().isOnline(),[R,A,C,M]=xE(n,m),B=j.useRef({}).current,V=et(o)?et(r.fallback)?Pr:r.fallback[m]:o,I=(je,Be)=>{for(const Me in B){const Re=Me;if(Re==="data"){if(!a(je[Re],Be[Re])&&(!et(je[Re])||!a(fe,Be[Re])))return!1}else if(Be[Re]!==je[Re])return!1}return!0},D=!S.current,U=j.useMemo(()=>{const je=R(),Be=M(),Me=P=>{const L=Vi(P);return delete L._k,(()=>{if(!m||!t||N().isPaused())return!1;if(D&&!et(s))return s;const se=et(V)?L.data:V;return et(se)||l})()?{isValidating:!0,isLoading:!0,...L}:L},Re=Me(je),Ke=je===Be?Re:Me(Be);let Ze=Re;return[()=>{const P=Me(R());return I(P,Ze)?(Ze.data=P.data,Ze.isLoading=P.isLoading,Ze.isValidating=P.isValidating,Ze.error=P.error,Ze):(Ze=P,P)},()=>Ke]},[n,m]),G=Ig.useSyncExternalStore(j.useCallback(je=>C(m,(Be,Me)=>{I(Me,Be)||je()}),[n,m]),U[0],U[1]),q=x[m]&&x[m].length>0,K=G.data,te=et(K)?V&&gE(V)?$v(V):V:K,Z=G.error,de=j.useRef(te),fe=p?et(K)?et(de.current)?te:de.current:K:te,Ie=m&&et(te),De=j.useRef(null);!Bs&&Ig.useSyncExternalStore(vL,()=>(De.current=!1,De),()=>(De.current=!0,De));const oe=De.current;h&&oe&&!i&&Ie&&console.warn(`Missing pre-initiated data for serialized key "${m}" during server-side rendering. Data fetching should be initiated on the server and provided to SWR via fallback data. You can set "strictServerPrefetchWarning: false" to disable this warning.`);const J=!m||!t||N().isPaused()||q&&!et(Z)?!1:D&&!et(s)?s:i?et(te)?!1:l:et(te)||l,we=D&&J,Y=et(G.isValidating)?we:G.isValidating,Ne=et(G.isLoading)?we:G.isLoading,xe=j.useCallback(async je=>{const Be=k.current;if(!m||!Be||b.current||N().isPaused())return!1;let Me,Re,Ke=!0;const Ze=je||{},P=!y[m]||!Ze.dedupe,L=()=>I_?!b.current&&m===_.current&&S.current:m===_.current,H={isValidating:!1,isLoading:!1},se=()=>{A(H)},re=()=>{const ne=y[m];ne&&ne[1]===Re&&delete y[m]},Q={isValidating:!0};et(R().data)&&(Q.isLoading=!0);try{if(P&&(A(Q),r.loadingTimeout&&et(R().data)&&setTimeout(()=>{Ke&&L()&&N().onLoadingSlow(m,r)},r.loadingTimeout),y[m]=[Be(w),Vg()]),[Me,Re]=y[m],Me=await Me,P&&setTimeout(re,r.dedupingInterval),!y[m]||y[m][1]!==Re)return P&&L()&&N().onDiscarded(m),!1;H.error=Pr;const ne=v[m];if(!et(ne)&&(Re<=ne[0]||Re<=ne[1]||ne[1]===0))return se(),P&&L()&&N().onDiscarded(m),!1;const ke=R().data;H.data=a(ke,Me)?ke:Me,P&&L()&&N().onSuccess(Me,m,r)}catch(ne){re();const ke=N(),{shouldRetryOnError:W}=ke;ke.isPaused()||(H.error=ne,P&&L()&&(ke.onError(ne,m,ke),(W===!0||fa(W)&&W(ne))&&(!N().revalidateOnFocus||!N().revalidateOnReconnect||T())&&ke.onErrorRetry(ne,m,ke,le=>{const _e=x[m];_e&&_e[0]&&_e[0]($_,le)},{retryCount:(Ze.retryCount||0)+1,dedupe:!0})))}return Ke=!1,se(),!0},[m,n]),$e=j.useCallback((...je)=>bE(n,_.current,...je),[]);if(Pv(()=>{k.current=t,O.current=r,et(K)||(de.current=K)}),Pv(()=>{if(!m)return;const je=xe.bind(Pr,Rv);let Be=0;N().revalidateOnFocus&&(Be=Date.now()+N().focusThrottleInterval);const Re=mL(m,x,(Ke,Ze={})=>{if(Ke==mE){const P=Date.now();N().revalidateOnFocus&&P>Be&&T()&&(Be=P+N().focusThrottleInterval,je())}else if(Ke==vE)N().revalidateOnReconnect&&T()&&je();else{if(Ke==yE)return xe();if(Ke==$_)return xe(Ze)}});return b.current=!1,_.current=m,S.current=!0,A({_k:w}),J&&(y[m]||(et(te)||Bs?je():Z3(je))),()=>{b.current=!0,Re()}},[m]),Pv(()=>{let je;function Be(){const Re=fa(c)?c(R().data):c;Re&&je!==-1&&(je=setTimeout(Me,Re))}function Me(){!R().error&&(f||N().isVisible())&&(d||N().isOnline())?xe(Rv).then(Be):Be()}return Be(),()=>{je&&(clearTimeout(je),je=-1)}},[c,f,d,m]),j.useDebugValue(fe),i){if(!I_&&Bs&&Ie)throw new Error("Fallback data is required when using Suspense in SSR.");Ie&&(k.current=t,O.current=r,b.current=!1);const je=g[m],Be=!et(je)&&Ie?$e(je):L_;if($v(Be),!et(Z)&&Ie)throw Z;const Me=Ie?xe(Rv):L_;!et(fe)&&Ie&&(Me.status="fulfilled",Me.value=!0),$v(Me)}return{mutate:$e,get data(){return B.data=!0,fe},get error(){return B.error=!0,Z},get isValidating(){return B.isValidating=!0,Y},get isLoading(){return B.isLoading=!0,Ne}}},Bt=hL(yL),gL={green:"bg-emerald-500/10 text-emerald-600 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-300",gray:"bg-slate-900/[0.04] text-slate-700 ring-1 ring-inset ring-slate-900/8 dark:bg-white/[0.05] dark:text-slate-300 dark:ring-white/8",blue:"bg-sky-500/12 text-sky-700 ring-1 ring-inset ring-sky-500/22 dark:bg-sky-400/14 dark:text-sky-200 dark:ring-sky-400/28",red:"bg-red-500/10 text-red-600 ring-1 ring-inset ring-red-500/20 dark:text-red-300",orange:"bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/20 dark:text-orange-300",purple:"bg-purple-500/10 text-purple-600 ring-1 ring-inset ring-purple-500/20 dark:text-purple-300"};function Ue({variant:e="gray",children:t}){return u.jsx("span",{className:`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium tracking-wide ${gL[e]}`,children:t})}function Ae({children:e,className:t="",onClick:r}){const n=`relative overflow-hidden rounded-[30px] border border-slate-200 bg-white shadow-[0_18px_60px_-42px_rgba(15,23,42,0.15)] dark:border-[#2a303a] dark:bg-[#11151d] dark:shadow-[0_18px_60px_-42px_rgba(0,0,0,0.7)] ${r?"cursor-pointer text-left transition duration-300 hover:-translate-y-0.5 hover:border-[#3b82f6]/35 hover:bg-slate-50 dark:hover:bg-[#141923]":""} ${t}`,a=u.jsxs(u.Fragment,{children:[u.jsx("div",{className:"absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#3b82f6]/25 to-transparent dark:via-[#3b82f6]/30"}),u.jsx("div",{className:"absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.55),transparent_26%)] dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.02),transparent_26%)]"}),u.jsx("div",{className:"relative",children:e})]});return r?u.jsx("button",{type:"button",onClick:r,className:n,children:a}):u.jsx("div",{className:n,children:a})}function nt({children:e,className:t=""}){return u.jsx("div",{className:`border-b border-slate-200 px-6 py-5 dark:border-[#2a303a] ${t}`,children:e})}function Ge({children:e,className:t=""}){return u.jsx("div",{className:`px-6 py-5 ${t}`,children:e})}function hn({children:e,className:t=""}){return u.jsx("p",{className:`text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8390a7] ${t}`,children:e})}function jE({children:e,className:t=""}){return u.jsx("div",{className:`rounded-[22px] border border-slate-200 bg-white/80 px-4 py-4 shadow-[0_14px_30px_-28px_rgba(15,23,42,0.18)] dark:border-[#2a303a] dark:bg-[#161b24] dark:shadow-none ${t}`,children:e})}const Iv=[{value:"15m",label:"15m",detail:"Last 15 mins",badge:"Live 15m",summaryLabel:"Errors last 15 mins"},{value:"1h",label:"1h",detail:"Last hour",badge:"Live 1h",summaryLabel:"Errors last hour"},{value:"24h",label:"1 day",detail:"Last 1 day",badge:"Live 1d",summaryLabel:"Errors last 1 day"}];function xL(){const[e,t]=j.useState("loading");return j.useEffect(()=>{fetch("/health").then(r=>t(r.ok?"up":"down")).catch(()=>t("down"))},[]),e}function rf(e){return new Intl.NumberFormat(void 0,{notation:"compact",maximumFractionDigits:e&&e<100?1:0}).format(e||0)}function Mv(e){return e==null||Number.isNaN(e)?"0%":`${e.toFixed(e>=100?0:1)}%`}function Dv(e){return e?new Intl.DateTimeFormat(void 0,{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit"}).format(new Date(e)):"No updates yet"}function bL(e){return e==="up"?"Healthy":e==="down"?"Needs attention":"Checking"}function Lv({label:e,value:t,detail:r}){return u.jsxs("div",{className:"rounded-[22px] border border-slate-200 bg-white px-4 py-4 dark:border-[#2a303a] dark:bg-[#161b24]",children:[u.jsx(hn,{children:e}),u.jsx("p",{className:"mt-3 text-2xl font-semibold tracking-tight text-slate-950 dark:text-white",children:t}),u.jsx("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#b2bdd1]",children:r})]})}function nf({icon:e,label:t,value:r,detail:n}){return u.jsx(Ae,{className:"p-5",children:u.jsxs("div",{className:"flex items-start justify-between gap-4",children:[u.jsxs("div",{children:[u.jsx(hn,{children:t}),u.jsx("p",{className:"mt-4 text-3xl font-semibold tracking-tight text-slate-950 dark:text-white",children:r}),u.jsx("p",{className:"mt-2 text-sm text-slate-500 dark:text-[#b2bdd1]",children:n})]}),u.jsx("div",{className:"rounded-2xl border border-slate-200 bg-slate-50 p-3 dark:border-[#2a303a] dark:bg-[#161b24]",children:u.jsx(e,{className:"h-5 w-5 text-brand-600 dark:text-sky-300"})})]})})}function wL(){return u.jsxs("div",{className:"grid gap-5 pt-8 lg:grid-cols-[1.1fr_0.9fr]",children:[u.jsxs(Ae,{className:"p-7",children:[u.jsx(hn,{children:"Merchant required"}),u.jsx("h2",{className:"mt-4 max-w-xl text-3xl font-semibold tracking-tight text-slate-950 dark:text-white",children:"Set a merchant in the top bar to turn this into a live overview."}),u.jsx("p",{className:"mt-4 max-w-xl text-sm leading-7 text-slate-600 dark:text-[#b2bdd1]",children:"Once selected, this page will show business-facing status: service health, active routing, request count, and the gateway currently handling the most traffic."})]}),u.jsx(Ae,{className:"p-7",children:u.jsx("div",{className:"space-y-5",children:[{icon:zs,title:"System status",text:"Check whether the service is reachable."},{icon:Gu,title:"Routing setup",text:"See whether a strategy is configured."},{icon:xp,title:"Gateway activity",text:"View recent request distribution by gateway."}].map(e=>u.jsxs("div",{className:"flex items-start gap-4",children:[u.jsx("div",{className:"rounded-2xl border border-slate-200 bg-slate-50 p-3 dark:border-[#2a303a] dark:bg-[#161b24]",children:u.jsx(e.icon,{className:"h-5 w-5 text-brand-600 dark:text-sky-300"})}),u.jsxs("div",{children:[u.jsx("p",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:e.title}),u.jsx("p",{className:"mt-1 text-sm leading-6 text-slate-600 dark:text-[#b2bdd1]",children:e.text})]})]},e.title))})})]})}function _L(){var k,O,N,T,R,A,C,M,B,V;const e=xd(),{merchantId:t}=ka(),r=xL(),[n,a]=j.useState("24h"),{data:i}=Bt(t?`/routing/list/active/${t}`:null,()=>Et(`/routing/list/active/${t}`),{shouldRetryOnError:!1}),{data:o}=Bt(t?["/rule/get","successRate",t]:null,()=>Et("/rule/get",{merchant_id:t,algorithm:"successRate"}),{shouldRetryOnError:!1}),s=t?`/analytics/overview?scope=current&range=${n}&merchant_id=${encodeURIComponent(t)}`:null,l=t?`/analytics/routing-stats?scope=current&range=${n}&merchant_id=${encodeURIComponent(t)}`:null,c=Bt(s,pn,{refreshInterval:15e3,revalidateOnFocus:!0,shouldRetryOnError:!1}),f=Bt(l,pn,{refreshInterval:15e3,revalidateOnFocus:!0,shouldRetryOnError:!1}),d=(i==null?void 0:i[0])||null,p=(i||[]).some(I=>{var D;return((D=I.algorithm_data||I.algorithm)==null?void 0:D.type)==="advanced"}),h=((k=c.data)==null?void 0:k.generated_at_ms)||((O=f.data)==null?void 0:O.generated_at_ms)||void 0,v=((T=(((N=c.data)==null?void 0:N.route_hits)||[]).find(I=>I.route==="/decide_gateway"))==null?void 0:T.count)||0,y=((A=(R=c.data)==null?void 0:R.top_errors)==null?void 0:A.reduce((I,D)=>I+D.count,0))||0,g=j.useMemo(()=>{var U;const I=new Map;for(const G of((U=f.data)==null?void 0:U.gateway_share)||[])I.set(G.gateway,(I.get(G.gateway)||0)+G.count);const D=Array.from(I.values()).reduce((G,q)=>G+q,0);return Array.from(I.entries()).map(([G,q])=>({gateway:G,count:q,share:D?q/D*100:0})).sort((G,q)=>q.count-G.count)},[f.data]),m=((C=g[0])==null?void 0:C.gateway)||((V=(B=(M=c.data)==null?void 0:M.top_scores)==null?void 0:B[0])==null?void 0:V.gateway),w=Iv.find(I=>I.value===n)||Iv[1],S=[r==="up",!!d,!!(o!=null&&o.data),p].filter(Boolean).length,b=[{label:"Service health",description:r==="up"?"Service is reachable.":"Please verify service health.",state:r==="up"?"Healthy":r==="down"?"Issue":"Checking",icon:r==="up"?GD:r==="down"?qD:HD,route:void 0},{label:"Routing strategy",description:d?d.name:"No active routing configured.",state:d?"Configured":"Not set",icon:Gu,route:"/routing"},{label:"Auth-rate config",description:o!=null&&o.data?"Configured and available.":"Not configured yet.",state:o!=null&&o.data?"Configured":"Not set",icon:t3,route:"/routing/sr"},{label:"Rule-based routing",description:p?"Enabled for this merchant.":"Not enabled.",state:p?"Enabled":"Optional",icon:r3,route:"/routing/rules"}],_=t?r==="up"?{label:"System live",variant:"green"}:r==="down"?{label:"Attention needed",variant:"red"}:{label:"Checking status",variant:"gray"}:{label:"Merchant not selected",variant:"orange"};return u.jsxs("div",{className:"relative mx-auto max-w-[1380px]",children:[u.jsxs("div",{className:"pointer-events-none absolute inset-0 -z-10 overflow-hidden",children:[u.jsx("div",{className:"absolute -left-16 top-0 h-72 w-72 rounded-full bg-sky-500/10 blur-3xl dark:bg-sky-500/8"}),u.jsx("div",{className:"absolute right-0 top-12 h-80 w-80 rounded-full bg-brand-500/10 blur-3xl dark:bg-brand-500/10"})]}),u.jsxs("section",{className:"relative overflow-hidden rounded-[40px] border border-slate-200 bg-white px-5 py-5 shadow-[0_28px_90px_-56px_rgba(15,23,42,0.16)] md:px-6 md:py-6 dark:border-[#232933] dark:bg-[#090c12] dark:shadow-[0_28px_90px_-56px_rgba(0,0,0,0.72)]",children:[u.jsx("div",{className:"absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#3b82f6]/25 to-transparent dark:via-[#3b82f6]/35"}),u.jsxs("header",{className:"relative flex flex-col gap-4 border-b border-slate-200 pb-5 dark:border-[#232933]",children:[u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[u.jsx(Ue,{variant:_.variant,children:_.label}),t?u.jsx(Ue,{variant:"blue",children:t}):null,h?u.jsxs("span",{className:"text-xs font-medium uppercase tracking-[0.18em] text-slate-500 dark:text-[#7d879b]",children:["Last sync ",Dv(h)]}):null]}),u.jsxs("div",{children:[u.jsx("h1",{className:"text-4xl font-semibold tracking-tight text-slate-950 md:text-[4rem] dark:text-white",children:"Overview"}),u.jsx("p",{className:"mt-2 max-w-2xl text-sm leading-7 text-slate-600 dark:text-[#a6b0c3]",children:"Basic business-facing view of system status, setup, request volume, and gateway activity."}),u.jsx("div",{className:"mt-4 inline-flex rounded-2xl border border-slate-200 bg-slate-50 p-1 dark:border-[#2a303a] dark:bg-[#121720]",children:Iv.map(I=>{const D=I.value===n;return u.jsx("button",{type:"button",onClick:()=>a(I.value),className:`rounded-[14px] px-3 py-2 text-xs font-semibold transition ${D?"bg-white text-slate-950 shadow-sm dark:bg-[#1a2332] dark:text-white":"text-slate-500 hover:text-slate-900 dark:text-[#8ea0bb] dark:hover:text-white"}`,children:I.label},I.value)})})]})]}),t?u.jsxs(u.Fragment,{children:[u.jsxs("div",{className:"grid gap-5 pt-8 xl:grid-cols-[1.15fr_0.85fr]",children:[u.jsx(Ae,{className:"p-6 md:p-7",children:u.jsxs("div",{className:"flex h-full flex-col justify-between",children:[u.jsxs("div",{children:[u.jsx(hn,{children:"Traffic leader"}),u.jsxs("div",{className:"mt-5 flex flex-wrap items-end gap-4",children:[u.jsx("h2",{className:"text-[2.5rem] font-semibold tracking-[-0.05em] text-slate-950 md:text-[3rem] dark:text-white",children:(m==null?void 0:m.toUpperCase())||"--"}),u.jsxs("div",{className:"pb-2",children:[u.jsx("p",{className:"text-lg font-medium text-slate-700 dark:text-[#d5dded]",children:g[0]?Mv(g[0].share):"0%"}),u.jsx("p",{className:"mt-1 text-xs uppercase tracking-[0.16em] text-slate-500 dark:text-[#8390a7]",children:"Share in selected window"})]})]}),u.jsx("p",{className:"mt-4 max-w-xl text-sm leading-7 text-slate-600 dark:text-[#a6b0c3]",children:d?`${d.name} is the current routing strategy for this merchant.`:"No active routing strategy is configured for this merchant yet."})]}),u.jsxs("div",{className:"mt-8 grid gap-3 sm:grid-cols-3",children:[u.jsx(Lv,{label:"Requests",value:rf(v),detail:w.detail}),u.jsx(Lv,{label:"Setup ready",value:`${S}/4`,detail:"Core basics configured"}),u.jsx(Lv,{label:"Last sync",value:h?Dv(h):"--",detail:"Latest refresh"})]})]})}),u.jsxs("div",{className:"grid gap-4 sm:grid-cols-2 xl:grid-cols-1",children:[u.jsx(nf,{icon:zs,label:"System status",value:bL(r),detail:r==="up"?"Service is reachable":"Please verify service health"}),u.jsx(nf,{icon:Gu,label:"Active routing",value:(d==null?void 0:d.name)||"Not set",detail:d?"Currently selected strategy":"No routing configured yet"}),u.jsxs("div",{className:"grid gap-4 sm:grid-cols-2 xl:grid-cols-2",children:[u.jsx(nf,{icon:N_,label:"Requests",value:rf(v),detail:w.detail}),u.jsx(nf,{icon:xp,label:"Top gateway",value:(m==null?void 0:m.toUpperCase())||"--",detail:g[0]?`${Mv(g[0].share)} of traffic`:"No activity yet"})]})]})]}),u.jsxs("div",{className:"mt-6 grid gap-6 xl:grid-cols-[1.02fr_0.98fr]",children:[u.jsxs(Ae,{className:"p-6",children:[u.jsxs("div",{className:"flex items-center justify-between gap-4",children:[u.jsxs("div",{children:[u.jsx(hn,{children:"Current setup"}),u.jsx("p",{className:"mt-2 text-sm text-slate-600 dark:text-[#a6b0c3]",children:"The status cards you can explain in a demo without technical jargon."})]}),u.jsxs(Ue,{variant:S>=3?"green":"orange",children:[S,"/4 ready"]})]}),u.jsx("div",{className:"mt-5 grid gap-4 md:grid-cols-2",children:b.map(I=>u.jsx(Ae,{className:"min-h-[158px] p-5",onClick:I.route?()=>e(I.route):void 0,children:u.jsxs("div",{className:"flex h-full flex-col justify-between",children:[u.jsxs("div",{className:"flex items-start justify-between gap-4",children:[u.jsx("div",{className:"rounded-2xl border border-slate-200 bg-slate-50 p-3 dark:border-[#2a303a] dark:bg-[#161b24]",children:u.jsx(I.icon,{className:"h-5 w-5 text-brand-600 dark:text-sky-300"})}),u.jsx(Ue,{variant:I.state==="Healthy"||I.state==="Configured"||I.state==="Enabled"?"green":I.state==="Issue"?"red":I.state==="Checking"||I.state==="Optional"?"gray":"orange",children:I.state})]}),u.jsxs("div",{className:"mt-6",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:I.label}),u.jsx("p",{className:"mt-2 text-sm leading-6 text-slate-600 dark:text-[#a6b0c3]",children:I.description})]})]})},I.label))})]}),u.jsxs(Ae,{className:"p-6",children:[u.jsxs("div",{className:"flex items-center justify-between gap-4",children:[u.jsxs("div",{children:[u.jsx(hn,{children:"Gateway activity"}),u.jsx("p",{className:"mt-2 text-sm text-slate-600 dark:text-[#a6b0c3]",children:"Request distribution by gateway for the selected window."})]}),u.jsx(Ue,{variant:"blue",children:w.badge})]}),u.jsx("div",{className:"mt-6 space-y-4",children:g.length?g.slice(0,4).map((I,D)=>u.jsxs("div",{className:"rounded-[24px] border border-slate-200 bg-slate-50/80 p-4 dark:border-[#2a303a] dark:bg-[#121720]",children:[u.jsxs("div",{className:"flex items-center justify-between gap-4",children:[u.jsxs("div",{className:"flex items-center gap-3",children:[u.jsx("span",{className:"h-2.5 w-2.5 rounded-full",style:{backgroundColor:["#38bdf8","#60a5fa","#22c55e","#f59e0b"][D]||"#38bdf8"}}),u.jsxs("div",{children:[u.jsx("p",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:I.gateway.toUpperCase()}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#98a3b8]",children:[rf(I.count)," requests"]})]})]}),u.jsx("p",{className:"text-sm font-medium text-slate-950 dark:text-white",children:Mv(I.share)})]}),u.jsx("div",{className:"mt-4 h-2 rounded-full bg-slate-200 dark:bg-[#232933]",children:u.jsx("div",{className:"h-full rounded-full bg-gradient-to-r from-sky-400 via-blue-500 to-cyan-300",style:{width:`${Math.max(10,I.share)}%`}})})]},I.gateway)):u.jsxs("div",{className:"rounded-[24px] border border-dashed border-white/10 px-5 py-10 text-center",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:"No gateway activity yet"}),u.jsx("p",{className:"mt-2 text-sm leading-6 text-slate-600 dark:text-[#a6b0c3]",children:"Once requests start flowing, this section will show traffic by gateway."})]})})]})]}),u.jsxs("div",{className:"mt-6 grid gap-6 xl:grid-cols-[0.86fr_1.14fr]",children:[u.jsxs(Ae,{className:"p-6",children:[u.jsx(hn,{children:"Quick summary"}),u.jsx("div",{className:"mt-5 space-y-4",children:[{label:"Selected merchant",value:t},{label:"Last sync",value:Dv(h)},{label:w.summaryLabel,value:rf(y)},{label:"Top gateway",value:(m==null?void 0:m.toUpperCase())||"No activity"}].map(I=>u.jsxs("div",{className:"flex items-center justify-between gap-4 rounded-[20px] border border-slate-200 bg-slate-50/80 px-4 py-3 dark:border-[#2a303a] dark:bg-[#121720]",children:[u.jsx("span",{className:"text-sm text-slate-600 dark:text-[#a6b0c3]",children:I.label}),u.jsx("span",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:I.value})]},I.label))})]}),u.jsx("div",{className:"grid gap-4 md:grid-cols-3",children:[{label:"Routing Hub",text:"Configure routing strategies.",icon:Gu,route:"/routing"},{label:"Analytics",text:"Inspect request and gateway trends.",icon:xp,route:"/analytics"},{label:"Audit Trail",text:"Review individual decision records.",icon:N_,route:"/audit"}].map(I=>u.jsx(Ae,{className:"p-5",onClick:()=>e(I.route),children:u.jsxs("div",{className:"flex h-full flex-col justify-between",children:[u.jsx("div",{className:"inline-flex w-fit rounded-2xl border border-slate-200 bg-slate-50 p-3 dark:border-[#2a303a] dark:bg-[#161b24]",children:u.jsx(I.icon,{className:"h-5 w-5 text-brand-600 dark:text-sky-300"})}),u.jsxs("div",{className:"mt-10",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-950 dark:text-white",children:I.label}),u.jsx("p",{className:"mt-2 text-sm leading-6 text-slate-600 dark:text-[#a6b0c3]",children:I.text}),u.jsxs("div",{className:"mt-4 inline-flex items-center gap-2 text-sm font-medium text-brand-600 dark:text-sky-300",children:[u.jsx("span",{children:"Open"}),u.jsx(oE,{className:"h-4 w-4"})]})]})]})},I.label))})]})]}):u.jsx(wL,{})]})]})}function SL(){const e=xd(),{merchantId:t}=ka(),{data:r}=Bt(t?`/routing/list/active/${t}`:null,()=>Et(`/routing/list/active/${t}`)),{data:n}=Bt(t?["/rule/get","successRate",t]:null,()=>Et("/rule/get",{merchant_id:t,algorithm:"successRate"})),a=[{id:"sr",title:"Auth-Rate Based Routing",description:"Dynamically route to the best-performing gateway based on real-time authorization rates.",icon:lE,route:"/routing/sr",algorithmType:"successRate",checkConfigured:()=>{var i;return!!((i=n==null?void 0:n.config)!=null&&i.data)}},{id:"rules",title:"Rule-Based Routing",description:"Declarative routing rules to route payments based on conditions and attributes.",icon:YD,route:"/routing/rules",algorithmType:"advanced",checkConfigured:()=>(r||[]).some(i=>{var o;return((o=i.algorithm_data||i.algorithm)==null?void 0:o.type)==="advanced"})},{id:"volume",title:"Volume Split",description:"Distribute payment traffic across gateways by configurable percentage splits.",icon:bp,route:"/routing/volume",algorithmType:"volume_split",checkConfigured:()=>(r||[]).some(i=>{var o;return((o=i.algorithm_data||i.algorithm)==null?void 0:o.type)==="volume_split"})},{id:"debit",title:"Network Routing",description:"Optimise debit network fees with acquirer-aware network-based routing.",icon:KD,route:"/routing/debit",algorithmType:"debitRouting",checkConfigured:()=>!1}];return u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900",children:"Routing Hub"}),u.jsx("p",{className:"text-sm text-slate-500 mt-1",children:"Click on any routing strategy to configure"})]}),u.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4",children:a.map(i=>{const o=i.icon,s=i.checkConfigured();return u.jsx(Ae,{className:"flex flex-col hover:border-brand-300 cursor-pointer transition-all hover:shadow-md",onClick:()=>e(i.route),children:u.jsxs(Ge,{className:"flex-1 flex flex-col gap-3",children:[u.jsxs("div",{className:"flex items-start justify-between",children:[u.jsx("div",{className:"p-2 bg-brand-50 rounded-lg border border-[#1c2d50]",children:u.jsx(o,{size:20,className:"text-brand-500"})}),u.jsx(Ue,{variant:s?"green":"gray",children:s?"Configured":"Not Configured"})]}),u.jsxs("div",{children:[u.jsx("h3",{className:"font-semibold text-slate-900",children:i.title}),u.jsx("p",{className:"text-sm text-slate-500 mt-1",children:i.description})]}),u.jsx("div",{className:"mt-auto pt-2",children:u.jsx("span",{className:"text-sm text-brand-600 font-medium",children:s?"Manage →":"Setup →"})})]})},i.id)})})]})}var wd=e=>e.type==="checkbox",xo=e=>e instanceof Date,qr=e=>e==null;const kE=e=>typeof e=="object";var Gt=e=>!qr(e)&&!Array.isArray(e)&&kE(e)&&!xo(e),jL=e=>Gt(e)&&e.target?wd(e.target)?e.target.checked:e.target.value:e,kL=(e,t)=>t.split(".").some((r,n,a)=>!isNaN(Number(r))&&e.has(a.slice(0,n).join("."))),OL=e=>{const t=e.constructor&&e.constructor.prototype;return Gt(t)&&t.hasOwnProperty("isPrototypeOf")},Db=typeof window<"u"&&typeof window.HTMLElement<"u"&&typeof document<"u";function Ot(e){if(e instanceof Date)return new Date(e);const t=typeof FileList<"u"&&e instanceof FileList;if(Db&&(e instanceof Blob||t))return e;const r=Array.isArray(e);if(!r&&!(Gt(e)&&OL(e)))return e;const n=r?[]:Object.create(Object.getPrototypeOf(e));for(const a in e)Object.prototype.hasOwnProperty.call(e,a)&&(n[a]=Ot(e[a]));return n}var rm=e=>/^\w*$/.test(e),vt=e=>e===void 0,nm=e=>Array.isArray(e)?e.filter(Boolean):[],Lb=e=>nm(e.replace(/["|']|\]/g,"").split(/\.|\[/)),pe=(e,t,r)=>{if(!t||!Gt(e))return r;const n=(rm(t)?[t]:Lb(t)).reduce((a,i)=>qr(a)?a:a[i],e);return vt(n)||n===e?vt(e[t])?r:e[t]:n},ca=e=>typeof e=="boolean",Zn=e=>typeof e=="function",ct=(e,t,r)=>{let n=-1;const a=rm(t)?[t]:Lb(t),i=a.length,o=i-1;for(;++nE.useContext(AE);var NL=(e,t,r,n=!0)=>{const a={defaultValues:t._defaultValues};for(const i in e)Object.defineProperty(a,i,{get:()=>{const o=i;return t._proxyFormState[o]!==Cn.all&&(t._proxyFormState[o]=!n||Cn.all),e[o]}});return a};const NE=typeof window<"u"?E.useLayoutEffect:E.useEffect;var Dr=e=>typeof e=="string",EL=(e,t,r,n,a)=>Dr(e)?(n&&t.watch.add(e),pe(r,e,a)):Array.isArray(e)?e.map(i=>(n&&t.watch.add(i),pe(r,i))):(n&&(t.watchAll=!0),r),Wg=e=>qr(e)||!kE(e);function wi(e,t,r=new WeakSet){if(Wg(e)||Wg(t))return Object.is(e,t);if(xo(e)&&xo(t))return Object.is(e.getTime(),t.getTime());const n=Object.keys(e),a=Object.keys(t);if(n.length!==a.length)return!1;if(r.has(e)||r.has(t))return!0;r.add(e),r.add(t);for(const i of n){const o=e[i];if(!a.includes(i))return!1;if(i!=="ref"){const s=t[i];if(xo(o)&&xo(s)||(Gt(o)||Array.isArray(o))&&(Gt(s)||Array.isArray(s))?!wi(o,s,r):!Object.is(o,s))return!1}}return!0}const PL=E.createContext(null);PL.displayName="HookFormContext";var EE=(e,t,r,n,a)=>t?{...r[e],types:{...r[e]&&r[e].types?r[e].types:{},[n]:a||!0}}:{},Zr=e=>Array.isArray(e)?e:[e],F_=()=>{let e=[];return{get observers(){return e},next:a=>{for(const i of e)i.next&&i.next(a)},subscribe:a=>(e.push(a),{unsubscribe:()=>{e=e.filter(i=>i!==a)}}),unsubscribe:()=>{e=[]}}};function PE(e,t){const r={};for(const n in e)if(e.hasOwnProperty(n)){const a=e[n],i=t[n];if(a&&Gt(a)&&i){const o=PE(a,i);Gt(o)&&(r[n]=o)}else e[n]&&(r[n]=i)}return r}var Ar=e=>Gt(e)&&!Object.keys(e).length,Fb=e=>e.type==="file",wp=e=>{if(!Db)return!1;const t=e?e.ownerDocument:0;return e instanceof(t&&t.defaultView?t.defaultView.HTMLElement:HTMLElement)},CE=e=>e.type==="select-multiple",zb=e=>e.type==="radio",CL=e=>zb(e)||wd(e),zv=e=>wp(e)&&e.isConnected;function TL(e,t){const r=t.slice(0,-1).length;let n=0;for(;n{for(const t in e)if(Zn(e[t]))return!0;return!1};function TE(e){return Array.isArray(e)||Gt(e)&&!RL(e)}function Hg(e,t={}){for(const r in e){const n=e[r];TE(n)?(t[r]=Array.isArray(n)?[]:{},Hg(n,t[r])):vt(n)||(t[r]=!0)}return t}function Eu(e,t,r){r||(r=Hg(t));for(const n in e){const a=e[n];if(TE(a))vt(t)||Wg(r[n])?r[n]=Hg(a,Array.isArray(a)?[]:{}):Eu(a,qr(t)?{}:t[n],r[n]);else{const i=t[n];r[n]=!wi(a,i)}}return r}const z_={value:!1,isValid:!1},B_={value:!0,isValid:!0};var $E=e=>{if(Array.isArray(e)){if(e.length>1){const t=e.filter(r=>r&&r.checked&&!r.disabled).map(r=>r.value);return{value:t,isValid:!!t.length}}return e[0].checked&&!e[0].disabled?e[0].attributes&&!vt(e[0].attributes.value)?vt(e[0].value)||e[0].value===""?B_:{value:e[0].value,isValid:!0}:B_:z_}return z_},RE=(e,{valueAsNumber:t,valueAsDate:r,setValueAs:n})=>vt(e)?e:t?e===""?NaN:e&&+e:r&&Dr(e)?new Date(e):n?n(e):e;const U_={isValid:!1,value:null};var IE=e=>Array.isArray(e)?e.reduce((t,r)=>r&&r.checked&&!r.disabled?{isValid:!0,value:r.value}:t,U_):U_;function V_(e){const t=e.ref;return Fb(t)?t.files:zb(t)?IE(e.refs).value:CE(t)?[...t.selectedOptions].map(({value:r})=>r):wd(t)?$E(e.refs).value:RE(vt(t.value)?e.ref.value:t.value,e)}var IL=e=>e.substring(0,e.search(/\.\d+(\.|$)/))||e,ML=(e,t,r,n)=>{const a={};for(const i of e){const o=pe(t,i);o&&ct(a,i,o._f)}return{criteriaMode:r,names:[...e],fields:a,shouldUseNativeValidation:n}},_p=e=>e instanceof RegExp,su=e=>vt(e)?e:_p(e)?e.source:Gt(e)?_p(e.value)?e.value.source:e.value:e,As=e=>({isOnSubmit:!e||e===Cn.onSubmit,isOnBlur:e===Cn.onBlur,isOnChange:e===Cn.onChange,isOnAll:e===Cn.all,isOnTouch:e===Cn.onTouched});const W_="AsyncFunction";var DL=e=>!!e&&!!e.validate&&!!(Zn(e.validate)&&e.validate.constructor.name===W_||Gt(e.validate)&&Object.values(e.validate).find(t=>t.constructor.name===W_)),LL=e=>e.mount&&(e.required||e.min||e.max||e.maxLength||e.minLength||e.pattern||e.validate),Gg=(e,t,r)=>!r&&(t.watchAll||t.watch.has(e)||[...t.watch].some(n=>e.startsWith(n)&&/^\.\w+/.test(e.slice(n.length))));const Us=(e,t,r,n)=>{for(const a of r||Object.keys(e)){const i=pe(e,a);if(i){const{_f:o,...s}=i;if(o){if(o.refs&&o.refs[0]&&t(o.refs[0],a)&&!n)return!0;if(o.ref&&t(o.ref,o.name)&&!n)return!0;if(Us(s,t))break}else if(Gt(s)&&Us(s,t))break}}};function H_(e,t,r){const n=pe(e,r);if(n||rm(r))return{error:n,name:r};const a=r.split(".");for(;a.length;){const i=a.join("."),o=pe(t,i),s=pe(e,i);if(o&&!Array.isArray(o)&&r!==i)return{name:r};if(s&&s.type)return{name:i,error:s};if(s&&s.root&&s.root.type)return{name:`${i}.root`,error:s.root};a.pop()}return{name:r}}var FL=(e,t,r,n)=>{r(e);const{name:a,...i}=e;return Ar(i)||Object.keys(i).length>=Object.keys(t).length||Object.keys(i).find(o=>t[o]===(!n||Cn.all))},zL=(e,t,r)=>!e||!t||e===t||Zr(e).some(n=>n&&(r?n===t:n.startsWith(t)||t.startsWith(n))),BL=(e,t,r,n,a)=>a.isOnAll?!1:!r&&a.isOnTouch?!(t||e):(r?n.isOnBlur:a.isOnBlur)?!e:(r?n.isOnChange:a.isOnChange)?e:!0,UL=(e,t)=>!nm(pe(e,t)).length&&Vt(e,t),ME=(e,t,r)=>{const n=Zr(pe(e,r));return ct(n,OE,t[r]),ct(e,r,n),e};function G_(e,t,r="validate"){if(Dr(e)||Array.isArray(e)&&e.every(Dr)||ca(e)&&!e)return{type:r,message:Dr(e)?e:"",ref:t}}var ns=e=>Gt(e)&&!_p(e)?e:{value:e,message:""},qg=async(e,t,r,n,a,i)=>{const{ref:o,refs:s,required:l,maxLength:c,minLength:f,min:d,max:p,pattern:h,validate:x,name:v,valueAsNumber:y,mount:g}=e._f,m=pe(r,v);if(!g||t.has(v))return{};const w=s?s[0]:o,S=A=>{a&&w.reportValidity&&(w.setCustomValidity(ca(A)?"":A||""),w.reportValidity())},b={},_=zb(o),k=wd(o),O=_||k,N=(y||Fb(o))&&vt(o.value)&&vt(m)||wp(o)&&o.value===""||m===""||Array.isArray(m)&&!m.length,T=EE.bind(null,v,n,b),R=(A,C,M,B=Hn.maxLength,V=Hn.minLength)=>{const I=A?C:M;b[v]={type:A?B:V,message:I,ref:o,...T(A?B:V,I)}};if(i?!Array.isArray(m)||!m.length:l&&(!O&&(N||qr(m))||ca(m)&&!m||k&&!$E(s).isValid||_&&!IE(s).isValid)){const{value:A,message:C}=Dr(l)?{value:!!l,message:l}:ns(l);if(A&&(b[v]={type:Hn.required,message:C,ref:w,...T(Hn.required,C)},!n))return S(C),b}if(!N&&(!qr(d)||!qr(p))){let A,C;const M=ns(p),B=ns(d);if(!qr(m)&&!isNaN(m)){const V=o.valueAsNumber||m&&+m;qr(M.value)||(A=V>M.value),qr(B.value)||(C=Vnew Date(new Date().toDateString()+" "+G),D=o.type=="time",U=o.type=="week";Dr(M.value)&&m&&(A=D?I(m)>I(M.value):U?m>M.value:V>new Date(M.value)),Dr(B.value)&&m&&(C=D?I(m)+A.value,B=!qr(C.value)&&m.length<+C.value;if((M||B)&&(R(M,A.message,C.message),!n))return S(b[v].message),b}if(h&&!N&&Dr(m)){const{value:A,message:C}=ns(h);if(_p(A)&&!m.match(A)&&(b[v]={type:Hn.pattern,message:C,ref:o,...T(Hn.pattern,C)},!n))return S(C),b}if(x){if(Zn(x)){const A=await x(m,r),C=G_(A,w);if(C&&(b[v]={...C,...T(Hn.validate,C.message)},!n))return S(C.message),b}else if(Gt(x)){let A={};for(const C in x){if(!Ar(A)&&!n)break;const M=G_(await x[C](m,r),w,C);M&&(A={...M,...T(C,M.message)},S(M.message),n&&(b[v]=A))}if(!Ar(A)&&(b[v]={ref:w,...A},!n))return b}}return S(!0),b};const VL={mode:Cn.onSubmit,reValidateMode:Cn.onChange,shouldFocusError:!0};function WL(e={}){let t={...VL,...e},r={submitCount:0,isDirty:!1,isReady:!1,isLoading:Zn(t.defaultValues),isValidating:!1,isSubmitted:!1,isSubmitting:!1,isSubmitSuccessful:!1,isValid:!1,touchedFields:{},dirtyFields:{},validatingFields:{},errors:t.errors||{},disabled:t.disabled||!1},n={},a=Gt(t.defaultValues)||Gt(t.values)?Ot(t.defaultValues||t.values)||{}:{},i=t.shouldUnregister?{}:Ot(a),o={action:!1,mount:!1,watch:!1,keepIsValid:!1},s={mount:new Set,disabled:new Set,unMount:new Set,array:new Set,watch:new Set,registerName:new Set},l,c=0;const f={isDirty:!1,dirtyFields:!1,validatingFields:!1,touchedFields:!1,isValidating:!1,isValid:!1,errors:!1},d={...f};let p={...d};const h={array:F_(),state:F_()},x=t.criteriaMode===Cn.all,v=P=>L=>{clearTimeout(c),c=setTimeout(P,L)},y=async P=>{if(!o.keepIsValid&&!t.disabled&&(d.isValid||p.isValid||P)){let L;t.resolver?(L=Ar((await N()).errors),g()):L=await A({fields:n,onlyCheckValid:!0,eventType:rs.VALID}),L!==r.isValid&&h.state.next({isValid:L})}},g=(P,L)=>{!t.disabled&&(d.isValidating||d.validatingFields||p.isValidating||p.validatingFields)&&((P||Array.from(s.mount)).forEach(H=>{H&&(L?ct(r.validatingFields,H,L):Vt(r.validatingFields,H))}),h.state.next({validatingFields:r.validatingFields,isValidating:!Ar(r.validatingFields)}))},m=P=>{const L=Eu(a,i),H=IL(P);ct(r.dirtyFields,H,pe(L,H))},w=(P,L=[],H,se,re=!0,Q=!0)=>{if(se&&H&&!t.disabled){if(o.action=!0,Q&&Array.isArray(pe(n,P))){const ne=H(pe(n,P),se.argA,se.argB);re&&ct(n,P,ne)}if(Q&&Array.isArray(pe(r.errors,P))){const ne=H(pe(r.errors,P),se.argA,se.argB);re&&ct(r.errors,P,ne),UL(r.errors,P)}if((d.touchedFields||p.touchedFields)&&Q&&Array.isArray(pe(r.touchedFields,P))){const ne=H(pe(r.touchedFields,P),se.argA,se.argB);re&&ct(r.touchedFields,P,ne)}(d.dirtyFields||p.dirtyFields)&&m(P),h.state.next({name:P,isDirty:M(P,L),dirtyFields:r.dirtyFields,errors:r.errors,isValid:r.isValid})}else ct(i,P,L)},S=(P,L)=>{ct(r.errors,P,L),h.state.next({errors:r.errors})},b=P=>{r.errors=P,h.state.next({errors:r.errors,isValid:!1})},_=(P,L,H,se)=>{const re=pe(n,P);if(re){const Q=pe(i,P,vt(H)?pe(a,P):H);vt(Q)||se&&se.defaultChecked||L?ct(i,P,L?Q:V_(re._f)):I(P,Q),o.mount&&!o.action&&y()}},k=(P,L,H,se,re)=>{let Q=!1,ne=!1;const ke={name:P};if(!t.disabled){if(!H||se){(d.isDirty||p.isDirty)&&(ne=r.isDirty,r.isDirty=ke.isDirty=M(),Q=ne!==ke.isDirty);const W=wi(pe(a,P),L);ne=!!pe(r.dirtyFields,P),W?Vt(r.dirtyFields,P):ct(r.dirtyFields,P,!0),ke.dirtyFields=r.dirtyFields,Q=Q||(d.dirtyFields||p.dirtyFields)&&ne!==!W}if(H){const W=pe(r.touchedFields,P);W||(ct(r.touchedFields,P,H),ke.touchedFields=r.touchedFields,Q=Q||(d.touchedFields||p.touchedFields)&&W!==H)}Q&&re&&h.state.next(ke)}return Q?ke:{}},O=(P,L,H,se)=>{const re=pe(r.errors,P),Q=(d.isValid||p.isValid)&&ca(L)&&r.isValid!==L;if(t.delayError&&H?(l=v(()=>S(P,H)),l(t.delayError)):(clearTimeout(c),l=null,H?ct(r.errors,P,H):Vt(r.errors,P)),(H?!wi(re,H):re)||!Ar(se)||Q){const ne={...se,...Q&&ca(L)?{isValid:L}:{},errors:r.errors,name:P};r={...r,...ne},h.state.next(ne)}},N=async P=>(g(P,!0),await t.resolver(i,t.context,ML(P||s.mount,n,t.criteriaMode,t.shouldUseNativeValidation))),T=async P=>{const{errors:L}=await N(P);if(g(P),P)for(const H of P){const se=pe(L,H);se?ct(r.errors,H,se):Vt(r.errors,H)}else r.errors=L;return L},R=async({name:P,eventType:L})=>{if(e.validate){const H=await e.validate({formValues:i,formState:r,name:P,eventType:L});if(Gt(H))for(const se in H)H[se]&&fe(`${Fv}.${se}`,{message:Dr(H.message)?H.message:"",type:Hn.validate});else Dr(H)||!H?fe(Fv,{message:H||"",type:Hn.validate}):de(Fv);return H}return!0},A=async({fields:P,onlyCheckValid:L,name:H,eventType:se,context:re={valid:!0,runRootValidation:!1}})=>{if(e.validate&&(re.runRootValidation=!0,!await R({name:H,eventType:se})&&(re.valid=!1,L)))return re.valid;for(const Q in P){const ne=P[Q];if(ne){const{_f:ke,...W}=ne;if(ke){const le=s.array.has(ke.name),_e=ne._f&&DL(ne._f);_e&&d.validatingFields&&g([ke.name],!0);const rt=await qg(ne,s.disabled,i,x,t.shouldUseNativeValidation&&!L,le);if(_e&&d.validatingFields&&g([ke.name]),rt[ke.name]&&(re.valid=!1,L)||(!L&&(pe(rt,ke.name)?le?ME(r.errors,rt,ke.name):ct(r.errors,ke.name,rt[ke.name]):Vt(r.errors,ke.name)),e.shouldUseNativeValidation&&rt[ke.name]))break}!Ar(W)&&await A({context:re,onlyCheckValid:L,fields:W,name:Q,eventType:se})}}return re.valid},C=()=>{for(const P of s.unMount){const L=pe(n,P);L&&(L._f.refs?L._f.refs.every(H=>!zv(H)):!zv(L._f.ref))&&J(P)}s.unMount=new Set},M=(P,L)=>!t.disabled&&(P&&L&&ct(i,P,L),!wi(te(),a)),B=(P,L,H)=>EL(P,s,{...o.mount?i:vt(L)?a:Dr(P)?{[P]:L}:L},H,L),V=P=>nm(pe(o.mount?i:a,P,t.shouldUnregister?pe(a,P,[]):[])),I=(P,L,H={})=>{const se=pe(n,P);let re=L;if(se){const Q=se._f;Q&&(!Q.disabled&&ct(i,P,RE(L,Q)),re=wp(Q.ref)&&qr(L)?"":L,CE(Q.ref)?[...Q.ref.options].forEach(ne=>ne.selected=re.includes(ne.value)):Q.refs?wd(Q.ref)?Q.refs.forEach(ne=>{(!ne.defaultChecked||!ne.disabled)&&(Array.isArray(re)?ne.checked=!!re.find(ke=>ke===ne.value):ne.checked=re===ne.value||!!re)}):Q.refs.forEach(ne=>ne.checked=ne.value===re):Fb(Q.ref)?Q.ref.value="":(Q.ref.value=re,Q.ref.type||h.state.next({name:P,values:Ot(i)})))}(H.shouldDirty||H.shouldTouch)&&k(P,re,H.shouldTouch,H.shouldDirty,!0),H.shouldValidate&&K(P)},D=(P,L,H)=>{for(const se in L){if(!L.hasOwnProperty(se))return;const re=L[se],Q=P+"."+se,ne=pe(n,Q);(s.array.has(P)||Gt(re)||ne&&!ne._f)&&!xo(re)?D(Q,re,H):I(Q,re,H)}},U=(P,L,H={})=>{const se=pe(n,P),re=s.array.has(P),Q=Ot(L);ct(i,P,Q),re?(h.array.next({name:P,values:Ot(i)}),(d.isDirty||d.dirtyFields||p.isDirty||p.dirtyFields)&&H.shouldDirty&&(m(P),h.state.next({name:P,dirtyFields:r.dirtyFields,isDirty:M(P,Q)}))):se&&!se._f&&!qr(Q)?D(P,Q,H):I(P,Q,H),Gg(P,s)?h.state.next({...r,name:P,values:Ot(i)}):h.state.next({name:o.mount?P:void 0,values:Ot(i)})},G=async P=>{o.mount=!0;const L=P.target;let H=L.name,se=!0;const re=pe(n,H),Q=W=>{se=Number.isNaN(W)||xo(W)&&isNaN(W.getTime())||wi(W,pe(i,H,W))},ne=As(t.mode),ke=As(t.reValidateMode);if(re){let W,le;const _e=L.type?V_(re._f):jL(P),rt=P.type===rs.BLUR||P.type===rs.FOCUS_OUT,$r=!LL(re._f)&&!e.validate&&!t.resolver&&!pe(r.errors,H)&&!re._f.deps||BL(rt,pe(r.touchedFields,H),r.isSubmitted,ke,ne),Jt=Gg(H,s,rt);ct(i,H,_e),rt?(!L||!L.readOnly)&&(re._f.onBlur&&re._f.onBlur(P),l&&l(0)):re._f.onChange&&re._f.onChange(P);const Rr=k(H,_e,rt),Na=!Ar(Rr)||Jt;if(!rt&&h.state.next({name:H,type:P.type,values:Ot(i)}),$r)return(d.isValid||p.isValid)&&(t.mode==="onBlur"?rt&&y():rt||y()),Na&&h.state.next({name:H,...Jt?{}:Rr});if(!t.resolver&&e.validate&&await R({name:H,eventType:P.type}),!rt&&Jt&&h.state.next({...r}),t.resolver){const{errors:Ea}=await N([H]);if(g([H]),Q(_e),se){const ui=H_(r.errors,n,H),Pa=H_(Ea,n,ui.name||H);W=Pa.error,H=Pa.name,le=Ar(Ea)}}else g([H],!0),W=(await qg(re,s.disabled,i,x,t.shouldUseNativeValidation))[H],g([H]),Q(_e),se&&(W?le=!1:(d.isValid||p.isValid)&&(le=await A({fields:n,onlyCheckValid:!0,name:H,eventType:P.type})));se&&(re._f.deps&&(!Array.isArray(re._f.deps)||re._f.deps.length>0)&&K(re._f.deps),O(H,le,W,Rr))}},q=(P,L)=>{if(pe(r.errors,L)&&P.focus)return P.focus(),1},K=async(P,L={})=>{let H,se;const re=Zr(P);if(t.resolver){const Q=await T(vt(P)?P:re);H=Ar(Q),se=P?!re.some(ne=>pe(Q,ne)):H}else P?(se=(await Promise.all(re.map(async Q=>{const ne=pe(n,Q);return await A({fields:ne&&ne._f?{[Q]:ne}:ne,eventType:rs.TRIGGER})}))).every(Boolean),!(!se&&!r.isValid)&&y()):se=H=await A({fields:n,name:P,eventType:rs.TRIGGER});return h.state.next({...!Dr(P)||(d.isValid||p.isValid)&&H!==r.isValid?{}:{name:P},...t.resolver||!P?{isValid:H}:{},errors:r.errors}),L.shouldFocus&&!se&&Us(n,q,P?re:s.mount),se},te=(P,L)=>{let H={...o.mount?i:a};return L&&(H=PE(L.dirtyFields?r.dirtyFields:r.touchedFields,H)),vt(P)?H:Dr(P)?pe(H,P):P.map(se=>pe(H,se))},Z=(P,L)=>({invalid:!!pe((L||r).errors,P),isDirty:!!pe((L||r).dirtyFields,P),error:pe((L||r).errors,P),isValidating:!!pe(r.validatingFields,P),isTouched:!!pe((L||r).touchedFields,P)}),de=P=>{const L=P?Zr(P):void 0;L==null||L.forEach(H=>Vt(r.errors,H)),L?L.forEach(H=>{h.state.next({name:H,errors:r.errors})}):h.state.next({errors:{}})},fe=(P,L,H)=>{const se=(pe(n,P,{_f:{}})._f||{}).ref,re=pe(r.errors,P)||{},{ref:Q,message:ne,type:ke,...W}=re;ct(r.errors,P,{...W,...L,ref:se}),h.state.next({name:P,errors:r.errors,isValid:!1}),H&&H.shouldFocus&&se&&se.focus&&se.focus()},Ie=(P,L)=>Zn(P)?h.state.subscribe({next:H=>"values"in H&&P(B(void 0,L),H)}):B(P,L,!0),De=P=>h.state.subscribe({next:L=>{zL(P.name,L.name,P.exact)&&FL(L,P.formState||d,Re,P.reRenderRoot)&&P.callback({values:{...i},...r,...L,defaultValues:a})}}).unsubscribe,oe=P=>(o.mount=!0,p={...p,...P.formState},De({...P,formState:{...f,...P.formState}})),J=(P,L={})=>{for(const H of P?Zr(P):s.mount)s.mount.delete(H),s.array.delete(H),L.keepValue||(Vt(n,H),Vt(i,H)),!L.keepError&&Vt(r.errors,H),!L.keepDirty&&Vt(r.dirtyFields,H),!L.keepTouched&&Vt(r.touchedFields,H),!L.keepIsValidating&&Vt(r.validatingFields,H),!t.shouldUnregister&&!L.keepDefaultValue&&Vt(a,H);h.state.next({values:Ot(i)}),h.state.next({...r,...L.keepDirty?{isDirty:M()}:{}}),!L.keepIsValid&&y()},we=({disabled:P,name:L})=>{if(ca(P)&&o.mount||P||s.disabled.has(L)){const re=s.disabled.has(L)!==!!P;P?s.disabled.add(L):s.disabled.delete(L),re&&o.mount&&!o.action&&y()}},Y=(P,L={})=>{let H=pe(n,P);const se=ca(L.disabled)||ca(t.disabled),re=!s.registerName.has(P)&&H&&!H._f.mount;return ct(n,P,{...H||{},_f:{...H&&H._f?H._f:{ref:{name:P}},name:P,mount:!0,...L}}),s.mount.add(P),H&&!re?we({disabled:ca(L.disabled)?L.disabled:t.disabled,name:P}):_(P,!0,L.value),{...se?{disabled:L.disabled||t.disabled}:{},...t.progressive?{required:!!L.required,min:su(L.min),max:su(L.max),minLength:su(L.minLength),maxLength:su(L.maxLength),pattern:su(L.pattern)}:{},name:P,onChange:G,onBlur:G,ref:Q=>{if(Q){s.registerName.add(P),Y(P,L),s.registerName.delete(P),H=pe(n,P);const ne=vt(Q.value)&&Q.querySelectorAll&&Q.querySelectorAll("input,select,textarea")[0]||Q,ke=CL(ne),W=H._f.refs||[];if(ke?W.find(le=>le===ne):ne===H._f.ref)return;ct(n,P,{_f:{...H._f,...ke?{refs:[...W.filter(zv),ne,...Array.isArray(pe(a,P))?[{}]:[]],ref:{type:ne.type,name:P}}:{ref:ne}}}),_(P,!1,void 0,ne)}else H=pe(n,P,{}),H._f&&(H._f.mount=!1),(t.shouldUnregister||L.shouldUnregister)&&!(kL(s.array,P)&&o.action)&&s.unMount.add(P)}}},Ne=()=>t.shouldFocusError&&Us(n,q,s.mount),xe=P=>{ca(P)&&(h.state.next({disabled:P}),Us(n,(L,H)=>{const se=pe(n,H);se&&(L.disabled=se._f.disabled||P,Array.isArray(se._f.refs)&&se._f.refs.forEach(re=>{re.disabled=se._f.disabled||P}))},0,!1))},$e=(P,L)=>async H=>{let se;H&&(H.preventDefault&&H.preventDefault(),H.persist&&H.persist());let re=Ot(i);if(h.state.next({isSubmitting:!0}),t.resolver){const{errors:Q,values:ne}=await N();g(),r.errors=Q,re=Ot(ne)}else await A({fields:n,eventType:rs.SUBMIT});if(s.disabled.size)for(const Q of s.disabled)Vt(re,Q);if(Vt(r.errors,OE),Ar(r.errors)){h.state.next({errors:{}});try{await P(re,H)}catch(Q){se=Q}}else L&&await L({...r.errors},H),Ne(),setTimeout(Ne);if(h.state.next({isSubmitted:!0,isSubmitting:!1,isSubmitSuccessful:Ar(r.errors)&&!se,submitCount:r.submitCount+1,errors:r.errors}),se)throw se},Ee=(P,L={})=>{pe(n,P)&&(vt(L.defaultValue)?U(P,Ot(pe(a,P))):(U(P,L.defaultValue),ct(a,P,Ot(L.defaultValue))),L.keepTouched||Vt(r.touchedFields,P),L.keepDirty||(Vt(r.dirtyFields,P),r.isDirty=L.defaultValue?M(P,Ot(pe(a,P))):M()),L.keepError||(Vt(r.errors,P),d.isValid&&y()),h.state.next({...r}))},je=(P,L={})=>{const H=P?Ot(P):a,se=Ot(H),re=Ar(P),Q=re?a:se;if(L.keepDefaultValues||(a=H),!L.keepValues){if(L.keepDirtyValues){const ne=new Set([...s.mount,...Object.keys(Eu(a,i))]);for(const ke of Array.from(ne)){const W=pe(r.dirtyFields,ke),le=pe(i,ke),_e=pe(Q,ke);W&&!vt(le)?ct(Q,ke,le):!W&&!vt(_e)&&U(ke,_e)}}else{if(Db&&vt(P))for(const ne of s.mount){const ke=pe(n,ne);if(ke&&ke._f){const W=Array.isArray(ke._f.refs)?ke._f.refs[0]:ke._f.ref;if(wp(W)){const le=W.closest("form");if(le){le.reset();break}}}}if(L.keepFieldsRef)for(const ne of s.mount)U(ne,pe(Q,ne));else n={}}i=t.shouldUnregister?L.keepDefaultValues?Ot(a):{}:Ot(Q),h.array.next({values:{...Q}}),h.state.next({values:{...Q}})}s={mount:L.keepDirtyValues?s.mount:new Set,unMount:new Set,array:new Set,registerName:new Set,disabled:new Set,watch:new Set,watchAll:!1,focus:""},o.mount=!d.isValid||!!L.keepIsValid||!!L.keepDirtyValues||!t.shouldUnregister&&!Ar(Q),o.watch=!!t.shouldUnregister,o.keepIsValid=!!L.keepIsValid,o.action=!1,L.keepErrors||(r.errors={}),h.state.next({submitCount:L.keepSubmitCount?r.submitCount:0,isDirty:re?!1:L.keepDirty?r.isDirty:!!(L.keepDefaultValues&&!wi(P,a)),isSubmitted:L.keepIsSubmitted?r.isSubmitted:!1,dirtyFields:re?{}:L.keepDirtyValues?L.keepDefaultValues&&i?Eu(a,i):r.dirtyFields:L.keepDefaultValues&&P?Eu(a,P):L.keepDirty?r.dirtyFields:{},touchedFields:L.keepTouched?r.touchedFields:{},errors:L.keepErrors?r.errors:{},isSubmitSuccessful:L.keepIsSubmitSuccessful?r.isSubmitSuccessful:!1,isSubmitting:!1,defaultValues:a})},Be=(P,L)=>je(Zn(P)?P(i):P,{...t.resetOptions,...L}),Me=(P,L={})=>{const H=pe(n,P),se=H&&H._f;if(se){const re=se.refs?se.refs[0]:se.ref;re.focus&&setTimeout(()=>{re.focus(),L.shouldSelect&&Zn(re.select)&&re.select()})}},Re=P=>{r={...r,...P}},Ze={control:{register:Y,unregister:J,getFieldState:Z,handleSubmit:$e,setError:fe,_subscribe:De,_runSchema:N,_updateIsValidating:g,_focusError:Ne,_getWatch:B,_getDirty:M,_setValid:y,_setFieldArray:w,_setDisabledField:we,_setErrors:b,_getFieldArray:V,_reset:je,_resetDefaultValues:()=>Zn(t.defaultValues)&&t.defaultValues().then(P=>{Be(P,t.resetOptions),h.state.next({isLoading:!1})}),_removeUnmounted:C,_disableForm:xe,_subjects:h,_proxyFormState:d,get _fields(){return n},get _formValues(){return i},get _state(){return o},set _state(P){o=P},get _defaultValues(){return a},get _names(){return s},set _names(P){s=P},get _formState(){return r},get _options(){return t},set _options(P){t={...t,...P}}},subscribe:oe,trigger:K,register:Y,handleSubmit:$e,watch:Ie,setValue:U,getValues:te,reset:Be,resetField:Ee,clearErrors:de,unregister:J,setError:fe,setFocus:Me,getFieldState:Z};return{...Ze,formControl:Ze}}var fi=()=>{if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();const e=typeof performance>"u"?Date.now():performance.now()*1e3;return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const r=(Math.random()*16+e)%16|0;return(t=="x"?r:r&3|8).toString(16)})},Bv=(e,t,r={})=>r.shouldFocus||vt(r.shouldFocus)?r.focusName||`${e}.${vt(r.focusIndex)?t:r.focusIndex}.`:"",Uv=(e,t)=>[...e,...Zr(t)],Vv=e=>Array.isArray(e)?e.map(()=>{}):void 0;function Wv(e,t,r){return[...e.slice(0,t),...Zr(r),...e.slice(t)]}var Hv=(e,t,r)=>Array.isArray(e)?(vt(e[r])&&(e[r]=void 0),e.splice(r,0,e.splice(t,1)[0]),e):[],Gv=(e,t)=>[...Zr(t),...Zr(e)];function HL(e,t){let r=0;const n=[...e];for(const a of t)n.splice(a-r,1),r++;return nm(n).length?n:[]}var qv=(e,t)=>vt(t)?[]:HL(e,Zr(t).sort((r,n)=>r-n)),Kv=(e,t,r)=>{[e[t],e[r]]=[e[r],e[t]]},q_=(e,t,r)=>(e[t]=r,e);function GL(e){const t=AL(),{control:r=t,name:n,keyName:a="id",shouldUnregister:i,rules:o}=e,[s,l]=E.useState(r._getFieldArray(n)),c=E.useRef(r._getFieldArray(n).map(fi)),f=E.useRef(!1);r._names.array.add(n),E.useMemo(()=>o&&s.length>=0&&r.register(n,o),[r,n,s.length,o]),NE(()=>r._subjects.array.subscribe({next:({values:S,name:b})=>{if(b===n||!b){const _=pe(S,n);Array.isArray(_)&&(l(_),c.current=_.map(fi))}}}).unsubscribe,[r,n]);const d=E.useCallback(S=>{f.current=!0,r._setFieldArray(n,S)},[r,n]),p=(S,b)=>{const _=Zr(Ot(S)),k=Uv(r._getFieldArray(n),_);r._names.focus=Bv(n,k.length-1,b),c.current=Uv(c.current,_.map(fi)),d(k),l(k),r._setFieldArray(n,k,Uv,{argA:Vv(S)})},h=(S,b)=>{const _=Zr(Ot(S)),k=Gv(r._getFieldArray(n),_);r._names.focus=Bv(n,0,b),c.current=Gv(c.current,_.map(fi)),d(k),l(k),r._setFieldArray(n,k,Gv,{argA:Vv(S)})},x=S=>{const b=qv(r._getFieldArray(n),S);c.current=qv(c.current,S),d(b),l(b),!Array.isArray(pe(r._fields,n))&&ct(r._fields,n,void 0),r._setFieldArray(n,b,qv,{argA:S})},v=(S,b,_)=>{const k=Zr(Ot(b)),O=Wv(r._getFieldArray(n),S,k);r._names.focus=Bv(n,S,_),c.current=Wv(c.current,S,k.map(fi)),d(O),l(O),r._setFieldArray(n,O,Wv,{argA:S,argB:Vv(b)})},y=(S,b)=>{const _=r._getFieldArray(n);Kv(_,S,b),Kv(c.current,S,b),d(_),l(_),r._setFieldArray(n,_,Kv,{argA:S,argB:b},!1)},g=(S,b)=>{const _=r._getFieldArray(n);Hv(_,S,b),Hv(c.current,S,b),d(_),l(_),r._setFieldArray(n,_,Hv,{argA:S,argB:b},!1)},m=(S,b)=>{const _=Ot(b),k=q_(r._getFieldArray(n),S,_);c.current=[...k].map((O,N)=>!O||N===S?fi():c.current[N]),d(k),l([...k]),r._setFieldArray(n,k,q_,{argA:S,argB:_},!0,!1)},w=S=>{const b=Zr(Ot(S));c.current=b.map(fi),d([...b]),l([...b]),r._setFieldArray(n,[...b],_=>_,{},!0,!1)};return E.useEffect(()=>{if(r._state.action=!1,Gg(n,r._names)&&r._subjects.state.next({...r._formState}),f.current&&(!As(r._options.mode).isOnSubmit||r._formState.isSubmitted)&&!As(r._options.reValidateMode).isOnSubmit)if(r._options.resolver)r._runSchema([n]).then(S=>{r._updateIsValidating([n]);const b=pe(S.errors,n),_=pe(r._formState.errors,n);(_?!b&&_.type||b&&(_.type!==b.type||_.message!==b.message):b&&b.type)&&(b?ct(r._formState.errors,n,b):Vt(r._formState.errors,n),r._subjects.state.next({errors:r._formState.errors}))});else{const S=pe(r._fields,n);S&&S._f&&!(As(r._options.reValidateMode).isOnSubmit&&As(r._options.mode).isOnSubmit)&&qg(S,r._names.disabled,r._formValues,r._options.criteriaMode===Cn.all,r._options.shouldUseNativeValidation,!0).then(b=>!Ar(b)&&r._subjects.state.next({errors:ME(r._formState.errors,b,n)}))}r._subjects.state.next({name:n,values:Ot(r._formValues)}),r._names.focus&&Us(r._fields,(S,b)=>{if(r._names.focus&&b.startsWith(r._names.focus)&&S.focus)return S.focus(),1}),r._names.focus="",r._setValid(),f.current=!1},[s,n,r]),E.useEffect(()=>(!pe(r._formValues,n)&&r._setFieldArray(n),()=>{const S=(b,_)=>{const k=pe(r._fields,b);k&&k._f&&(k._f.mount=_)};r._options.shouldUnregister||i?r.unregister(n):S(n,!1)}),[n,r,a,i]),{swap:E.useCallback(y,[d,n,r]),move:E.useCallback(g,[d,n,r]),prepend:E.useCallback(h,[d,n,r]),append:E.useCallback(p,[d,n,r]),remove:E.useCallback(x,[d,n,r]),insert:E.useCallback(v,[d,n,r]),update:E.useCallback(m,[d,n,r]),replace:E.useCallback(w,[d,n,r]),fields:E.useMemo(()=>s.map((S,b)=>({...S,[a]:c.current[b]||fi()})),[s,a])}}function qL(e={}){const t=E.useRef(void 0),r=E.useRef(void 0),[n,a]=E.useState({isDirty:!1,isValidating:!1,isLoading:Zn(e.defaultValues),isSubmitted:!1,isSubmitting:!1,isSubmitSuccessful:!1,isValid:!1,submitCount:0,dirtyFields:{},touchedFields:{},validatingFields:{},errors:e.errors||{},disabled:e.disabled||!1,isReady:!1,defaultValues:Zn(e.defaultValues)?void 0:e.defaultValues});if(!t.current)if(e.formControl)t.current={...e.formControl,formState:n},e.defaultValues&&!Zn(e.defaultValues)&&e.formControl.reset(e.defaultValues,e.resetOptions);else{const{formControl:o,...s}=WL(e);t.current={...s,formState:n}}const i=t.current.control;return i._options=e,NE(()=>{const o=i._subscribe({formState:i._proxyFormState,callback:()=>a({...i._formState}),reRenderRoot:!0});return a(s=>({...s,isReady:!0})),i._formState.isReady=!0,o},[i]),E.useEffect(()=>i._disableForm(e.disabled),[i,e.disabled]),E.useEffect(()=>{e.mode&&(i._options.mode=e.mode),e.reValidateMode&&(i._options.reValidateMode=e.reValidateMode)},[i,e.mode,e.reValidateMode]),E.useEffect(()=>{e.errors&&(i._setErrors(e.errors),i._focusError())},[i,e.errors]),E.useEffect(()=>{e.shouldUnregister&&i._subjects.state.next({values:i._getWatch()})},[i,e.shouldUnregister]),E.useEffect(()=>{if(i._proxyFormState.isDirty){const o=i._getDirty();o!==n.isDirty&&i._subjects.state.next({isDirty:o})}},[i,n.isDirty]),E.useEffect(()=>{var o;e.values&&!wi(e.values,r.current)?(i._reset(e.values,{keepFieldsRef:!0,...i._options.resetOptions}),!((o=i._options.resetOptions)===null||o===void 0)&&o.keepIsValid||i._setValid(),r.current=e.values,a(s=>({...s}))):i._resetDefaultValues()},[i,e.values]),E.useEffect(()=>{i._state.mount||(i._setValid(),i._state.mount=!0),i._state.watch&&(i._state.watch=!1,i._subjects.state.next({...i._formState})),i._removeUnmounted()}),t.current.formState=E.useMemo(()=>NL(n,i),[i,n]),t.current}const K_=(e,t,r)=>{if(e&&"reportValidity"in e){const n=pe(r,t);e.setCustomValidity(n&&n.message||""),e.reportValidity()}},DE=(e,t)=>{for(const r in t.fields){const n=t.fields[r];n&&n.ref&&"reportValidity"in n.ref?K_(n.ref,r,e):n.refs&&n.refs.forEach(a=>K_(a,r,e))}},KL=(e,t)=>{t.shouldUseNativeValidation&&DE(e,t);const r={};for(const n in e){const a=pe(t.fields,n),i=Object.assign(e[n]||{},{ref:a&&a.ref});if(XL(t.names||Object.keys(e),n)){const o=Object.assign({},pe(r,n));ct(o,"root",i),ct(r,n,o)}else ct(r,n,i)}return r},XL=(e,t)=>e.some(r=>r.startsWith(t+"."));var YL=function(e,t){for(var r={};e.length;){var n=e[0],a=n.code,i=n.message,o=n.path.join(".");if(!r[o])if("unionErrors"in n){var s=n.unionErrors[0].errors[0];r[o]={message:s.message,type:s.code}}else r[o]={message:i,type:a};if("unionErrors"in n&&n.unionErrors.forEach(function(f){return f.errors.forEach(function(d){return e.push(d)})}),t){var l=r[o].types,c=l&&l[n.code];r[o]=EE(o,t,r,a,c?[].concat(c,n.message):n.message)}e.shift()}return r},ZL=function(e,t,r){return r===void 0&&(r={}),function(n,a,i){try{return Promise.resolve(function(o,s){try{var l=Promise.resolve(e[r.mode==="sync"?"parse":"parseAsync"](n,t)).then(function(c){return i.shouldUseNativeValidation&&DE({},i),{errors:{},values:r.raw?n:c}})}catch(c){return s(c)}return l&&l.then?l.then(void 0,s):l}(0,function(o){if(function(s){return Array.isArray(s==null?void 0:s.errors)}(o))return{values:{},errors:KL(YL(o.errors,!i.shouldUseNativeValidation&&i.criteriaMode==="all"),i)};throw o}))}catch(o){return Promise.reject(o)}}},ot;(function(e){e.assertEqual=a=>{};function t(a){}e.assertIs=t;function r(a){throw new Error}e.assertNever=r,e.arrayToEnum=a=>{const i={};for(const o of a)i[o]=o;return i},e.getValidEnumValues=a=>{const i=e.objectKeys(a).filter(s=>typeof a[a[s]]!="number"),o={};for(const s of i)o[s]=a[s];return e.objectValues(o)},e.objectValues=a=>e.objectKeys(a).map(function(i){return a[i]}),e.objectKeys=typeof Object.keys=="function"?a=>Object.keys(a):a=>{const i=[];for(const o in a)Object.prototype.hasOwnProperty.call(a,o)&&i.push(o);return i},e.find=(a,i)=>{for(const o of a)if(i(o))return o},e.isInteger=typeof Number.isInteger=="function"?a=>Number.isInteger(a):a=>typeof a=="number"&&Number.isFinite(a)&&Math.floor(a)===a;function n(a,i=" | "){return a.map(o=>typeof o=="string"?`'${o}'`:o).join(i)}e.joinValues=n,e.jsonStringifyReplacer=(a,i)=>typeof i=="bigint"?i.toString():i})(ot||(ot={}));var X_;(function(e){e.mergeShapes=(t,r)=>({...t,...r})})(X_||(X_={}));const be=ot.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),vi=e=>{switch(typeof e){case"undefined":return be.undefined;case"string":return be.string;case"number":return Number.isNaN(e)?be.nan:be.number;case"boolean":return be.boolean;case"function":return be.function;case"bigint":return be.bigint;case"symbol":return be.symbol;case"object":return Array.isArray(e)?be.array:e===null?be.null:e.then&&typeof e.then=="function"&&e.catch&&typeof e.catch=="function"?be.promise:typeof Map<"u"&&e instanceof Map?be.map:typeof Set<"u"&&e instanceof Set?be.set:typeof Date<"u"&&e instanceof Date?be.date:be.object;default:return be.unknown}},ue=ot.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]);class ei extends Error{get errors(){return this.issues}constructor(t){super(),this.issues=[],this.addIssue=n=>{this.issues=[...this.issues,n]},this.addIssues=(n=[])=>{this.issues=[...this.issues,...n]};const r=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,r):this.__proto__=r,this.name="ZodError",this.issues=t}format(t){const r=t||function(i){return i.message},n={_errors:[]},a=i=>{for(const o of i.issues)if(o.code==="invalid_union")o.unionErrors.map(a);else if(o.code==="invalid_return_type")a(o.returnTypeError);else if(o.code==="invalid_arguments")a(o.argumentsError);else if(o.path.length===0)n._errors.push(r(o));else{let s=n,l=0;for(;lr.message){const r={},n=[];for(const a of this.issues)if(a.path.length>0){const i=a.path[0];r[i]=r[i]||[],r[i].push(t(a))}else n.push(t(a));return{formErrors:n,fieldErrors:r}}get formErrors(){return this.flatten()}}ei.create=e=>new ei(e);const Kg=(e,t)=>{let r;switch(e.code){case ue.invalid_type:e.received===be.undefined?r="Required":r=`Expected ${e.expected}, received ${e.received}`;break;case ue.invalid_literal:r=`Invalid literal value, expected ${JSON.stringify(e.expected,ot.jsonStringifyReplacer)}`;break;case ue.unrecognized_keys:r=`Unrecognized key(s) in object: ${ot.joinValues(e.keys,", ")}`;break;case ue.invalid_union:r="Invalid input";break;case ue.invalid_union_discriminator:r=`Invalid discriminator value. Expected ${ot.joinValues(e.options)}`;break;case ue.invalid_enum_value:r=`Invalid enum value. Expected ${ot.joinValues(e.options)}, received '${e.received}'`;break;case ue.invalid_arguments:r="Invalid function arguments";break;case ue.invalid_return_type:r="Invalid function return type";break;case ue.invalid_date:r="Invalid date";break;case ue.invalid_string:typeof e.validation=="object"?"includes"in e.validation?(r=`Invalid input: must include "${e.validation.includes}"`,typeof e.validation.position=="number"&&(r=`${r} at one or more positions greater than or equal to ${e.validation.position}`)):"startsWith"in e.validation?r=`Invalid input: must start with "${e.validation.startsWith}"`:"endsWith"in e.validation?r=`Invalid input: must end with "${e.validation.endsWith}"`:ot.assertNever(e.validation):e.validation!=="regex"?r=`Invalid ${e.validation}`:r="Invalid";break;case ue.too_small:e.type==="array"?r=`Array must contain ${e.exact?"exactly":e.inclusive?"at least":"more than"} ${e.minimum} element(s)`:e.type==="string"?r=`String must contain ${e.exact?"exactly":e.inclusive?"at least":"over"} ${e.minimum} character(s)`:e.type==="number"?r=`Number must be ${e.exact?"exactly equal to ":e.inclusive?"greater than or equal to ":"greater than "}${e.minimum}`:e.type==="bigint"?r=`Number must be ${e.exact?"exactly equal to ":e.inclusive?"greater than or equal to ":"greater than "}${e.minimum}`:e.type==="date"?r=`Date must be ${e.exact?"exactly equal to ":e.inclusive?"greater than or equal to ":"greater than "}${new Date(Number(e.minimum))}`:r="Invalid input";break;case ue.too_big:e.type==="array"?r=`Array must contain ${e.exact?"exactly":e.inclusive?"at most":"less than"} ${e.maximum} element(s)`:e.type==="string"?r=`String must contain ${e.exact?"exactly":e.inclusive?"at most":"under"} ${e.maximum} character(s)`:e.type==="number"?r=`Number must be ${e.exact?"exactly":e.inclusive?"less than or equal to":"less than"} ${e.maximum}`:e.type==="bigint"?r=`BigInt must be ${e.exact?"exactly":e.inclusive?"less than or equal to":"less than"} ${e.maximum}`:e.type==="date"?r=`Date must be ${e.exact?"exactly":e.inclusive?"smaller than or equal to":"smaller than"} ${new Date(Number(e.maximum))}`:r="Invalid input";break;case ue.custom:r="Invalid input";break;case ue.invalid_intersection_types:r="Intersection results could not be merged";break;case ue.not_multiple_of:r=`Number must be a multiple of ${e.multipleOf}`;break;case ue.not_finite:r="Number must be finite";break;default:r=t.defaultError,ot.assertNever(e)}return{message:r}};let QL=Kg;function JL(){return QL}const e4=e=>{const{data:t,path:r,errorMaps:n,issueData:a}=e,i=[...r,...a.path||[]],o={...a,path:i};if(a.message!==void 0)return{...a,path:i,message:a.message};let s="";const l=n.filter(c=>!!c).slice().reverse();for(const c of l)s=c(o,{data:t,defaultError:s}).message;return{...a,path:i,message:s}};function he(e,t){const r=JL(),n=e4({issueData:t,data:e.data,path:e.path,errorMaps:[e.common.contextualErrorMap,e.schemaErrorMap,r,r===Kg?void 0:Kg].filter(a=>!!a)});e.common.issues.push(n)}class xn{constructor(){this.value="valid"}dirty(){this.value==="valid"&&(this.value="dirty")}abort(){this.value!=="aborted"&&(this.value="aborted")}static mergeArray(t,r){const n=[];for(const a of r){if(a.status==="aborted")return Ve;a.status==="dirty"&&t.dirty(),n.push(a.value)}return{status:t.value,value:n}}static async mergeObjectAsync(t,r){const n=[];for(const a of r){const i=await a.key,o=await a.value;n.push({key:i,value:o})}return xn.mergeObjectSync(t,n)}static mergeObjectSync(t,r){const n={};for(const a of r){const{key:i,value:o}=a;if(i.status==="aborted"||o.status==="aborted")return Ve;i.status==="dirty"&&t.dirty(),o.status==="dirty"&&t.dirty(),i.value!=="__proto__"&&(typeof o.value<"u"||a.alwaysSet)&&(n[i.value]=o.value)}return{status:t.value,value:n}}}const Ve=Object.freeze({status:"aborted"}),Pu=e=>({status:"dirty",value:e}),Fn=e=>({status:"valid",value:e}),Y_=e=>e.status==="aborted",Z_=e=>e.status==="dirty",tl=e=>e.status==="valid",Sp=e=>typeof Promise<"u"&&e instanceof Promise;var Se;(function(e){e.errToObj=t=>typeof t=="string"?{message:t}:t||{},e.toString=t=>typeof t=="string"?t:t==null?void 0:t.message})(Se||(Se={}));class Wi{constructor(t,r,n,a){this._cachedPath=[],this.parent=t,this.data=r,this._path=n,this._key=a}get path(){return this._cachedPath.length||(Array.isArray(this._key)?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}}const Q_=(e,t)=>{if(tl(t))return{success:!0,data:t.value};if(!e.common.issues.length)throw new Error("Validation failed but no issues detected.");return{success:!1,get error(){if(this._error)return this._error;const r=new ei(e.common.issues);return this._error=r,this._error}}};function Ye(e){if(!e)return{};const{errorMap:t,invalid_type_error:r,required_error:n,description:a}=e;if(t&&(r||n))throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return t?{errorMap:t,description:a}:{errorMap:(o,s)=>{const{message:l}=e;return o.code==="invalid_enum_value"?{message:l??s.defaultError}:typeof s.data>"u"?{message:l??n??s.defaultError}:o.code!=="invalid_type"?{message:s.defaultError}:{message:l??r??s.defaultError}},description:a}}class it{get description(){return this._def.description}_getType(t){return vi(t.data)}_getOrReturnCtx(t,r){return r||{common:t.parent.common,data:t.data,parsedType:vi(t.data),schemaErrorMap:this._def.errorMap,path:t.path,parent:t.parent}}_processInputParams(t){return{status:new xn,ctx:{common:t.parent.common,data:t.data,parsedType:vi(t.data),schemaErrorMap:this._def.errorMap,path:t.path,parent:t.parent}}}_parseSync(t){const r=this._parse(t);if(Sp(r))throw new Error("Synchronous parse encountered promise.");return r}_parseAsync(t){const r=this._parse(t);return Promise.resolve(r)}parse(t,r){const n=this.safeParse(t,r);if(n.success)return n.data;throw n.error}safeParse(t,r){const n={common:{issues:[],async:(r==null?void 0:r.async)??!1,contextualErrorMap:r==null?void 0:r.errorMap},path:(r==null?void 0:r.path)||[],schemaErrorMap:this._def.errorMap,parent:null,data:t,parsedType:vi(t)},a=this._parseSync({data:t,path:n.path,parent:n});return Q_(n,a)}"~validate"(t){var n,a;const r={common:{issues:[],async:!!this["~standard"].async},path:[],schemaErrorMap:this._def.errorMap,parent:null,data:t,parsedType:vi(t)};if(!this["~standard"].async)try{const i=this._parseSync({data:t,path:[],parent:r});return tl(i)?{value:i.value}:{issues:r.common.issues}}catch(i){(a=(n=i==null?void 0:i.message)==null?void 0:n.toLowerCase())!=null&&a.includes("encountered")&&(this["~standard"].async=!0),r.common={issues:[],async:!0}}return this._parseAsync({data:t,path:[],parent:r}).then(i=>tl(i)?{value:i.value}:{issues:r.common.issues})}async parseAsync(t,r){const n=await this.safeParseAsync(t,r);if(n.success)return n.data;throw n.error}async safeParseAsync(t,r){const n={common:{issues:[],contextualErrorMap:r==null?void 0:r.errorMap,async:!0},path:(r==null?void 0:r.path)||[],schemaErrorMap:this._def.errorMap,parent:null,data:t,parsedType:vi(t)},a=this._parse({data:t,path:n.path,parent:n}),i=await(Sp(a)?a:Promise.resolve(a));return Q_(n,i)}refine(t,r){const n=a=>typeof r=="string"||typeof r>"u"?{message:r}:typeof r=="function"?r(a):r;return this._refinement((a,i)=>{const o=t(a),s=()=>i.addIssue({code:ue.custom,...n(a)});return typeof Promise<"u"&&o instanceof Promise?o.then(l=>l?!0:(s(),!1)):o?!0:(s(),!1)})}refinement(t,r){return this._refinement((n,a)=>t(n)?!0:(a.addIssue(typeof r=="function"?r(n,a):r),!1))}_refinement(t){return new Fo({schema:this,typeName:We.ZodEffects,effect:{type:"refinement",refinement:t}})}superRefine(t){return this._refinement(t)}constructor(t){this.spa=this.safeParseAsync,this._def=t,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this),this["~standard"]={version:1,vendor:"zod",validate:r=>this["~validate"](r)}}optional(){return Di.create(this,this._def)}nullable(){return al.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return ya.create(this)}promise(){return Ap.create(this,this._def)}or(t){return kp.create([this,t],this._def)}and(t){return Op.create(this,t,this._def)}transform(t){return new Fo({...Ye(this._def),schema:this,typeName:We.ZodEffects,effect:{type:"transform",transform:t}})}default(t){const r=typeof t=="function"?t:()=>t;return new Yg({...Ye(this._def),innerType:this,defaultValue:r,typeName:We.ZodDefault})}brand(){return new S4({typeName:We.ZodBranded,type:this,...Ye(this._def)})}catch(t){const r=typeof t=="function"?t:()=>t;return new Zg({...Ye(this._def),innerType:this,catchValue:r,typeName:We.ZodCatch})}describe(t){const r=this.constructor;return new r({...this._def,description:t})}pipe(t){return Bb.create(this,t)}readonly(){return Qg.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}}const t4=/^c[^\s-]{8,}$/i,r4=/^[0-9a-z]+$/,n4=/^[0-9A-HJKMNP-TV-Z]{26}$/i,a4=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,i4=/^[a-z0-9_-]{21}$/i,o4=/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/,s4=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,l4=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,u4="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$";let Xv;const c4=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,d4=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/,f4=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,p4=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,h4=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,m4=/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,LE="((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))",v4=new RegExp(`^${LE}$`);function FE(e){let t="[0-5]\\d";e.precision?t=`${t}\\.\\d{${e.precision}}`:e.precision==null&&(t=`${t}(\\.\\d+)?`);const r=e.precision?"+":"?";return`([01]\\d|2[0-3]):[0-5]\\d(:${t})${r}`}function y4(e){return new RegExp(`^${FE(e)}$`)}function g4(e){let t=`${LE}T${FE(e)}`;const r=[];return r.push(e.local?"Z?":"Z"),e.offset&&r.push("([+-]\\d{2}:?\\d{2})"),t=`${t}(${r.join("|")})`,new RegExp(`^${t}$`)}function x4(e,t){return!!((t==="v4"||!t)&&c4.test(e)||(t==="v6"||!t)&&f4.test(e))}function b4(e,t){if(!o4.test(e))return!1;try{const[r]=e.split(".");if(!r)return!1;const n=r.replace(/-/g,"+").replace(/_/g,"/").padEnd(r.length+(4-r.length%4)%4,"="),a=JSON.parse(atob(n));return!(typeof a!="object"||a===null||"typ"in a&&(a==null?void 0:a.typ)!=="JWT"||!a.alg||t&&a.alg!==t)}catch{return!1}}function w4(e,t){return!!((t==="v4"||!t)&&d4.test(e)||(t==="v6"||!t)&&p4.test(e))}class Ua extends it{_parse(t){if(this._def.coerce&&(t.data=String(t.data)),this._getType(t)!==be.string){const i=this._getOrReturnCtx(t);return he(i,{code:ue.invalid_type,expected:be.string,received:i.parsedType}),Ve}const n=new xn;let a;for(const i of this._def.checks)if(i.kind==="min")t.data.lengthi.value&&(a=this._getOrReturnCtx(t,a),he(a,{code:ue.too_big,maximum:i.value,type:"string",inclusive:!0,exact:!1,message:i.message}),n.dirty());else if(i.kind==="length"){const o=t.data.length>i.value,s=t.data.lengtht.test(a),{validation:r,code:ue.invalid_string,...Se.errToObj(n)})}_addCheck(t){return new Ua({...this._def,checks:[...this._def.checks,t]})}email(t){return this._addCheck({kind:"email",...Se.errToObj(t)})}url(t){return this._addCheck({kind:"url",...Se.errToObj(t)})}emoji(t){return this._addCheck({kind:"emoji",...Se.errToObj(t)})}uuid(t){return this._addCheck({kind:"uuid",...Se.errToObj(t)})}nanoid(t){return this._addCheck({kind:"nanoid",...Se.errToObj(t)})}cuid(t){return this._addCheck({kind:"cuid",...Se.errToObj(t)})}cuid2(t){return this._addCheck({kind:"cuid2",...Se.errToObj(t)})}ulid(t){return this._addCheck({kind:"ulid",...Se.errToObj(t)})}base64(t){return this._addCheck({kind:"base64",...Se.errToObj(t)})}base64url(t){return this._addCheck({kind:"base64url",...Se.errToObj(t)})}jwt(t){return this._addCheck({kind:"jwt",...Se.errToObj(t)})}ip(t){return this._addCheck({kind:"ip",...Se.errToObj(t)})}cidr(t){return this._addCheck({kind:"cidr",...Se.errToObj(t)})}datetime(t){return typeof t=="string"?this._addCheck({kind:"datetime",precision:null,offset:!1,local:!1,message:t}):this._addCheck({kind:"datetime",precision:typeof(t==null?void 0:t.precision)>"u"?null:t==null?void 0:t.precision,offset:(t==null?void 0:t.offset)??!1,local:(t==null?void 0:t.local)??!1,...Se.errToObj(t==null?void 0:t.message)})}date(t){return this._addCheck({kind:"date",message:t})}time(t){return typeof t=="string"?this._addCheck({kind:"time",precision:null,message:t}):this._addCheck({kind:"time",precision:typeof(t==null?void 0:t.precision)>"u"?null:t==null?void 0:t.precision,...Se.errToObj(t==null?void 0:t.message)})}duration(t){return this._addCheck({kind:"duration",...Se.errToObj(t)})}regex(t,r){return this._addCheck({kind:"regex",regex:t,...Se.errToObj(r)})}includes(t,r){return this._addCheck({kind:"includes",value:t,position:r==null?void 0:r.position,...Se.errToObj(r==null?void 0:r.message)})}startsWith(t,r){return this._addCheck({kind:"startsWith",value:t,...Se.errToObj(r)})}endsWith(t,r){return this._addCheck({kind:"endsWith",value:t,...Se.errToObj(r)})}min(t,r){return this._addCheck({kind:"min",value:t,...Se.errToObj(r)})}max(t,r){return this._addCheck({kind:"max",value:t,...Se.errToObj(r)})}length(t,r){return this._addCheck({kind:"length",value:t,...Se.errToObj(r)})}nonempty(t){return this.min(1,Se.errToObj(t))}trim(){return new Ua({...this._def,checks:[...this._def.checks,{kind:"trim"}]})}toLowerCase(){return new Ua({...this._def,checks:[...this._def.checks,{kind:"toLowerCase"}]})}toUpperCase(){return new Ua({...this._def,checks:[...this._def.checks,{kind:"toUpperCase"}]})}get isDatetime(){return!!this._def.checks.find(t=>t.kind==="datetime")}get isDate(){return!!this._def.checks.find(t=>t.kind==="date")}get isTime(){return!!this._def.checks.find(t=>t.kind==="time")}get isDuration(){return!!this._def.checks.find(t=>t.kind==="duration")}get isEmail(){return!!this._def.checks.find(t=>t.kind==="email")}get isURL(){return!!this._def.checks.find(t=>t.kind==="url")}get isEmoji(){return!!this._def.checks.find(t=>t.kind==="emoji")}get isUUID(){return!!this._def.checks.find(t=>t.kind==="uuid")}get isNANOID(){return!!this._def.checks.find(t=>t.kind==="nanoid")}get isCUID(){return!!this._def.checks.find(t=>t.kind==="cuid")}get isCUID2(){return!!this._def.checks.find(t=>t.kind==="cuid2")}get isULID(){return!!this._def.checks.find(t=>t.kind==="ulid")}get isIP(){return!!this._def.checks.find(t=>t.kind==="ip")}get isCIDR(){return!!this._def.checks.find(t=>t.kind==="cidr")}get isBase64(){return!!this._def.checks.find(t=>t.kind==="base64")}get isBase64url(){return!!this._def.checks.find(t=>t.kind==="base64url")}get minLength(){let t=null;for(const r of this._def.checks)r.kind==="min"&&(t===null||r.value>t)&&(t=r.value);return t}get maxLength(){let t=null;for(const r of this._def.checks)r.kind==="max"&&(t===null||r.valuenew Ua({checks:[],typeName:We.ZodString,coerce:(e==null?void 0:e.coerce)??!1,...Ye(e)});function _4(e,t){const r=(e.toString().split(".")[1]||"").length,n=(t.toString().split(".")[1]||"").length,a=r>n?r:n,i=Number.parseInt(e.toFixed(a).replace(".","")),o=Number.parseInt(t.toFixed(a).replace(".",""));return i%o/10**a}class Mo extends it{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(t){if(this._def.coerce&&(t.data=Number(t.data)),this._getType(t)!==be.number){const i=this._getOrReturnCtx(t);return he(i,{code:ue.invalid_type,expected:be.number,received:i.parsedType}),Ve}let n;const a=new xn;for(const i of this._def.checks)i.kind==="int"?ot.isInteger(t.data)||(n=this._getOrReturnCtx(t,n),he(n,{code:ue.invalid_type,expected:"integer",received:"float",message:i.message}),a.dirty()):i.kind==="min"?(i.inclusive?t.datai.value:t.data>=i.value)&&(n=this._getOrReturnCtx(t,n),he(n,{code:ue.too_big,maximum:i.value,type:"number",inclusive:i.inclusive,exact:!1,message:i.message}),a.dirty()):i.kind==="multipleOf"?_4(t.data,i.value)!==0&&(n=this._getOrReturnCtx(t,n),he(n,{code:ue.not_multiple_of,multipleOf:i.value,message:i.message}),a.dirty()):i.kind==="finite"?Number.isFinite(t.data)||(n=this._getOrReturnCtx(t,n),he(n,{code:ue.not_finite,message:i.message}),a.dirty()):ot.assertNever(i);return{status:a.value,value:t.data}}gte(t,r){return this.setLimit("min",t,!0,Se.toString(r))}gt(t,r){return this.setLimit("min",t,!1,Se.toString(r))}lte(t,r){return this.setLimit("max",t,!0,Se.toString(r))}lt(t,r){return this.setLimit("max",t,!1,Se.toString(r))}setLimit(t,r,n,a){return new Mo({...this._def,checks:[...this._def.checks,{kind:t,value:r,inclusive:n,message:Se.toString(a)}]})}_addCheck(t){return new Mo({...this._def,checks:[...this._def.checks,t]})}int(t){return this._addCheck({kind:"int",message:Se.toString(t)})}positive(t){return this._addCheck({kind:"min",value:0,inclusive:!1,message:Se.toString(t)})}negative(t){return this._addCheck({kind:"max",value:0,inclusive:!1,message:Se.toString(t)})}nonpositive(t){return this._addCheck({kind:"max",value:0,inclusive:!0,message:Se.toString(t)})}nonnegative(t){return this._addCheck({kind:"min",value:0,inclusive:!0,message:Se.toString(t)})}multipleOf(t,r){return this._addCheck({kind:"multipleOf",value:t,message:Se.toString(r)})}finite(t){return this._addCheck({kind:"finite",message:Se.toString(t)})}safe(t){return this._addCheck({kind:"min",inclusive:!0,value:Number.MIN_SAFE_INTEGER,message:Se.toString(t)})._addCheck({kind:"max",inclusive:!0,value:Number.MAX_SAFE_INTEGER,message:Se.toString(t)})}get minValue(){let t=null;for(const r of this._def.checks)r.kind==="min"&&(t===null||r.value>t)&&(t=r.value);return t}get maxValue(){let t=null;for(const r of this._def.checks)r.kind==="max"&&(t===null||r.valuet.kind==="int"||t.kind==="multipleOf"&&ot.isInteger(t.value))}get isFinite(){let t=null,r=null;for(const n of this._def.checks){if(n.kind==="finite"||n.kind==="int"||n.kind==="multipleOf")return!0;n.kind==="min"?(r===null||n.value>r)&&(r=n.value):n.kind==="max"&&(t===null||n.valuenew Mo({checks:[],typeName:We.ZodNumber,coerce:(e==null?void 0:e.coerce)||!1,...Ye(e)});class Do extends it{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(t){if(this._def.coerce)try{t.data=BigInt(t.data)}catch{return this._getInvalidInput(t)}if(this._getType(t)!==be.bigint)return this._getInvalidInput(t);let n;const a=new xn;for(const i of this._def.checks)i.kind==="min"?(i.inclusive?t.datai.value:t.data>=i.value)&&(n=this._getOrReturnCtx(t,n),he(n,{code:ue.too_big,type:"bigint",maximum:i.value,inclusive:i.inclusive,message:i.message}),a.dirty()):i.kind==="multipleOf"?t.data%i.value!==BigInt(0)&&(n=this._getOrReturnCtx(t,n),he(n,{code:ue.not_multiple_of,multipleOf:i.value,message:i.message}),a.dirty()):ot.assertNever(i);return{status:a.value,value:t.data}}_getInvalidInput(t){const r=this._getOrReturnCtx(t);return he(r,{code:ue.invalid_type,expected:be.bigint,received:r.parsedType}),Ve}gte(t,r){return this.setLimit("min",t,!0,Se.toString(r))}gt(t,r){return this.setLimit("min",t,!1,Se.toString(r))}lte(t,r){return this.setLimit("max",t,!0,Se.toString(r))}lt(t,r){return this.setLimit("max",t,!1,Se.toString(r))}setLimit(t,r,n,a){return new Do({...this._def,checks:[...this._def.checks,{kind:t,value:r,inclusive:n,message:Se.toString(a)}]})}_addCheck(t){return new Do({...this._def,checks:[...this._def.checks,t]})}positive(t){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!1,message:Se.toString(t)})}negative(t){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!1,message:Se.toString(t)})}nonpositive(t){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!0,message:Se.toString(t)})}nonnegative(t){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!0,message:Se.toString(t)})}multipleOf(t,r){return this._addCheck({kind:"multipleOf",value:t,message:Se.toString(r)})}get minValue(){let t=null;for(const r of this._def.checks)r.kind==="min"&&(t===null||r.value>t)&&(t=r.value);return t}get maxValue(){let t=null;for(const r of this._def.checks)r.kind==="max"&&(t===null||r.valuenew Do({checks:[],typeName:We.ZodBigInt,coerce:(e==null?void 0:e.coerce)??!1,...Ye(e)});class jp extends it{_parse(t){if(this._def.coerce&&(t.data=!!t.data),this._getType(t)!==be.boolean){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.boolean,received:n.parsedType}),Ve}return Fn(t.data)}}jp.create=e=>new jp({typeName:We.ZodBoolean,coerce:(e==null?void 0:e.coerce)||!1,...Ye(e)});class rl extends it{_parse(t){if(this._def.coerce&&(t.data=new Date(t.data)),this._getType(t)!==be.date){const i=this._getOrReturnCtx(t);return he(i,{code:ue.invalid_type,expected:be.date,received:i.parsedType}),Ve}if(Number.isNaN(t.data.getTime())){const i=this._getOrReturnCtx(t);return he(i,{code:ue.invalid_date}),Ve}const n=new xn;let a;for(const i of this._def.checks)i.kind==="min"?t.data.getTime()i.value&&(a=this._getOrReturnCtx(t,a),he(a,{code:ue.too_big,message:i.message,inclusive:!0,exact:!1,maximum:i.value,type:"date"}),n.dirty()):ot.assertNever(i);return{status:n.value,value:new Date(t.data.getTime())}}_addCheck(t){return new rl({...this._def,checks:[...this._def.checks,t]})}min(t,r){return this._addCheck({kind:"min",value:t.getTime(),message:Se.toString(r)})}max(t,r){return this._addCheck({kind:"max",value:t.getTime(),message:Se.toString(r)})}get minDate(){let t=null;for(const r of this._def.checks)r.kind==="min"&&(t===null||r.value>t)&&(t=r.value);return t!=null?new Date(t):null}get maxDate(){let t=null;for(const r of this._def.checks)r.kind==="max"&&(t===null||r.valuenew rl({checks:[],coerce:(e==null?void 0:e.coerce)||!1,typeName:We.ZodDate,...Ye(e)});class J_ extends it{_parse(t){if(this._getType(t)!==be.symbol){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.symbol,received:n.parsedType}),Ve}return Fn(t.data)}}J_.create=e=>new J_({typeName:We.ZodSymbol,...Ye(e)});class eS extends it{_parse(t){if(this._getType(t)!==be.undefined){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.undefined,received:n.parsedType}),Ve}return Fn(t.data)}}eS.create=e=>new eS({typeName:We.ZodUndefined,...Ye(e)});class tS extends it{_parse(t){if(this._getType(t)!==be.null){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.null,received:n.parsedType}),Ve}return Fn(t.data)}}tS.create=e=>new tS({typeName:We.ZodNull,...Ye(e)});class rS extends it{constructor(){super(...arguments),this._any=!0}_parse(t){return Fn(t.data)}}rS.create=e=>new rS({typeName:We.ZodAny,...Ye(e)});class nS extends it{constructor(){super(...arguments),this._unknown=!0}_parse(t){return Fn(t.data)}}nS.create=e=>new nS({typeName:We.ZodUnknown,...Ye(e)});class Hi extends it{_parse(t){const r=this._getOrReturnCtx(t);return he(r,{code:ue.invalid_type,expected:be.never,received:r.parsedType}),Ve}}Hi.create=e=>new Hi({typeName:We.ZodNever,...Ye(e)});class aS extends it{_parse(t){if(this._getType(t)!==be.undefined){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.void,received:n.parsedType}),Ve}return Fn(t.data)}}aS.create=e=>new aS({typeName:We.ZodVoid,...Ye(e)});class ya extends it{_parse(t){const{ctx:r,status:n}=this._processInputParams(t),a=this._def;if(r.parsedType!==be.array)return he(r,{code:ue.invalid_type,expected:be.array,received:r.parsedType}),Ve;if(a.exactLength!==null){const o=r.data.length>a.exactLength.value,s=r.data.lengtha.maxLength.value&&(he(r,{code:ue.too_big,maximum:a.maxLength.value,type:"array",inclusive:!0,exact:!1,message:a.maxLength.message}),n.dirty()),r.common.async)return Promise.all([...r.data].map((o,s)=>a.type._parseAsync(new Wi(r,o,r.path,s)))).then(o=>xn.mergeArray(n,o));const i=[...r.data].map((o,s)=>a.type._parseSync(new Wi(r,o,r.path,s)));return xn.mergeArray(n,i)}get element(){return this._def.type}min(t,r){return new ya({...this._def,minLength:{value:t,message:Se.toString(r)}})}max(t,r){return new ya({...this._def,maxLength:{value:t,message:Se.toString(r)}})}length(t,r){return new ya({...this._def,exactLength:{value:t,message:Se.toString(r)}})}nonempty(t){return this.min(1,t)}}ya.create=(e,t)=>new ya({type:e,minLength:null,maxLength:null,exactLength:null,typeName:We.ZodArray,...Ye(t)});function ps(e){if(e instanceof Xt){const t={};for(const r in e.shape){const n=e.shape[r];t[r]=Di.create(ps(n))}return new Xt({...e._def,shape:()=>t})}else return e instanceof ya?new ya({...e._def,type:ps(e.element)}):e instanceof Di?Di.create(ps(e.unwrap())):e instanceof al?al.create(ps(e.unwrap())):e instanceof Lo?Lo.create(e.items.map(t=>ps(t))):e}class Xt extends it{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(this._cached!==null)return this._cached;const t=this._def.shape(),r=ot.objectKeys(t);return this._cached={shape:t,keys:r},this._cached}_parse(t){if(this._getType(t)!==be.object){const c=this._getOrReturnCtx(t);return he(c,{code:ue.invalid_type,expected:be.object,received:c.parsedType}),Ve}const{status:n,ctx:a}=this._processInputParams(t),{shape:i,keys:o}=this._getCached(),s=[];if(!(this._def.catchall instanceof Hi&&this._def.unknownKeys==="strip"))for(const c in a.data)o.includes(c)||s.push(c);const l=[];for(const c of o){const f=i[c],d=a.data[c];l.push({key:{status:"valid",value:c},value:f._parse(new Wi(a,d,a.path,c)),alwaysSet:c in a.data})}if(this._def.catchall instanceof Hi){const c=this._def.unknownKeys;if(c==="passthrough")for(const f of s)l.push({key:{status:"valid",value:f},value:{status:"valid",value:a.data[f]}});else if(c==="strict")s.length>0&&(he(a,{code:ue.unrecognized_keys,keys:s}),n.dirty());else if(c!=="strip")throw new Error("Internal ZodObject error: invalid unknownKeys value.")}else{const c=this._def.catchall;for(const f of s){const d=a.data[f];l.push({key:{status:"valid",value:f},value:c._parse(new Wi(a,d,a.path,f)),alwaysSet:f in a.data})}}return a.common.async?Promise.resolve().then(async()=>{const c=[];for(const f of l){const d=await f.key,p=await f.value;c.push({key:d,value:p,alwaysSet:f.alwaysSet})}return c}).then(c=>xn.mergeObjectSync(n,c)):xn.mergeObjectSync(n,l)}get shape(){return this._def.shape()}strict(t){return Se.errToObj,new Xt({...this._def,unknownKeys:"strict",...t!==void 0?{errorMap:(r,n)=>{var i,o;const a=((o=(i=this._def).errorMap)==null?void 0:o.call(i,r,n).message)??n.defaultError;return r.code==="unrecognized_keys"?{message:Se.errToObj(t).message??a}:{message:a}}}:{}})}strip(){return new Xt({...this._def,unknownKeys:"strip"})}passthrough(){return new Xt({...this._def,unknownKeys:"passthrough"})}extend(t){return new Xt({...this._def,shape:()=>({...this._def.shape(),...t})})}merge(t){return new Xt({unknownKeys:t._def.unknownKeys,catchall:t._def.catchall,shape:()=>({...this._def.shape(),...t._def.shape()}),typeName:We.ZodObject})}setKey(t,r){return this.augment({[t]:r})}catchall(t){return new Xt({...this._def,catchall:t})}pick(t){const r={};for(const n of ot.objectKeys(t))t[n]&&this.shape[n]&&(r[n]=this.shape[n]);return new Xt({...this._def,shape:()=>r})}omit(t){const r={};for(const n of ot.objectKeys(this.shape))t[n]||(r[n]=this.shape[n]);return new Xt({...this._def,shape:()=>r})}deepPartial(){return ps(this)}partial(t){const r={};for(const n of ot.objectKeys(this.shape)){const a=this.shape[n];t&&!t[n]?r[n]=a:r[n]=a.optional()}return new Xt({...this._def,shape:()=>r})}required(t){const r={};for(const n of ot.objectKeys(this.shape))if(t&&!t[n])r[n]=this.shape[n];else{let i=this.shape[n];for(;i instanceof Di;)i=i._def.innerType;r[n]=i}return new Xt({...this._def,shape:()=>r})}keyof(){return zE(ot.objectKeys(this.shape))}}Xt.create=(e,t)=>new Xt({shape:()=>e,unknownKeys:"strip",catchall:Hi.create(),typeName:We.ZodObject,...Ye(t)});Xt.strictCreate=(e,t)=>new Xt({shape:()=>e,unknownKeys:"strict",catchall:Hi.create(),typeName:We.ZodObject,...Ye(t)});Xt.lazycreate=(e,t)=>new Xt({shape:e,unknownKeys:"strip",catchall:Hi.create(),typeName:We.ZodObject,...Ye(t)});class kp extends it{_parse(t){const{ctx:r}=this._processInputParams(t),n=this._def.options;function a(i){for(const s of i)if(s.result.status==="valid")return s.result;for(const s of i)if(s.result.status==="dirty")return r.common.issues.push(...s.ctx.common.issues),s.result;const o=i.map(s=>new ei(s.ctx.common.issues));return he(r,{code:ue.invalid_union,unionErrors:o}),Ve}if(r.common.async)return Promise.all(n.map(async i=>{const o={...r,common:{...r.common,issues:[]},parent:null};return{result:await i._parseAsync({data:r.data,path:r.path,parent:o}),ctx:o}})).then(a);{let i;const o=[];for(const l of n){const c={...r,common:{...r.common,issues:[]},parent:null},f=l._parseSync({data:r.data,path:r.path,parent:c});if(f.status==="valid")return f;f.status==="dirty"&&!i&&(i={result:f,ctx:c}),c.common.issues.length&&o.push(c.common.issues)}if(i)return r.common.issues.push(...i.ctx.common.issues),i.result;const s=o.map(l=>new ei(l));return he(r,{code:ue.invalid_union,unionErrors:s}),Ve}}get options(){return this._def.options}}kp.create=(e,t)=>new kp({options:e,typeName:We.ZodUnion,...Ye(t)});function Xg(e,t){const r=vi(e),n=vi(t);if(e===t)return{valid:!0,data:e};if(r===be.object&&n===be.object){const a=ot.objectKeys(t),i=ot.objectKeys(e).filter(s=>a.indexOf(s)!==-1),o={...e,...t};for(const s of i){const l=Xg(e[s],t[s]);if(!l.valid)return{valid:!1};o[s]=l.data}return{valid:!0,data:o}}else if(r===be.array&&n===be.array){if(e.length!==t.length)return{valid:!1};const a=[];for(let i=0;i{if(Y_(i)||Y_(o))return Ve;const s=Xg(i.value,o.value);return s.valid?((Z_(i)||Z_(o))&&r.dirty(),{status:r.value,value:s.data}):(he(n,{code:ue.invalid_intersection_types}),Ve)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then(([i,o])=>a(i,o)):a(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}}Op.create=(e,t,r)=>new Op({left:e,right:t,typeName:We.ZodIntersection,...Ye(r)});class Lo extends it{_parse(t){const{status:r,ctx:n}=this._processInputParams(t);if(n.parsedType!==be.array)return he(n,{code:ue.invalid_type,expected:be.array,received:n.parsedType}),Ve;if(n.data.lengththis._def.items.length&&(he(n,{code:ue.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:"array"}),r.dirty());const i=[...n.data].map((o,s)=>{const l=this._def.items[s]||this._def.rest;return l?l._parse(new Wi(n,o,n.path,s)):null}).filter(o=>!!o);return n.common.async?Promise.all(i).then(o=>xn.mergeArray(r,o)):xn.mergeArray(r,i)}get items(){return this._def.items}rest(t){return new Lo({...this._def,rest:t})}}Lo.create=(e,t)=>{if(!Array.isArray(e))throw new Error("You must pass an array of schemas to z.tuple([ ... ])");return new Lo({items:e,typeName:We.ZodTuple,rest:null,...Ye(t)})};class iS extends it{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(t){const{status:r,ctx:n}=this._processInputParams(t);if(n.parsedType!==be.map)return he(n,{code:ue.invalid_type,expected:be.map,received:n.parsedType}),Ve;const a=this._def.keyType,i=this._def.valueType,o=[...n.data.entries()].map(([s,l],c)=>({key:a._parse(new Wi(n,s,n.path,[c,"key"])),value:i._parse(new Wi(n,l,n.path,[c,"value"]))}));if(n.common.async){const s=new Map;return Promise.resolve().then(async()=>{for(const l of o){const c=await l.key,f=await l.value;if(c.status==="aborted"||f.status==="aborted")return Ve;(c.status==="dirty"||f.status==="dirty")&&r.dirty(),s.set(c.value,f.value)}return{status:r.value,value:s}})}else{const s=new Map;for(const l of o){const c=l.key,f=l.value;if(c.status==="aborted"||f.status==="aborted")return Ve;(c.status==="dirty"||f.status==="dirty")&&r.dirty(),s.set(c.value,f.value)}return{status:r.value,value:s}}}}iS.create=(e,t,r)=>new iS({valueType:t,keyType:e,typeName:We.ZodMap,...Ye(r)});class kc extends it{_parse(t){const{status:r,ctx:n}=this._processInputParams(t);if(n.parsedType!==be.set)return he(n,{code:ue.invalid_type,expected:be.set,received:n.parsedType}),Ve;const a=this._def;a.minSize!==null&&n.data.sizea.maxSize.value&&(he(n,{code:ue.too_big,maximum:a.maxSize.value,type:"set",inclusive:!0,exact:!1,message:a.maxSize.message}),r.dirty());const i=this._def.valueType;function o(l){const c=new Set;for(const f of l){if(f.status==="aborted")return Ve;f.status==="dirty"&&r.dirty(),c.add(f.value)}return{status:r.value,value:c}}const s=[...n.data.values()].map((l,c)=>i._parse(new Wi(n,l,n.path,c)));return n.common.async?Promise.all(s).then(l=>o(l)):o(s)}min(t,r){return new kc({...this._def,minSize:{value:t,message:Se.toString(r)}})}max(t,r){return new kc({...this._def,maxSize:{value:t,message:Se.toString(r)}})}size(t,r){return this.min(t,r).max(t,r)}nonempty(t){return this.min(1,t)}}kc.create=(e,t)=>new kc({valueType:e,minSize:null,maxSize:null,typeName:We.ZodSet,...Ye(t)});class oS extends it{get schema(){return this._def.getter()}_parse(t){const{ctx:r}=this._processInputParams(t);return this._def.getter()._parse({data:r.data,path:r.path,parent:r})}}oS.create=(e,t)=>new oS({getter:e,typeName:We.ZodLazy,...Ye(t)});class sS extends it{_parse(t){if(t.data!==this._def.value){const r=this._getOrReturnCtx(t);return he(r,{received:r.data,code:ue.invalid_literal,expected:this._def.value}),Ve}return{status:"valid",value:t.data}}get value(){return this._def.value}}sS.create=(e,t)=>new sS({value:e,typeName:We.ZodLiteral,...Ye(t)});function zE(e,t){return new nl({values:e,typeName:We.ZodEnum,...Ye(t)})}class nl extends it{_parse(t){if(typeof t.data!="string"){const r=this._getOrReturnCtx(t),n=this._def.values;return he(r,{expected:ot.joinValues(n),received:r.parsedType,code:ue.invalid_type}),Ve}if(this._cache||(this._cache=new Set(this._def.values)),!this._cache.has(t.data)){const r=this._getOrReturnCtx(t),n=this._def.values;return he(r,{received:r.data,code:ue.invalid_enum_value,options:n}),Ve}return Fn(t.data)}get options(){return this._def.values}get enum(){const t={};for(const r of this._def.values)t[r]=r;return t}get Values(){const t={};for(const r of this._def.values)t[r]=r;return t}get Enum(){const t={};for(const r of this._def.values)t[r]=r;return t}extract(t,r=this._def){return nl.create(t,{...this._def,...r})}exclude(t,r=this._def){return nl.create(this.options.filter(n=>!t.includes(n)),{...this._def,...r})}}nl.create=zE;class lS extends it{_parse(t){const r=ot.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(t);if(n.parsedType!==be.string&&n.parsedType!==be.number){const a=ot.objectValues(r);return he(n,{expected:ot.joinValues(a),received:n.parsedType,code:ue.invalid_type}),Ve}if(this._cache||(this._cache=new Set(ot.getValidEnumValues(this._def.values))),!this._cache.has(t.data)){const a=ot.objectValues(r);return he(n,{received:n.data,code:ue.invalid_enum_value,options:a}),Ve}return Fn(t.data)}get enum(){return this._def.values}}lS.create=(e,t)=>new lS({values:e,typeName:We.ZodNativeEnum,...Ye(t)});class Ap extends it{unwrap(){return this._def.type}_parse(t){const{ctx:r}=this._processInputParams(t);if(r.parsedType!==be.promise&&r.common.async===!1)return he(r,{code:ue.invalid_type,expected:be.promise,received:r.parsedType}),Ve;const n=r.parsedType===be.promise?r.data:Promise.resolve(r.data);return Fn(n.then(a=>this._def.type.parseAsync(a,{path:r.path,errorMap:r.common.contextualErrorMap})))}}Ap.create=(e,t)=>new Ap({type:e,typeName:We.ZodPromise,...Ye(t)});class Fo extends it{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===We.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(t){const{status:r,ctx:n}=this._processInputParams(t),a=this._def.effect||null,i={addIssue:o=>{he(n,o),o.fatal?r.abort():r.dirty()},get path(){return n.path}};if(i.addIssue=i.addIssue.bind(i),a.type==="preprocess"){const o=a.transform(n.data,i);if(n.common.async)return Promise.resolve(o).then(async s=>{if(r.value==="aborted")return Ve;const l=await this._def.schema._parseAsync({data:s,path:n.path,parent:n});return l.status==="aborted"?Ve:l.status==="dirty"||r.value==="dirty"?Pu(l.value):l});{if(r.value==="aborted")return Ve;const s=this._def.schema._parseSync({data:o,path:n.path,parent:n});return s.status==="aborted"?Ve:s.status==="dirty"||r.value==="dirty"?Pu(s.value):s}}if(a.type==="refinement"){const o=s=>{const l=a.refinement(s,i);if(n.common.async)return Promise.resolve(l);if(l instanceof Promise)throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");return s};if(n.common.async===!1){const s=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return s.status==="aborted"?Ve:(s.status==="dirty"&&r.dirty(),o(s.value),{status:r.value,value:s.value})}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(s=>s.status==="aborted"?Ve:(s.status==="dirty"&&r.dirty(),o(s.value).then(()=>({status:r.value,value:s.value}))))}if(a.type==="transform")if(n.common.async===!1){const o=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!tl(o))return Ve;const s=a.transform(o.value,i);if(s instanceof Promise)throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.");return{status:r.value,value:s}}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(o=>tl(o)?Promise.resolve(a.transform(o.value,i)).then(s=>({status:r.value,value:s})):Ve);ot.assertNever(a)}}Fo.create=(e,t,r)=>new Fo({schema:e,typeName:We.ZodEffects,effect:t,...Ye(r)});Fo.createWithPreprocess=(e,t,r)=>new Fo({schema:t,effect:{type:"preprocess",transform:e},typeName:We.ZodEffects,...Ye(r)});class Di extends it{_parse(t){return this._getType(t)===be.undefined?Fn(void 0):this._def.innerType._parse(t)}unwrap(){return this._def.innerType}}Di.create=(e,t)=>new Di({innerType:e,typeName:We.ZodOptional,...Ye(t)});class al extends it{_parse(t){return this._getType(t)===be.null?Fn(null):this._def.innerType._parse(t)}unwrap(){return this._def.innerType}}al.create=(e,t)=>new al({innerType:e,typeName:We.ZodNullable,...Ye(t)});class Yg extends it{_parse(t){const{ctx:r}=this._processInputParams(t);let n=r.data;return r.parsedType===be.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:r.path,parent:r})}removeDefault(){return this._def.innerType}}Yg.create=(e,t)=>new Yg({innerType:e,typeName:We.ZodDefault,defaultValue:typeof t.default=="function"?t.default:()=>t.default,...Ye(t)});class Zg extends it{_parse(t){const{ctx:r}=this._processInputParams(t),n={...r,common:{...r.common,issues:[]}},a=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return Sp(a)?a.then(i=>({status:"valid",value:i.status==="valid"?i.value:this._def.catchValue({get error(){return new ei(n.common.issues)},input:n.data})})):{status:"valid",value:a.status==="valid"?a.value:this._def.catchValue({get error(){return new ei(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}}Zg.create=(e,t)=>new Zg({innerType:e,typeName:We.ZodCatch,catchValue:typeof t.catch=="function"?t.catch:()=>t.catch,...Ye(t)});class uS extends it{_parse(t){if(this._getType(t)!==be.nan){const n=this._getOrReturnCtx(t);return he(n,{code:ue.invalid_type,expected:be.nan,received:n.parsedType}),Ve}return{status:"valid",value:t.data}}}uS.create=e=>new uS({typeName:We.ZodNaN,...Ye(e)});class S4 extends it{_parse(t){const{ctx:r}=this._processInputParams(t),n=r.data;return this._def.type._parse({data:n,path:r.path,parent:r})}unwrap(){return this._def.type}}class Bb extends it{_parse(t){const{status:r,ctx:n}=this._processInputParams(t);if(n.common.async)return(async()=>{const i=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return i.status==="aborted"?Ve:i.status==="dirty"?(r.dirty(),Pu(i.value)):this._def.out._parseAsync({data:i.value,path:n.path,parent:n})})();{const a=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return a.status==="aborted"?Ve:a.status==="dirty"?(r.dirty(),{status:"dirty",value:a.value}):this._def.out._parseSync({data:a.value,path:n.path,parent:n})}}static create(t,r){return new Bb({in:t,out:r,typeName:We.ZodPipeline})}}class Qg extends it{_parse(t){const r=this._def.innerType._parse(t),n=a=>(tl(a)&&(a.value=Object.freeze(a.value)),a);return Sp(r)?r.then(a=>n(a)):n(r)}unwrap(){return this._def.innerType}}Qg.create=(e,t)=>new Qg({innerType:e,typeName:We.ZodReadonly,...Ye(t)});var We;(function(e){e.ZodString="ZodString",e.ZodNumber="ZodNumber",e.ZodNaN="ZodNaN",e.ZodBigInt="ZodBigInt",e.ZodBoolean="ZodBoolean",e.ZodDate="ZodDate",e.ZodSymbol="ZodSymbol",e.ZodUndefined="ZodUndefined",e.ZodNull="ZodNull",e.ZodAny="ZodAny",e.ZodUnknown="ZodUnknown",e.ZodNever="ZodNever",e.ZodVoid="ZodVoid",e.ZodArray="ZodArray",e.ZodObject="ZodObject",e.ZodUnion="ZodUnion",e.ZodDiscriminatedUnion="ZodDiscriminatedUnion",e.ZodIntersection="ZodIntersection",e.ZodTuple="ZodTuple",e.ZodRecord="ZodRecord",e.ZodMap="ZodMap",e.ZodSet="ZodSet",e.ZodFunction="ZodFunction",e.ZodLazy="ZodLazy",e.ZodLiteral="ZodLiteral",e.ZodEnum="ZodEnum",e.ZodEffects="ZodEffects",e.ZodNativeEnum="ZodNativeEnum",e.ZodOptional="ZodOptional",e.ZodNullable="ZodNullable",e.ZodDefault="ZodDefault",e.ZodCatch="ZodCatch",e.ZodPromise="ZodPromise",e.ZodBranded="ZodBranded",e.ZodPipeline="ZodPipeline",e.ZodReadonly="ZodReadonly"})(We||(We={}));const cS=Ua.create,qu=Mo.create;Do.create;jp.create;rl.create;Hi.create;const j4=ya.create,BE=Xt.create;kp.create;Op.create;Lo.create;nl.create;Ap.create;Di.create;al.create;const Ku=Fo.createWithPreprocess,UE={string:e=>Ua.create({...e,coerce:!0}),number:e=>Mo.create({...e,coerce:!0}),boolean:e=>jp.create({...e,coerce:!0}),bigint:e=>Do.create({...e,coerce:!0}),date:e=>rl.create({...e,coerce:!0})},k4={primary:"bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 shadow-sm border border-transparent dark:bg-white dark:text-black dark:hover:bg-slate-200",secondary:"bg-white text-slate-700 border border-slate-200 hover:bg-slate-50 hover:text-slate-900 disabled:opacity-40 shadow-sm dark:bg-[#121214] dark:text-[#a1a1aa] dark:border-[#27272a] dark:hover:bg-[#18181b] dark:hover:text-white",ghost:"text-slate-500 hover:text-slate-900 hover:bg-slate-100 disabled:opacity-40 dark:text-[#a1a1aa] dark:hover:text-white dark:hover:bg-[#121214]",danger:"bg-red-50 text-red-600 hover:bg-red-100 border border-red-200 disabled:opacity-40 dark:bg-[#2a0505] dark:text-red-500 dark:hover:bg-[#380808] dark:border-[#5c1c1c]"},O4={sm:"px-4 py-1.5 text-xs font-semibold",md:"px-5 py-2.5 text-sm font-semibold"};function Ce({variant:e="primary",size:t="md",className:r="",...n}){return u.jsx("button",{className:`relative inline-flex items-center justify-center gap-2 rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-500/50 focus:ring-offset-1 focus:ring-offset-transparent focus:border-transparent ${k4[e]} ${O4[t]} ${r}`,...n,children:n.children})}function Xr({error:e}){return e?u.jsx("div",{className:"rounded-lg border border-red-500/20 bg-red-500/8 px-4 py-3 text-sm text-red-400 font-mono",children:e}):null}function pr({size:e=20}){return u.jsxs("svg",{className:"animate-spin text-brand-500",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[u.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),u.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"})]})}const A4={SR_SELECTION_V3_ROUTING:"bg-blue-100 text-blue-800",PRIORITY_LOGIC:"bg-purple-100 text-purple-800",NTW_BASED_ROUTING:"bg-green-100 text-green-800",SR_SELECTION_V3_ROUTING_WITH_HEDGING:"bg-orange-100 text-orange-800",HEDGING:"bg-orange-100 text-orange-800"},N4=["card","card_redirect","pay_later","wallet","bank_redirect","bank_transfer","crypto","bank_debit","reward","real_time_payment","upi","voucher","gift_card","open_banking","mobile_payment"],E4={card:["credit","debit"],bank_debit:["ach","sepa","bacs","becs"],bank_transfer:["ach","sepa","sepa_bank_transfer","bacs","multibanco","pix","pse","permata_bank_transfer","bca_bank_transfer","bni_va","bri_va","cimb_va","danamon_va","mandiri_va","local_bank_transfer","instant_bank_transfer"],wallet:["amazon_pay","apple_pay","google_pay","paypal","ali_pay","ali_pay_hk","dana","mb_way","mobile_pay","samsung_pay","twint","vipps","touch_n_go","swish","we_chat_pay","go_pay","gcash","momo","kakao_pay","cashapp","mifinity","paze"],pay_later:["affirm","alma","afterpay_clearpay","klarna","pay_bright","atome","walley"],upi:["upi_collect","upi_intent"],voucher:["boleto","efecty","pago_efectivo","red_compra","red_pagos","indomaret","alfamart","oxxo","seven_eleven","lawson","mini_stop","family_mart","seicomart","pay_easy"],bank_redirect:["giropay","ideal","sofort","eft","eps","bancontact_card","blik","local_bank_redirect","online_banking_thailand","online_banking_czech_republic","online_banking_finland","online_banking_fpx","online_banking_poland","online_banking_slovakia","przelewy24","trustly","bizum","interac","open_banking_uk","open_banking_pis"],gift_card:["givex","pay_safe_card"],card_redirect:["knet","benefit","momo_atm","card_redirect"],real_time_payment:["fps","duit_now","prompt_pay","viet_qr"],crypto:["crypto_currency"],reward:["evoucher","classic_reward"],open_banking:["open_banking_pis"],mobile_payment:["direct_carrier_billing"]},P4=BE({paymentMethodType:cS().min(1),paymentMethod:cS().min(1),bucketSize:UE.number().int().positive(),hedgingPercent:Ku(e=>e===""||e===null?null:Number(e),qu().nullable()),latencyThreshold:Ku(e=>e===""||e===null?null:Number(e),qu().nullable())}),C4=BE({defaultBucketSize:UE.number().int().positive(),defaultSuccessRate:Ku(e=>e===""||e===null?null:Number(e),qu().min(0).max(1).nullable()),defaultLatencyThreshold:Ku(e=>e===""||e===null?null:Number(e),qu().nullable()),defaultHedgingPercent:Ku(e=>e===""||e===null?null:Number(e),qu().nullable()),subLevelInputConfig:j4(P4)});function T4(){var C,M,B,V,I;const{merchantId:e}=ka(),[t,r]=j.useState(!1),[n,a]=j.useState(null),[i,o]=j.useState(!1),[s,l]=j.useState(!1),[c,f]=j.useState(!1),[d,p]=j.useState(null),{data:h,isLoading:x,mutate:v}=Bt(e?["rule-sr",e]:null,()=>Et("/rule/get",{merchant_id:e,algorithm:"successRate"}),{shouldRetryOnError:!1}),{register:y,control:g,handleSubmit:m,reset:w,watch:S,formState:{errors:b}}=qL({resolver:ZL(C4),defaultValues:{defaultBucketSize:200,defaultSuccessRate:.5,defaultLatencyThreshold:null,defaultHedgingPercent:null,subLevelInputConfig:[]}});j.useEffect(()=>{var D;if((D=h==null?void 0:h.config)!=null&&D.data){const U=h.config.data;w({defaultBucketSize:U.defaultBucketSize??200,defaultSuccessRate:U.defaultSuccessRate??.5,defaultLatencyThreshold:U.defaultLatencyThreshold??null,defaultHedgingPercent:U.defaultHedgingPercent??null,subLevelInputConfig:U.subLevelInputConfig??[]})}},[h,w]);const{fields:_,append:k,remove:O}=GL({control:g,name:"subLevelInputConfig"}),N=S("subLevelInputConfig");async function T(){try{await Et("/merchant-account/create",{merchant_id:e,gateway_success_rate_based_decider_input:null})}catch{}}async function R(D){if(!e){a("Set a Merchant ID first.");return}r(!0),a(null),o(!1);try{await T(),await Et(h?"/rule/update":"/rule/create",{merchant_id:e,config:{type:"successRate",data:{defaultBucketSize:D.defaultBucketSize,defaultSuccessRate:D.defaultSuccessRate,defaultLatencyThreshold:D.defaultLatencyThreshold,defaultHedgingPercent:D.defaultHedgingPercent,subLevelInputConfig:D.subLevelInputConfig.length>0?D.subLevelInputConfig:null}}}),o(!0),v()}catch(U){a(U instanceof Error?U.message:String(U))}finally{r(!1)}}async function A(){if(e){f(!0),p(null);try{await Et("/rule/delete",{merchant_id:e,algorithm:"successRate"}),v(void 0,{revalidate:!1})}catch(D){p(D instanceof Error?D.message:String(D))}finally{f(!1)}}}return u.jsxs("div",{className:"space-y-6 max-w-5xl",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900",children:"Auth-Rate Based Routing"}),u.jsx("p",{className:"text-sm text-slate-500 mt-1",children:"Configure success-rate based gateway routing"})]}),!e&&u.jsx("div",{className:"rounded-lg border border-yellow-200 bg-yellow-50 px-4 py-3 text-sm text-yellow-800",children:"Set a Merchant ID in the top bar to load and save configuration."}),e&&!x&&u.jsxs(Ae,{children:[u.jsxs(nt,{className:"flex flex-row items-center justify-between",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Configuration Status"}),u.jsx("p",{className:"text-xs text-slate-500 mt-0.5",children:(C=h==null?void 0:h.config)!=null&&C.data?"Success Rate routing is configured and active":"No Success Rate configuration found"})]}),u.jsx(Ue,{variant:(M=h==null?void 0:h.config)!=null&&M.data?"green":"gray",children:(B=h==null?void 0:h.config)!=null&&B.data?"Active":"Not Configured"})]}),((V=h==null?void 0:h.config)==null?void 0:V.data)&&u.jsxs(Ge,{className:"border-t border-slate-100 dark:border-[#222226]",children:[u.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-600",children:[u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Last Modified:"}),u.jsx("span",{className:"ml-1 font-medium",children:h.modified_at?new Date(h.modified_at).toLocaleString():"Unknown"})]}),u.jsxs(Ce,{type:"button",variant:"secondary",size:"sm",onClick:()=>{confirm("Are you sure you want to clear the Success Rate configuration? This will disable SR-based routing.")&&A()},disabled:c,children:[u.jsx(Ja,{size:14,className:"mr-1"}),c?"Clearing...":"Clear Configuration"]})]}),d&&u.jsx("p",{className:"text-xs text-red-500 mt-2",children:d})]})]}),x?u.jsx("div",{className:"flex justify-center py-12",children:u.jsx(pr,{})}):u.jsxs("form",{onSubmit:m(R),className:"space-y-6",children:[u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Default Success Rate Config"}),u.jsx("p",{className:"text-xs text-slate-500 mt-0.5",children:"Base settings used when there is no payment-method-specific override."})]})}),u.jsxs(Ge,{className:"grid gap-4 md:grid-cols-2 xl:grid-cols-4",children:[u.jsxs("label",{className:"space-y-1",children:[u.jsx("span",{className:"text-xs text-slate-500",children:"Bucket Size"}),u.jsx("input",{type:"number",...y("defaultBucketSize"),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 w-full focus:outline-none focus:ring-1 focus:ring-brand-500"}),b.defaultBucketSize&&u.jsx("p",{className:"text-xs text-red-500",children:b.defaultBucketSize.message})]}),u.jsxs("label",{className:"space-y-1",children:[u.jsx("span",{className:"text-xs text-slate-500",children:"Success Rate"}),u.jsx("input",{type:"number",step:"0.1",min:"0",max:"1",...y("defaultSuccessRate"),placeholder:"0.5",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 w-full focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("label",{className:"space-y-1",children:[u.jsx("span",{className:"text-xs text-slate-500",children:"Hedging %"}),u.jsx("input",{type:"number",step:"0.1",...y("defaultHedgingPercent"),placeholder:"null",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 w-full focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("label",{className:"space-y-1",children:[u.jsx("span",{className:"text-xs text-slate-500",children:"Latency Threshold (ms)"}),u.jsx("input",{type:"number",...y("defaultLatencyThreshold"),placeholder:"null",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 w-full focus:outline-none focus:ring-1 focus:ring-brand-500"})]})]})]}),u.jsxs(Ae,{children:[u.jsxs(nt,{className:"flex flex-row items-center justify-between",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Sub-Level Overrides"}),u.jsx("p",{className:"text-xs text-slate-500 mt-0.5",children:"Optional overrides for specific payment method type and method combinations."})]}),u.jsxs(Ce,{type:"button",variant:"secondary",size:"sm",onClick:()=>k({paymentMethodType:"card",paymentMethod:"credit",bucketSize:20,hedgingPercent:null,latencyThreshold:null}),children:[u.jsx(Ui,{size:14})," Add Level"]})]}),u.jsx(Ge,{className:"overflow-x-auto p-0",children:_.length?u.jsxs("table",{className:"w-full text-sm",children:[u.jsx("thead",{children:u.jsxs("tr",{className:"text-left text-xs text-slate-500 border-b border-slate-200 dark:border-[#1c1c24] bg-slate-50 dark:bg-[#0a0a0f]",children:[u.jsx("th",{className:"px-4 py-2",children:"Payment Method Type"}),u.jsx("th",{className:"px-4 py-2",children:"Payment Method"}),u.jsx("th",{className:"px-4 py-2",children:"Bucket Size"}),u.jsx("th",{className:"px-4 py-2",children:"Hedging %"}),u.jsx("th",{className:"px-4 py-2",children:"Latency Threshold (ms)"}),u.jsx("th",{className:"px-4 py-2"})]})}),u.jsx("tbody",{children:_.map((D,U)=>{var K;const G=((K=N==null?void 0:N[U])==null?void 0:K.paymentMethodType)||"",q=E4[G]||[];return u.jsxs("tr",{className:"border-b border-slate-200 dark:border-[#1c1c24] hover:bg-slate-100 dark:bg-[#0f0f16] transition-colors",children:[u.jsx("td",{className:"px-4 py-2",children:u.jsx("select",{...y(`subLevelInputConfig.${U}.paymentMethodType`),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:N4.map(te=>u.jsx("option",{value:te,children:te},te))})}),u.jsx("td",{className:"px-4 py-2",children:u.jsx("select",{...y(`subLevelInputConfig.${U}.paymentMethod`),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:(q.length?q:["credit","debit"]).map(te=>u.jsx("option",{value:te,children:te},te))})}),u.jsx("td",{className:"px-4 py-2",children:u.jsx("input",{type:"number",...y(`subLevelInputConfig.${U}.bucketSize`),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 w-20 focus:outline-none focus:ring-1 focus:ring-brand-500"})}),u.jsx("td",{className:"px-4 py-2",children:u.jsx("input",{type:"number",step:"0.1",...y(`subLevelInputConfig.${U}.hedgingPercent`),placeholder:"null",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 w-20 focus:outline-none focus:ring-1 focus:ring-brand-500"})}),u.jsx("td",{className:"px-4 py-2",children:u.jsx("input",{type:"number",...y(`subLevelInputConfig.${U}.latencyThreshold`),placeholder:"null",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 w-24 focus:outline-none focus:ring-1 focus:ring-brand-500"})}),u.jsx("td",{className:"px-4 py-2",children:u.jsx("button",{type:"button",onClick:()=>O(U),className:"text-slate-400 hover:text-red-500",children:u.jsx(Ja,{size:14})})})]},D.id)})})]}):u.jsx("div",{className:"px-4 py-8 text-sm text-slate-500",children:"No sub-level overrides configured. The default row above is the only active configuration."})})]}),u.jsx(Xr,{error:n}),i&&u.jsx("div",{className:"rounded-lg border border-emerald-500/20 bg-emerald-500/8 px-4 py-3 text-sm text-emerald-400",children:"Configuration saved successfully."}),((I=h==null?void 0:h.config)==null?void 0:I.data)&&u.jsxs(Ae,{children:[u.jsxs(nt,{className:"flex flex-row items-center justify-between",children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Current Active Configuration"}),u.jsxs(Ce,{type:"button",variant:"ghost",size:"sm",onClick:()=>l(!s),children:[u.jsx(em,{size:14,className:"mr-1"}),s?"Hide":"View"]})]}),s&&u.jsx(Ge,{children:u.jsxs("div",{className:"text-xs text-slate-600 space-y-4",children:[u.jsxs("div",{className:"border-b border-slate-200 dark:border-[#222226] pb-3",children:[u.jsx("h3",{className:"font-medium text-slate-700 mb-2",children:"Default Settings"}),u.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-5 gap-3",children:[u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Bucket Size:"}),u.jsx("p",{className:"font-medium",children:h.config.data.defaultBucketSize})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Success Rate:"}),u.jsx("p",{className:"font-medium",children:h.config.data.defaultSuccessRate??"Not set"})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Hedging %:"}),u.jsx("p",{className:"font-medium",children:h.config.data.defaultHedgingPercent??"Not set"})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Latency Threshold:"}),u.jsxs("p",{className:"font-medium",children:[h.config.data.defaultLatencyThreshold??"Not set"," ms"]})]})]})]}),h.config.data.subLevelInputConfig&&h.config.data.subLevelInputConfig.length>0&&u.jsxs("div",{children:[u.jsx("h3",{className:"font-medium text-slate-700 mb-2",children:"Sub-Level Configurations"}),u.jsx("div",{className:"space-y-2",children:h.config.data.subLevelInputConfig.map((D,U)=>u.jsx("div",{className:"bg-slate-50 dark:bg-[#151518] rounded-lg p-3",children:u.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-5 gap-2 text-xs",children:[u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Payment Type:"}),u.jsx("p",{className:"font-medium capitalize",children:D.paymentMethodType})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Payment Method:"}),u.jsx("p",{className:"font-medium",children:D.paymentMethod})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Bucket Size:"}),u.jsx("p",{className:"font-medium",children:D.bucketSize})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Hedging %:"}),u.jsx("p",{className:"font-medium",children:D.hedgingPercent??"Default"})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-slate-500",children:"Latency Threshold:"}),u.jsxs("p",{className:"font-medium",children:[D.latencyThreshold??"Default"," ms"]})]})]})},U))})]}),u.jsxs("div",{className:"border-t border-gray-200 pt-3",children:[u.jsx("h3",{className:"font-medium text-slate-700 mb-2",children:"Raw Configuration (JSON)"}),u.jsx("pre",{className:"bg-slate-900 dark:bg-[#0f0f11] text-slate-100 border border-transparent dark:border-[#222226] rounded-lg p-3 text-xs overflow-auto max-h-64",children:JSON.stringify(h.config,null,2)})]})]})})]}),u.jsx(Ce,{type:"submit",disabled:t||!e,children:t?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Saving…"]}):"Save Configuration"})]})]})}function $4(){for(var e=arguments.length,t=new Array(e),r=0;rn=>{t.forEach(a=>a(n))},t)}const am=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Il(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function Ub(e){return"nodeType"in e}function rn(e){var t,r;return e?Il(e)?e:Ub(e)&&(t=(r=e.ownerDocument)==null?void 0:r.defaultView)!=null?t:window:window}function Vb(e){const{Document:t}=rn(e);return e instanceof t}function _d(e){return Il(e)?!1:e instanceof rn(e).HTMLElement}function VE(e){return e instanceof rn(e).SVGElement}function Ml(e){return e?Il(e)?e.document:Ub(e)?Vb(e)?e:_d(e)||VE(e)?e.ownerDocument:document:document:document}const Sa=am?j.useLayoutEffect:j.useEffect;function Wb(e){const t=j.useRef(e);return Sa(()=>{t.current=e}),j.useCallback(function(){for(var r=arguments.length,n=new Array(r),a=0;a{e.current=setInterval(n,a)},[]),r=j.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,r]}function Oc(e,t){t===void 0&&(t=[e]);const r=j.useRef(e);return Sa(()=>{r.current!==e&&(r.current=e)},t),r}function Sd(e,t){const r=j.useRef();return j.useMemo(()=>{const n=e(r.current);return r.current=n,n},[...t])}function Np(e){const t=Wb(e),r=j.useRef(null),n=j.useCallback(a=>{a!==r.current&&(t==null||t(a,r.current)),r.current=a},[]);return[r,n]}function Jg(e){const t=j.useRef();return j.useEffect(()=>{t.current=e},[e]),t.current}let Yv={};function jd(e,t){return j.useMemo(()=>{if(t)return t;const r=Yv[e]==null?0:Yv[e]+1;return Yv[e]=r,e+"-"+r},[e,t])}function WE(e){return function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),a=1;a{const s=Object.entries(o);for(const[l,c]of s){const f=i[l];f!=null&&(i[l]=f+e*c)}return i},{...t})}}const Vs=WE(1),Ac=WE(-1);function I4(e){return"clientX"in e&&"clientY"in e}function Hb(e){if(!e)return!1;const{KeyboardEvent:t}=rn(e.target);return t&&e instanceof t}function M4(e){if(!e)return!1;const{TouchEvent:t}=rn(e.target);return t&&e instanceof t}function ex(e){if(M4(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:r}=e.touches[0];return{x:t,y:r}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:r}=e.changedTouches[0];return{x:t,y:r}}}return I4(e)?{x:e.clientX,y:e.clientY}:null}const Nc=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:r}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(r?Math.round(r):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:r}=e;return"scaleX("+t+") scaleY("+r+")"}},Transform:{toString(e){if(e)return[Nc.Translate.toString(e),Nc.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:r,easing:n}=e;return t+" "+r+"ms "+n}}}),dS="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function D4(e){return e.matches(dS)?e:e.querySelector(dS)}const L4={display:"none"};function F4(e){let{id:t,value:r}=e;return E.createElement("div",{id:t,style:L4},r)}function z4(e){let{id:t,announcement:r,ariaLiveType:n="assertive"}=e;const a={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return E.createElement("div",{id:t,style:a,role:"status","aria-live":n,"aria-atomic":!0},r)}function B4(){const[e,t]=j.useState("");return{announce:j.useCallback(n=>{n!=null&&t(n)},[]),announcement:e}}const HE=j.createContext(null);function U4(e){const t=j.useContext(HE);j.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of ");return t(e)},[e,t])}function V4(){const[e]=j.useState(()=>new Set),t=j.useCallback(n=>(e.add(n),()=>e.delete(n)),[e]);return[j.useCallback(n=>{let{type:a,event:i}=n;e.forEach(o=>{var s;return(s=o[a])==null?void 0:s.call(o,i)})},[e]),t]}const W4={draggable:` + To pick up a draggable item, press the space bar. + While dragging, use the arrow keys to move the item. + Press space again to drop the item in its new position, or press escape to cancel. + `},H4={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was moved over droppable area "+r.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was dropped over droppable area "+r.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function G4(e){let{announcements:t=H4,container:r,hiddenTextDescribedById:n,screenReaderInstructions:a=W4}=e;const{announce:i,announcement:o}=B4(),s=jd("DndLiveRegion"),[l,c]=j.useState(!1);if(j.useEffect(()=>{c(!0)},[]),U4(j.useMemo(()=>({onDragStart(d){let{active:p}=d;i(t.onDragStart({active:p}))},onDragMove(d){let{active:p,over:h}=d;t.onDragMove&&i(t.onDragMove({active:p,over:h}))},onDragOver(d){let{active:p,over:h}=d;i(t.onDragOver({active:p,over:h}))},onDragEnd(d){let{active:p,over:h}=d;i(t.onDragEnd({active:p,over:h}))},onDragCancel(d){let{active:p,over:h}=d;i(t.onDragCancel({active:p,over:h}))}}),[i,t])),!l)return null;const f=E.createElement(E.Fragment,null,E.createElement(F4,{id:n,value:a.draggable}),E.createElement(z4,{id:s,announcement:o}));return r?Os.createPortal(f,r):f}var ir;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(ir||(ir={}));function Ep(){}function fS(e,t){return j.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function q4(){for(var e=arguments.length,t=new Array(e),r=0;r[...t].filter(n=>n!=null),[...t])}const aa=Object.freeze({x:0,y:0});function GE(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function qE(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return r-n}function K4(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return n-r}function pS(e){let{left:t,top:r,height:n,width:a}=e;return[{x:t,y:r},{x:t+a,y:r},{x:t,y:r+n},{x:t+a,y:r+n}]}function KE(e,t){if(!e||e.length===0)return null;const[r]=e;return r[t]}function hS(e,t,r){return t===void 0&&(t=e.left),r===void 0&&(r=e.top),{x:t+e.width*.5,y:r+e.height*.5}}const X4=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const a=hS(t,t.left,t.top),i=[];for(const o of n){const{id:s}=o,l=r.get(s);if(l){const c=GE(hS(l),a);i.push({id:s,data:{droppableContainer:o,value:c}})}}return i.sort(qE)},Y4=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const a=pS(t),i=[];for(const o of n){const{id:s}=o,l=r.get(s);if(l){const c=pS(l),f=a.reduce((p,h,x)=>p+GE(c[x],h),0),d=Number((f/4).toFixed(4));i.push({id:s,data:{droppableContainer:o,value:d}})}}return i.sort(qE)};function Z4(e,t){const r=Math.max(t.top,e.top),n=Math.max(t.left,e.left),a=Math.min(t.left+t.width,e.left+e.width),i=Math.min(t.top+t.height,e.top+e.height),o=a-n,s=i-r;if(n{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const a=[];for(const i of n){const{id:o}=i,s=r.get(o);if(s){const l=Z4(s,t);l>0&&a.push({id:o,data:{droppableContainer:i,value:l}})}}return a.sort(K4)};function J4(e,t,r){return{...e,scaleX:t&&r?t.width/r.width:1,scaleY:t&&r?t.height/r.height:1}}function XE(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:aa}function e5(e){return function(r){for(var n=arguments.length,a=new Array(n>1?n-1:0),i=1;i({...o,top:o.top+e*s.y,bottom:o.bottom+e*s.y,left:o.left+e*s.x,right:o.right+e*s.x}),{...r})}}const t5=e5(1);function r5(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function n5(e,t,r){const n=r5(t);if(!n)return e;const{scaleX:a,scaleY:i,x:o,y:s}=n,l=e.left-o-(1-a)*parseFloat(r),c=e.top-s-(1-i)*parseFloat(r.slice(r.indexOf(" ")+1)),f=a?e.width/a:e.width,d=i?e.height/i:e.height;return{width:f,height:d,top:c,right:l+f,bottom:c+d,left:l}}const a5={ignoreTransform:!1};function Dl(e,t){t===void 0&&(t=a5);let r=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:f}=rn(e).getComputedStyle(e);c&&(r=n5(r,c,f))}const{top:n,left:a,width:i,height:o,bottom:s,right:l}=r;return{top:n,left:a,width:i,height:o,bottom:s,right:l}}function mS(e){return Dl(e,{ignoreTransform:!0})}function i5(e){const t=e.innerWidth,r=e.innerHeight;return{top:0,left:0,right:t,bottom:r,width:t,height:r}}function o5(e,t){return t===void 0&&(t=rn(e).getComputedStyle(e)),t.position==="fixed"}function s5(e,t){t===void 0&&(t=rn(e).getComputedStyle(e));const r=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(a=>{const i=t[a];return typeof i=="string"?r.test(i):!1})}function im(e,t){const r=[];function n(a){if(t!=null&&r.length>=t||!a)return r;if(Vb(a)&&a.scrollingElement!=null&&!r.includes(a.scrollingElement))return r.push(a.scrollingElement),r;if(!_d(a)||VE(a)||r.includes(a))return r;const i=rn(e).getComputedStyle(a);return a!==e&&s5(a,i)&&r.push(a),o5(a,i)?r:n(a.parentNode)}return e?n(e):r}function YE(e){const[t]=im(e,1);return t??null}function Zv(e){return!am||!e?null:Il(e)?e:Ub(e)?Vb(e)||e===Ml(e).scrollingElement?window:_d(e)?e:null:null}function ZE(e){return Il(e)?e.scrollX:e.scrollLeft}function QE(e){return Il(e)?e.scrollY:e.scrollTop}function tx(e){return{x:ZE(e),y:QE(e)}}var hr;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(hr||(hr={}));function JE(e){return!am||!e?!1:e===document.scrollingElement}function eP(e){const t={x:0,y:0},r=JE(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},n={x:e.scrollWidth-r.width,y:e.scrollHeight-r.height},a=e.scrollTop<=t.y,i=e.scrollLeft<=t.x,o=e.scrollTop>=n.y,s=e.scrollLeft>=n.x;return{isTop:a,isLeft:i,isBottom:o,isRight:s,maxScroll:n,minScroll:t}}const l5={x:.2,y:.2};function u5(e,t,r,n,a){let{top:i,left:o,right:s,bottom:l}=r;n===void 0&&(n=10),a===void 0&&(a=l5);const{isTop:c,isBottom:f,isLeft:d,isRight:p}=eP(e),h={x:0,y:0},x={x:0,y:0},v={height:t.height*a.y,width:t.width*a.x};return!c&&i<=t.top+v.height?(h.y=hr.Backward,x.y=n*Math.abs((t.top+v.height-i)/v.height)):!f&&l>=t.bottom-v.height&&(h.y=hr.Forward,x.y=n*Math.abs((t.bottom-v.height-l)/v.height)),!p&&s>=t.right-v.width?(h.x=hr.Forward,x.x=n*Math.abs((t.right-v.width-s)/v.width)):!d&&o<=t.left+v.width&&(h.x=hr.Backward,x.x=n*Math.abs((t.left+v.width-o)/v.width)),{direction:h,speed:x}}function c5(e){if(e===document.scrollingElement){const{innerWidth:i,innerHeight:o}=window;return{top:0,left:0,right:i,bottom:o,width:i,height:o}}const{top:t,left:r,right:n,bottom:a}=e.getBoundingClientRect();return{top:t,left:r,right:n,bottom:a,width:e.clientWidth,height:e.clientHeight}}function tP(e){return e.reduce((t,r)=>Vs(t,tx(r)),aa)}function d5(e){return e.reduce((t,r)=>t+ZE(r),0)}function f5(e){return e.reduce((t,r)=>t+QE(r),0)}function p5(e,t){if(t===void 0&&(t=Dl),!e)return;const{top:r,left:n,bottom:a,right:i}=t(e);YE(e)&&(a<=0||i<=0||r>=window.innerHeight||n>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const h5=[["x",["left","right"],d5],["y",["top","bottom"],f5]];class Gb{constructor(t,r){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const n=im(r),a=tP(n);this.rect={...t},this.width=t.width,this.height=t.height;for(const[i,o,s]of h5)for(const l of o)Object.defineProperty(this,l,{get:()=>{const c=s(n),f=a[i]-c;return this.rect[l]+f},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Xu{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(r=>{var n;return(n=this.target)==null?void 0:n.removeEventListener(...r)})},this.target=t}add(t,r,n){var a;(a=this.target)==null||a.addEventListener(t,r,n),this.listeners.push([t,r,n])}}function m5(e){const{EventTarget:t}=rn(e);return e instanceof t?e:Ml(e)}function Qv(e,t){const r=Math.abs(e.x),n=Math.abs(e.y);return typeof t=="number"?Math.sqrt(r**2+n**2)>t:"x"in t&&"y"in t?r>t.x&&n>t.y:"x"in t?r>t.x:"y"in t?n>t.y:!1}var Nn;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(Nn||(Nn={}));function vS(e){e.preventDefault()}function v5(e){e.stopPropagation()}var tt;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(tt||(tt={}));const rP={start:[tt.Space,tt.Enter],cancel:[tt.Esc],end:[tt.Space,tt.Enter,tt.Tab]},y5=(e,t)=>{let{currentCoordinates:r}=t;switch(e.code){case tt.Right:return{...r,x:r.x+25};case tt.Left:return{...r,x:r.x-25};case tt.Down:return{...r,y:r.y+25};case tt.Up:return{...r,y:r.y-25}}};class qb{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:r}}=t;this.props=t,this.listeners=new Xu(Ml(r)),this.windowListeners=new Xu(rn(r)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(Nn.Resize,this.handleCancel),this.windowListeners.add(Nn.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(Nn.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:r}=this.props,n=t.node.current;n&&p5(n),r(aa)}handleKeyDown(t){if(Hb(t)){const{active:r,context:n,options:a}=this.props,{keyboardCodes:i=rP,coordinateGetter:o=y5,scrollBehavior:s="smooth"}=a,{code:l}=t;if(i.end.includes(l)){this.handleEnd(t);return}if(i.cancel.includes(l)){this.handleCancel(t);return}const{collisionRect:c}=n.current,f=c?{x:c.left,y:c.top}:aa;this.referenceCoordinates||(this.referenceCoordinates=f);const d=o(t,{active:r,context:n.current,currentCoordinates:f});if(d){const p=Ac(d,f),h={x:0,y:0},{scrollableAncestors:x}=n.current;for(const v of x){const y=t.code,{isTop:g,isRight:m,isLeft:w,isBottom:S,maxScroll:b,minScroll:_}=eP(v),k=c5(v),O={x:Math.min(y===tt.Right?k.right-k.width/2:k.right,Math.max(y===tt.Right?k.left:k.left+k.width/2,d.x)),y:Math.min(y===tt.Down?k.bottom-k.height/2:k.bottom,Math.max(y===tt.Down?k.top:k.top+k.height/2,d.y))},N=y===tt.Right&&!m||y===tt.Left&&!w,T=y===tt.Down&&!S||y===tt.Up&&!g;if(N&&O.x!==d.x){const R=v.scrollLeft+p.x,A=y===tt.Right&&R<=b.x||y===tt.Left&&R>=_.x;if(A&&!p.y){v.scrollTo({left:R,behavior:s});return}A?h.x=v.scrollLeft-R:h.x=y===tt.Right?v.scrollLeft-b.x:v.scrollLeft-_.x,h.x&&v.scrollBy({left:-h.x,behavior:s});break}else if(T&&O.y!==d.y){const R=v.scrollTop+p.y,A=y===tt.Down&&R<=b.y||y===tt.Up&&R>=_.y;if(A&&!p.x){v.scrollTo({top:R,behavior:s});return}A?h.y=v.scrollTop-R:h.y=y===tt.Down?v.scrollTop-b.y:v.scrollTop-_.y,h.y&&v.scrollBy({top:-h.y,behavior:s});break}}this.handleMove(t,Vs(Ac(d,this.referenceCoordinates),h))}}}handleMove(t,r){const{onMove:n}=this.props;t.preventDefault(),n(r)}handleEnd(t){const{onEnd:r}=this.props;t.preventDefault(),this.detach(),r()}handleCancel(t){const{onCancel:r}=this.props;t.preventDefault(),this.detach(),r()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}qb.activators=[{eventName:"onKeyDown",handler:(e,t,r)=>{let{keyboardCodes:n=rP,onActivation:a}=t,{active:i}=r;const{code:o}=e.nativeEvent;if(n.start.includes(o)){const s=i.activatorNode.current;return s&&e.target!==s?!1:(e.preventDefault(),a==null||a({event:e.nativeEvent}),!0)}return!1}}];function yS(e){return!!(e&&"distance"in e)}function gS(e){return!!(e&&"delay"in e)}class Kb{constructor(t,r,n){var a;n===void 0&&(n=m5(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=r;const{event:i}=t,{target:o}=i;this.props=t,this.events=r,this.document=Ml(o),this.documentListeners=new Xu(this.document),this.listeners=new Xu(n),this.windowListeners=new Xu(rn(o)),this.initialCoordinates=(a=ex(i))!=null?a:aa,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:r,bypassActivationConstraint:n}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(Nn.Resize,this.handleCancel),this.windowListeners.add(Nn.DragStart,vS),this.windowListeners.add(Nn.VisibilityChange,this.handleCancel),this.windowListeners.add(Nn.ContextMenu,vS),this.documentListeners.add(Nn.Keydown,this.handleKeydown),r){if(n!=null&&n({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(gS(r)){this.timeoutId=setTimeout(this.handleStart,r.delay),this.handlePending(r);return}if(yS(r)){this.handlePending(r);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,r){const{active:n,onPending:a}=this.props;a(n,t,this.initialCoordinates,r)}handleStart(){const{initialCoordinates:t}=this,{onStart:r}=this.props;t&&(this.activated=!0,this.documentListeners.add(Nn.Click,v5,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(Nn.SelectionChange,this.removeTextSelection),r(t))}handleMove(t){var r;const{activated:n,initialCoordinates:a,props:i}=this,{onMove:o,options:{activationConstraint:s}}=i;if(!a)return;const l=(r=ex(t))!=null?r:aa,c=Ac(a,l);if(!n&&s){if(yS(s)){if(s.tolerance!=null&&Qv(c,s.tolerance))return this.handleCancel();if(Qv(c,s.distance))return this.handleStart()}if(gS(s)&&Qv(c,s.tolerance))return this.handleCancel();this.handlePending(s,c);return}t.cancelable&&t.preventDefault(),o(l)}handleEnd(){const{onAbort:t,onEnd:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleCancel(){const{onAbort:t,onCancel:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleKeydown(t){t.code===tt.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const g5={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class Xb extends Kb{constructor(t){const{event:r}=t,n=Ml(r.target);super(t,g5,n)}}Xb.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return!r.isPrimary||r.button!==0?!1:(n==null||n({event:r}),!0)}}];const x5={move:{name:"mousemove"},end:{name:"mouseup"}};var rx;(function(e){e[e.RightClick=2]="RightClick"})(rx||(rx={}));class b5 extends Kb{constructor(t){super(t,x5,Ml(t.event.target))}}b5.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return r.button===rx.RightClick?!1:(n==null||n({event:r}),!0)}}];const Jv={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class w5 extends Kb{constructor(t){super(t,Jv)}static setup(){return window.addEventListener(Jv.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(Jv.move.name,t)};function t(){}}}w5.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;const{touches:a}=r;return a.length>1?!1:(n==null||n({event:r}),!0)}}];var Yu;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(Yu||(Yu={}));var Pp;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(Pp||(Pp={}));function _5(e){let{acceleration:t,activator:r=Yu.Pointer,canScroll:n,draggingRect:a,enabled:i,interval:o=5,order:s=Pp.TreeOrder,pointerCoordinates:l,scrollableAncestors:c,scrollableAncestorRects:f,delta:d,threshold:p}=e;const h=j5({delta:d,disabled:!i}),[x,v]=R4(),y=j.useRef({x:0,y:0}),g=j.useRef({x:0,y:0}),m=j.useMemo(()=>{switch(r){case Yu.Pointer:return l?{top:l.y,bottom:l.y,left:l.x,right:l.x}:null;case Yu.DraggableRect:return a}},[r,a,l]),w=j.useRef(null),S=j.useCallback(()=>{const _=w.current;if(!_)return;const k=y.current.x*g.current.x,O=y.current.y*g.current.y;_.scrollBy(k,O)},[]),b=j.useMemo(()=>s===Pp.TreeOrder?[...c].reverse():c,[s,c]);j.useEffect(()=>{if(!i||!c.length||!m){v();return}for(const _ of b){if((n==null?void 0:n(_))===!1)continue;const k=c.indexOf(_),O=f[k];if(!O)continue;const{direction:N,speed:T}=u5(_,O,m,t,p);for(const R of["x","y"])h[R][N[R]]||(T[R]=0,N[R]=0);if(T.x>0||T.y>0){v(),w.current=_,x(S,o),y.current=T,g.current=N;return}}y.current={x:0,y:0},g.current={x:0,y:0},v()},[t,S,n,v,i,o,JSON.stringify(m),JSON.stringify(h),x,c,b,f,JSON.stringify(p)])}const S5={x:{[hr.Backward]:!1,[hr.Forward]:!1},y:{[hr.Backward]:!1,[hr.Forward]:!1}};function j5(e){let{delta:t,disabled:r}=e;const n=Jg(t);return Sd(a=>{if(r||!n||!a)return S5;const i={x:Math.sign(t.x-n.x),y:Math.sign(t.y-n.y)};return{x:{[hr.Backward]:a.x[hr.Backward]||i.x===-1,[hr.Forward]:a.x[hr.Forward]||i.x===1},y:{[hr.Backward]:a.y[hr.Backward]||i.y===-1,[hr.Forward]:a.y[hr.Forward]||i.y===1}}},[r,t,n])}function k5(e,t){const r=t!=null?e.get(t):void 0,n=r?r.node.current:null;return Sd(a=>{var i;return t==null?null:(i=n??a)!=null?i:null},[n,t])}function O5(e,t){return j.useMemo(()=>e.reduce((r,n)=>{const{sensor:a}=n,i=a.activators.map(o=>({eventName:o.eventName,handler:t(o.handler,n)}));return[...r,...i]},[]),[e,t])}var Ec;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(Ec||(Ec={}));var nx;(function(e){e.Optimized="optimized"})(nx||(nx={}));const xS=new Map;function A5(e,t){let{dragging:r,dependencies:n,config:a}=t;const[i,o]=j.useState(null),{frequency:s,measure:l,strategy:c}=a,f=j.useRef(e),d=y(),p=Oc(d),h=j.useCallback(function(g){g===void 0&&(g=[]),!p.current&&o(m=>m===null?g:m.concat(g.filter(w=>!m.includes(w))))},[p]),x=j.useRef(null),v=Sd(g=>{if(d&&!r)return xS;if(!g||g===xS||f.current!==e||i!=null){const m=new Map;for(let w of e){if(!w)continue;if(i&&i.length>0&&!i.includes(w.id)&&w.rect.current){m.set(w.id,w.rect.current);continue}const S=w.node.current,b=S?new Gb(l(S),S):null;w.rect.current=b,b&&m.set(w.id,b)}return m}return g},[e,i,r,d,l]);return j.useEffect(()=>{f.current=e},[e]),j.useEffect(()=>{d||h()},[r,d]),j.useEffect(()=>{i&&i.length>0&&o(null)},[JSON.stringify(i)]),j.useEffect(()=>{d||typeof s!="number"||x.current!==null||(x.current=setTimeout(()=>{h(),x.current=null},s))},[s,d,h,...n]),{droppableRects:v,measureDroppableContainers:h,measuringScheduled:i!=null};function y(){switch(c){case Ec.Always:return!1;case Ec.BeforeDragging:return r;default:return!r}}}function nP(e,t){return Sd(r=>e?r||(typeof t=="function"?t(e):e):null,[t,e])}function N5(e,t){return nP(e,t)}function E5(e){let{callback:t,disabled:r}=e;const n=Wb(t),a=j.useMemo(()=>{if(r||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:i}=window;return new i(n)},[n,r]);return j.useEffect(()=>()=>a==null?void 0:a.disconnect(),[a]),a}function om(e){let{callback:t,disabled:r}=e;const n=Wb(t),a=j.useMemo(()=>{if(r||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:i}=window;return new i(n)},[r]);return j.useEffect(()=>()=>a==null?void 0:a.disconnect(),[a]),a}function P5(e){return new Gb(Dl(e),e)}function bS(e,t,r){t===void 0&&(t=P5);const[n,a]=j.useState(null);function i(){a(l=>{if(!e)return null;if(e.isConnected===!1){var c;return(c=l??r)!=null?c:null}const f=t(e);return JSON.stringify(l)===JSON.stringify(f)?l:f})}const o=E5({callback(l){if(e)for(const c of l){const{type:f,target:d}=c;if(f==="childList"&&d instanceof HTMLElement&&d.contains(e)){i();break}}}}),s=om({callback:i});return Sa(()=>{i(),e?(s==null||s.observe(e),o==null||o.observe(document.body,{childList:!0,subtree:!0})):(s==null||s.disconnect(),o==null||o.disconnect())},[e]),n}function C5(e){const t=nP(e);return XE(e,t)}const wS=[];function T5(e){const t=j.useRef(e),r=Sd(n=>e?n&&n!==wS&&e&&t.current&&e.parentNode===t.current.parentNode?n:im(e):wS,[e]);return j.useEffect(()=>{t.current=e},[e]),r}function $5(e){const[t,r]=j.useState(null),n=j.useRef(e),a=j.useCallback(i=>{const o=Zv(i.target);o&&r(s=>s?(s.set(o,tx(o)),new Map(s)):null)},[]);return j.useEffect(()=>{const i=n.current;if(e!==i){o(i);const s=e.map(l=>{const c=Zv(l);return c?(c.addEventListener("scroll",a,{passive:!0}),[c,tx(c)]):null}).filter(l=>l!=null);r(s.length?new Map(s):null),n.current=e}return()=>{o(e),o(i)};function o(s){s.forEach(l=>{const c=Zv(l);c==null||c.removeEventListener("scroll",a)})}},[a,e]),j.useMemo(()=>e.length?t?Array.from(t.values()).reduce((i,o)=>Vs(i,o),aa):tP(e):aa,[e,t])}function _S(e,t){t===void 0&&(t=[]);const r=j.useRef(null);return j.useEffect(()=>{r.current=null},t),j.useEffect(()=>{const n=e!==aa;n&&!r.current&&(r.current=e),!n&&r.current&&(r.current=null)},[e]),r.current?Ac(e,r.current):aa}function R5(e){j.useEffect(()=>{if(!am)return;const t=e.map(r=>{let{sensor:n}=r;return n.setup==null?void 0:n.setup()});return()=>{for(const r of t)r==null||r()}},e.map(t=>{let{sensor:r}=t;return r}))}function I5(e,t){return j.useMemo(()=>e.reduce((r,n)=>{let{eventName:a,handler:i}=n;return r[a]=o=>{i(o,t)},r},{}),[e,t])}function aP(e){return j.useMemo(()=>e?i5(e):null,[e])}const SS=[];function M5(e,t){t===void 0&&(t=Dl);const[r]=e,n=aP(r?rn(r):null),[a,i]=j.useState(SS);function o(){i(()=>e.length?e.map(l=>JE(l)?n:new Gb(t(l),l)):SS)}const s=om({callback:o});return Sa(()=>{s==null||s.disconnect(),o(),e.forEach(l=>s==null?void 0:s.observe(l))},[e]),a}function D5(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return _d(t)?t:e}function L5(e){let{measure:t}=e;const[r,n]=j.useState(null),a=j.useCallback(c=>{for(const{target:f}of c)if(_d(f)){n(d=>{const p=t(f);return d?{...d,width:p.width,height:p.height}:p});break}},[t]),i=om({callback:a}),o=j.useCallback(c=>{const f=D5(c);i==null||i.disconnect(),f&&(i==null||i.observe(f)),n(f?t(f):null)},[t,i]),[s,l]=Np(o);return j.useMemo(()=>({nodeRef:s,rect:r,setRef:l}),[r,s,l])}const F5=[{sensor:Xb,options:{}},{sensor:qb,options:{}}],z5={current:{}},Wf={draggable:{measure:mS},droppable:{measure:mS,strategy:Ec.WhileDragging,frequency:nx.Optimized},dragOverlay:{measure:Dl}};class Zu extends Map{get(t){var r;return t!=null&&(r=super.get(t))!=null?r:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:r}=t;return!r})}getNodeFor(t){var r,n;return(r=(n=this.get(t))==null?void 0:n.node.current)!=null?r:void 0}}const B5={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new Zu,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:Ep},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:Wf,measureDroppableContainers:Ep,windowRect:null,measuringScheduled:!1},U5={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:Ep,draggableNodes:new Map,over:null,measureDroppableContainers:Ep},sm=j.createContext(U5),iP=j.createContext(B5);function V5(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Zu}}}function W5(e,t){switch(t.type){case ir.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case ir.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case ir.DragEnd:case ir.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case ir.RegisterDroppable:{const{element:r}=t,{id:n}=r,a=new Zu(e.droppable.containers);return a.set(n,r),{...e,droppable:{...e.droppable,containers:a}}}case ir.SetDroppableDisabled:{const{id:r,key:n,disabled:a}=t,i=e.droppable.containers.get(r);if(!i||n!==i.key)return e;const o=new Zu(e.droppable.containers);return o.set(r,{...i,disabled:a}),{...e,droppable:{...e.droppable,containers:o}}}case ir.UnregisterDroppable:{const{id:r,key:n}=t,a=e.droppable.containers.get(r);if(!a||n!==a.key)return e;const i=new Zu(e.droppable.containers);return i.delete(r),{...e,droppable:{...e.droppable,containers:i}}}default:return e}}function H5(e){let{disabled:t}=e;const{active:r,activatorEvent:n,draggableNodes:a}=j.useContext(sm),i=Jg(n),o=Jg(r==null?void 0:r.id);return j.useEffect(()=>{if(!t&&!n&&i&&o!=null){if(!Hb(i)||document.activeElement===i.target)return;const s=a.get(o);if(!s)return;const{activatorNode:l,node:c}=s;if(!l.current&&!c.current)return;requestAnimationFrame(()=>{for(const f of[l.current,c.current]){if(!f)continue;const d=D4(f);if(d){d.focus();break}}})}},[n,t,a,o,i]),null}function G5(e,t){let{transform:r,...n}=t;return e!=null&&e.length?e.reduce((a,i)=>i({transform:a,...n}),r):r}function q5(e){return j.useMemo(()=>({draggable:{...Wf.draggable,...e==null?void 0:e.draggable},droppable:{...Wf.droppable,...e==null?void 0:e.droppable},dragOverlay:{...Wf.dragOverlay,...e==null?void 0:e.dragOverlay}}),[e==null?void 0:e.draggable,e==null?void 0:e.droppable,e==null?void 0:e.dragOverlay])}function K5(e){let{activeNode:t,measure:r,initialRect:n,config:a=!0}=e;const i=j.useRef(!1),{x:o,y:s}=typeof a=="boolean"?{x:a,y:a}:a;Sa(()=>{if(!o&&!s||!t){i.current=!1;return}if(i.current||!n)return;const c=t==null?void 0:t.node.current;if(!c||c.isConnected===!1)return;const f=r(c),d=XE(f,n);if(o||(d.x=0),s||(d.y=0),i.current=!0,Math.abs(d.x)>0||Math.abs(d.y)>0){const p=YE(c);p&&p.scrollBy({top:d.y,left:d.x})}},[t,o,s,n,r])}const oP=j.createContext({...aa,scaleX:1,scaleY:1});var yi;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(yi||(yi={}));const X5=j.memo(function(t){var r,n,a,i;let{id:o,accessibility:s,autoScroll:l=!0,children:c,sensors:f=F5,collisionDetection:d=Q4,measuring:p,modifiers:h,...x}=t;const v=j.useReducer(W5,void 0,V5),[y,g]=v,[m,w]=V4(),[S,b]=j.useState(yi.Uninitialized),_=S===yi.Initialized,{draggable:{active:k,nodes:O,translate:N},droppable:{containers:T}}=y,R=k!=null?O.get(k):null,A=j.useRef({initial:null,translated:null}),C=j.useMemo(()=>{var Mt;return k!=null?{id:k,data:(Mt=R==null?void 0:R.data)!=null?Mt:z5,rect:A}:null},[k,R]),M=j.useRef(null),[B,V]=j.useState(null),[I,D]=j.useState(null),U=Oc(x,Object.values(x)),G=jd("DndDescribedBy",o),q=j.useMemo(()=>T.getEnabled(),[T]),K=q5(p),{droppableRects:te,measureDroppableContainers:Z,measuringScheduled:de}=A5(q,{dragging:_,dependencies:[N.x,N.y],config:K.droppable}),fe=k5(O,k),Ie=j.useMemo(()=>I?ex(I):null,[I]),De=Pa(),oe=N5(fe,K.draggable.measure);K5({activeNode:k!=null?O.get(k):null,config:De.layoutShiftCompensation,initialRect:oe,measure:K.draggable.measure});const J=bS(fe,K.draggable.measure,oe),we=bS(fe?fe.parentElement:null),Y=j.useRef({activatorEvent:null,active:null,activeNode:fe,collisionRect:null,collisions:null,droppableRects:te,draggableNodes:O,draggingNode:null,draggingNodeRect:null,droppableContainers:T,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),Ne=T.getNodeFor((r=Y.current.over)==null?void 0:r.id),xe=L5({measure:K.dragOverlay.measure}),$e=(n=xe.nodeRef.current)!=null?n:fe,Ee=_?(a=xe.rect)!=null?a:J:null,je=!!(xe.nodeRef.current&&xe.rect),Be=C5(je?null:J),Me=aP($e?rn($e):null),Re=T5(_?Ne??fe:null),Ke=M5(Re),Ze=G5(h,{transform:{x:N.x-Be.x,y:N.y-Be.y,scaleX:1,scaleY:1},activatorEvent:I,active:C,activeNodeRect:J,containerNodeRect:we,draggingNodeRect:Ee,over:Y.current.over,overlayNodeRect:xe.rect,scrollableAncestors:Re,scrollableAncestorRects:Ke,windowRect:Me}),P=Ie?Vs(Ie,N):null,L=$5(Re),H=_S(L),se=_S(L,[J]),re=Vs(Ze,H),Q=Ee?t5(Ee,Ze):null,ne=C&&Q?d({active:C,collisionRect:Q,droppableRects:te,droppableContainers:q,pointerCoordinates:P}):null,ke=KE(ne,"id"),[W,le]=j.useState(null),_e=je?Ze:Vs(Ze,se),rt=J4(_e,(i=W==null?void 0:W.rect)!=null?i:null,J),$r=j.useRef(null),Jt=j.useCallback((Mt,er)=>{let{sensor:cr,options:an}=er;if(M.current==null)return;const gr=O.get(M.current);if(!gr)return;const Ut=Mt.nativeEvent,jr=new cr({active:M.current,activeNode:gr,event:Ut,options:an,context:Y,onAbort(Dt){if(!O.get(Dt))return;const{onDragAbort:qe}=U.current,Ir={id:Dt};qe==null||qe(Ir),m({type:"onDragAbort",event:Ir})},onPending(Dt,St,qe,Ir){if(!O.get(Dt))return;const{onDragPending:Lt}=U.current,Xe={id:Dt,constraint:St,initialCoordinates:qe,offset:Ir};Lt==null||Lt(Xe),m({type:"onDragPending",event:Xe})},onStart(Dt){const St=M.current;if(St==null)return;const qe=O.get(St);if(!qe)return;const{onDragStart:Ir}=U.current,tr={activatorEvent:Ut,active:{id:St,data:qe.data,rect:A}};Os.unstable_batchedUpdates(()=>{Ir==null||Ir(tr),b(yi.Initializing),g({type:ir.DragStart,initialCoordinates:Dt,active:St}),m({type:"onDragStart",event:tr}),V($r.current),D(Ut)})},onMove(Dt){g({type:ir.DragMove,coordinates:Dt})},onEnd:_n(ir.DragEnd),onCancel:_n(ir.DragCancel)});$r.current=jr;function _n(Dt){return async function(){const{active:qe,collisions:Ir,over:tr,scrollAdjustedTranslate:Lt}=Y.current;let Xe=null;if(qe&&Lt){const{cancelDrop:Un}=U.current;Xe={activatorEvent:Ut,active:qe,collisions:Ir,delta:Lt,over:tr},Dt===ir.DragEnd&&typeof Un=="function"&&await Promise.resolve(Un(Xe))&&(Dt=ir.DragCancel)}M.current=null,Os.unstable_batchedUpdates(()=>{g({type:Dt}),b(yi.Uninitialized),le(null),V(null),D(null),$r.current=null;const Un=Dt===ir.DragEnd?"onDragEnd":"onDragCancel";if(Xe){const on=U.current[Un];on==null||on(Xe),m({type:Un,event:Xe})}})}}},[O]),Rr=j.useCallback((Mt,er)=>(cr,an)=>{const gr=cr.nativeEvent,Ut=O.get(an);if(M.current!==null||!Ut||gr.dndKit||gr.defaultPrevented)return;const jr={active:Ut};Mt(cr,er.options,jr)===!0&&(gr.dndKit={capturedBy:er.sensor},M.current=an,Jt(cr,er))},[O,Jt]),Na=O5(f,Rr);R5(f),Sa(()=>{J&&S===yi.Initializing&&b(yi.Initialized)},[J,S]),j.useEffect(()=>{const{onDragMove:Mt}=U.current,{active:er,activatorEvent:cr,collisions:an,over:gr}=Y.current;if(!er||!cr)return;const Ut={active:er,activatorEvent:cr,collisions:an,delta:{x:re.x,y:re.y},over:gr};Os.unstable_batchedUpdates(()=>{Mt==null||Mt(Ut),m({type:"onDragMove",event:Ut})})},[re.x,re.y]),j.useEffect(()=>{const{active:Mt,activatorEvent:er,collisions:cr,droppableContainers:an,scrollAdjustedTranslate:gr}=Y.current;if(!Mt||M.current==null||!er||!gr)return;const{onDragOver:Ut}=U.current,jr=an.get(ke),_n=jr&&jr.rect.current?{id:jr.id,rect:jr.rect.current,data:jr.data,disabled:jr.disabled}:null,Dt={active:Mt,activatorEvent:er,collisions:cr,delta:{x:gr.x,y:gr.y},over:_n};Os.unstable_batchedUpdates(()=>{le(_n),Ut==null||Ut(Dt),m({type:"onDragOver",event:Dt})})},[ke]),Sa(()=>{Y.current={activatorEvent:I,active:C,activeNode:fe,collisionRect:Q,collisions:ne,droppableRects:te,draggableNodes:O,draggingNode:$e,draggingNodeRect:Ee,droppableContainers:T,over:W,scrollableAncestors:Re,scrollAdjustedTranslate:re},A.current={initial:Ee,translated:Q}},[C,fe,ne,Q,O,$e,Ee,te,T,W,Re,re]),_5({...De,delta:N,draggingRect:Q,pointerCoordinates:P,scrollableAncestors:Re,scrollableAncestorRects:Ke});const Ea=j.useMemo(()=>({active:C,activeNode:fe,activeNodeRect:J,activatorEvent:I,collisions:ne,containerNodeRect:we,dragOverlay:xe,draggableNodes:O,droppableContainers:T,droppableRects:te,over:W,measureDroppableContainers:Z,scrollableAncestors:Re,scrollableAncestorRects:Ke,measuringConfiguration:K,measuringScheduled:de,windowRect:Me}),[C,fe,J,I,ne,we,xe,O,T,te,W,Z,Re,Ke,K,de,Me]),ui=j.useMemo(()=>({activatorEvent:I,activators:Na,active:C,activeNodeRect:J,ariaDescribedById:{draggable:G},dispatch:g,draggableNodes:O,over:W,measureDroppableContainers:Z}),[I,Na,C,J,g,G,O,W,Z]);return E.createElement(HE.Provider,{value:w},E.createElement(sm.Provider,{value:ui},E.createElement(iP.Provider,{value:Ea},E.createElement(oP.Provider,{value:rt},c)),E.createElement(H5,{disabled:(s==null?void 0:s.restoreFocus)===!1})),E.createElement(G4,{...s,hiddenTextDescribedById:G}));function Pa(){const Mt=(B==null?void 0:B.autoScrollEnabled)===!1,er=typeof l=="object"?l.enabled===!1:l===!1,cr=_&&!Mt&&!er;return typeof l=="object"?{...l,enabled:cr}:{enabled:cr}}}),Y5=j.createContext(null),jS="button",Z5="Draggable";function Q5(e){let{id:t,data:r,disabled:n=!1,attributes:a}=e;const i=jd(Z5),{activators:o,activatorEvent:s,active:l,activeNodeRect:c,ariaDescribedById:f,draggableNodes:d,over:p}=j.useContext(sm),{role:h=jS,roleDescription:x="draggable",tabIndex:v=0}=a??{},y=(l==null?void 0:l.id)===t,g=j.useContext(y?oP:Y5),[m,w]=Np(),[S,b]=Np(),_=I5(o,t),k=Oc(r);Sa(()=>(d.set(t,{id:t,key:i,node:m,activatorNode:S,data:k}),()=>{const N=d.get(t);N&&N.key===i&&d.delete(t)}),[d,t]);const O=j.useMemo(()=>({role:h,tabIndex:v,"aria-disabled":n,"aria-pressed":y&&h===jS?!0:void 0,"aria-roledescription":x,"aria-describedby":f.draggable}),[n,h,v,y,x,f.draggable]);return{active:l,activatorEvent:s,activeNodeRect:c,attributes:O,isDragging:y,listeners:n?void 0:_,node:m,over:p,setNodeRef:w,setActivatorNodeRef:b,transform:g}}function J5(){return j.useContext(iP)}const e6="Droppable",t6={timeout:25};function r6(e){let{data:t,disabled:r=!1,id:n,resizeObserverConfig:a}=e;const i=jd(e6),{active:o,dispatch:s,over:l,measureDroppableContainers:c}=j.useContext(sm),f=j.useRef({disabled:r}),d=j.useRef(!1),p=j.useRef(null),h=j.useRef(null),{disabled:x,updateMeasurementsFor:v,timeout:y}={...t6,...a},g=Oc(v??n),m=j.useCallback(()=>{if(!d.current){d.current=!0;return}h.current!=null&&clearTimeout(h.current),h.current=setTimeout(()=>{c(Array.isArray(g.current)?g.current:[g.current]),h.current=null},y)},[y]),w=om({callback:m,disabled:x||!o}),S=j.useCallback((O,N)=>{w&&(N&&(w.unobserve(N),d.current=!1),O&&w.observe(O))},[w]),[b,_]=Np(S),k=Oc(t);return j.useEffect(()=>{!w||!b.current||(w.disconnect(),d.current=!1,w.observe(b.current))},[b,w]),j.useEffect(()=>(s({type:ir.RegisterDroppable,element:{id:n,key:i,disabled:r,node:b,rect:p,data:k}}),()=>s({type:ir.UnregisterDroppable,key:i,id:n})),[n]),j.useEffect(()=>{r!==f.current.disabled&&(s({type:ir.SetDroppableDisabled,id:n,key:i,disabled:r}),f.current.disabled=r)},[n,i,r,s]),{active:o,rect:p,isOver:(l==null?void 0:l.id)===n,node:b,over:l,setNodeRef:_}}function Yb(e,t,r){const n=e.slice();return n.splice(r<0?n.length+r:r,0,n.splice(t,1)[0]),n}function n6(e,t){return e.reduce((r,n,a)=>{const i=t.get(n);return i&&(r[a]=i),r},Array(e.length))}function af(e){return e!==null&&e>=0}function a6(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let r=0;r{let{rects:t,activeIndex:r,overIndex:n,index:a}=e;const i=Yb(t,n,r),o=t[a],s=i[a];return!s||!o?null:{x:s.left-o.left,y:s.top-o.top,scaleX:s.width/o.width,scaleY:s.height/o.height}},of={scaleX:1,scaleY:1},o6=e=>{var t;let{activeIndex:r,activeNodeRect:n,index:a,rects:i,overIndex:o}=e;const s=(t=i[r])!=null?t:n;if(!s)return null;if(a===r){const c=i[o];return c?{x:0,y:rr&&a<=o?{x:0,y:-s.height-l,...of}:a=o?{x:0,y:s.height+l,...of}:{x:0,y:0,...of}};function s6(e,t,r){const n=e[t],a=e[t-1],i=e[t+1];return n?rn.map(_=>typeof _=="object"&&"id"in _?_.id:_),[n]),x=o!=null,v=o?h.indexOf(o.id):-1,y=c?h.indexOf(c.id):-1,g=j.useRef(h),m=!a6(h,g.current),w=y!==-1&&v===-1||m,S=i6(i);Sa(()=>{m&&x&&f(h)},[m,h,x,f]),j.useEffect(()=>{g.current=h},[h]);const b=j.useMemo(()=>({activeIndex:v,containerId:d,disabled:S,disableTransforms:w,items:h,overIndex:y,useDragOverlay:p,sortedRects:n6(h,l),strategy:a}),[v,d,S.draggable,S.droppable,w,h,y,l,p,a]);return E.createElement(uP.Provider,{value:b},t)}const u6=e=>{let{id:t,items:r,activeIndex:n,overIndex:a}=e;return Yb(r,n,a).indexOf(t)},c6=e=>{let{containerId:t,isSorting:r,wasDragging:n,index:a,items:i,newIndex:o,previousItems:s,previousContainerId:l,transition:c}=e;return!c||!n||s!==i&&a===o?!1:r?!0:o!==a&&t===l},d6={duration:200,easing:"ease"},cP="transform",f6=Nc.Transition.toString({property:cP,duration:0,easing:"linear"}),p6={roleDescription:"sortable"};function h6(e){let{disabled:t,index:r,node:n,rect:a}=e;const[i,o]=j.useState(null),s=j.useRef(r);return Sa(()=>{if(!t&&r!==s.current&&n.current){const l=a.current;if(l){const c=Dl(n.current,{ignoreTransform:!0}),f={x:l.left-c.left,y:l.top-c.top,scaleX:l.width/c.width,scaleY:l.height/c.height};(f.x||f.y)&&o(f)}}r!==s.current&&(s.current=r)},[t,r,n,a]),j.useEffect(()=>{i&&o(null)},[i]),i}function m6(e){let{animateLayoutChanges:t=c6,attributes:r,disabled:n,data:a,getNewIndex:i=u6,id:o,strategy:s,resizeObserverConfig:l,transition:c=d6}=e;const{items:f,containerId:d,activeIndex:p,disabled:h,disableTransforms:x,sortedRects:v,overIndex:y,useDragOverlay:g,strategy:m}=j.useContext(uP),w=v6(n,h),S=f.indexOf(o),b=j.useMemo(()=>({sortable:{containerId:d,index:S,items:f},...a}),[d,a,S,f]),_=j.useMemo(()=>f.slice(f.indexOf(o)),[f,o]),{rect:k,node:O,isOver:N,setNodeRef:T}=r6({id:o,data:b,disabled:w.droppable,resizeObserverConfig:{updateMeasurementsFor:_,...l}}),{active:R,activatorEvent:A,activeNodeRect:C,attributes:M,setNodeRef:B,listeners:V,isDragging:I,over:D,setActivatorNodeRef:U,transform:G}=Q5({id:o,data:b,attributes:{...p6,...r},disabled:w.draggable}),q=$4(T,B),K=!!R,te=K&&!x&&af(p)&&af(y),Z=!g&&I,de=Z&&te?G:null,Ie=te?de??(s??m)({rects:v,activeNodeRect:C,activeIndex:p,overIndex:y,index:S}):null,De=af(p)&&af(y)?i({id:o,items:f,activeIndex:p,overIndex:y}):S,oe=R==null?void 0:R.id,J=j.useRef({activeId:oe,items:f,newIndex:De,containerId:d}),we=f!==J.current.items,Y=t({active:R,containerId:d,isDragging:I,isSorting:K,id:o,index:S,items:f,newIndex:J.current.newIndex,previousItems:J.current.items,previousContainerId:J.current.containerId,transition:c,wasDragging:J.current.activeId!=null}),Ne=h6({disabled:!Y,index:S,node:O,rect:k});return j.useEffect(()=>{K&&J.current.newIndex!==De&&(J.current.newIndex=De),d!==J.current.containerId&&(J.current.containerId=d),f!==J.current.items&&(J.current.items=f)},[K,De,d,f]),j.useEffect(()=>{if(oe===J.current.activeId)return;if(oe&&!J.current.activeId){J.current.activeId=oe;return}const $e=setTimeout(()=>{J.current.activeId=oe},50);return()=>clearTimeout($e)},[oe]),{active:R,activeIndex:p,attributes:M,data:b,rect:k,index:S,newIndex:De,items:f,isOver:N,isSorting:K,isDragging:I,listeners:V,node:O,overIndex:y,over:D,setNodeRef:q,setActivatorNodeRef:U,setDroppableNodeRef:T,setDraggableNodeRef:B,transform:Ne??Ie,transition:xe()};function xe(){if(Ne||we&&J.current.newIndex===S)return f6;if(!(Z&&!Hb(A)||!c)&&(K||Y))return Nc.Transition.toString({...c,property:cP})}}function v6(e,t){var r,n;return typeof e=="boolean"?{draggable:e,droppable:!1}:{draggable:(r=e==null?void 0:e.draggable)!=null?r:t.draggable,droppable:(n=e==null?void 0:e.droppable)!=null?n:t.droppable}}function Cp(e){if(!e)return!1;const t=e.data.current;return!!(t&&"sortable"in t&&typeof t.sortable=="object"&&"containerId"in t.sortable&&"items"in t.sortable&&"index"in t.sortable)}const y6=[tt.Down,tt.Right,tt.Up,tt.Left],g6=(e,t)=>{let{context:{active:r,collisionRect:n,droppableRects:a,droppableContainers:i,over:o,scrollableAncestors:s}}=t;if(y6.includes(e.code)){if(e.preventDefault(),!r||!n)return;const l=[];i.getEnabled().forEach(d=>{if(!d||d!=null&&d.disabled)return;const p=a.get(d.id);if(p)switch(e.code){case tt.Down:n.topp.top&&l.push(d);break;case tt.Left:n.left>p.left&&l.push(d);break;case tt.Right:n.left1&&(f=c[1].id),f!=null){const d=i.get(r.id),p=i.get(f),h=p?a.get(p.id):null,x=p==null?void 0:p.node.current;if(x&&h&&d&&p){const y=im(x).some((_,k)=>s[k]!==_),g=dP(d,p),m=x6(d,p),w=y||!g?{x:0,y:0}:{x:m?n.width-h.width:0,y:m?n.height-h.height:0},S={x:h.left,y:h.top};return w.x&&w.y?S:Ac(S,w)}}}};function dP(e,t){return!Cp(e)||!Cp(t)?!1:e.data.current.sortable.containerId===t.data.current.sortable.containerId}function x6(e,t){return!Cp(e)||!Cp(t)||!dP(e,t)?!1:e.data.current.sortable.index{if(!a||typeof a!="object")return{};const i=a.keys;return i&&typeof i=="object"?i:a},r={...t(e.keys),...t((n=e.routing_config)==null?void 0:n.keys)};return Object.keys(r).length===0?[]:Object.entries(r).map(([a,i])=>{const o=(i.type||i.data_type||"str_value").toString().toLowerCase(),s={key:a,type:o};return i.values&&(s.values=Array.isArray(i.values)?i.values.map(l=>l.trim()):i.values.split(",").map(l=>l.trim())),i.min_value!==void 0&&(s.min_value=i.min_value),i.max_value!==void 0&&(s.max_value=i.max_value),i.min_length!==void 0&&(s.min_length=i.min_length),i.max_length!==void 0&&(s.max_length=i.max_length),i.exact_length!==void 0&&(s.exact_length=i.exact_length),i.regex&&(s.regex=i.regex),s})}function fP(){const{data:e,error:t,isLoading:r}=Bt("/config/routing-keys",pn,{refreshInterval:0,revalidateOnFocus:!1}),n=b6(e||null),a=n.reduce((o,s)=>(o[s.key]=s,o),{}),i={};return n.forEach(o=>{i[o.key]={type:o.type,values:o.values||[]}}),{config:e,keys:n,keysByName:a,routingKeysConfig:i,isLoading:r,error:t,getKeyValues:o=>{var s;return((s=a[o])==null?void 0:s.values)||[]},isIntegerKey:o=>{var s;return((s=a[o])==null?void 0:s.type)==="integer"},isEnumKey:o=>{var s;return((s=a[o])==null?void 0:s.type)==="enum"}}}const w6={"==":"equal","!=":"not_equal",">":"greater_than","<":"less_than",">=":"greater_than_equal","<=":"less_than_equal"};function _6({id:e,name:t,onRemove:r}){const{attributes:n,listeners:a,setNodeRef:i,transform:o,transition:s}=m6({id:e}),l={transform:Nc.Transform.toString(o),transition:s};return u.jsxs("div",{ref:i,style:l,className:"flex items-center gap-2 bg-slate-100 dark:bg-[#111118] border border-slate-200 dark:border-[#1c1c24] rounded-lg px-2 py-1.5",children:[u.jsx("span",{...n,...a,className:"cursor-grab text-slate-400",children:u.jsx(XD,{size:14})}),u.jsx("span",{className:"text-sm flex-1 font-mono",children:t}),u.jsx("button",{type:"button",onClick:r,className:"text-red-400 hover:text-red-600",children:u.jsx(Ja,{size:12})})]})}function pP({gateways:e,onChange:t}){const[r,n]=j.useState(""),[a,i]=j.useState(""),o=q4(fS(Xb),fS(qb,{coordinateGetter:g6}));function s(c){const{active:f,over:d}=c;if(d&&f.id!==d.id){const p=e.findIndex(x=>x.id===f.id),h=e.findIndex(x=>x.id===d.id);t(Yb(e,p,h))}}function l(){r.trim()&&(t([...e,{id:crypto.randomUUID(),gatewayName:r.trim(),gatewayId:a.trim()}]),n(""),i(""))}return u.jsxs("div",{className:"space-y-2",children:[u.jsx(X5,{sensors:o,collisionDetection:X4,onDragEnd:s,children:u.jsx(l6,{items:e.map(c=>c.id),strategy:o6,children:e.map((c,f)=>u.jsx(_6,{id:c.id,name:`${f+1}. ${c.gatewayName}${c.gatewayId?` (${c.gatewayId})`:""}`,onRemove:()=>t(e.filter(d=>d.id!==c.id))},c.id))})}),u.jsxs("div",{className:"flex gap-2",children:[u.jsx("input",{value:r,onChange:c=>n(c.target.value),onKeyDown:c=>c.key==="Enter"&&(c.preventDefault(),l()),placeholder:"gateway_name",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("input",{value:a,onChange:c=>i(c.target.value),onKeyDown:c=>c.key==="Enter"&&(c.preventDefault(),l()),placeholder:"gateway_id",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsxs(Ce,{type:"button",size:"sm",variant:"secondary",onClick:l,children:[u.jsx(Ui,{size:13})," Add"]})]})]})}function hP({gateways:e,onChange:t}){const[r,n]=j.useState(""),[a,i]=j.useState(""),o=e.reduce((l,c)=>l+c.split,0);function s(){r.trim()&&(t([...e,{id:crypto.randomUUID(),gatewayName:r.trim(),gatewayId:a.trim(),split:0}]),n(""),i(""))}return u.jsxs("div",{className:"space-y-2",children:[e.map(l=>u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx("input",{value:l.gatewayName,onChange:c=>t(e.map(f=>f.id===l.id?{...f,gatewayName:c.target.value}:f)),placeholder:"gateway_name",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-32 focus:outline-none"}),u.jsx("input",{value:l.gatewayId,onChange:c=>t(e.map(f=>f.id===l.id?{...f,gatewayId:c.target.value}:f)),placeholder:"gateway_id",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-28 focus:outline-none"}),u.jsx("input",{type:"range",min:0,max:100,value:l.split,onChange:c=>t(e.map(f=>f.id===l.id?{...f,split:Number(c.target.value)}:f)),className:"flex-1 accent-brand-500"}),u.jsxs("span",{className:"text-sm w-10 text-right",children:[l.split,"%"]}),u.jsx("button",{type:"button",onClick:()=>t(e.filter(c=>c.id!==l.id)),className:"text-red-400 hover:text-red-600",children:u.jsx(Ja,{size:12})})]},l.id)),u.jsxs("div",{className:`text-xs font-medium ${o===100?"text-emerald-400":"text-red-400"}`,children:["Total: ",o,"% ",o!==100&&"(must equal 100)"]}),u.jsxs("div",{className:"flex gap-2",children:[u.jsx("input",{value:r,onChange:l=>n(l.target.value),onKeyDown:l=>l.key==="Enter"&&(l.preventDefault(),s()),placeholder:"gateway_name",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("input",{value:a,onChange:l=>i(l.target.value),onKeyDown:l=>l.key==="Enter"&&(l.preventDefault(),s()),placeholder:"gateway_id",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsxs(Ce,{type:"button",size:"sm",variant:"secondary",onClick:s,children:[u.jsx(Ui,{size:13})," Add"]})]})]})}function S6({row:e,onChange:t,onRemove:r,routingKeys:n}){var l;const a=n[e.lhs],i=(a==null?void 0:a.type)==="enum",s=(a==null?void 0:a.type)==="integer"?[">","<",">=","<=","==","!="]:["==","!="];return u.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[u.jsx("select",{value:e.lhs,onChange:c=>t({...e,lhs:c.target.value,value:"",operator:"=="}),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs focus:outline-none",children:Object.keys(n).map(c=>u.jsx("option",{value:c,children:c},c))}),u.jsx("select",{value:e.operator,onChange:c=>t({...e,operator:c.target.value}),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs focus:outline-none",children:s.map(c=>u.jsx("option",{value:c,children:c},c))}),i?u.jsxs("select",{value:e.value,onChange:c=>t({...e,value:c.target.value}),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs focus:outline-none",children:[u.jsx("option",{value:"",children:"select..."}),(((l=n[e.lhs])==null?void 0:l.values)||[]).map(c=>u.jsx("option",{value:c,children:c},c))]}):u.jsx("input",{type:"number",value:e.value,onChange:c=>t({...e,value:c.target.value}),placeholder:"value",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-24 focus:outline-none"}),u.jsx("button",{type:"button",onClick:r,className:"text-red-400 hover:text-red-600",children:u.jsx(Ja,{size:12})})]})}function j6({block:e,onChange:t,onRemove:r,routingKeys:n}){var f;const[a,i]=j.useState(!1),o=Object.keys(n)[0]||"payment_method",l=(((f=n[o])==null?void 0:f.values)||[])[0]||"";function c(){t({...e,conditions:[...e.conditions,{id:crypto.randomUUID(),lhs:o,operator:"==",value:l}]})}return u.jsxs("div",{className:"border border-slate-200 dark:border-[#1c1c24] rounded-xl",children:[u.jsxs("div",{className:"flex items-center justify-between px-4 py-2.5 bg-[#0d0d12] rounded-t-xl cursor-pointer",onClick:()=>i(!a),children:[u.jsx("input",{value:e.name,onChange:d=>{d.stopPropagation(),t({...e,name:d.target.value})},onClick:d=>d.stopPropagation(),placeholder:"Rule name",className:"bg-transparent text-sm font-medium focus:outline-none border-b border-transparent focus:border-[#28282f] text-slate-900"}),u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx("button",{type:"button",onClick:d=>{d.stopPropagation(),r()},className:"text-red-400 hover:text-red-600",children:u.jsx(Ja,{size:14})}),a?u.jsx(Au,{size:14}):u.jsx(Nu,{size:14})]})]}),!a&&u.jsxs("div",{className:"px-4 py-3 space-y-3",children:[u.jsxs("div",{children:[u.jsx("p",{className:"text-xs font-medium text-slate-500 mb-2",children:"CONDITIONS"}),u.jsxs("div",{className:"space-y-2",children:[e.conditions.map(d=>u.jsx(S6,{row:d,routingKeys:n,onChange:p=>t({...e,conditions:e.conditions.map(h=>h.id===d.id?p:h)}),onRemove:()=>t({...e,conditions:e.conditions.filter(p=>p.id!==d.id)})},d.id)),u.jsxs(Ce,{type:"button",variant:"ghost",size:"sm",onClick:c,children:[u.jsx(Ui,{size:12})," Add Condition"]})]})]}),u.jsxs("div",{children:[u.jsx("p",{className:"text-xs font-medium text-slate-500 mb-2",children:"OUTPUT"}),u.jsx("div",{className:"flex gap-4 mb-3",children:["priority","volume_split"].map(d=>u.jsxs("label",{className:"flex items-center gap-1.5 text-xs cursor-pointer",children:[u.jsx("input",{type:"radio",checked:e.outputType===d,onChange:()=>t({...e,outputType:d}),className:"accent-brand-500"}),d==="priority"?"Priority":"Volume Split"]},d))}),e.outputType==="priority"?u.jsx(pP,{gateways:e.priorityGateways,onChange:d=>t({...e,priorityGateways:d})}):u.jsx(hP,{gateways:e.volumeGateways,onChange:d=>t({...e,volumeGateways:d})})]})]})]})}function k6(e,t,r){function n(i,o,s){return i==="priority"?{priority:o.map(l=>({gateway_name:l.gatewayName,gateway_id:l.gatewayId||null}))}:{volume_split:s.map(l=>({split:l.split,output:{gateway_name:l.gatewayName,gateway_id:l.gatewayId||null}}))}}function a(i){return i==="priority"?"priority":"volume_split"}return{globals:{},default_selection:n(t.type,t.priorityGateways,t.volumeGateways),rules:e.map(i=>({name:i.name,routing_type:a(i.outputType),output:n(i.outputType,i.priorityGateways,i.volumeGateways),statements:[{condition:i.conditions.map(o=>{var s,l;return{lhs:o.lhs,comparison:w6[o.operator]||o.operator,value:{type:((s=r[o.lhs])==null?void 0:s.type)==="integer"?"number":"enum_variant",value:((l=r[o.lhs])==null?void 0:l.type)==="integer"?Number(o.value):o.value},metadata:{}}})}]}))}}function O6(){const{merchantId:e}=ka(),{routingKeysConfig:t,isLoading:r,error:n}=fP(),a=t,i=Object.keys(a).length>0,o=!r&&(!i||!!n),[s,l]=j.useState(""),[c,f]=j.useState(""),[d,p]=j.useState([]),[h,x]=j.useState({type:"priority",priorityGateways:[],volumeGateways:[]}),[v,y]=j.useState(!1),[g,m]=j.useState(!1),[w,S]=j.useState(null),[b,_]=j.useState(null),[k,O]=j.useState(!1),[N,T]=j.useState(null),[R,A]=j.useState(!1),[C,M]=j.useState(new Set),{data:B,mutate:V}=Bt(e?`/routing/list/${e}`:null,()=>Et(`/routing/list/${e}`)),{data:I}=Bt(e?`/routing/list/active/${e}`:null,()=>Et(`/routing/list/active/${e}`)),D=new Set((I||[]).map(Z=>Z.id)),U=k6(d,h,a);async function G(Z){if(Z.preventDefault(),!e){S("Set a Merchant ID first.");return}if(o){S("Routing key config is unavailable. Ensure backend /config/routing-keys is reachable and valid.");return}if(!s.trim()){S("Rule name is required.");return}m(!0),S(null),_(null);try{const de=await Et("/routing/create",{name:s.trim(),description:c,created_by:e,algorithm_for:"payment",algorithm:{type:"advanced",data:U}});_(de.id),V()}catch(de){S(String(de))}finally{m(!1)}}async function q(Z){if(e){O(!0),T(null),A(!1);try{await Et("/routing/activate",{created_by:e,routing_algorithm_id:Z}),A(!0),V()}catch(de){T(String(de))}finally{O(!1)}}}function K(Z){M(de=>{const fe=new Set(de);return fe.has(Z)?fe.delete(Z):fe.add(Z),fe})}function te(){p(Z=>[...Z,{id:crypto.randomUUID(),name:`Rule ${Z.length+1}`,conditions:[],outputType:"priority",priorityGateways:[],volumeGateways:[]}])}return u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900",children:"Rule-Based Routing"}),u.jsx("p",{className:"text-sm text-slate-500 mt-1",children:"Create declarative routing rules"})]}),u.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-3 gap-6",children:[u.jsxs("div",{className:"lg:col-span-1 space-y-3",children:[u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Existing Rules"})}),u.jsx(Ge,{className:"p-0",children:e?B?B.length===0?u.jsx("p",{className:"px-4 py-3 text-sm text-slate-400",children:"No rules yet."}):u.jsx("div",{className:"divide-y divide-slate-100 dark:divide-[#222226]",children:B.map(Z=>{const de=D.has(Z.id),fe=C.has(Z.id),Ie=Z.algorithm_data||Z.algorithm;return u.jsxs("div",{children:[u.jsxs("div",{className:"flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-start sm:justify-between",children:[u.jsxs("div",{className:"min-w-0 flex-1",children:[u.jsx("p",{className:"truncate font-medium",children:Z.name}),u.jsx("p",{className:"text-xs text-slate-400 capitalize",children:Ie==null?void 0:Ie.type})]}),u.jsxs("div",{className:"flex shrink-0 flex-wrap items-center gap-2 sm:justify-end",children:[u.jsx(Ue,{variant:de?"green":"gray",children:de?"Active":"Inactive"}),u.jsxs(Ce,{size:"sm",variant:"ghost",onClick:()=>K(Z.id),children:[u.jsx(em,{size:14,className:"mr-1"}),fe?"Hide":"View"]}),!de&&u.jsx(Ce,{size:"sm",variant:"ghost",onClick:()=>q(Z.id),disabled:k,children:"Activate"})]})]}),fe&&u.jsx("div",{className:"bg-slate-50 px-4 py-3 dark:bg-[#151518]",children:u.jsxs("div",{className:"space-y-2 text-xs text-slate-600",children:[u.jsxs("p",{children:[u.jsx("strong",{children:"ID:"})," ",Z.id]}),u.jsxs("p",{children:[u.jsx("strong",{children:"Description:"})," ",Z.description||"N/A"]}),u.jsxs("p",{children:[u.jsx("strong",{children:"Algorithm For:"})," ",Z.algorithm_for]}),Z.created_at&&u.jsxs("p",{children:[u.jsx("strong",{children:"Created:"})," ",new Date(Z.created_at).toLocaleString()]}),u.jsxs("div",{children:[u.jsx("strong",{children:"Configuration:"}),u.jsx("pre",{className:"mt-1 max-h-48 overflow-auto rounded border border-transparent bg-slate-100 p-2 text-xs dark:border-[#222226] dark:bg-[#0f0f11]",children:JSON.stringify(Ie,null,2)})]})]})})]},Z.id)})}):u.jsx("p",{className:"px-4 py-3 text-sm text-slate-400",children:"Loading..."}):u.jsx("p",{className:"px-4 py-3 text-sm text-slate-400",children:"Set merchant ID to load rules."})})]}),N&&u.jsx(Xr,{error:N}),R&&u.jsx("div",{className:"rounded-lg border border-emerald-500/20 bg-emerald-500/8 px-3 py-2 text-sm text-emerald-400",children:"Rule activated successfully."})]}),u.jsxs("div",{className:"lg:col-span-2 space-y-4",children:[u.jsx("form",{onSubmit:G,className:"space-y-4",children:u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Rule Builder"})}),u.jsxs(Ge,{className:"space-y-4",children:[u.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs text-slate-500 mb-1",children:"Rule Name *"}),u.jsx("input",{value:s,onChange:Z=>l(Z.target.value),placeholder:"my-rule",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm w-full focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs text-slate-500 mb-1",children:"Description"}),u.jsx("input",{value:c,onChange:Z=>f(Z.target.value),placeholder:"Optional description",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm w-full focus:outline-none focus:ring-1 focus:ring-brand-500"})]})]}),u.jsxs("div",{className:"space-y-3",children:[u.jsx("p",{className:"text-xs font-medium text-slate-500 uppercase tracking-wide",children:"Rules"}),r&&u.jsx("p",{className:"text-sm text-slate-500",children:"Loading routing keys from backend..."}),o&&u.jsx(Xr,{error:"Routing keys are unavailable from backend (/config/routing-keys). Rule Builder is disabled until this is fixed."}),d.map(Z=>u.jsx(j6,{block:Z,routingKeys:a,onChange:de=>p(fe=>fe.map(Ie=>Ie.id===Z.id?de:Ie)),onRemove:()=>p(de=>de.filter(fe=>fe.id!==Z.id))},Z.id)),u.jsxs(Ce,{type:"button",variant:"secondary",size:"sm",onClick:te,disabled:o,children:[u.jsx(Ui,{size:14})," Add Rule Block"]})]}),u.jsxs("div",{className:"border border-slate-200 dark:border-[#1c1c24] rounded-xl px-4 py-3",children:[u.jsx("p",{className:"text-xs font-medium text-slate-500 mb-2",children:"DEFAULT SELECTION (Fallback)"}),u.jsx("div",{className:"flex gap-4 mb-3",children:["priority","volume_split"].map(Z=>u.jsxs("label",{className:"flex items-center gap-1.5 text-xs cursor-pointer",children:[u.jsx("input",{type:"radio",checked:h.type===Z,onChange:()=>x({...h,type:Z}),className:"accent-brand-500"}),Z==="priority"?"Priority":"Volume Split"]},Z))}),h.type==="priority"?u.jsx(pP,{gateways:h.priorityGateways,onChange:Z=>x({...h,priorityGateways:Z})}):u.jsx(hP,{gateways:h.volumeGateways,onChange:Z=>x({...h,volumeGateways:Z})})]}),u.jsx(Xr,{error:w}),b&&u.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-emerald-500/8 px-3 py-2 text-sm text-emerald-400 flex items-center justify-between",children:[u.jsxs("span",{children:["Rule created (ID: ",b,")"]}),u.jsx(Ce,{type:"button",size:"sm",onClick:()=>q(b),disabled:k,children:"Activate Now"})]}),u.jsxs("div",{className:"flex gap-3",children:[u.jsx(Ce,{type:"submit",disabled:g||o,children:g?"Creating...":"Create Rule"}),u.jsx(Ce,{type:"button",variant:"secondary",size:"sm",onClick:()=>y(!v),children:v?"Hide JSON":"Preview JSON"})]})]})]})}),v&&u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"JSON Preview"})}),u.jsx(Ge,{children:u.jsx("pre",{className:"text-xs text-slate-600 overflow-auto max-h-64 bg-[#07070b] rounded-lg p-4 font-mono border border-slate-200 dark:border-[#1c1c24]",children:JSON.stringify({name:s,description:c,created_by:e,algorithm_for:"payment",algorithm:{type:"advanced",data:U}},null,2)})})]})]})]})]})}function mP(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var a=e.length;for(t=0;t-1}var kF=jF,OF=um;function AF(e,t){var r=this.__data__,n=OF(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}var NF=AF,EF=dF,PF=xF,CF=_F,TF=kF,$F=NF;function Bl(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t0?1:-1},bo=function(t){return zo(t)&&t.indexOf("%")===t.length-1},ae=function(t){return Jz(t)&&!Vl(t)},nB=function(t){return Fe(t)},lr=function(t){return ae(t)||zo(t)},aB=0,Yo=function(t){var r=++aB;return"".concat(t||"").concat(r)},Fr=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,a=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!ae(t)&&!zo(t))return n;var i;if(bo(t)){var o=t.indexOf("%");i=r*parseFloat(t.slice(0,o))/100}else i=+t;return Vl(i)&&(i=n),a&&i>r&&(i=r),i},_i=function(t){if(!t)return null;var r=Object.keys(t);return r&&r.length?t[r[0]]:null},iB=function(t){if(!Array.isArray(t))return!1;for(var r=t.length,n={},a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function fB(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function ix(e){"@babel/helpers - typeof";return ix=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ix(e)}var MS={click:"onClick",mousedown:"onMouseDown",mouseup:"onMouseUp",mouseover:"onMouseOver",mousemove:"onMouseMove",mouseout:"onMouseOut",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",touchcancel:"onTouchCancel",touchend:"onTouchEnd",touchmove:"onTouchMove",touchstart:"onTouchStart",contextmenu:"onContextMenu",dblclick:"onDoubleClick"},qa=function(t){return typeof t=="string"?t:t?t.displayName||t.name||"Component":""},DS=null,ry=null,lw=function e(t){if(t===DS&&Array.isArray(ry))return ry;var r=[];return j.Children.forEach(t,function(n){Fe(n)||(Kz.isFragment(n)?r=r.concat(e(n.props.children)):r.push(n))}),ry=r,DS=t,r};function yn(e,t){var r=[],n=[];return Array.isArray(t)?n=t.map(function(a){return qa(a)}):n=[qa(t)],lw(e).forEach(function(a){var i=vn(a,"type.displayName")||vn(a,"type.name");n.indexOf(i)!==-1&&r.push(a)}),r}function cn(e,t){var r=yn(e,t);return r&&r[0]}var LS=function(t){if(!t||!t.props)return!1;var r=t.props,n=r.width,a=r.height;return!(!ae(n)||n<=0||!ae(a)||a<=0)},pB=["a","altGlyph","altGlyphDef","altGlyphItem","animate","animateColor","animateMotion","animateTransform","circle","clipPath","color-profile","cursor","defs","desc","ellipse","feBlend","feColormatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","font","font-face","font-face-format","font-face-name","font-face-url","foreignObject","g","glyph","glyphRef","hkern","image","line","lineGradient","marker","mask","metadata","missing-glyph","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tref","tspan","use","view","vkern"],hB=function(t){return t&&t.type&&zo(t.type)&&pB.indexOf(t.type)>=0},NP=function(t){return t&&ix(t)==="object"&&"clipDot"in t},mB=function(t,r,n,a){var i,o=(i=ty==null?void 0:ty[a])!==null&&i!==void 0?i:[];return r.startsWith("data-")||!Te(t)&&(a&&o.includes(r)||lB.includes(r))||n&&sw.includes(r)},Oe=function(t,r,n){if(!t||typeof t=="function"||typeof t=="boolean")return null;var a=t;if(j.isValidElement(t)&&(a=t.props),!Fl(a))return null;var i={};return Object.keys(a).forEach(function(o){var s;mB((s=a)===null||s===void 0?void 0:s[o],o,r,n)&&(i[o]=a[o])}),i},ox=function e(t,r){if(t===r)return!0;var n=j.Children.count(t);if(n!==j.Children.count(r))return!1;if(n===0)return!0;if(n===1)return FS(Array.isArray(t)?t[0]:t,Array.isArray(r)?r[0]:r);for(var a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function bB(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function lx(e){var t=e.children,r=e.width,n=e.height,a=e.viewBox,i=e.className,o=e.style,s=e.title,l=e.desc,c=xB(e,gB),f=a||{width:r,height:n,x:0,y:0},d=He("recharts-surface",i);return E.createElement("svg",sx({},Oe(c,!0,"svg"),{className:d,width:r,height:n,style:o,viewBox:"".concat(f.x," ").concat(f.y," ").concat(f.width," ").concat(f.height)}),E.createElement("title",null,s),E.createElement("desc",null,l),t)}var wB=["children","className"];function ux(){return ux=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function SB(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var Qe=E.forwardRef(function(e,t){var r=e.children,n=e.className,a=_B(e,wB),i=He("recharts-layer",n);return E.createElement("g",ux({className:i},Oe(a,!0),{ref:t}),r)}),ra=function(t,r){for(var n=arguments.length,a=new Array(n>2?n-2:0),i=2;ia?0:a+t),r=r>a?a:r,r<0&&(r+=a),a=t>r?0:r-t>>>0,t>>>=0;for(var i=Array(a);++n=n?e:OB(e,t,r)}var NB=AB,EB="\\ud800-\\udfff",PB="\\u0300-\\u036f",CB="\\ufe20-\\ufe2f",TB="\\u20d0-\\u20ff",$B=PB+CB+TB,RB="\\ufe0e\\ufe0f",IB="\\u200d",MB=RegExp("["+IB+EB+$B+RB+"]");function DB(e){return MB.test(e)}var EP=DB;function LB(e){return e.split("")}var FB=LB,PP="\\ud800-\\udfff",zB="\\u0300-\\u036f",BB="\\ufe20-\\ufe2f",UB="\\u20d0-\\u20ff",VB=zB+BB+UB,WB="\\ufe0e\\ufe0f",HB="["+PP+"]",cx="["+VB+"]",dx="\\ud83c[\\udffb-\\udfff]",GB="(?:"+cx+"|"+dx+")",CP="[^"+PP+"]",TP="(?:\\ud83c[\\udde6-\\uddff]){2}",$P="[\\ud800-\\udbff][\\udc00-\\udfff]",qB="\\u200d",RP=GB+"?",IP="["+WB+"]?",KB="(?:"+qB+"(?:"+[CP,TP,$P].join("|")+")"+IP+RP+")*",XB=IP+RP+KB,YB="(?:"+[CP+cx+"?",cx,TP,$P,HB].join("|")+")",ZB=RegExp(dx+"(?="+dx+")|"+YB+XB,"g");function QB(e){return e.match(ZB)||[]}var JB=QB,e9=FB,t9=EP,r9=JB;function n9(e){return t9(e)?r9(e):e9(e)}var a9=n9,i9=NB,o9=EP,s9=a9,l9=_P;function u9(e){return function(t){t=l9(t);var r=o9(t)?s9(t):void 0,n=r?r[0]:t.charAt(0),a=r?i9(r,1).join(""):t.slice(1);return n[e]()+a}}var c9=u9,d9=c9,f9=d9("toUpperCase"),p9=f9;const Sm=dt(p9);function bt(e){return function(){return e}}const MP=Math.cos,Rp=Math.sin,oa=Math.sqrt,Ip=Math.PI,jm=2*Ip,fx=Math.PI,px=2*fx,co=1e-6,h9=px-co;function DP(e){this._+=e[0];for(let t=1,r=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return DP;const r=10**t;return function(n){this._+=n[0];for(let a=1,i=n.length;aco)if(!(Math.abs(d*l-c*f)>co)||!i)this._append`L${this._x1=t},${this._y1=r}`;else{let h=n-o,x=a-s,v=l*l+c*c,y=h*h+x*x,g=Math.sqrt(v),m=Math.sqrt(p),w=i*Math.tan((fx-Math.acos((v+p-y)/(2*g*m)))/2),S=w/m,b=w/g;Math.abs(S-1)>co&&this._append`L${t+S*f},${r+S*d}`,this._append`A${i},${i},0,0,${+(d*h>f*x)},${this._x1=t+b*l},${this._y1=r+b*c}`}}arc(t,r,n,a,i,o){if(t=+t,r=+r,n=+n,o=!!o,n<0)throw new Error(`negative radius: ${n}`);let s=n*Math.cos(a),l=n*Math.sin(a),c=t+s,f=r+l,d=1^o,p=o?a-i:i-a;this._x1===null?this._append`M${c},${f}`:(Math.abs(this._x1-c)>co||Math.abs(this._y1-f)>co)&&this._append`L${c},${f}`,n&&(p<0&&(p=p%px+px),p>h9?this._append`A${n},${n},0,1,${d},${t-s},${r-l}A${n},${n},0,1,${d},${this._x1=c},${this._y1=f}`:p>co&&this._append`A${n},${n},0,${+(p>=fx)},${d},${this._x1=t+n*Math.cos(i)},${this._y1=r+n*Math.sin(i)}`)}rect(t,r,n,a){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}h${n=+n}v${+a}h${-n}Z`}toString(){return this._}}function uw(e){let t=3;return e.digits=function(r){if(!arguments.length)return t;if(r==null)t=null;else{const n=Math.floor(r);if(!(n>=0))throw new RangeError(`invalid digits: ${r}`);t=n}return e},()=>new v9(t)}function cw(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function LP(e){this._context=e}LP.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function km(e){return new LP(e)}function FP(e){return e[0]}function zP(e){return e[1]}function BP(e,t){var r=bt(!0),n=null,a=km,i=null,o=uw(s);e=typeof e=="function"?e:e===void 0?FP:bt(e),t=typeof t=="function"?t:t===void 0?zP:bt(t);function s(l){var c,f=(l=cw(l)).length,d,p=!1,h;for(n==null&&(i=a(h=o())),c=0;c<=f;++c)!(c=h;--x)s.point(w[x],S[x]);s.lineEnd(),s.areaEnd()}g&&(w[p]=+e(y,p,d),S[p]=+t(y,p,d),s.point(n?+n(y,p,d):w[p],r?+r(y,p,d):S[p]))}if(m)return s=null,m+""||null}function f(){return BP().defined(a).curve(o).context(i)}return c.x=function(d){return arguments.length?(e=typeof d=="function"?d:bt(+d),n=null,c):e},c.x0=function(d){return arguments.length?(e=typeof d=="function"?d:bt(+d),c):e},c.x1=function(d){return arguments.length?(n=d==null?null:typeof d=="function"?d:bt(+d),c):n},c.y=function(d){return arguments.length?(t=typeof d=="function"?d:bt(+d),r=null,c):t},c.y0=function(d){return arguments.length?(t=typeof d=="function"?d:bt(+d),c):t},c.y1=function(d){return arguments.length?(r=d==null?null:typeof d=="function"?d:bt(+d),c):r},c.lineX0=c.lineY0=function(){return f().x(e).y(t)},c.lineY1=function(){return f().x(e).y(r)},c.lineX1=function(){return f().x(n).y(t)},c.defined=function(d){return arguments.length?(a=typeof d=="function"?d:bt(!!d),c):a},c.curve=function(d){return arguments.length?(o=d,i!=null&&(s=o(i)),c):o},c.context=function(d){return arguments.length?(d==null?i=s=null:s=o(i=d),c):i},c}class UP{constructor(t,r){this._context=t,this._x=r}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,r){switch(t=+t,r=+r,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,r):this._context.moveTo(t,r);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,r,t,r):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+r)/2,t,this._y0,t,r);break}}this._x0=t,this._y0=r}}function y9(e){return new UP(e,!0)}function g9(e){return new UP(e,!1)}const dw={draw(e,t){const r=oa(t/Ip);e.moveTo(r,0),e.arc(0,0,r,0,jm)}},x9={draw(e,t){const r=oa(t/5)/2;e.moveTo(-3*r,-r),e.lineTo(-r,-r),e.lineTo(-r,-3*r),e.lineTo(r,-3*r),e.lineTo(r,-r),e.lineTo(3*r,-r),e.lineTo(3*r,r),e.lineTo(r,r),e.lineTo(r,3*r),e.lineTo(-r,3*r),e.lineTo(-r,r),e.lineTo(-3*r,r),e.closePath()}},VP=oa(1/3),b9=VP*2,w9={draw(e,t){const r=oa(t/b9),n=r*VP;e.moveTo(0,-r),e.lineTo(n,0),e.lineTo(0,r),e.lineTo(-n,0),e.closePath()}},_9={draw(e,t){const r=oa(t),n=-r/2;e.rect(n,n,r,r)}},S9=.8908130915292852,WP=Rp(Ip/10)/Rp(7*Ip/10),j9=Rp(jm/10)*WP,k9=-MP(jm/10)*WP,O9={draw(e,t){const r=oa(t*S9),n=j9*r,a=k9*r;e.moveTo(0,-r),e.lineTo(n,a);for(let i=1;i<5;++i){const o=jm*i/5,s=MP(o),l=Rp(o);e.lineTo(l*r,-s*r),e.lineTo(s*n-l*a,l*n+s*a)}e.closePath()}},ny=oa(3),A9={draw(e,t){const r=-oa(t/(ny*3));e.moveTo(0,r*2),e.lineTo(-ny*r,-r),e.lineTo(ny*r,-r),e.closePath()}},Sn=-.5,jn=oa(3)/2,hx=1/oa(12),N9=(hx/2+1)*3,E9={draw(e,t){const r=oa(t/N9),n=r/2,a=r*hx,i=n,o=r*hx+r,s=-i,l=o;e.moveTo(n,a),e.lineTo(i,o),e.lineTo(s,l),e.lineTo(Sn*n-jn*a,jn*n+Sn*a),e.lineTo(Sn*i-jn*o,jn*i+Sn*o),e.lineTo(Sn*s-jn*l,jn*s+Sn*l),e.lineTo(Sn*n+jn*a,Sn*a-jn*n),e.lineTo(Sn*i+jn*o,Sn*o-jn*i),e.lineTo(Sn*s+jn*l,Sn*l-jn*s),e.closePath()}};function P9(e,t){let r=null,n=uw(a);e=typeof e=="function"?e:bt(e||dw),t=typeof t=="function"?t:bt(t===void 0?64:+t);function a(){let i;if(r||(r=i=n()),e.apply(this,arguments).draw(r,+t.apply(this,arguments)),i)return r=null,i+""||null}return a.type=function(i){return arguments.length?(e=typeof i=="function"?i:bt(i),a):e},a.size=function(i){return arguments.length?(t=typeof i=="function"?i:bt(+i),a):t},a.context=function(i){return arguments.length?(r=i??null,a):r},a}function Mp(){}function Dp(e,t,r){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+r)/6)}function HP(e){this._context=e}HP.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Dp(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Dp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function C9(e){return new HP(e)}function GP(e){this._context=e}GP.prototype={areaStart:Mp,areaEnd:Mp,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:Dp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function T9(e){return new GP(e)}function qP(e){this._context=e}qP.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var r=(this._x0+4*this._x1+e)/6,n=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(r,n):this._context.moveTo(r,n);break;case 3:this._point=4;default:Dp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function $9(e){return new qP(e)}function KP(e){this._context=e}KP.prototype={areaStart:Mp,areaEnd:Mp,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function R9(e){return new KP(e)}function BS(e){return e<0?-1:1}function US(e,t,r){var n=e._x1-e._x0,a=t-e._x1,i=(e._y1-e._y0)/(n||a<0&&-0),o=(r-e._y1)/(a||n<0&&-0),s=(i*a+o*n)/(n+a);return(BS(i)+BS(o))*Math.min(Math.abs(i),Math.abs(o),.5*Math.abs(s))||0}function VS(e,t){var r=e._x1-e._x0;return r?(3*(e._y1-e._y0)/r-t)/2:t}function ay(e,t,r){var n=e._x0,a=e._y0,i=e._x1,o=e._y1,s=(i-n)/3;e._context.bezierCurveTo(n+s,a+s*t,i-s,o-s*r,i,o)}function Lp(e){this._context=e}Lp.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:ay(this,this._t0,VS(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var r=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,ay(this,VS(this,r=US(this,e,t)),r);break;default:ay(this,this._t0,r=US(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=r}}};function XP(e){this._context=new YP(e)}(XP.prototype=Object.create(Lp.prototype)).point=function(e,t){Lp.prototype.point.call(this,t,e)};function YP(e){this._context=e}YP.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,r,n,a,i){this._context.bezierCurveTo(t,e,n,r,i,a)}};function I9(e){return new Lp(e)}function M9(e){return new XP(e)}function ZP(e){this._context=e}ZP.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,r=e.length;if(r)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),r===2)this._context.lineTo(e[1],t[1]);else for(var n=WS(e),a=WS(t),i=0,o=1;o=0;--t)a[t]=(o[t]-a[t+1])/i[t];for(i[r-1]=(e[r]+a[r-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var r=this._x*(1-this._t)+e*this._t;this._context.lineTo(r,this._y),this._context.lineTo(r,t)}break}}this._x=e,this._y=t}};function L9(e){return new Om(e,.5)}function F9(e){return new Om(e,0)}function z9(e){return new Om(e,1)}function il(e,t){if((o=e.length)>1)for(var r=1,n,a,i=e[t[0]],o,s=i.length;r=0;)r[t]=t;return r}function B9(e,t){return e[t]}function U9(e){const t=[];return t.key=e,t}function V9(){var e=bt([]),t=mx,r=il,n=B9;function a(i){var o=Array.from(e.apply(this,arguments),U9),s,l=o.length,c=-1,f;for(const d of i)for(s=0,++c;s0){for(var r,n,a=0,i=e[0].length,o;a0){for(var r=0,n=e[t[0]],a,i=n.length;r0)||!((i=(a=e[t[0]]).length)>0))){for(var r=0,n=1,a,i,o;n=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Q9(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var QP={symbolCircle:dw,symbolCross:x9,symbolDiamond:w9,symbolSquare:_9,symbolStar:O9,symbolTriangle:A9,symbolWye:E9},J9=Math.PI/180,eU=function(t){var r="symbol".concat(Sm(t));return QP[r]||dw},tU=function(t,r,n){if(r==="area")return t;switch(n){case"cross":return 5*t*t/9;case"diamond":return .5*t*t/Math.sqrt(3);case"square":return t*t;case"star":{var a=18*J9;return 1.25*t*t*(Math.tan(a)-Math.tan(a*2)*Math.pow(Math.tan(a),2))}case"triangle":return Math.sqrt(3)*t*t/4;case"wye":return(21-10*Math.sqrt(3))*t*t/8;default:return Math.PI*t*t/4}},rU=function(t,r){QP["symbol".concat(Sm(t))]=r},fw=function(t){var r=t.type,n=r===void 0?"circle":r,a=t.size,i=a===void 0?64:a,o=t.sizeType,s=o===void 0?"area":o,l=Z9(t,q9),c=GS(GS({},l),{},{type:n,size:i,sizeType:s}),f=function(){var y=eU(n),g=P9().type(y).size(tU(i,s,n));return g()},d=c.className,p=c.cx,h=c.cy,x=Oe(c,!0);return p===+p&&h===+h&&i===+i?E.createElement("path",vx({},x,{className:He("recharts-symbols",d),transform:"translate(".concat(p,", ").concat(h,")"),d:f()})):null};fw.registerSymbol=rU;function ol(e){"@babel/helpers - typeof";return ol=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ol(e)}function yx(){return yx=Object.assign?Object.assign.bind():function(e){for(var t=1;t`);var m=h.inactive?c:h.color;return E.createElement("li",yx({className:y,style:d,key:"legend-item-".concat(x)},Bo(n.props,h,x)),E.createElement(lx,{width:o,height:o,viewBox:f,style:p},n.renderIcon(h)),E.createElement("span",{className:"recharts-legend-item-text",style:{color:m}},v?v(g,h,x):g))})}},{key:"render",value:function(){var n=this.props,a=n.payload,i=n.layout,o=n.align;if(!a||!a.length)return null;var s={padding:0,margin:0,textAlign:i==="horizontal"?o:"left"};return E.createElement("ul",{className:"recharts-default-legend",style:s},this.renderItems())}}])}(j.PureComponent);Cc(pw,"displayName","Legend");Cc(pw,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"middle",inactiveColor:"#ccc"});var fU=cm;function pU(){this.__data__=new fU,this.size=0}var hU=pU;function mU(e){var t=this.__data__,r=t.delete(e);return this.size=t.size,r}var vU=mU;function yU(e){return this.__data__.get(e)}var gU=yU;function xU(e){return this.__data__.has(e)}var bU=xU,wU=cm,_U=ew,SU=tw,jU=200;function kU(e,t){var r=this.__data__;if(r instanceof wU){var n=r.__data__;if(!_U||n.lengths))return!1;var c=i.get(e),f=i.get(t);if(c&&f)return c==t&&f==e;var d=-1,p=!0,h=r&qU?new VU:void 0;for(i.set(e,t),i.set(t,e);++d-1&&e%1==0&&e-1&&e%1==0&&e<=ZV}var yw=QV,JV=oi,eW=yw,tW=si,rW="[object Arguments]",nW="[object Array]",aW="[object Boolean]",iW="[object Date]",oW="[object Error]",sW="[object Function]",lW="[object Map]",uW="[object Number]",cW="[object Object]",dW="[object RegExp]",fW="[object Set]",pW="[object String]",hW="[object WeakMap]",mW="[object ArrayBuffer]",vW="[object DataView]",yW="[object Float32Array]",gW="[object Float64Array]",xW="[object Int8Array]",bW="[object Int16Array]",wW="[object Int32Array]",_W="[object Uint8Array]",SW="[object Uint8ClampedArray]",jW="[object Uint16Array]",kW="[object Uint32Array]",At={};At[yW]=At[gW]=At[xW]=At[bW]=At[wW]=At[_W]=At[SW]=At[jW]=At[kW]=!0;At[rW]=At[nW]=At[mW]=At[aW]=At[vW]=At[iW]=At[oW]=At[sW]=At[lW]=At[uW]=At[cW]=At[dW]=At[fW]=At[pW]=At[hW]=!1;function OW(e){return tW(e)&&eW(e.length)&&!!At[JV(e)]}var AW=OW;function NW(e){return function(t){return e(t)}}var uC=NW,Up={exports:{}};Up.exports;(function(e,t){var r=vP,n=t&&!t.nodeType&&t,a=n&&!0&&e&&!e.nodeType&&e,i=a&&a.exports===n,o=i&&r.process,s=function(){try{var l=a&&a.require&&a.require("util").types;return l||o&&o.binding&&o.binding("util")}catch{}}();e.exports=s})(Up,Up.exports);var EW=Up.exports,PW=AW,CW=uC,JS=EW,ej=JS&&JS.isTypedArray,TW=ej?CW(ej):PW,cC=TW,$W=MV,RW=mw,IW=nn,MW=lC,DW=vw,LW=cC,FW=Object.prototype,zW=FW.hasOwnProperty;function BW(e,t){var r=IW(e),n=!r&&RW(e),a=!r&&!n&&MW(e),i=!r&&!n&&!a&&LW(e),o=r||n||a||i,s=o?$W(e.length,String):[],l=s.length;for(var c in e)(t||zW.call(e,c))&&!(o&&(c=="length"||a&&(c=="offset"||c=="parent")||i&&(c=="buffer"||c=="byteLength"||c=="byteOffset")||DW(c,l)))&&s.push(c);return s}var UW=BW,VW=Object.prototype;function WW(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||VW;return e===r}var HW=WW;function GW(e,t){return function(r){return e(t(r))}}var dC=GW,qW=dC,KW=qW(Object.keys,Object),XW=KW,YW=HW,ZW=XW,QW=Object.prototype,JW=QW.hasOwnProperty;function e7(e){if(!YW(e))return ZW(e);var t=[];for(var r in Object(e))JW.call(e,r)&&r!="constructor"&&t.push(r);return t}var t7=e7,r7=Qb,n7=yw;function a7(e){return e!=null&&n7(e.length)&&!r7(e)}var Od=a7,i7=UW,o7=t7,s7=Od;function l7(e){return s7(e)?i7(e):o7(e)}var Am=l7,u7=jV,c7=RV,d7=Am;function f7(e){return u7(e,d7,c7)}var p7=f7,tj=p7,h7=1,m7=Object.prototype,v7=m7.hasOwnProperty;function y7(e,t,r,n,a,i){var o=r&h7,s=tj(e),l=s.length,c=tj(t),f=c.length;if(l!=f&&!o)return!1;for(var d=l;d--;){var p=s[d];if(!(o?p in t:v7.call(t,p)))return!1}var h=i.get(e),x=i.get(t);if(h&&x)return h==t&&x==e;var v=!0;i.set(e,t),i.set(t,e);for(var y=o;++d-1}var mG=hG;function vG(e,t,r){for(var n=-1,a=e==null?0:e.length;++n=CG){var c=t?null:EG(e);if(c)return PG(c);o=!1,a=NG,l=new kG}else l=t?[]:s;e:for(;++n=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function qG(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function KG(e){return e.value}function XG(e,t){if(E.isValidElement(e))return E.cloneElement(e,t);if(typeof e=="function")return E.createElement(e,t);t.ref;var r=GG(t,LG);return E.createElement(pw,r)}var yj=1,Jn=function(e){function t(){var r;FG(this,t);for(var n=arguments.length,a=new Array(n),i=0;iyj||Math.abs(a.height-this.lastBoundingBox.height)>yj)&&(this.lastBoundingBox.width=a.width,this.lastBoundingBox.height=a.height,n&&n(a)):(this.lastBoundingBox.width!==-1||this.lastBoundingBox.height!==-1)&&(this.lastBoundingBox.width=-1,this.lastBoundingBox.height=-1,n&&n(null))}},{key:"getBBoxSnapshot",value:function(){return this.lastBoundingBox.width>=0&&this.lastBoundingBox.height>=0?Ta({},this.lastBoundingBox):{width:0,height:0}}},{key:"getDefaultPosition",value:function(n){var a=this.props,i=a.layout,o=a.align,s=a.verticalAlign,l=a.margin,c=a.chartWidth,f=a.chartHeight,d,p;if(!n||(n.left===void 0||n.left===null)&&(n.right===void 0||n.right===null))if(o==="center"&&i==="vertical"){var h=this.getBBoxSnapshot();d={left:((c||0)-h.width)/2}}else d=o==="right"?{right:l&&l.right||0}:{left:l&&l.left||0};if(!n||(n.top===void 0||n.top===null)&&(n.bottom===void 0||n.bottom===null))if(s==="middle"){var x=this.getBBoxSnapshot();p={top:((f||0)-x.height)/2}}else p=s==="bottom"?{bottom:l&&l.bottom||0}:{top:l&&l.top||0};return Ta(Ta({},d),p)}},{key:"render",value:function(){var n=this,a=this.props,i=a.content,o=a.width,s=a.height,l=a.wrapperStyle,c=a.payloadUniqBy,f=a.payload,d=Ta(Ta({position:"absolute",width:o||"auto",height:s||"auto"},this.getDefaultPosition(l)),l);return E.createElement("div",{className:"recharts-legend-wrapper",style:d,ref:function(h){n.wrapperNode=h}},XG(i,Ta(Ta({},this.props),{},{payload:gC(f,c,KG)})))}}],[{key:"getWithHeight",value:function(n,a){var i=Ta(Ta({},this.defaultProps),n.props),o=i.layout;return o==="vertical"&&ae(n.props.height)?{height:n.props.height}:o==="horizontal"?{width:n.props.width||a}:null}}])}(j.PureComponent);Nm(Jn,"displayName","Legend");Nm(Jn,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"bottom"});var gj=kd,YG=mw,ZG=nn,xj=gj?gj.isConcatSpreadable:void 0;function QG(e){return ZG(e)||YG(e)||!!(xj&&e&&e[xj])}var JG=QG,eq=oC,tq=JG;function wC(e,t,r,n,a){var i=-1,o=e.length;for(r||(r=tq),a||(a=[]);++i0&&r(s)?t>1?wC(s,t-1,r,n,a):eq(a,s):n||(a[a.length]=s)}return a}var _C=wC;function rq(e){return function(t,r,n){for(var a=-1,i=Object(t),o=n(t),s=o.length;s--;){var l=o[e?s:++a];if(r(i[l],l,i)===!1)break}return t}}var nq=rq,aq=nq,iq=aq(),oq=iq,sq=oq,lq=Am;function uq(e,t){return e&&sq(e,t,lq)}var SC=uq,cq=Od;function dq(e,t){return function(r,n){if(r==null)return r;if(!cq(r))return e(r,n);for(var a=r.length,i=t?a:-1,o=Object(r);(t?i--:++it||i&&o&&l&&!s&&!c||n&&o&&l||!r&&l||!a)return 1;if(!n&&!i&&!c&&e=s)return l;var c=r[n];return l*(c=="desc"?-1:1)}}return e.index-t.index}var kq=jq,ly=nw,Oq=aw,Aq=Aa,Nq=jC,Eq=bq,Pq=uC,Cq=kq,Tq=Gl,$q=nn;function Rq(e,t,r){t.length?t=ly(t,function(i){return $q(i)?function(o){return Oq(o,i.length===1?i[0]:i)}:i}):t=[Tq];var n=-1;t=ly(t,Pq(Aq));var a=Nq(e,function(i,o,s){var l=ly(t,function(c){return c(i)});return{criteria:l,index:++n,value:i}});return Eq(a,function(i,o){return Cq(i,o,r)})}var Iq=Rq;function Mq(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var Dq=Mq,Lq=Dq,wj=Math.max;function Fq(e,t,r){return t=wj(t===void 0?e.length-1:t,0),function(){for(var n=arguments,a=-1,i=wj(n.length-t,0),o=Array(i);++a0){if(++t>=Xq)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}var Jq=Qq,eK=Kq,tK=Jq,rK=tK(eK),nK=rK,aK=Gl,iK=zq,oK=nK;function sK(e,t){return oK(iK(e,t,aK),e+"")}var lK=sK,uK=Jb,cK=Od,dK=vw,fK=Yi;function pK(e,t,r){if(!fK(r))return!1;var n=typeof t;return(n=="number"?cK(r)&&dK(t,r.length):n=="string"&&t in r)?uK(r[t],e):!1}var Em=pK,hK=_C,mK=Iq,vK=lK,Sj=Em,yK=vK(function(e,t){if(e==null)return[];var r=t.length;return r>1&&Sj(e,t[0],t[1])?t=[]:r>2&&Sj(t[0],t[1],t[2])&&(t=[t[0]]),mK(e,hK(t,1),[])}),gK=yK;const bw=dt(gK);function Tc(e){"@babel/helpers - typeof";return Tc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Tc(e)}function kx(){return kx=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=t.x),"".concat(uu,"-left"),ae(r)&&t&&ae(t.x)&&r=t.y),"".concat(uu,"-top"),ae(n)&&t&&ae(t.y)&&nv?Math.max(f,l[n]):Math.max(d,l[n])}function $K(e){var t=e.translateX,r=e.translateY,n=e.useTranslate3d;return{transform:n?"translate3d(".concat(t,"px, ").concat(r,"px, 0)"):"translate(".concat(t,"px, ").concat(r,"px)")}}function RK(e){var t=e.allowEscapeViewBox,r=e.coordinate,n=e.offsetTopLeft,a=e.position,i=e.reverseDirection,o=e.tooltipBox,s=e.useTranslate3d,l=e.viewBox,c,f,d;return o.height>0&&o.width>0&&r?(f=Oj({allowEscapeViewBox:t,coordinate:r,key:"x",offsetTopLeft:n,position:a,reverseDirection:i,tooltipDimension:o.width,viewBox:l,viewBoxDimension:l.width}),d=Oj({allowEscapeViewBox:t,coordinate:r,key:"y",offsetTopLeft:n,position:a,reverseDirection:i,tooltipDimension:o.height,viewBox:l,viewBoxDimension:l.height}),c=$K({translateX:f,translateY:d,useTranslate3d:s})):c=CK,{cssProperties:c,cssClasses:TK({translateX:f,translateY:d,coordinate:r})}}function ll(e){"@babel/helpers - typeof";return ll=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ll(e)}function Aj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Nj(e){for(var t=1;tEj||Math.abs(n.height-this.state.lastBoundingBox.height)>Ej)&&this.setState({lastBoundingBox:{width:n.width,height:n.height}})}else(this.state.lastBoundingBox.width!==-1||this.state.lastBoundingBox.height!==-1)&&this.setState({lastBoundingBox:{width:-1,height:-1}})}},{key:"componentDidMount",value:function(){document.addEventListener("keydown",this.handleKeyDown),this.updateBBox()}},{key:"componentWillUnmount",value:function(){document.removeEventListener("keydown",this.handleKeyDown)}},{key:"componentDidUpdate",value:function(){var n,a;this.props.active&&this.updateBBox(),this.state.dismissed&&(((n=this.props.coordinate)===null||n===void 0?void 0:n.x)!==this.state.dismissedAtCoordinate.x||((a=this.props.coordinate)===null||a===void 0?void 0:a.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}},{key:"render",value:function(){var n=this,a=this.props,i=a.active,o=a.allowEscapeViewBox,s=a.animationDuration,l=a.animationEasing,c=a.children,f=a.coordinate,d=a.hasPayload,p=a.isAnimationActive,h=a.offset,x=a.position,v=a.reverseDirection,y=a.useTranslate3d,g=a.viewBox,m=a.wrapperStyle,w=RK({allowEscapeViewBox:o,coordinate:f,offsetTopLeft:h,position:x,reverseDirection:v,tooltipBox:this.state.lastBoundingBox,useTranslate3d:y,viewBox:g}),S=w.cssClasses,b=w.cssProperties,_=Nj(Nj({transition:p&&i?"transform ".concat(s,"ms ").concat(l):void 0},b),{},{pointerEvents:"none",visibility:!this.state.dismissed&&i&&d?"visible":"hidden",position:"absolute",top:0,left:0},m);return E.createElement("div",{tabIndex:-1,className:S,style:_,ref:function(O){n.wrapperNode=O}},c)}}])}(j.PureComponent),WK=function(){return!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout)},Zi={isSsr:WK()};function ul(e){"@babel/helpers - typeof";return ul=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ul(e)}function Pj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Cj(e){for(var t=1;t0;return E.createElement(VK,{allowEscapeViewBox:o,animationDuration:s,animationEasing:l,isAnimationActive:p,active:i,coordinate:f,hasPayload:_,offset:h,position:y,reverseDirection:g,useTranslate3d:m,viewBox:w,wrapperStyle:S},eX(c,Cj(Cj({},this.props),{},{payload:b})))}}])}(j.PureComponent);ww(wr,"displayName","Tooltip");ww(wr,"defaultProps",{accessibilityLayer:!1,allowEscapeViewBox:{x:!1,y:!1},animationDuration:400,animationEasing:"ease",contentStyle:{},coordinate:{x:0,y:0},cursor:!0,cursorStyle:{},filterNull:!0,isAnimationActive:!Zi.isSsr,itemStyle:{},labelStyle:{},offset:10,reverseDirection:{x:!1,y:!1},separator:" : ",trigger:"hover",useTranslate3d:!1,viewBox:{x:0,y:0,height:0,width:0},wrapperStyle:{}});var tX=Oa,rX=function(){return tX.Date.now()},nX=rX,aX=/\s/;function iX(e){for(var t=e.length;t--&&aX.test(e.charAt(t)););return t}var oX=iX,sX=oX,lX=/^\s+/;function uX(e){return e&&e.slice(0,sX(e)+1).replace(lX,"")}var cX=uX,dX=cX,Tj=Yi,fX=Ll,$j=NaN,pX=/^[-+]0x[0-9a-f]+$/i,hX=/^0b[01]+$/i,mX=/^0o[0-7]+$/i,vX=parseInt;function yX(e){if(typeof e=="number")return e;if(fX(e))return $j;if(Tj(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=Tj(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=dX(e);var r=hX.test(e);return r||mX.test(e)?vX(e.slice(2),r?2:8):pX.test(e)?$j:+e}var PC=yX,gX=Yi,cy=nX,Rj=PC,xX="Expected a function",bX=Math.max,wX=Math.min;function _X(e,t,r){var n,a,i,o,s,l,c=0,f=!1,d=!1,p=!0;if(typeof e!="function")throw new TypeError(xX);t=Rj(t)||0,gX(r)&&(f=!!r.leading,d="maxWait"in r,i=d?bX(Rj(r.maxWait)||0,t):i,p="trailing"in r?!!r.trailing:p);function h(_){var k=n,O=a;return n=a=void 0,c=_,o=e.apply(O,k),o}function x(_){return c=_,s=setTimeout(g,t),f?h(_):o}function v(_){var k=_-l,O=_-c,N=t-k;return d?wX(N,i-O):N}function y(_){var k=_-l,O=_-c;return l===void 0||k>=t||k<0||d&&O>=i}function g(){var _=cy();if(y(_))return m(_);s=setTimeout(g,v(_))}function m(_){return s=void 0,p&&n?h(_):(n=a=void 0,o)}function w(){s!==void 0&&clearTimeout(s),c=0,n=l=a=s=void 0}function S(){return s===void 0?o:m(cy())}function b(){var _=cy(),k=y(_);if(n=arguments,a=this,l=_,k){if(s===void 0)return x(l);if(d)return clearTimeout(s),s=setTimeout(g,t),h(l)}return s===void 0&&(s=setTimeout(g,t)),o}return b.cancel=w,b.flush=S,b}var SX=_X,jX=SX,kX=Yi,OX="Expected a function";function AX(e,t,r){var n=!0,a=!0;if(typeof e!="function")throw new TypeError(OX);return kX(r)&&(n="leading"in r?!!r.leading:n,a="trailing"in r?!!r.trailing:a),jX(e,t,{leading:n,maxWait:t,trailing:a})}var NX=AX;const CC=dt(NX);function Rc(e){"@babel/helpers - typeof";return Rc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Rc(e)}function Ij(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function cf(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&(C=CC(C,v,{trailing:!0,leading:!1}));var M=new ResizeObserver(C),B=b.current.getBoundingClientRect(),V=B.width,I=B.height;return R(V,I),M.observe(b.current),function(){M.disconnect()}},[R,v]);var A=j.useMemo(function(){var C=N.containerWidth,M=N.containerHeight;if(C<0||M<0)return null;ra(bo(o)||bo(l),`The width(%s) and height(%s) are both fixed numbers, + maybe you don't need to use a ResponsiveContainer.`,o,l),ra(!r||r>0,"The aspect(%s) must be greater than zero.",r);var B=bo(o)?C:o,V=bo(l)?M:l;r&&r>0&&(B?V=B/r:V&&(B=V*r),p&&V>p&&(V=p)),ra(B>0||V>0,`The width(%s) and height(%s) of chart should be greater than 0, + please check the style of container, or the props width(%s) and height(%s), + or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the + height and width.`,B,V,o,l,f,d,r);var I=!Array.isArray(h)&&qa(h.type).endsWith("Chart");return E.Children.map(h,function(D){return E.isValidElement(D)?j.cloneElement(D,cf({width:B,height:V},I?{style:cf({height:"100%",width:"100%",maxHeight:V,maxWidth:B},D.props.style)}:{})):D})},[r,h,l,p,d,f,N,o]);return E.createElement("div",{id:y?"".concat(y):void 0,className:He("recharts-responsive-container",g),style:cf(cf({},S),{},{width:o,height:l,minWidth:f,minHeight:d,maxHeight:p}),ref:b},A)}),Li=function(t){return null};Li.displayName="Cell";function Ic(e){"@babel/helpers - typeof";return Ic=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ic(e)}function Dj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Ex(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||Zi.isSsr)return{width:0,height:0};var n=UX(r),a=JSON.stringify({text:t,copyStyle:n});if(as.widthCache[a])return as.widthCache[a];try{var i=document.getElementById(Lj);i||(i=document.createElement("span"),i.setAttribute("id",Lj),i.setAttribute("aria-hidden","true"),document.body.appendChild(i));var o=Ex(Ex({},BX),n);Object.assign(i.style,o),i.textContent="".concat(t);var s=i.getBoundingClientRect(),l={width:s.width,height:s.height};return as.widthCache[a]=l,++as.cacheCount>zX&&(as.cacheCount=0,as.widthCache={}),l}catch{return{width:0,height:0}}},VX=function(t){return{top:t.top+window.scrollY-document.documentElement.clientTop,left:t.left+window.scrollX-document.documentElement.clientLeft}};function Mc(e){"@babel/helpers - typeof";return Mc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Mc(e)}function Gp(e,t){return qX(e)||GX(e,t)||HX(e,t)||WX()}function WX(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function HX(e,t){if(e){if(typeof e=="string")return Fj(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Fj(e,t)}}function Fj(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function sY(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Hj(e,t){return dY(e)||cY(e,t)||uY(e,t)||lY()}function lY(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function uY(e,t){if(e){if(typeof e=="string")return Gj(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Gj(e,t)}}function Gj(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&arguments[0]!==void 0?arguments[0]:[];return B.reduce(function(V,I){var D=I.word,U=I.width,G=V[V.length-1];if(G&&(a==null||i||G.width+U+nI.width?V:I})};if(!f)return h;for(var v="…",y=function(B){var V=d.slice(0,B),I=IC({breakAll:c,style:l,children:V+v}).wordsWithComputedWidth,D=p(I),U=D.length>o||x(D).width>Number(a);return[U,D]},g=0,m=d.length-1,w=0,S;g<=m&&w<=d.length-1;){var b=Math.floor((g+m)/2),_=b-1,k=y(_),O=Hj(k,2),N=O[0],T=O[1],R=y(b),A=Hj(R,1),C=A[0];if(!N&&!C&&(g=b+1),N&&C&&(m=b-1),!N&&C){S=T;break}w++}return S||h},qj=function(t){var r=Fe(t)?[]:t.toString().split(RC);return[{words:r}]},pY=function(t){var r=t.width,n=t.scaleToFit,a=t.children,i=t.style,o=t.breakAll,s=t.maxLines;if((r||n)&&!Zi.isSsr){var l,c,f=IC({breakAll:o,children:a,style:i});if(f){var d=f.wordsWithComputedWidth,p=f.spaceWidth;l=d,c=p}else return qj(a);return fY({breakAll:o,children:a,maxLines:s,style:i},l,c,r,n)}return qj(a)},Kj="#808080",Uo=function(t){var r=t.x,n=r===void 0?0:r,a=t.y,i=a===void 0?0:a,o=t.lineHeight,s=o===void 0?"1em":o,l=t.capHeight,c=l===void 0?"0.71em":l,f=t.scaleToFit,d=f===void 0?!1:f,p=t.textAnchor,h=p===void 0?"start":p,x=t.verticalAnchor,v=x===void 0?"end":x,y=t.fill,g=y===void 0?Kj:y,m=Wj(t,iY),w=j.useMemo(function(){return pY({breakAll:m.breakAll,children:m.children,maxLines:m.maxLines,scaleToFit:d,style:m.style,width:m.width})},[m.breakAll,m.children,m.maxLines,d,m.style,m.width]),S=m.dx,b=m.dy,_=m.angle,k=m.className,O=m.breakAll,N=Wj(m,oY);if(!lr(n)||!lr(i))return null;var T=n+(ae(S)?S:0),R=i+(ae(b)?b:0),A;switch(v){case"start":A=dy("calc(".concat(c,")"));break;case"middle":A=dy("calc(".concat((w.length-1)/2," * -").concat(s," + (").concat(c," / 2))"));break;default:A=dy("calc(".concat(w.length-1," * -").concat(s,")"));break}var C=[];if(d){var M=w[0].width,B=m.width;C.push("scale(".concat((ae(B)?B/M:1)/M,")"))}return _&&C.push("rotate(".concat(_,", ").concat(T,", ").concat(R,")")),C.length&&(N.transform=C.join(" ")),E.createElement("text",Px({},Oe(N,!0),{x:T,y:R,className:He("recharts-text",k),textAnchor:h,fill:g.includes("url")?Kj:g}),w.map(function(V,I){var D=V.words.join(O?"":" ");return E.createElement("tspan",{x:T,dy:I===0?A:s,key:"".concat(D,"-").concat(I)},D)}))};function Fi(e,t){return e==null||t==null?NaN:et?1:e>=t?0:NaN}function hY(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function _w(e){let t,r,n;e.length!==2?(t=Fi,r=(s,l)=>Fi(e(s),l),n=(s,l)=>e(s)-l):(t=e===Fi||e===hY?e:mY,r=e,n=e);function a(s,l,c=0,f=s.length){if(c>>1;r(s[d],l)<0?c=d+1:f=d}while(c>>1;r(s[d],l)<=0?c=d+1:f=d}while(cc&&n(s[d-1],l)>-n(s[d],l)?d-1:d}return{left:a,center:o,right:i}}function mY(){return 0}function MC(e){return e===null?NaN:+e}function*vY(e,t){for(let r of e)r!=null&&(r=+r)>=r&&(yield r)}const yY=_w(Fi),Ad=yY.right;_w(MC).center;class Xj extends Map{constructor(t,r=bY){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:r}}),t!=null)for(const[n,a]of t)this.set(n,a)}get(t){return super.get(Yj(this,t))}has(t){return super.has(Yj(this,t))}set(t,r){return super.set(gY(this,t),r)}delete(t){return super.delete(xY(this,t))}}function Yj({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):r}function gY({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):(e.set(n,r),r)}function xY({_intern:e,_key:t},r){const n=t(r);return e.has(n)&&(r=e.get(n),e.delete(n)),r}function bY(e){return e!==null&&typeof e=="object"?e.valueOf():e}function wY(e=Fi){if(e===Fi)return DC;if(typeof e!="function")throw new TypeError("compare is not a function");return(t,r)=>{const n=e(t,r);return n||n===0?n:(e(r,r)===0)-(e(t,t)===0)}}function DC(e,t){return(e==null||!(e>=e))-(t==null||!(t>=t))||(et?1:0)}const _Y=Math.sqrt(50),SY=Math.sqrt(10),jY=Math.sqrt(2);function qp(e,t,r){const n=(t-e)/Math.max(0,r),a=Math.floor(Math.log10(n)),i=n/Math.pow(10,a),o=i>=_Y?10:i>=SY?5:i>=jY?2:1;let s,l,c;return a<0?(c=Math.pow(10,-a)/o,s=Math.round(e*c),l=Math.round(t*c),s/ct&&--l,c=-c):(c=Math.pow(10,a)*o,s=Math.round(e/c),l=Math.round(t/c),s*ct&&--l),l0))return[];if(e===t)return[e];const n=t=a))return[];const s=i-a+1,l=new Array(s);if(n)if(o<0)for(let c=0;c=n)&&(r=n);return r}function Qj(e,t){let r;for(const n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);return r}function LC(e,t,r=0,n=1/0,a){if(t=Math.floor(t),r=Math.floor(Math.max(0,r)),n=Math.floor(Math.min(e.length-1,n)),!(r<=t&&t<=n))return e;for(a=a===void 0?DC:wY(a);n>r;){if(n-r>600){const l=n-r+1,c=t-r+1,f=Math.log(l),d=.5*Math.exp(2*f/3),p=.5*Math.sqrt(f*d*(l-d)/l)*(c-l/2<0?-1:1),h=Math.max(r,Math.floor(t-c*d/l+p)),x=Math.min(n,Math.floor(t+(l-c)*d/l+p));LC(e,t,h,x,a)}const i=e[t];let o=r,s=n;for(cu(e,r,t),a(e[n],i)>0&&cu(e,r,n);o0;)--s}a(e[r],i)===0?cu(e,r,s):(++s,cu(e,s,n)),s<=t&&(r=s+1),t<=s&&(n=s-1)}return e}function cu(e,t,r){const n=e[t];e[t]=e[r],e[r]=n}function kY(e,t,r){if(e=Float64Array.from(vY(e)),!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return Qj(e);if(t>=1)return Zj(e);var n,a=(n-1)*t,i=Math.floor(a),o=Zj(LC(e,i).subarray(0,i+1)),s=Qj(e.subarray(i+1));return o+(s-o)*(a-i)}}function OY(e,t,r=MC){if(!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return+r(e[0],0,e);if(t>=1)return+r(e[n-1],n-1,e);var n,a=(n-1)*t,i=Math.floor(a),o=+r(e[i],i,e),s=+r(e[i+1],i+1,e);return o+(s-o)*(a-i)}}function AY(e,t,r){e=+e,t=+t,r=(a=arguments.length)<2?(t=e,e=0,1):a<3?1:+r;for(var n=-1,a=Math.max(0,Math.ceil((t-e)/r))|0,i=new Array(a);++n>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):r===8?ff(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):r===4?ff(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=EY.exec(e))?new Qr(t[1],t[2],t[3],1):(t=PY.exec(e))?new Qr(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=CY.exec(e))?ff(t[1],t[2],t[3],t[4]):(t=TY.exec(e))?ff(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=$Y.exec(e))?ik(t[1],t[2]/100,t[3]/100,1):(t=RY.exec(e))?ik(t[1],t[2]/100,t[3]/100,t[4]):Jj.hasOwnProperty(e)?rk(Jj[e]):e==="transparent"?new Qr(NaN,NaN,NaN,0):null}function rk(e){return new Qr(e>>16&255,e>>8&255,e&255,1)}function ff(e,t,r,n){return n<=0&&(e=t=r=NaN),new Qr(e,t,r,n)}function DY(e){return e instanceof Nd||(e=zc(e)),e?(e=e.rgb(),new Qr(e.r,e.g,e.b,e.opacity)):new Qr}function Ix(e,t,r,n){return arguments.length===1?DY(e):new Qr(e,t,r,n??1)}function Qr(e,t,r,n){this.r=+e,this.g=+t,this.b=+r,this.opacity=+n}jw(Qr,Ix,zC(Nd,{brighter(e){return e=e==null?Kp:Math.pow(Kp,e),new Qr(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?Lc:Math.pow(Lc,e),new Qr(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Qr(No(this.r),No(this.g),No(this.b),Xp(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:nk,formatHex:nk,formatHex8:LY,formatRgb:ak,toString:ak}));function nk(){return`#${wo(this.r)}${wo(this.g)}${wo(this.b)}`}function LY(){return`#${wo(this.r)}${wo(this.g)}${wo(this.b)}${wo((isNaN(this.opacity)?1:this.opacity)*255)}`}function ak(){const e=Xp(this.opacity);return`${e===1?"rgb(":"rgba("}${No(this.r)}, ${No(this.g)}, ${No(this.b)}${e===1?")":`, ${e})`}`}function Xp(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function No(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function wo(e){return e=No(e),(e<16?"0":"")+e.toString(16)}function ik(e,t,r,n){return n<=0?e=t=r=NaN:r<=0||r>=1?e=t=NaN:t<=0&&(e=NaN),new Qn(e,t,r,n)}function BC(e){if(e instanceof Qn)return new Qn(e.h,e.s,e.l,e.opacity);if(e instanceof Nd||(e=zc(e)),!e)return new Qn;if(e instanceof Qn)return e;e=e.rgb();var t=e.r/255,r=e.g/255,n=e.b/255,a=Math.min(t,r,n),i=Math.max(t,r,n),o=NaN,s=i-a,l=(i+a)/2;return s?(t===i?o=(r-n)/s+(r0&&l<1?0:o,new Qn(o,s,l,e.opacity)}function FY(e,t,r,n){return arguments.length===1?BC(e):new Qn(e,t,r,n??1)}function Qn(e,t,r,n){this.h=+e,this.s=+t,this.l=+r,this.opacity=+n}jw(Qn,FY,zC(Nd,{brighter(e){return e=e==null?Kp:Math.pow(Kp,e),new Qn(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?Lc:Math.pow(Lc,e),new Qn(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*t,a=2*r-n;return new Qr(fy(e>=240?e-240:e+120,a,n),fy(e,a,n),fy(e<120?e+240:e-120,a,n),this.opacity)},clamp(){return new Qn(ok(this.h),pf(this.s),pf(this.l),Xp(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=Xp(this.opacity);return`${e===1?"hsl(":"hsla("}${ok(this.h)}, ${pf(this.s)*100}%, ${pf(this.l)*100}%${e===1?")":`, ${e})`}`}}));function ok(e){return e=(e||0)%360,e<0?e+360:e}function pf(e){return Math.max(0,Math.min(1,e||0))}function fy(e,t,r){return(e<60?t+(r-t)*e/60:e<180?r:e<240?t+(r-t)*(240-e)/60:t)*255}const kw=e=>()=>e;function zY(e,t){return function(r){return e+r*t}}function BY(e,t,r){return e=Math.pow(e,r),t=Math.pow(t,r)-e,r=1/r,function(n){return Math.pow(e+n*t,r)}}function UY(e){return(e=+e)==1?UC:function(t,r){return r-t?BY(t,r,e):kw(isNaN(t)?r:t)}}function UC(e,t){var r=t-e;return r?zY(e,r):kw(isNaN(e)?t:e)}const sk=function e(t){var r=UY(t);function n(a,i){var o=r((a=Ix(a)).r,(i=Ix(i)).r),s=r(a.g,i.g),l=r(a.b,i.b),c=UC(a.opacity,i.opacity);return function(f){return a.r=o(f),a.g=s(f),a.b=l(f),a.opacity=c(f),a+""}}return n.gamma=e,n}(1);function VY(e,t){t||(t=[]);var r=e?Math.min(t.length,e.length):0,n=t.slice(),a;return function(i){for(a=0;ar&&(i=t.slice(r,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(a=a[0])?s[o]?s[o]+=a:s[++o]=a:(s[++o]=null,l.push({i:o,x:Yp(n,a)})),r=py.lastIndex;return rt&&(r=e,e=t,t=r),function(n){return Math.max(e,Math.min(t,n))}}function eZ(e,t,r){var n=e[0],a=e[1],i=t[0],o=t[1];return a2?tZ:eZ,l=c=null,d}function d(p){return p==null||isNaN(p=+p)?i:(l||(l=s(e.map(n),t,r)))(n(o(p)))}return d.invert=function(p){return o(a((c||(c=s(t,e.map(n),Yp)))(p)))},d.domain=function(p){return arguments.length?(e=Array.from(p,Zp),f()):e.slice()},d.range=function(p){return arguments.length?(t=Array.from(p),f()):t.slice()},d.rangeRound=function(p){return t=Array.from(p),r=Ow,f()},d.clamp=function(p){return arguments.length?(o=p?!0:zr,f()):o!==zr},d.interpolate=function(p){return arguments.length?(r=p,f()):r},d.unknown=function(p){return arguments.length?(i=p,d):i},function(p,h){return n=p,a=h,f()}}function Aw(){return Pm()(zr,zr)}function rZ(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function Qp(e,t){if(!isFinite(e)||e===0)return null;var r=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"),n=e.slice(0,r);return[n.length>1?n[0]+n.slice(2):n,+e.slice(r+1)]}function cl(e){return e=Qp(Math.abs(e)),e?e[1]:NaN}function nZ(e,t){return function(r,n){for(var a=r.length,i=[],o=0,s=e[0],l=0;a>0&&s>0&&(l+s+1>n&&(s=Math.max(1,n-l)),i.push(r.substring(a-=s,a+s)),!((l+=s+1)>n));)s=e[o=(o+1)%e.length];return i.reverse().join(t)}}function aZ(e){return function(t){return t.replace(/[0-9]/g,function(r){return e[+r]})}}var iZ=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Bc(e){if(!(t=iZ.exec(e)))throw new Error("invalid format: "+e);var t;return new Nw({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}Bc.prototype=Nw.prototype;function Nw(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}Nw.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function oZ(e){e:for(var t=e.length,r=1,n=-1,a;r0&&(n=0);break}return n>0?e.slice(0,n)+e.slice(a+1):e}var Jp;function sZ(e,t){var r=Qp(e,t);if(!r)return Jp=void 0,e.toPrecision(t);var n=r[0],a=r[1],i=a-(Jp=Math.max(-8,Math.min(8,Math.floor(a/3)))*3)+1,o=n.length;return i===o?n:i>o?n+new Array(i-o+1).join("0"):i>0?n.slice(0,i)+"."+n.slice(i):"0."+new Array(1-i).join("0")+Qp(e,Math.max(0,t+i-1))[0]}function uk(e,t){var r=Qp(e,t);if(!r)return e+"";var n=r[0],a=r[1];return a<0?"0."+new Array(-a).join("0")+n:n.length>a+1?n.slice(0,a+1)+"."+n.slice(a+1):n+new Array(a-n.length+2).join("0")}const ck={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:rZ,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>uk(e*100,t),r:uk,s:sZ,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function dk(e){return e}var fk=Array.prototype.map,pk=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function lZ(e){var t=e.grouping===void 0||e.thousands===void 0?dk:nZ(fk.call(e.grouping,Number),e.thousands+""),r=e.currency===void 0?"":e.currency[0]+"",n=e.currency===void 0?"":e.currency[1]+"",a=e.decimal===void 0?".":e.decimal+"",i=e.numerals===void 0?dk:aZ(fk.call(e.numerals,String)),o=e.percent===void 0?"%":e.percent+"",s=e.minus===void 0?"−":e.minus+"",l=e.nan===void 0?"NaN":e.nan+"";function c(d,p){d=Bc(d);var h=d.fill,x=d.align,v=d.sign,y=d.symbol,g=d.zero,m=d.width,w=d.comma,S=d.precision,b=d.trim,_=d.type;_==="n"?(w=!0,_="g"):ck[_]||(S===void 0&&(S=12),b=!0,_="g"),(g||h==="0"&&x==="=")&&(g=!0,h="0",x="=");var k=(p&&p.prefix!==void 0?p.prefix:"")+(y==="$"?r:y==="#"&&/[boxX]/.test(_)?"0"+_.toLowerCase():""),O=(y==="$"?n:/[%p]/.test(_)?o:"")+(p&&p.suffix!==void 0?p.suffix:""),N=ck[_],T=/[defgprs%]/.test(_);S=S===void 0?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,S)):Math.max(0,Math.min(20,S));function R(A){var C=k,M=O,B,V,I;if(_==="c")M=N(A)+M,A="";else{A=+A;var D=A<0||1/A<0;if(A=isNaN(A)?l:N(Math.abs(A),S),b&&(A=oZ(A)),D&&+A==0&&v!=="+"&&(D=!1),C=(D?v==="("?v:s:v==="-"||v==="("?"":v)+C,M=(_==="s"&&!isNaN(A)&&Jp!==void 0?pk[8+Jp/3]:"")+M+(D&&v==="("?")":""),T){for(B=-1,V=A.length;++BI||I>57){M=(I===46?a+A.slice(B+1):A.slice(B))+M,A=A.slice(0,B);break}}}w&&!g&&(A=t(A,1/0));var U=C.length+A.length+M.length,G=U>1)+C+A+M+G.slice(U);break;default:A=G+C+A+M;break}return i(A)}return R.toString=function(){return d+""},R}function f(d,p){var h=Math.max(-8,Math.min(8,Math.floor(cl(p)/3)))*3,x=Math.pow(10,-h),v=c((d=Bc(d),d.type="f",d),{suffix:pk[8+h/3]});return function(y){return v(x*y)}}return{format:c,formatPrefix:f}}var hf,Ew,VC;uZ({thousands:",",grouping:[3],currency:["$",""]});function uZ(e){return hf=lZ(e),Ew=hf.format,VC=hf.formatPrefix,hf}function cZ(e){return Math.max(0,-cl(Math.abs(e)))}function dZ(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(cl(t)/3)))*3-cl(Math.abs(e)))}function fZ(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,cl(t)-cl(e))+1}function WC(e,t,r,n){var a=$x(e,t,r),i;switch(n=Bc(n??",f"),n.type){case"s":{var o=Math.max(Math.abs(e),Math.abs(t));return n.precision==null&&!isNaN(i=dZ(a,o))&&(n.precision=i),VC(n,o)}case"":case"e":case"g":case"p":case"r":{n.precision==null&&!isNaN(i=fZ(a,Math.max(Math.abs(e),Math.abs(t))))&&(n.precision=i-(n.type==="e"));break}case"f":case"%":{n.precision==null&&!isNaN(i=cZ(a))&&(n.precision=i-(n.type==="%")*2);break}}return Ew(n)}function Qi(e){var t=e.domain;return e.ticks=function(r){var n=t();return Cx(n[0],n[n.length-1],r??10)},e.tickFormat=function(r,n){var a=t();return WC(a[0],a[a.length-1],r??10,n)},e.nice=function(r){r==null&&(r=10);var n=t(),a=0,i=n.length-1,o=n[a],s=n[i],l,c,f=10;for(s0;){if(c=Tx(o,s,r),c===l)return n[a]=o,n[i]=s,t(n);if(c>0)o=Math.floor(o/c)*c,s=Math.ceil(s/c)*c;else if(c<0)o=Math.ceil(o*c)/c,s=Math.floor(s*c)/c;else break;l=c}return e},e}function eh(){var e=Aw();return e.copy=function(){return Ed(e,eh())},Bn.apply(e,arguments),Qi(e)}function HC(e){var t;function r(n){return n==null||isNaN(n=+n)?t:n}return r.invert=r,r.domain=r.range=function(n){return arguments.length?(e=Array.from(n,Zp),r):e.slice()},r.unknown=function(n){return arguments.length?(t=n,r):t},r.copy=function(){return HC(e).unknown(t)},e=arguments.length?Array.from(e,Zp):[0,1],Qi(r)}function GC(e,t){e=e.slice();var r=0,n=e.length-1,a=e[r],i=e[n],o;return iMath.pow(e,t)}function yZ(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function vk(e){return(t,r)=>-e(-t,r)}function Pw(e){const t=e(hk,mk),r=t.domain;let n=10,a,i;function o(){return a=yZ(n),i=vZ(n),r()[0]<0?(a=vk(a),i=vk(i),e(pZ,hZ)):e(hk,mk),t}return t.base=function(s){return arguments.length?(n=+s,o()):n},t.domain=function(s){return arguments.length?(r(s),o()):r()},t.ticks=s=>{const l=r();let c=l[0],f=l[l.length-1];const d=f0){for(;p<=h;++p)for(x=1;xf)break;g.push(v)}}else for(;p<=h;++p)for(x=n-1;x>=1;--x)if(v=p>0?x/i(-p):x*i(p),!(vf)break;g.push(v)}g.length*2{if(s==null&&(s=10),l==null&&(l=n===10?"s":","),typeof l!="function"&&(!(n%1)&&(l=Bc(l)).precision==null&&(l.trim=!0),l=Ew(l)),s===1/0)return l;const c=Math.max(1,n*s/t.ticks().length);return f=>{let d=f/i(Math.round(a(f)));return d*nr(GC(r(),{floor:s=>i(Math.floor(a(s))),ceil:s=>i(Math.ceil(a(s)))})),t}function qC(){const e=Pw(Pm()).domain([1,10]);return e.copy=()=>Ed(e,qC()).base(e.base()),Bn.apply(e,arguments),e}function yk(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function gk(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function Cw(e){var t=1,r=e(yk(t),gk(t));return r.constant=function(n){return arguments.length?e(yk(t=+n),gk(t)):t},Qi(r)}function KC(){var e=Cw(Pm());return e.copy=function(){return Ed(e,KC()).constant(e.constant())},Bn.apply(e,arguments)}function xk(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function gZ(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function xZ(e){return e<0?-e*e:e*e}function Tw(e){var t=e(zr,zr),r=1;function n(){return r===1?e(zr,zr):r===.5?e(gZ,xZ):e(xk(r),xk(1/r))}return t.exponent=function(a){return arguments.length?(r=+a,n()):r},Qi(t)}function $w(){var e=Tw(Pm());return e.copy=function(){return Ed(e,$w()).exponent(e.exponent())},Bn.apply(e,arguments),e}function bZ(){return $w.apply(null,arguments).exponent(.5)}function bk(e){return Math.sign(e)*e*e}function wZ(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function XC(){var e=Aw(),t=[0,1],r=!1,n;function a(i){var o=wZ(e(i));return isNaN(o)?n:r?Math.round(o):o}return a.invert=function(i){return e.invert(bk(i))},a.domain=function(i){return arguments.length?(e.domain(i),a):e.domain()},a.range=function(i){return arguments.length?(e.range((t=Array.from(i,Zp)).map(bk)),a):t.slice()},a.rangeRound=function(i){return a.range(i).round(!0)},a.round=function(i){return arguments.length?(r=!!i,a):r},a.clamp=function(i){return arguments.length?(e.clamp(i),a):e.clamp()},a.unknown=function(i){return arguments.length?(n=i,a):n},a.copy=function(){return XC(e.domain(),t).round(r).clamp(e.clamp()).unknown(n)},Bn.apply(a,arguments),Qi(a)}function YC(){var e=[],t=[],r=[],n;function a(){var o=0,s=Math.max(1,t.length);for(r=new Array(s-1);++o0?r[s-1]:e[0],s=r?[n[r-1],t]:[n[c-1],n[c]]},o.unknown=function(l){return arguments.length&&(i=l),o},o.thresholds=function(){return n.slice()},o.copy=function(){return ZC().domain([e,t]).range(a).unknown(i)},Bn.apply(Qi(o),arguments)}function QC(){var e=[.5],t=[0,1],r,n=1;function a(i){return i!=null&&i<=i?t[Ad(e,i,0,n)]:r}return a.domain=function(i){return arguments.length?(e=Array.from(i),n=Math.min(e.length,t.length-1),a):e.slice()},a.range=function(i){return arguments.length?(t=Array.from(i),n=Math.min(e.length,t.length-1),a):t.slice()},a.invertExtent=function(i){var o=t.indexOf(i);return[e[o-1],e[o]]},a.unknown=function(i){return arguments.length?(r=i,a):r},a.copy=function(){return QC().domain(e).range(t).unknown(r)},Bn.apply(a,arguments)}const hy=new Date,my=new Date;function ur(e,t,r,n){function a(i){return e(i=arguments.length===0?new Date:new Date(+i)),i}return a.floor=i=>(e(i=new Date(+i)),i),a.ceil=i=>(e(i=new Date(i-1)),t(i,1),e(i),i),a.round=i=>{const o=a(i),s=a.ceil(i);return i-o(t(i=new Date(+i),o==null?1:Math.floor(o)),i),a.range=(i,o,s)=>{const l=[];if(i=a.ceil(i),s=s==null?1:Math.floor(s),!(i0))return l;let c;do l.push(c=new Date(+i)),t(i,s),e(i);while(cur(o=>{if(o>=o)for(;e(o),!i(o);)o.setTime(o-1)},(o,s)=>{if(o>=o)if(s<0)for(;++s<=0;)for(;t(o,-1),!i(o););else for(;--s>=0;)for(;t(o,1),!i(o););}),r&&(a.count=(i,o)=>(hy.setTime(+i),my.setTime(+o),e(hy),e(my),Math.floor(r(hy,my))),a.every=i=>(i=Math.floor(i),!isFinite(i)||!(i>0)?null:i>1?a.filter(n?o=>n(o)%i===0:o=>a.count(0,o)%i===0):a)),a}const th=ur(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);th.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?ur(t=>{t.setTime(Math.floor(t/e)*e)},(t,r)=>{t.setTime(+t+r*e)},(t,r)=>(r-t)/e):th);th.range;const Va=1e3,Rn=Va*60,Wa=Rn*60,ti=Wa*24,Rw=ti*7,wk=ti*30,vy=ti*365,_o=ur(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*Va)},(e,t)=>(t-e)/Va,e=>e.getUTCSeconds());_o.range;const Iw=ur(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Va)},(e,t)=>{e.setTime(+e+t*Rn)},(e,t)=>(t-e)/Rn,e=>e.getMinutes());Iw.range;const Mw=ur(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*Rn)},(e,t)=>(t-e)/Rn,e=>e.getUTCMinutes());Mw.range;const Dw=ur(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*Va-e.getMinutes()*Rn)},(e,t)=>{e.setTime(+e+t*Wa)},(e,t)=>(t-e)/Wa,e=>e.getHours());Dw.range;const Lw=ur(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*Wa)},(e,t)=>(t-e)/Wa,e=>e.getUTCHours());Lw.range;const Pd=ur(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*Rn)/ti,e=>e.getDate()-1);Pd.range;const Cm=ur(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/ti,e=>e.getUTCDate()-1);Cm.range;const JC=ur(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/ti,e=>Math.floor(e/ti));JC.range;function Zo(e){return ur(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,r)=>{t.setDate(t.getDate()+r*7)},(t,r)=>(r-t-(r.getTimezoneOffset()-t.getTimezoneOffset())*Rn)/Rw)}const Tm=Zo(0),rh=Zo(1),_Z=Zo(2),SZ=Zo(3),dl=Zo(4),jZ=Zo(5),kZ=Zo(6);Tm.range;rh.range;_Z.range;SZ.range;dl.range;jZ.range;kZ.range;function Qo(e){return ur(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCDate(t.getUTCDate()+r*7)},(t,r)=>(r-t)/Rw)}const $m=Qo(0),nh=Qo(1),OZ=Qo(2),AZ=Qo(3),fl=Qo(4),NZ=Qo(5),EZ=Qo(6);$m.range;nh.range;OZ.range;AZ.range;fl.range;NZ.range;EZ.range;const Fw=ur(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());Fw.range;const zw=ur(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());zw.range;const ri=ur(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());ri.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:ur(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,r)=>{t.setFullYear(t.getFullYear()+r*e)});ri.range;const ni=ur(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());ni.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:ur(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCFullYear(t.getUTCFullYear()+r*e)});ni.range;function eT(e,t,r,n,a,i){const o=[[_o,1,Va],[_o,5,5*Va],[_o,15,15*Va],[_o,30,30*Va],[i,1,Rn],[i,5,5*Rn],[i,15,15*Rn],[i,30,30*Rn],[a,1,Wa],[a,3,3*Wa],[a,6,6*Wa],[a,12,12*Wa],[n,1,ti],[n,2,2*ti],[r,1,Rw],[t,1,wk],[t,3,3*wk],[e,1,vy]];function s(c,f,d){const p=fy).right(o,p);if(h===o.length)return e.every($x(c/vy,f/vy,d));if(h===0)return th.every(Math.max($x(c,f,d),1));const[x,v]=o[p/o[h-1][2]53)return null;"w"in Y||(Y.w=1),"Z"in Y?(xe=gy(du(Y.y,0,1)),$e=xe.getUTCDay(),xe=$e>4||$e===0?nh.ceil(xe):nh(xe),xe=Cm.offset(xe,(Y.V-1)*7),Y.y=xe.getUTCFullYear(),Y.m=xe.getUTCMonth(),Y.d=xe.getUTCDate()+(Y.w+6)%7):(xe=yy(du(Y.y,0,1)),$e=xe.getDay(),xe=$e>4||$e===0?rh.ceil(xe):rh(xe),xe=Pd.offset(xe,(Y.V-1)*7),Y.y=xe.getFullYear(),Y.m=xe.getMonth(),Y.d=xe.getDate()+(Y.w+6)%7)}else("W"in Y||"U"in Y)&&("w"in Y||(Y.w="u"in Y?Y.u%7:"W"in Y?1:0),$e="Z"in Y?gy(du(Y.y,0,1)).getUTCDay():yy(du(Y.y,0,1)).getDay(),Y.m=0,Y.d="W"in Y?(Y.w+6)%7+Y.W*7-($e+5)%7:Y.w+Y.U*7-($e+6)%7);return"Z"in Y?(Y.H+=Y.Z/100|0,Y.M+=Y.Z%100,gy(Y)):yy(Y)}}function O(oe,J,we,Y){for(var Ne=0,xe=J.length,$e=we.length,Ee,je;Ne=$e)return-1;if(Ee=J.charCodeAt(Ne++),Ee===37){if(Ee=J.charAt(Ne++),je=b[Ee in _k?J.charAt(Ne++):Ee],!je||(Y=je(oe,we,Y))<0)return-1}else if(Ee!=we.charCodeAt(Y++))return-1}return Y}function N(oe,J,we){var Y=c.exec(J.slice(we));return Y?(oe.p=f.get(Y[0].toLowerCase()),we+Y[0].length):-1}function T(oe,J,we){var Y=h.exec(J.slice(we));return Y?(oe.w=x.get(Y[0].toLowerCase()),we+Y[0].length):-1}function R(oe,J,we){var Y=d.exec(J.slice(we));return Y?(oe.w=p.get(Y[0].toLowerCase()),we+Y[0].length):-1}function A(oe,J,we){var Y=g.exec(J.slice(we));return Y?(oe.m=m.get(Y[0].toLowerCase()),we+Y[0].length):-1}function C(oe,J,we){var Y=v.exec(J.slice(we));return Y?(oe.m=y.get(Y[0].toLowerCase()),we+Y[0].length):-1}function M(oe,J,we){return O(oe,t,J,we)}function B(oe,J,we){return O(oe,r,J,we)}function V(oe,J,we){return O(oe,n,J,we)}function I(oe){return o[oe.getDay()]}function D(oe){return i[oe.getDay()]}function U(oe){return l[oe.getMonth()]}function G(oe){return s[oe.getMonth()]}function q(oe){return a[+(oe.getHours()>=12)]}function K(oe){return 1+~~(oe.getMonth()/3)}function te(oe){return o[oe.getUTCDay()]}function Z(oe){return i[oe.getUTCDay()]}function de(oe){return l[oe.getUTCMonth()]}function fe(oe){return s[oe.getUTCMonth()]}function Ie(oe){return a[+(oe.getUTCHours()>=12)]}function De(oe){return 1+~~(oe.getUTCMonth()/3)}return{format:function(oe){var J=_(oe+="",w);return J.toString=function(){return oe},J},parse:function(oe){var J=k(oe+="",!1);return J.toString=function(){return oe},J},utcFormat:function(oe){var J=_(oe+="",S);return J.toString=function(){return oe},J},utcParse:function(oe){var J=k(oe+="",!0);return J.toString=function(){return oe},J}}}var _k={"-":"",_:" ",0:"0"},yr=/^\s*\d+/,IZ=/^%/,MZ=/[\\^$*+?|[\]().{}]/g;function st(e,t,r){var n=e<0?"-":"",a=(n?-e:e)+"",i=a.length;return n+(i[t.toLowerCase(),r]))}function LZ(e,t,r){var n=yr.exec(t.slice(r,r+1));return n?(e.w=+n[0],r+n[0].length):-1}function FZ(e,t,r){var n=yr.exec(t.slice(r,r+1));return n?(e.u=+n[0],r+n[0].length):-1}function zZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.U=+n[0],r+n[0].length):-1}function BZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.V=+n[0],r+n[0].length):-1}function UZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.W=+n[0],r+n[0].length):-1}function Sk(e,t,r){var n=yr.exec(t.slice(r,r+4));return n?(e.y=+n[0],r+n[0].length):-1}function jk(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),r+n[0].length):-1}function VZ(e,t,r){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(r,r+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),r+n[0].length):-1}function WZ(e,t,r){var n=yr.exec(t.slice(r,r+1));return n?(e.q=n[0]*3-3,r+n[0].length):-1}function HZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.m=n[0]-1,r+n[0].length):-1}function kk(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.d=+n[0],r+n[0].length):-1}function GZ(e,t,r){var n=yr.exec(t.slice(r,r+3));return n?(e.m=0,e.d=+n[0],r+n[0].length):-1}function Ok(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.H=+n[0],r+n[0].length):-1}function qZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.M=+n[0],r+n[0].length):-1}function KZ(e,t,r){var n=yr.exec(t.slice(r,r+2));return n?(e.S=+n[0],r+n[0].length):-1}function XZ(e,t,r){var n=yr.exec(t.slice(r,r+3));return n?(e.L=+n[0],r+n[0].length):-1}function YZ(e,t,r){var n=yr.exec(t.slice(r,r+6));return n?(e.L=Math.floor(n[0]/1e3),r+n[0].length):-1}function ZZ(e,t,r){var n=IZ.exec(t.slice(r,r+1));return n?r+n[0].length:-1}function QZ(e,t,r){var n=yr.exec(t.slice(r));return n?(e.Q=+n[0],r+n[0].length):-1}function JZ(e,t,r){var n=yr.exec(t.slice(r));return n?(e.s=+n[0],r+n[0].length):-1}function Ak(e,t){return st(e.getDate(),t,2)}function eQ(e,t){return st(e.getHours(),t,2)}function tQ(e,t){return st(e.getHours()%12||12,t,2)}function rQ(e,t){return st(1+Pd.count(ri(e),e),t,3)}function tT(e,t){return st(e.getMilliseconds(),t,3)}function nQ(e,t){return tT(e,t)+"000"}function aQ(e,t){return st(e.getMonth()+1,t,2)}function iQ(e,t){return st(e.getMinutes(),t,2)}function oQ(e,t){return st(e.getSeconds(),t,2)}function sQ(e){var t=e.getDay();return t===0?7:t}function lQ(e,t){return st(Tm.count(ri(e)-1,e),t,2)}function rT(e){var t=e.getDay();return t>=4||t===0?dl(e):dl.ceil(e)}function uQ(e,t){return e=rT(e),st(dl.count(ri(e),e)+(ri(e).getDay()===4),t,2)}function cQ(e){return e.getDay()}function dQ(e,t){return st(rh.count(ri(e)-1,e),t,2)}function fQ(e,t){return st(e.getFullYear()%100,t,2)}function pQ(e,t){return e=rT(e),st(e.getFullYear()%100,t,2)}function hQ(e,t){return st(e.getFullYear()%1e4,t,4)}function mQ(e,t){var r=e.getDay();return e=r>=4||r===0?dl(e):dl.ceil(e),st(e.getFullYear()%1e4,t,4)}function vQ(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+st(t/60|0,"0",2)+st(t%60,"0",2)}function Nk(e,t){return st(e.getUTCDate(),t,2)}function yQ(e,t){return st(e.getUTCHours(),t,2)}function gQ(e,t){return st(e.getUTCHours()%12||12,t,2)}function xQ(e,t){return st(1+Cm.count(ni(e),e),t,3)}function nT(e,t){return st(e.getUTCMilliseconds(),t,3)}function bQ(e,t){return nT(e,t)+"000"}function wQ(e,t){return st(e.getUTCMonth()+1,t,2)}function _Q(e,t){return st(e.getUTCMinutes(),t,2)}function SQ(e,t){return st(e.getUTCSeconds(),t,2)}function jQ(e){var t=e.getUTCDay();return t===0?7:t}function kQ(e,t){return st($m.count(ni(e)-1,e),t,2)}function aT(e){var t=e.getUTCDay();return t>=4||t===0?fl(e):fl.ceil(e)}function OQ(e,t){return e=aT(e),st(fl.count(ni(e),e)+(ni(e).getUTCDay()===4),t,2)}function AQ(e){return e.getUTCDay()}function NQ(e,t){return st(nh.count(ni(e)-1,e),t,2)}function EQ(e,t){return st(e.getUTCFullYear()%100,t,2)}function PQ(e,t){return e=aT(e),st(e.getUTCFullYear()%100,t,2)}function CQ(e,t){return st(e.getUTCFullYear()%1e4,t,4)}function TQ(e,t){var r=e.getUTCDay();return e=r>=4||r===0?fl(e):fl.ceil(e),st(e.getUTCFullYear()%1e4,t,4)}function $Q(){return"+0000"}function Ek(){return"%"}function Pk(e){return+e}function Ck(e){return Math.floor(+e/1e3)}var is,iT,oT;RQ({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function RQ(e){return is=RZ(e),iT=is.format,is.parse,oT=is.utcFormat,is.utcParse,is}function IQ(e){return new Date(e)}function MQ(e){return e instanceof Date?+e:+new Date(+e)}function Bw(e,t,r,n,a,i,o,s,l,c){var f=Aw(),d=f.invert,p=f.domain,h=c(".%L"),x=c(":%S"),v=c("%I:%M"),y=c("%I %p"),g=c("%a %d"),m=c("%b %d"),w=c("%B"),S=c("%Y");function b(_){return(l(_)<_?h:s(_)<_?x:o(_)<_?v:i(_)<_?y:n(_)<_?a(_)<_?g:m:r(_)<_?w:S)(_)}return f.invert=function(_){return new Date(d(_))},f.domain=function(_){return arguments.length?p(Array.from(_,MQ)):p().map(IQ)},f.ticks=function(_){var k=p();return e(k[0],k[k.length-1],_??10)},f.tickFormat=function(_,k){return k==null?b:c(k)},f.nice=function(_){var k=p();return(!_||typeof _.range!="function")&&(_=t(k[0],k[k.length-1],_??10)),_?p(GC(k,_)):f},f.copy=function(){return Ed(f,Bw(e,t,r,n,a,i,o,s,l,c))},f}function DQ(){return Bn.apply(Bw(TZ,$Z,ri,Fw,Tm,Pd,Dw,Iw,_o,iT).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)}function LQ(){return Bn.apply(Bw(PZ,CZ,ni,zw,$m,Cm,Lw,Mw,_o,oT).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)}function Rm(){var e=0,t=1,r,n,a,i,o=zr,s=!1,l;function c(d){return d==null||isNaN(d=+d)?l:o(a===0?.5:(d=(i(d)-r)*a,s?Math.max(0,Math.min(1,d)):d))}c.domain=function(d){return arguments.length?([e,t]=d,r=i(e=+e),n=i(t=+t),a=r===n?0:1/(n-r),c):[e,t]},c.clamp=function(d){return arguments.length?(s=!!d,c):s},c.interpolator=function(d){return arguments.length?(o=d,c):o};function f(d){return function(p){var h,x;return arguments.length?([h,x]=p,o=d(h,x),c):[o(0),o(1)]}}return c.range=f(ql),c.rangeRound=f(Ow),c.unknown=function(d){return arguments.length?(l=d,c):l},function(d){return i=d,r=d(e),n=d(t),a=r===n?0:1/(n-r),c}}function Ji(e,t){return t.domain(e.domain()).interpolator(e.interpolator()).clamp(e.clamp()).unknown(e.unknown())}function sT(){var e=Qi(Rm()(zr));return e.copy=function(){return Ji(e,sT())},li.apply(e,arguments)}function lT(){var e=Pw(Rm()).domain([1,10]);return e.copy=function(){return Ji(e,lT()).base(e.base())},li.apply(e,arguments)}function uT(){var e=Cw(Rm());return e.copy=function(){return Ji(e,uT()).constant(e.constant())},li.apply(e,arguments)}function Uw(){var e=Tw(Rm());return e.copy=function(){return Ji(e,Uw()).exponent(e.exponent())},li.apply(e,arguments)}function FQ(){return Uw.apply(null,arguments).exponent(.5)}function cT(){var e=[],t=zr;function r(n){if(n!=null&&!isNaN(n=+n))return t((Ad(e,n,1)-1)/(e.length-1))}return r.domain=function(n){if(!arguments.length)return e.slice();e=[];for(let a of n)a!=null&&!isNaN(a=+a)&&e.push(a);return e.sort(Fi),r},r.interpolator=function(n){return arguments.length?(t=n,r):t},r.range=function(){return e.map((n,a)=>t(a/(e.length-1)))},r.quantiles=function(n){return Array.from({length:n+1},(a,i)=>kY(e,i/n))},r.copy=function(){return cT(t).domain(e)},li.apply(r,arguments)}function Im(){var e=0,t=.5,r=1,n=1,a,i,o,s,l,c=zr,f,d=!1,p;function h(v){return isNaN(v=+v)?p:(v=.5+((v=+f(v))-i)*(n*vt}var hT=VQ,WQ=Mm,HQ=hT,GQ=Gl;function qQ(e){return e&&e.length?WQ(e,GQ,HQ):void 0}var KQ=qQ;const Ai=dt(KQ);function XQ(e,t){return ee.e^i.s<0?1:-1;for(n=i.d.length,a=e.d.length,t=0,r=ne.d[t]^i.s<0?1:-1;return n===a?0:n>a^i.s<0?1:-1};ge.decimalPlaces=ge.dp=function(){var e=this,t=e.d.length-1,r=(t-e.e)*Nt;if(t=e.d[t],t)for(;t%10==0;t/=10)r--;return r<0?0:r};ge.dividedBy=ge.div=function(e){return Ka(this,new this.constructor(e))};ge.dividedToIntegerBy=ge.idiv=function(e){var t=this,r=t.constructor;return yt(Ka(t,new r(e),0,1),r.precision)};ge.equals=ge.eq=function(e){return!this.cmp(e)};ge.exponent=function(){return Qt(this)};ge.greaterThan=ge.gt=function(e){return this.cmp(e)>0};ge.greaterThanOrEqualTo=ge.gte=function(e){return this.cmp(e)>=0};ge.isInteger=ge.isint=function(){return this.e>this.d.length-2};ge.isNegative=ge.isneg=function(){return this.s<0};ge.isPositive=ge.ispos=function(){return this.s>0};ge.isZero=function(){return this.s===0};ge.lessThan=ge.lt=function(e){return this.cmp(e)<0};ge.lessThanOrEqualTo=ge.lte=function(e){return this.cmp(e)<1};ge.logarithm=ge.log=function(e){var t,r=this,n=r.constructor,a=n.precision,i=a+5;if(e===void 0)e=new n(10);else if(e=new n(e),e.s<1||e.eq(dn))throw Error(Ln+"NaN");if(r.s<1)throw Error(Ln+(r.s?"NaN":"-Infinity"));return r.eq(dn)?new n(0):(Tt=!1,t=Ka(Uc(r,i),Uc(e,i),i),Tt=!0,yt(t,a))};ge.minus=ge.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?xT(t,e):yT(t,(e.s=-e.s,e))};ge.modulo=ge.mod=function(e){var t,r=this,n=r.constructor,a=n.precision;if(e=new n(e),!e.s)throw Error(Ln+"NaN");return r.s?(Tt=!1,t=Ka(r,e,0,1).times(e),Tt=!0,r.minus(t)):yt(new n(r),a)};ge.naturalExponential=ge.exp=function(){return gT(this)};ge.naturalLogarithm=ge.ln=function(){return Uc(this)};ge.negated=ge.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};ge.plus=ge.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?yT(t,e):xT(t,(e.s=-e.s,e))};ge.precision=ge.sd=function(e){var t,r,n,a=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(Eo+e);if(t=Qt(a)+1,n=a.d.length-1,r=n*Nt+1,n=a.d[n],n){for(;n%10==0;n/=10)r--;for(n=a.d[0];n>=10;n/=10)r++}return e&&t>r?t:r};ge.squareRoot=ge.sqrt=function(){var e,t,r,n,a,i,o,s=this,l=s.constructor;if(s.s<1){if(!s.s)return new l(0);throw Error(Ln+"NaN")}for(e=Qt(s),Tt=!1,a=Math.sqrt(+s),a==0||a==1/0?(t=pa(s.d),(t.length+e)%2==0&&(t+="0"),a=Math.sqrt(t),e=Xl((e+1)/2)-(e<0||e%2),a==1/0?t="5e"+e:(t=a.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),n=new l(t)):n=new l(a.toString()),r=l.precision,a=o=r+3;;)if(i=n,n=i.plus(Ka(s,i,o+2)).times(.5),pa(i.d).slice(0,o)===(t=pa(n.d)).slice(0,o)){if(t=t.slice(o-3,o+1),a==o&&t=="4999"){if(yt(i,r+1,0),i.times(i).eq(s)){n=i;break}}else if(t!="9999")break;o+=4}return Tt=!0,yt(n,r)};ge.times=ge.mul=function(e){var t,r,n,a,i,o,s,l,c,f=this,d=f.constructor,p=f.d,h=(e=new d(e)).d;if(!f.s||!e.s)return new d(0);for(e.s*=f.s,r=f.e+e.e,l=p.length,c=h.length,l=0;){for(t=0,a=l+n;a>n;)s=i[a]+h[n]*p[a-n-1]+t,i[a--]=s%dr|0,t=s/dr|0;i[a]=(i[a]+t)%dr|0}for(;!i[--o];)i.pop();return t?++r:i.shift(),e.d=i,e.e=r,Tt?yt(e,d.precision):e};ge.toDecimalPlaces=ge.todp=function(e,t){var r=this,n=r.constructor;return r=new n(r),e===void 0?r:(ja(e,0,Kl),t===void 0?t=n.rounding:ja(t,0,8),yt(r,e+Qt(r)+1,t))};ge.toExponential=function(e,t){var r,n=this,a=n.constructor;return e===void 0?r=Wo(n,!0):(ja(e,0,Kl),t===void 0?t=a.rounding:ja(t,0,8),n=yt(new a(n),e+1,t),r=Wo(n,!0,e+1)),r};ge.toFixed=function(e,t){var r,n,a=this,i=a.constructor;return e===void 0?Wo(a):(ja(e,0,Kl),t===void 0?t=i.rounding:ja(t,0,8),n=yt(new i(a),e+Qt(a)+1,t),r=Wo(n.abs(),!1,e+Qt(n)+1),a.isneg()&&!a.isZero()?"-"+r:r)};ge.toInteger=ge.toint=function(){var e=this,t=e.constructor;return yt(new t(e),Qt(e)+1,t.rounding)};ge.toNumber=function(){return+this};ge.toPower=ge.pow=function(e){var t,r,n,a,i,o,s=this,l=s.constructor,c=12,f=+(e=new l(e));if(!e.s)return new l(dn);if(s=new l(s),!s.s){if(e.s<1)throw Error(Ln+"Infinity");return s}if(s.eq(dn))return s;if(n=l.precision,e.eq(dn))return yt(s,n);if(t=e.e,r=e.d.length-1,o=t>=r,i=s.s,o){if((r=f<0?-f:f)<=vT){for(a=new l(dn),t=Math.ceil(n/Nt+4),Tt=!1;r%2&&(a=a.times(s),Rk(a.d,t)),r=Xl(r/2),r!==0;)s=s.times(s),Rk(s.d,t);return Tt=!0,e.s<0?new l(dn).div(a):yt(a,n)}}else if(i<0)throw Error(Ln+"NaN");return i=i<0&&e.d[Math.max(t,r)]&1?-1:1,s.s=1,Tt=!1,a=e.times(Uc(s,n+c)),Tt=!0,a=gT(a),a.s=i,a};ge.toPrecision=function(e,t){var r,n,a=this,i=a.constructor;return e===void 0?(r=Qt(a),n=Wo(a,r<=i.toExpNeg||r>=i.toExpPos)):(ja(e,1,Kl),t===void 0?t=i.rounding:ja(t,0,8),a=yt(new i(a),e,t),r=Qt(a),n=Wo(a,e<=r||r<=i.toExpNeg,e)),n};ge.toSignificantDigits=ge.tosd=function(e,t){var r=this,n=r.constructor;return e===void 0?(e=n.precision,t=n.rounding):(ja(e,1,Kl),t===void 0?t=n.rounding:ja(t,0,8)),yt(new n(r),e,t)};ge.toString=ge.valueOf=ge.val=ge.toJSON=ge[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Qt(e),r=e.constructor;return Wo(e,t<=r.toExpNeg||t>=r.toExpPos)};function yT(e,t){var r,n,a,i,o,s,l,c,f=e.constructor,d=f.precision;if(!e.s||!t.s)return t.s||(t=new f(e)),Tt?yt(t,d):t;if(l=e.d,c=t.d,o=e.e,a=t.e,l=l.slice(),i=o-a,i){for(i<0?(n=l,i=-i,s=c.length):(n=c,a=o,s=l.length),o=Math.ceil(d/Nt),s=o>s?o+1:s+1,i>s&&(i=s,n.length=1),n.reverse();i--;)n.push(0);n.reverse()}for(s=l.length,i=c.length,s-i<0&&(i=s,n=c,c=l,l=n),r=0;i;)r=(l[--i]=l[i]+c[i]+r)/dr|0,l[i]%=dr;for(r&&(l.unshift(r),++a),s=l.length;l[--s]==0;)l.pop();return t.d=l,t.e=a,Tt?yt(t,d):t}function ja(e,t,r){if(e!==~~e||er)throw Error(Eo+e)}function pa(e){var t,r,n,a=e.length-1,i="",o=e[0];if(a>0){for(i+=o,t=1;to?1:-1;else for(s=l=0;sa[s]?1:-1;break}return l}function r(n,a,i){for(var o=0;i--;)n[i]-=o,o=n[i]1;)n.shift()}return function(n,a,i,o){var s,l,c,f,d,p,h,x,v,y,g,m,w,S,b,_,k,O,N=n.constructor,T=n.s==a.s?1:-1,R=n.d,A=a.d;if(!n.s)return new N(n);if(!a.s)throw Error(Ln+"Division by zero");for(l=n.e-a.e,k=A.length,b=R.length,h=new N(T),x=h.d=[],c=0;A[c]==(R[c]||0);)++c;if(A[c]>(R[c]||0)&&--l,i==null?m=i=N.precision:o?m=i+(Qt(n)-Qt(a))+1:m=i,m<0)return new N(0);if(m=m/Nt+2|0,c=0,k==1)for(f=0,A=A[0],m++;(c1&&(A=e(A,f),R=e(R,f),k=A.length,b=R.length),S=k,v=R.slice(0,k),y=v.length;y=dr/2&&++_;do f=0,s=t(A,v,k,y),s<0?(g=v[0],k!=y&&(g=g*dr+(v[1]||0)),f=g/_|0,f>1?(f>=dr&&(f=dr-1),d=e(A,f),p=d.length,y=v.length,s=t(d,v,p,y),s==1&&(f--,r(d,k16)throw Error(Ww+Qt(e));if(!e.s)return new f(dn);for(Tt=!1,s=d,o=new f(.03125);e.abs().gte(.1);)e=e.times(o),c+=5;for(n=Math.log(po(2,c))/Math.LN10*2+5|0,s+=n,r=a=i=new f(dn),f.precision=s;;){if(a=yt(a.times(e),s),r=r.times(++l),o=i.plus(Ka(a,r,s)),pa(o.d).slice(0,s)===pa(i.d).slice(0,s)){for(;c--;)i=yt(i.times(i),s);return f.precision=d,t==null?(Tt=!0,yt(i,d)):i}i=o}}function Qt(e){for(var t=e.e*Nt,r=e.d[0];r>=10;r/=10)t++;return t}function xy(e,t,r){if(t>e.LN10.sd())throw Tt=!0,r&&(e.precision=r),Error(Ln+"LN10 precision limit exceeded");return yt(new e(e.LN10),t)}function gi(e){for(var t="";e--;)t+="0";return t}function Uc(e,t){var r,n,a,i,o,s,l,c,f,d=1,p=10,h=e,x=h.d,v=h.constructor,y=v.precision;if(h.s<1)throw Error(Ln+(h.s?"NaN":"-Infinity"));if(h.eq(dn))return new v(0);if(t==null?(Tt=!1,c=y):c=t,h.eq(10))return t==null&&(Tt=!0),xy(v,c);if(c+=p,v.precision=c,r=pa(x),n=r.charAt(0),i=Qt(h),Math.abs(i)<15e14){for(;n<7&&n!=1||n==1&&r.charAt(1)>3;)h=h.times(e),r=pa(h.d),n=r.charAt(0),d++;i=Qt(h),n>1?(h=new v("0."+r),i++):h=new v(n+"."+r.slice(1))}else return l=xy(v,c+2,y).times(i+""),h=Uc(new v(n+"."+r.slice(1)),c-p).plus(l),v.precision=y,t==null?(Tt=!0,yt(h,y)):h;for(s=o=h=Ka(h.minus(dn),h.plus(dn),c),f=yt(h.times(h),c),a=3;;){if(o=yt(o.times(f),c),l=s.plus(Ka(o,new v(a),c)),pa(l.d).slice(0,c)===pa(s.d).slice(0,c))return s=s.times(2),i!==0&&(s=s.plus(xy(v,c+2,y).times(i+""))),s=Ka(s,new v(d),c),v.precision=y,t==null?(Tt=!0,yt(s,y)):s;s=l,a+=2}}function $k(e,t){var r,n,a;for((r=t.indexOf("."))>-1&&(t=t.replace(".","")),(n=t.search(/e/i))>0?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;t.charCodeAt(n)===48;)++n;for(a=t.length;t.charCodeAt(a-1)===48;)--a;if(t=t.slice(n,a),t){if(a-=n,r=r-n-1,e.e=Xl(r/Nt),e.d=[],n=(r+1)%Nt,r<0&&(n+=Nt),nah||e.e<-ah))throw Error(Ww+r)}else e.s=0,e.e=0,e.d=[0];return e}function yt(e,t,r){var n,a,i,o,s,l,c,f,d=e.d;for(o=1,i=d[0];i>=10;i/=10)o++;if(n=t-o,n<0)n+=Nt,a=t,c=d[f=0];else{if(f=Math.ceil((n+1)/Nt),i=d.length,f>=i)return e;for(c=i=d[f],o=1;i>=10;i/=10)o++;n%=Nt,a=n-Nt+o}if(r!==void 0&&(i=po(10,o-a-1),s=c/i%10|0,l=t<0||d[f+1]!==void 0||c%i,l=r<4?(s||l)&&(r==0||r==(e.s<0?3:2)):s>5||s==5&&(r==4||l||r==6&&(n>0?a>0?c/po(10,o-a):0:d[f-1])%10&1||r==(e.s<0?8:7))),t<1||!d[0])return l?(i=Qt(e),d.length=1,t=t-i-1,d[0]=po(10,(Nt-t%Nt)%Nt),e.e=Xl(-t/Nt)||0):(d.length=1,d[0]=e.e=e.s=0),e;if(n==0?(d.length=f,i=1,f--):(d.length=f+1,i=po(10,Nt-n),d[f]=a>0?(c/po(10,o-a)%po(10,a)|0)*i:0),l)for(;;)if(f==0){(d[0]+=i)==dr&&(d[0]=1,++e.e);break}else{if(d[f]+=i,d[f]!=dr)break;d[f--]=0,i=1}for(n=d.length;d[--n]===0;)d.pop();if(Tt&&(e.e>ah||e.e<-ah))throw Error(Ww+Qt(e));return e}function xT(e,t){var r,n,a,i,o,s,l,c,f,d,p=e.constructor,h=p.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new p(e),Tt?yt(t,h):t;if(l=e.d,d=t.d,n=t.e,c=e.e,l=l.slice(),o=c-n,o){for(f=o<0,f?(r=l,o=-o,s=d.length):(r=d,n=c,s=l.length),a=Math.max(Math.ceil(h/Nt),s)+2,o>a&&(o=a,r.length=1),r.reverse(),a=o;a--;)r.push(0);r.reverse()}else{for(a=l.length,s=d.length,f=a0;--a)l[s++]=0;for(a=d.length;a>o;){if(l[--a]0?i=i.charAt(0)+"."+i.slice(1)+gi(n):o>1&&(i=i.charAt(0)+"."+i.slice(1)),i=i+(a<0?"e":"e+")+a):a<0?(i="0."+gi(-a-1)+i,r&&(n=r-o)>0&&(i+=gi(n))):a>=o?(i+=gi(a+1-o),r&&(n=r-a-1)>0&&(i=i+"."+gi(n))):((n=a+1)0&&(a+1===o&&(i+="."),i+=gi(n))),e.s<0?"-"+i:i}function Rk(e,t){if(e.length>t)return e.length=t,!0}function bT(e){var t,r,n;function a(i){var o=this;if(!(o instanceof a))return new a(i);if(o.constructor=a,i instanceof a){o.s=i.s,o.e=i.e,o.d=(i=i.d)?i.slice():i;return}if(typeof i=="number"){if(i*0!==0)throw Error(Eo+i);if(i>0)o.s=1;else if(i<0)i=-i,o.s=-1;else{o.s=0,o.e=0,o.d=[0];return}if(i===~~i&&i<1e7){o.e=0,o.d=[i];return}return $k(o,i.toString())}else if(typeof i!="string")throw Error(Eo+i);if(i.charCodeAt(0)===45?(i=i.slice(1),o.s=-1):o.s=1,vJ.test(i))$k(o,i);else throw Error(Eo+i)}if(a.prototype=ge,a.ROUND_UP=0,a.ROUND_DOWN=1,a.ROUND_CEIL=2,a.ROUND_FLOOR=3,a.ROUND_HALF_UP=4,a.ROUND_HALF_DOWN=5,a.ROUND_HALF_EVEN=6,a.ROUND_HALF_CEIL=7,a.ROUND_HALF_FLOOR=8,a.clone=bT,a.config=a.set=yJ,e===void 0&&(e={}),e)for(n=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=a[t+1]&&n<=a[t+2])this[r]=n;else throw Error(Eo+r+": "+n);if((n=e[r="LN10"])!==void 0)if(n==Math.LN10)this[r]=new this(n);else throw Error(Eo+r+": "+n);return this}var Hw=bT(mJ);dn=new Hw(1);const mt=Hw;function gJ(e){return _J(e)||wJ(e)||bJ(e)||xJ()}function xJ(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function bJ(e,t){if(e){if(typeof e=="string")return Lx(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Lx(e,t)}}function wJ(e){if(typeof Symbol<"u"&&Symbol.iterator in Object(e))return Array.from(e)}function _J(e){if(Array.isArray(e))return Lx(e)}function Lx(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=t?r.apply(void 0,a):e(t-o,Ik(function(){for(var s=arguments.length,l=new Array(s),c=0;ce.length)&&(t=e.length);for(var r=0,n=new Array(t);r"u"||!(Symbol.iterator in Object(e)))){var r=[],n=!0,a=!1,i=void 0;try{for(var o=e[Symbol.iterator](),s;!(n=(s=o.next()).done)&&(r.push(s.value),!(t&&r.length===t));n=!0);}catch(l){a=!0,i=l}finally{try{!n&&o.return!=null&&o.return()}finally{if(a)throw i}}return r}}function DJ(e){if(Array.isArray(e))return e}function kT(e){var t=Vc(e,2),r=t[0],n=t[1],a=r,i=n;return r>n&&(a=n,i=r),[a,i]}function OT(e,t,r){if(e.lte(0))return new mt(0);var n=Fm.getDigitCount(e.toNumber()),a=new mt(10).pow(n),i=e.div(a),o=n!==1?.05:.1,s=new mt(Math.ceil(i.div(o).toNumber())).add(r).mul(o),l=s.mul(a);return t?l:new mt(Math.ceil(l))}function LJ(e,t,r){var n=1,a=new mt(e);if(!a.isint()&&r){var i=Math.abs(e);i<1?(n=new mt(10).pow(Fm.getDigitCount(e)-1),a=new mt(Math.floor(a.div(n).toNumber())).mul(n)):i>1&&(a=new mt(Math.floor(e)))}else e===0?a=new mt(Math.floor((t-1)/2)):r||(a=new mt(Math.floor(e)));var o=Math.floor((t-1)/2),s=OJ(kJ(function(l){return a.add(new mt(l-o).mul(n)).toNumber()}),Fx);return s(0,t)}function AT(e,t,r,n){var a=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((t-e)/(r-1)))return{step:new mt(0),tickMin:new mt(0),tickMax:new mt(0)};var i=OT(new mt(t).sub(e).div(r-1),n,a),o;e<=0&&t>=0?o=new mt(0):(o=new mt(e).add(t).div(2),o=o.sub(new mt(o).mod(i)));var s=Math.ceil(o.sub(e).div(i).toNumber()),l=Math.ceil(new mt(t).sub(o).div(i).toNumber()),c=s+l+1;return c>r?AT(e,t,r,n,a+1):(c0?l+(r-c):l,s=t>0?s:s+(r-c)),{step:i,tickMin:o.sub(new mt(s).mul(i)),tickMax:o.add(new mt(l).mul(i))})}function FJ(e){var t=Vc(e,2),r=t[0],n=t[1],a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,o=Math.max(a,2),s=kT([r,n]),l=Vc(s,2),c=l[0],f=l[1];if(c===-1/0||f===1/0){var d=f===1/0?[c].concat(Bx(Fx(0,a-1).map(function(){return 1/0}))):[].concat(Bx(Fx(0,a-1).map(function(){return-1/0})),[f]);return r>n?zx(d):d}if(c===f)return LJ(c,a,i);var p=AT(c,f,o,i),h=p.step,x=p.tickMin,v=p.tickMax,y=Fm.rangeStep(x,v.add(new mt(.1).mul(h)),h);return r>n?zx(y):y}function zJ(e,t){var r=Vc(e,2),n=r[0],a=r[1],i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,o=kT([n,a]),s=Vc(o,2),l=s[0],c=s[1];if(l===-1/0||c===1/0)return[n,a];if(l===c)return[l];var f=Math.max(t,2),d=OT(new mt(c).sub(l).div(f-1),i,0),p=[].concat(Bx(Fm.rangeStep(new mt(l),new mt(c).sub(new mt(.99).mul(d)),d)),[c]);return n>a?zx(p):p}var BJ=ST(FJ),UJ=ST(zJ),VJ="Invariant failed";function Ho(e,t){throw new Error(VJ)}var WJ=["offset","layout","width","dataKey","data","dataPointFormatter","xAxis","yAxis"];function pl(e){"@babel/helpers - typeof";return pl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},pl(e)}function ih(){return ih=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function ZJ(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function QJ(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function JJ(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r1&&arguments[1]!==void 0?arguments[1]:[],a=arguments.length>2?arguments[2]:void 0,i=arguments.length>3?arguments[3]:void 0,o=-1,s=(r=n==null?void 0:n.length)!==null&&r!==void 0?r:0;if(s<=1)return 0;if(i&&i.axisType==="angleAxis"&&Math.abs(Math.abs(i.range[1]-i.range[0])-360)<=1e-6)for(var l=i.range,c=0;c0?a[c-1].coordinate:a[s-1].coordinate,d=a[c].coordinate,p=c>=s-1?a[0].coordinate:a[c+1].coordinate,h=void 0;if(Lr(d-f)!==Lr(p-d)){var x=[];if(Lr(p-d)===Lr(l[1]-l[0])){h=p;var v=d+l[1]-l[0];x[0]=Math.min(v,(v+f)/2),x[1]=Math.max(v,(v+f)/2)}else{h=f;var y=p+l[1]-l[0];x[0]=Math.min(d,(y+d)/2),x[1]=Math.max(d,(y+d)/2)}var g=[Math.min(d,(h+d)/2),Math.max(d,(h+d)/2)];if(t>g[0]&&t<=g[1]||t>=x[0]&&t<=x[1]){o=a[c].index;break}}else{var m=Math.min(f,p),w=Math.max(f,p);if(t>(m+d)/2&&t<=(w+d)/2){o=a[c].index;break}}}else for(var S=0;S0&&S(n[S].coordinate+n[S-1].coordinate)/2&&t<=(n[S].coordinate+n[S+1].coordinate)/2||S===s-1&&t>(n[S].coordinate+n[S-1].coordinate)/2){o=n[S].index;break}return o},Gw=function(t){var r,n=t,a=n.type.displayName,i=(r=t.type)!==null&&r!==void 0&&r.defaultProps?zt(zt({},t.type.defaultProps),t.props):t.props,o=i.stroke,s=i.fill,l;switch(a){case"Line":l=o;break;case"Area":case"Radar":l=o&&o!=="none"?o:s;break;default:l=s;break}return l},vee=function(t){var r=t.barSize,n=t.totalSize,a=t.stackGroups,i=a===void 0?{}:a;if(!i)return{};for(var o={},s=Object.keys(i),l=0,c=s.length;l=0});if(g&&g.length){var m=g[0].type.defaultProps,w=m!==void 0?zt(zt({},m),g[0].props):g[0].props,S=w.barSize,b=w[y];o[b]||(o[b]=[]);var _=Fe(S)?r:S;o[b].push({item:g[0],stackList:g.slice(1),barSize:Fe(_)?void 0:Fr(_,n,0)})}}return o},yee=function(t){var r=t.barGap,n=t.barCategoryGap,a=t.bandSize,i=t.sizeList,o=i===void 0?[]:i,s=t.maxBarSize,l=o.length;if(l<1)return null;var c=Fr(r,a,0,!0),f,d=[];if(o[0].barSize===+o[0].barSize){var p=!1,h=a/l,x=o.reduce(function(S,b){return S+b.barSize||0},0);x+=(l-1)*c,x>=a&&(x-=(l-1)*c,c=0),x>=a&&h>0&&(p=!0,h*=.9,x=l*h);var v=(a-x)/2>>0,y={offset:v-c,size:0};f=o.reduce(function(S,b){var _={item:b.item,position:{offset:y.offset+y.size+c,size:p?h:b.barSize}},k=[].concat(Lk(S),[_]);return y=k[k.length-1].position,b.stackList&&b.stackList.length&&b.stackList.forEach(function(O){k.push({item:O,position:y})}),k},d)}else{var g=Fr(n,a,0,!0);a-2*g-(l-1)*c<=0&&(c=0);var m=(a-2*g-(l-1)*c)/l;m>1&&(m>>=0);var w=s===+s?Math.min(m,s):m;f=o.reduce(function(S,b,_){var k=[].concat(Lk(S),[{item:b.item,position:{offset:g+(m+c)*_+(m-w)/2,size:w}}]);return b.stackList&&b.stackList.length&&b.stackList.forEach(function(O){k.push({item:O,position:k[k.length-1].position})}),k},d)}return f},gee=function(t,r,n,a){var i=n.children,o=n.width,s=n.margin,l=o-(s.left||0)-(s.right||0),c=CT({children:i,legendWidth:l});if(c){var f=a||{},d=f.width,p=f.height,h=c.align,x=c.verticalAlign,v=c.layout;if((v==="vertical"||v==="horizontal"&&x==="middle")&&h!=="center"&&ae(t[h]))return zt(zt({},t),{},Gs({},h,t[h]+(d||0)));if((v==="horizontal"||v==="vertical"&&h==="center")&&x!=="middle"&&ae(t[x]))return zt(zt({},t),{},Gs({},x,t[x]+(p||0)))}return t},xee=function(t,r,n){return Fe(r)?!0:t==="horizontal"?r==="yAxis":t==="vertical"||n==="x"?r==="xAxis":n==="y"?r==="yAxis":!0},TT=function(t,r,n,a,i){var o=r.props.children,s=yn(o,Cd).filter(function(c){return xee(a,i,c.props.direction)});if(s&&s.length){var l=s.map(function(c){return c.props.dataKey});return t.reduce(function(c,f){var d=Ht(f,n);if(Fe(d))return c;var p=Array.isArray(d)?[Dm(d),Ai(d)]:[d,d],h=l.reduce(function(x,v){var y=Ht(f,v,0),g=p[0]-Math.abs(Array.isArray(y)?y[0]:y),m=p[1]+Math.abs(Array.isArray(y)?y[1]:y);return[Math.min(g,x[0]),Math.max(m,x[1])]},[1/0,-1/0]);return[Math.min(h[0],c[0]),Math.max(h[1],c[1])]},[1/0,-1/0])}return null},bee=function(t,r,n,a,i){var o=r.map(function(s){return TT(t,s,n,i,a)}).filter(function(s){return!Fe(s)});return o&&o.length?o.reduce(function(s,l){return[Math.min(s[0],l[0]),Math.max(s[1],l[1])]},[1/0,-1/0]):null},$T=function(t,r,n,a,i){var o=r.map(function(l){var c=l.props.dataKey;return n==="number"&&c&&TT(t,l,c,a)||ec(t,c,n,i)});if(n==="number")return o.reduce(function(l,c){return[Math.min(l[0],c[0]),Math.max(l[1],c[1])]},[1/0,-1/0]);var s={};return o.reduce(function(l,c){for(var f=0,d=c.length;f=2?Lr(s[0]-s[1])*2*c:c,r&&(t.ticks||t.niceTicks)){var f=(t.ticks||t.niceTicks).map(function(d){var p=i?i.indexOf(d):d;return{coordinate:a(p)+c,value:d,offset:c}});return f.filter(function(d){return!Vl(d.coordinate)})}return t.isCategorical&&t.categoricalDomain?t.categoricalDomain.map(function(d,p){return{coordinate:a(d)+c,value:d,index:p,offset:c}}):a.ticks&&!n?a.ticks(t.tickCount).map(function(d){return{coordinate:a(d)+c,value:d,offset:c}}):a.domain().map(function(d,p){return{coordinate:a(d)+c,value:i?i[d]:d,index:p,offset:c}})},by=new WeakMap,mf=function(t,r){if(typeof r!="function")return t;by.has(t)||by.set(t,new WeakMap);var n=by.get(t);if(n.has(r))return n.get(r);var a=function(){t.apply(void 0,arguments),r.apply(void 0,arguments)};return n.set(r,a),a},MT=function(t,r,n){var a=t.scale,i=t.type,o=t.layout,s=t.axisType;if(a==="auto")return o==="radial"&&s==="radiusAxis"?{scale:Dc(),realScaleType:"band"}:o==="radial"&&s==="angleAxis"?{scale:eh(),realScaleType:"linear"}:i==="category"&&r&&(r.indexOf("LineChart")>=0||r.indexOf("AreaChart")>=0||r.indexOf("ComposedChart")>=0&&!n)?{scale:Ju(),realScaleType:"point"}:i==="category"?{scale:Dc(),realScaleType:"band"}:{scale:eh(),realScaleType:"linear"};if(zo(a)){var l="scale".concat(Sm(a));return{scale:(Tk[l]||Ju)(),realScaleType:Tk[l]?l:"point"}}return Te(a)?{scale:a}:{scale:Ju(),realScaleType:"point"}},zk=1e-4,DT=function(t){var r=t.domain();if(!(!r||r.length<=2)){var n=r.length,a=t.range(),i=Math.min(a[0],a[1])-zk,o=Math.max(a[0],a[1])+zk,s=t(r[0]),l=t(r[n-1]);(so||lo)&&t.domain([r[0],r[n-1]])}},wee=function(t,r){if(!t)return null;for(var n=0,a=t.length;na)&&(i[1]=a),i[0]>a&&(i[0]=a),i[1]=0?(t[s][n][0]=i,t[s][n][1]=i+l,i=t[s][n][1]):(t[s][n][0]=o,t[s][n][1]=o+l,o=t[s][n][1])}},jee=function(t){var r=t.length;if(!(r<=0))for(var n=0,a=t[0].length;n=0?(t[o][n][0]=i,t[o][n][1]=i+s,i=t[o][n][1]):(t[o][n][0]=0,t[o][n][1]=0)}},kee={sign:See,expand:W9,none:il,silhouette:H9,wiggle:G9,positive:jee},Oee=function(t,r,n){var a=r.map(function(s){return s.props.dataKey}),i=kee[n],o=V9().keys(a).value(function(s,l){return+Ht(s,l,0)}).order(mx).offset(i);return o(t)},Aee=function(t,r,n,a,i,o){if(!t)return null;var s=o?r.reverse():r,l={},c=s.reduce(function(d,p){var h,x=(h=p.type)!==null&&h!==void 0&&h.defaultProps?zt(zt({},p.type.defaultProps),p.props):p.props,v=x.stackId,y=x.hide;if(y)return d;var g=x[n],m=d[g]||{hasStack:!1,stackGroups:{}};if(lr(v)){var w=m.stackGroups[v]||{numericAxisId:n,cateAxisId:a,items:[]};w.items.push(p),m.hasStack=!0,m.stackGroups[v]=w}else m.stackGroups[Yo("_stackId_")]={numericAxisId:n,cateAxisId:a,items:[p]};return zt(zt({},d),{},Gs({},g,m))},l),f={};return Object.keys(c).reduce(function(d,p){var h=c[p];if(h.hasStack){var x={};h.stackGroups=Object.keys(h.stackGroups).reduce(function(v,y){var g=h.stackGroups[y];return zt(zt({},v),{},Gs({},y,{numericAxisId:n,cateAxisId:a,items:g.items,stackedData:Oee(t,g.items,i)}))},x)}return zt(zt({},d),{},Gs({},p,h))},f)},LT=function(t,r){var n=r.realScaleType,a=r.type,i=r.tickCount,o=r.originalDomain,s=r.allowDecimals,l=n||r.scale;if(l!=="auto"&&l!=="linear")return null;if(i&&a==="number"&&o&&(o[0]==="auto"||o[1]==="auto")){var c=t.domain();if(!c.length)return null;var f=BJ(c,i,s);return t.domain([Dm(f),Ai(f)]),{niceTicks:f}}if(i&&a==="number"){var d=t.domain(),p=UJ(d,i,s);return{niceTicks:p}}return null};function sh(e){var t=e.axis,r=e.ticks,n=e.bandSize,a=e.entry,i=e.index,o=e.dataKey;if(t.type==="category"){if(!t.allowDuplicatedCategory&&t.dataKey&&!Fe(a[t.dataKey])){var s=Tp(r,"value",a[t.dataKey]);if(s)return s.coordinate+n/2}return r[i]?r[i].coordinate+n/2:null}var l=Ht(a,Fe(o)?t.dataKey:o);return Fe(l)?null:t.scale(l)}var Bk=function(t){var r=t.axis,n=t.ticks,a=t.offset,i=t.bandSize,o=t.entry,s=t.index;if(r.type==="category")return n[s]?n[s].coordinate+a:null;var l=Ht(o,r.dataKey,r.domain[s]);return Fe(l)?null:r.scale(l)-i/2+a},Nee=function(t){var r=t.numericAxis,n=r.scale.domain();if(r.type==="number"){var a=Math.min(n[0],n[1]),i=Math.max(n[0],n[1]);return a<=0&&i>=0?0:i<0?i:a}return n[0]},Eee=function(t,r){var n,a=(n=t.type)!==null&&n!==void 0&&n.defaultProps?zt(zt({},t.type.defaultProps),t.props):t.props,i=a.stackId;if(lr(i)){var o=r[i];if(o){var s=o.items.indexOf(t);return s>=0?o.stackedData[s]:null}}return null},Pee=function(t){return t.reduce(function(r,n){return[Dm(n.concat([r[0]]).filter(ae)),Ai(n.concat([r[1]]).filter(ae))]},[1/0,-1/0])},FT=function(t,r,n){return Object.keys(t).reduce(function(a,i){var o=t[i],s=o.stackedData,l=s.reduce(function(c,f){var d=Pee(f.slice(r,n+1));return[Math.min(c[0],d[0]),Math.max(c[1],d[1])]},[1/0,-1/0]);return[Math.min(l[0],a[0]),Math.max(l[1],a[1])]},[1/0,-1/0]).map(function(a){return a===1/0||a===-1/0?0:a})},Uk=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,Vk=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,Hx=function(t,r,n){if(Te(t))return t(r,n);if(!Array.isArray(t))return r;var a=[];if(ae(t[0]))a[0]=n?t[0]:Math.min(t[0],r[0]);else if(Uk.test(t[0])){var i=+Uk.exec(t[0])[1];a[0]=r[0]-i}else Te(t[0])?a[0]=t[0](r[0]):a[0]=r[0];if(ae(t[1]))a[1]=n?t[1]:Math.max(t[1],r[1]);else if(Vk.test(t[1])){var o=+Vk.exec(t[1])[1];a[1]=r[1]+o}else Te(t[1])?a[1]=t[1](r[1]):a[1]=r[1];return a},lh=function(t,r,n){if(t&&t.scale&&t.scale.bandwidth){var a=t.scale.bandwidth();if(!n||a>0)return a}if(t&&r&&r.length>=2){for(var i=bw(r,function(d){return d.coordinate}),o=1/0,s=1,l=i.length;se.length)&&(t=e.length);for(var r=0,n=new Array(t);r2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(n.left||0)-(n.right||0)),Math.abs(r-(n.top||0)-(n.bottom||0)))/2},Fee=function(t,r,n,a,i){var o=t.width,s=t.height,l=t.startAngle,c=t.endAngle,f=Fr(t.cx,o,o/2),d=Fr(t.cy,s,s/2),p=UT(o,s,n),h=Fr(t.innerRadius,p,0),x=Fr(t.outerRadius,p,p*.8),v=Object.keys(r);return v.reduce(function(y,g){var m=r[g],w=m.domain,S=m.reversed,b;if(Fe(m.range))a==="angleAxis"?b=[l,c]:a==="radiusAxis"&&(b=[h,x]),S&&(b=[b[1],b[0]]);else{b=m.range;var _=b,k=$ee(_,2);l=k[0],c=k[1]}var O=MT(m,i),N=O.realScaleType,T=O.scale;T.domain(w).range(b),DT(T);var R=LT(T,Ma(Ma({},m),{},{realScaleType:N})),A=Ma(Ma(Ma({},m),R),{},{range:b,radius:x,realScaleType:N,scale:T,cx:f,cy:d,innerRadius:h,outerRadius:x,startAngle:l,endAngle:c});return Ma(Ma({},y),{},BT({},g,A))},{})},zee=function(t,r){var n=t.x,a=t.y,i=r.x,o=r.y;return Math.sqrt(Math.pow(n-i,2)+Math.pow(a-o,2))},Bee=function(t,r){var n=t.x,a=t.y,i=r.cx,o=r.cy,s=zee({x:n,y:a},{x:i,y:o});if(s<=0)return{radius:s};var l=(n-i)/s,c=Math.acos(l);return a>o&&(c=2*Math.PI-c),{radius:s,angle:Lee(c),angleInRadian:c}},Uee=function(t){var r=t.startAngle,n=t.endAngle,a=Math.floor(r/360),i=Math.floor(n/360),o=Math.min(a,i);return{startAngle:r-o*360,endAngle:n-o*360}},Vee=function(t,r){var n=r.startAngle,a=r.endAngle,i=Math.floor(n/360),o=Math.floor(a/360),s=Math.min(i,o);return t+s*360},qk=function(t,r){var n=t.x,a=t.y,i=Bee({x:n,y:a},r),o=i.radius,s=i.angle,l=r.innerRadius,c=r.outerRadius;if(oc)return!1;if(o===0)return!0;var f=Uee(r),d=f.startAngle,p=f.endAngle,h=s,x;if(d<=p){for(;h>p;)h-=360;for(;h=d&&h<=p}else{for(;h>d;)h-=360;for(;h=p&&h<=d}return x?Ma(Ma({},r),{},{radius:o,angle:Vee(h,r)}):null},VT=function(t){return!j.isValidElement(t)&&!Te(t)&&typeof t!="boolean"?t.className:""};function qc(e){"@babel/helpers - typeof";return qc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},qc(e)}var Wee=["offset"];function Hee(e){return Xee(e)||Kee(e)||qee(e)||Gee()}function Gee(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function qee(e,t){if(e){if(typeof e=="string")return Gx(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Gx(e,t)}}function Kee(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Xee(e){if(Array.isArray(e))return Gx(e)}function Gx(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Zee(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Kk(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function nr(e){for(var t=1;t=0?1:-1,w,S;a==="insideStart"?(w=h+m*o,S=v):a==="insideEnd"?(w=x-m*o,S=!v):a==="end"&&(w=x+m*o,S=v),S=g<=0?S:!S;var b=_t(c,f,y,w),_=_t(c,f,y,w+(S?1:-1)*359),k="M".concat(b.x,",").concat(b.y,` + A`).concat(y,",").concat(y,",0,1,").concat(S?0:1,`, + `).concat(_.x,",").concat(_.y),O=Fe(t.id)?Yo("recharts-radial-line-"):t.id;return E.createElement("text",Kc({},n,{dominantBaseline:"central",className:He("recharts-radial-bar-label",s)}),E.createElement("defs",null,E.createElement("path",{id:O,d:k})),E.createElement("textPath",{xlinkHref:"#".concat(O)},r))},ate=function(t){var r=t.viewBox,n=t.offset,a=t.position,i=r,o=i.cx,s=i.cy,l=i.innerRadius,c=i.outerRadius,f=i.startAngle,d=i.endAngle,p=(f+d)/2;if(a==="outside"){var h=_t(o,s,c+n,p),x=h.x,v=h.y;return{x,y:v,textAnchor:x>=o?"start":"end",verticalAnchor:"middle"}}if(a==="center")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"middle"};if(a==="centerTop")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"start"};if(a==="centerBottom")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"end"};var y=(l+c)/2,g=_t(o,s,y,p),m=g.x,w=g.y;return{x:m,y:w,textAnchor:"middle",verticalAnchor:"middle"}},ite=function(t){var r=t.viewBox,n=t.parentViewBox,a=t.offset,i=t.position,o=r,s=o.x,l=o.y,c=o.width,f=o.height,d=f>=0?1:-1,p=d*a,h=d>0?"end":"start",x=d>0?"start":"end",v=c>=0?1:-1,y=v*a,g=v>0?"end":"start",m=v>0?"start":"end";if(i==="top"){var w={x:s+c/2,y:l-d*a,textAnchor:"middle",verticalAnchor:h};return nr(nr({},w),n?{height:Math.max(l-n.y,0),width:c}:{})}if(i==="bottom"){var S={x:s+c/2,y:l+f+p,textAnchor:"middle",verticalAnchor:x};return nr(nr({},S),n?{height:Math.max(n.y+n.height-(l+f),0),width:c}:{})}if(i==="left"){var b={x:s-y,y:l+f/2,textAnchor:g,verticalAnchor:"middle"};return nr(nr({},b),n?{width:Math.max(b.x-n.x,0),height:f}:{})}if(i==="right"){var _={x:s+c+y,y:l+f/2,textAnchor:m,verticalAnchor:"middle"};return nr(nr({},_),n?{width:Math.max(n.x+n.width-_.x,0),height:f}:{})}var k=n?{width:c,height:f}:{};return i==="insideLeft"?nr({x:s+y,y:l+f/2,textAnchor:m,verticalAnchor:"middle"},k):i==="insideRight"?nr({x:s+c-y,y:l+f/2,textAnchor:g,verticalAnchor:"middle"},k):i==="insideTop"?nr({x:s+c/2,y:l+p,textAnchor:"middle",verticalAnchor:x},k):i==="insideBottom"?nr({x:s+c/2,y:l+f-p,textAnchor:"middle",verticalAnchor:h},k):i==="insideTopLeft"?nr({x:s+y,y:l+p,textAnchor:m,verticalAnchor:x},k):i==="insideTopRight"?nr({x:s+c-y,y:l+p,textAnchor:g,verticalAnchor:x},k):i==="insideBottomLeft"?nr({x:s+y,y:l+f-p,textAnchor:m,verticalAnchor:h},k):i==="insideBottomRight"?nr({x:s+c-y,y:l+f-p,textAnchor:g,verticalAnchor:h},k):Fl(i)&&(ae(i.x)||bo(i.x))&&(ae(i.y)||bo(i.y))?nr({x:s+Fr(i.x,c),y:l+Fr(i.y,f),textAnchor:"end",verticalAnchor:"end"},k):nr({x:s+c/2,y:l+f/2,textAnchor:"middle",verticalAnchor:"middle"},k)},ote=function(t){return"cx"in t&&ae(t.cx)};function mr(e){var t=e.offset,r=t===void 0?5:t,n=Yee(e,Wee),a=nr({offset:r},n),i=a.viewBox,o=a.position,s=a.value,l=a.children,c=a.content,f=a.className,d=f===void 0?"":f,p=a.textBreakAll;if(!i||Fe(s)&&Fe(l)&&!j.isValidElement(c)&&!Te(c))return null;if(j.isValidElement(c))return j.cloneElement(c,a);var h;if(Te(c)){if(h=j.createElement(c,a),j.isValidElement(h))return h}else h=tte(a);var x=ote(i),v=Oe(a,!0);if(x&&(o==="insideStart"||o==="insideEnd"||o==="end"))return nte(a,h,v);var y=x?ate(a):ite(a);return E.createElement(Uo,Kc({className:He("recharts-label",d)},v,y,{breakAll:p}),h)}mr.displayName="Label";var WT=function(t){var r=t.cx,n=t.cy,a=t.angle,i=t.startAngle,o=t.endAngle,s=t.r,l=t.radius,c=t.innerRadius,f=t.outerRadius,d=t.x,p=t.y,h=t.top,x=t.left,v=t.width,y=t.height,g=t.clockWise,m=t.labelViewBox;if(m)return m;if(ae(v)&&ae(y)){if(ae(d)&&ae(p))return{x:d,y:p,width:v,height:y};if(ae(h)&&ae(x))return{x:h,y:x,width:v,height:y}}return ae(d)&&ae(p)?{x:d,y:p,width:0,height:0}:ae(r)&&ae(n)?{cx:r,cy:n,startAngle:i||a||0,endAngle:o||a||0,innerRadius:c||0,outerRadius:f||l||s||0,clockWise:g}:t.viewBox?t.viewBox:{}},ste=function(t,r){return t?t===!0?E.createElement(mr,{key:"label-implicit",viewBox:r}):lr(t)?E.createElement(mr,{key:"label-implicit",viewBox:r,value:t}):j.isValidElement(t)?t.type===mr?j.cloneElement(t,{key:"label-implicit",viewBox:r}):E.createElement(mr,{key:"label-implicit",content:t,viewBox:r}):Te(t)?E.createElement(mr,{key:"label-implicit",content:t,viewBox:r}):Fl(t)?E.createElement(mr,Kc({viewBox:r},t,{key:"label-implicit"})):null:null},lte=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!t||!t.children&&n&&!t.label)return null;var a=t.children,i=WT(t),o=yn(a,mr).map(function(l,c){return j.cloneElement(l,{viewBox:r||i,key:"label-".concat(c)})});if(!n)return o;var s=ste(t.label,r||i);return[s].concat(Hee(o))};mr.parseViewBox=WT;mr.renderCallByParent=lte;function ute(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var cte=ute;const dte=dt(cte);function Xc(e){"@babel/helpers - typeof";return Xc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Xc(e)}var fte=["valueAccessor"],pte=["data","dataKey","clockWise","id","textBreakAll"];function hte(e){return gte(e)||yte(e)||vte(e)||mte()}function mte(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function vte(e,t){if(e){if(typeof e=="string")return qx(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return qx(e,t)}}function yte(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function gte(e){if(Array.isArray(e))return qx(e)}function qx(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function _te(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var Ste=function(t){return Array.isArray(t.value)?dte(t.value):t.value};function xa(e){var t=e.valueAccessor,r=t===void 0?Ste:t,n=Zk(e,fte),a=n.data,i=n.dataKey,o=n.clockWise,s=n.id,l=n.textBreakAll,c=Zk(n,pte);return!a||!a.length?null:E.createElement(Qe,{className:"recharts-label-list"},a.map(function(f,d){var p=Fe(i)?r(f,d):Ht(f&&f.payload,i),h=Fe(s)?{}:{id:"".concat(s,"-").concat(d)};return E.createElement(mr,ch({},Oe(f,!0),c,h,{parentViewBox:f.parentViewBox,value:p,textBreakAll:l,viewBox:mr.parseViewBox(Fe(o)?f:Yk(Yk({},f),{},{clockWise:o})),key:"label-".concat(d),index:d}))}))}xa.displayName="LabelList";function jte(e,t){return e?e===!0?E.createElement(xa,{key:"labelList-implicit",data:t}):E.isValidElement(e)||Te(e)?E.createElement(xa,{key:"labelList-implicit",data:t,content:e}):Fl(e)?E.createElement(xa,ch({data:t},e,{key:"labelList-implicit"})):null:null}function kte(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!e||!e.children&&r&&!e.label)return null;var n=e.children,a=yn(n,xa).map(function(o,s){return j.cloneElement(o,{data:t,key:"labelList-".concat(s)})});if(!r)return a;var i=jte(e.label,t);return[i].concat(hte(a))}xa.renderCallByParent=kte;function Yc(e){"@babel/helpers - typeof";return Yc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Yc(e)}function Kx(){return Kx=Object.assign?Object.assign.bind():function(e){for(var t=1;t180),",").concat(+(o>c),`, + `).concat(d.x,",").concat(d.y,` + `);if(a>0){var h=_t(r,n,a,o),x=_t(r,n,a,c);p+="L ".concat(x.x,",").concat(x.y,` + A `).concat(a,",").concat(a,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(o<=c),`, + `).concat(h.x,",").concat(h.y," Z")}else p+="L ".concat(r,",").concat(n," Z");return p},Pte=function(t){var r=t.cx,n=t.cy,a=t.innerRadius,i=t.outerRadius,o=t.cornerRadius,s=t.forceCornerRadius,l=t.cornerIsExternal,c=t.startAngle,f=t.endAngle,d=Lr(f-c),p=vf({cx:r,cy:n,radius:i,angle:c,sign:d,cornerRadius:o,cornerIsExternal:l}),h=p.circleTangency,x=p.lineTangency,v=p.theta,y=vf({cx:r,cy:n,radius:i,angle:f,sign:-d,cornerRadius:o,cornerIsExternal:l}),g=y.circleTangency,m=y.lineTangency,w=y.theta,S=l?Math.abs(c-f):Math.abs(c-f)-v-w;if(S<0)return s?"M ".concat(x.x,",").concat(x.y,` + a`).concat(o,",").concat(o,",0,0,1,").concat(o*2,`,0 + a`).concat(o,",").concat(o,",0,0,1,").concat(-o*2,`,0 + `):HT({cx:r,cy:n,innerRadius:a,outerRadius:i,startAngle:c,endAngle:f});var b="M ".concat(x.x,",").concat(x.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(d<0),",").concat(h.x,",").concat(h.y,` + A`).concat(i,",").concat(i,",0,").concat(+(S>180),",").concat(+(d<0),",").concat(g.x,",").concat(g.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(d<0),",").concat(m.x,",").concat(m.y,` + `);if(a>0){var _=vf({cx:r,cy:n,radius:a,angle:c,sign:d,isExternal:!0,cornerRadius:o,cornerIsExternal:l}),k=_.circleTangency,O=_.lineTangency,N=_.theta,T=vf({cx:r,cy:n,radius:a,angle:f,sign:-d,isExternal:!0,cornerRadius:o,cornerIsExternal:l}),R=T.circleTangency,A=T.lineTangency,C=T.theta,M=l?Math.abs(c-f):Math.abs(c-f)-N-C;if(M<0&&o===0)return"".concat(b,"L").concat(r,",").concat(n,"Z");b+="L".concat(A.x,",").concat(A.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(d<0),",").concat(R.x,",").concat(R.y,` + A`).concat(a,",").concat(a,",0,").concat(+(M>180),",").concat(+(d>0),",").concat(k.x,",").concat(k.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(d<0),",").concat(O.x,",").concat(O.y,"Z")}else b+="L".concat(r,",").concat(n,"Z");return b},Cte={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},GT=function(t){var r=Jk(Jk({},Cte),t),n=r.cx,a=r.cy,i=r.innerRadius,o=r.outerRadius,s=r.cornerRadius,l=r.forceCornerRadius,c=r.cornerIsExternal,f=r.startAngle,d=r.endAngle,p=r.className;if(o0&&Math.abs(f-d)<360?y=Pte({cx:n,cy:a,innerRadius:i,outerRadius:o,cornerRadius:Math.min(v,x/2),forceCornerRadius:l,cornerIsExternal:c,startAngle:f,endAngle:d}):y=HT({cx:n,cy:a,innerRadius:i,outerRadius:o,startAngle:f,endAngle:d}),E.createElement("path",Kx({},Oe(r,!0),{className:h,d:y,role:"img"}))};function Zc(e){"@babel/helpers - typeof";return Zc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Zc(e)}function Xx(){return Xx=Object.assign?Object.assign.bind():function(e){for(var t=1;tWte.call(e,t));function Jo(e,t){return e===t||!e&&!t&&e!==e&&t!==t}const qte="__v",Kte="__o",Xte="_owner",{getOwnPropertyDescriptor:a2,keys:i2}=Object;function Yte(e,t){return e.byteLength===t.byteLength&&dh(new Uint8Array(e),new Uint8Array(t))}function Zte(e,t,r){let n=e.length;if(t.length!==n)return!1;for(;n-- >0;)if(!r.equals(e[n],t[n],n,n,e,t,r))return!1;return!0}function Qte(e,t){return e.byteLength===t.byteLength&&dh(new Uint8Array(e.buffer,e.byteOffset,e.byteLength),new Uint8Array(t.buffer,t.byteOffset,t.byteLength))}function Jte(e,t){return Jo(e.getTime(),t.getTime())}function ere(e,t){return e.name===t.name&&e.message===t.message&&e.cause===t.cause&&e.stack===t.stack}function tre(e,t){return e===t}function o2(e,t,r){const n=e.size;if(n!==t.size)return!1;if(!n)return!0;const a=new Array(n),i=e.entries();let o,s,l=0;for(;(o=i.next())&&!o.done;){const c=t.entries();let f=!1,d=0;for(;(s=c.next())&&!s.done;){if(a[d]){d++;continue}const p=o.value,h=s.value;if(r.equals(p[0],h[0],l,d,e,t,r)&&r.equals(p[1],h[1],p[0],h[0],e,t,r)){f=a[d]=!0;break}d++}if(!f)return!1;l++}return!0}const rre=Jo;function nre(e,t,r){const n=i2(e);let a=n.length;if(i2(t).length!==a)return!1;for(;a-- >0;)if(!YT(e,t,r,n[a]))return!1;return!0}function vu(e,t,r){const n=n2(e);let a=n.length;if(n2(t).length!==a)return!1;let i,o,s;for(;a-- >0;)if(i=n[a],!YT(e,t,r,i)||(o=a2(e,i),s=a2(t,i),(o||s)&&(!o||!s||o.configurable!==s.configurable||o.enumerable!==s.enumerable||o.writable!==s.writable)))return!1;return!0}function are(e,t){return Jo(e.valueOf(),t.valueOf())}function ire(e,t){return e.source===t.source&&e.flags===t.flags}function s2(e,t,r){const n=e.size;if(n!==t.size)return!1;if(!n)return!0;const a=new Array(n),i=e.values();let o,s;for(;(o=i.next())&&!o.done;){const l=t.values();let c=!1,f=0;for(;(s=l.next())&&!s.done;){if(!a[f]&&r.equals(o.value,s.value,o.value,s.value,e,t,r)){c=a[f]=!0;break}f++}if(!c)return!1}return!0}function dh(e,t){let r=e.byteLength;if(t.byteLength!==r||e.byteOffset!==t.byteOffset)return!1;for(;r-- >0;)if(e[r]!==t[r])return!1;return!0}function ore(e,t){return e.hostname===t.hostname&&e.pathname===t.pathname&&e.protocol===t.protocol&&e.port===t.port&&e.hash===t.hash&&e.username===t.username&&e.password===t.password}function YT(e,t,r,n){return(n===Xte||n===Kte||n===qte)&&(e.$$typeof||t.$$typeof)?!0:Gte(t,n)&&r.equals(e[n],t[n],n,n,e,t,r)}const sre="[object ArrayBuffer]",lre="[object Arguments]",ure="[object Boolean]",cre="[object DataView]",dre="[object Date]",fre="[object Error]",pre="[object Map]",hre="[object Number]",mre="[object Object]",vre="[object RegExp]",yre="[object Set]",gre="[object String]",xre={"[object Int8Array]":!0,"[object Uint8Array]":!0,"[object Uint8ClampedArray]":!0,"[object Int16Array]":!0,"[object Uint16Array]":!0,"[object Int32Array]":!0,"[object Uint32Array]":!0,"[object Float16Array]":!0,"[object Float32Array]":!0,"[object Float64Array]":!0,"[object BigInt64Array]":!0,"[object BigUint64Array]":!0},bre="[object URL]",wre=Object.prototype.toString;function _re({areArrayBuffersEqual:e,areArraysEqual:t,areDataViewsEqual:r,areDatesEqual:n,areErrorsEqual:a,areFunctionsEqual:i,areMapsEqual:o,areNumbersEqual:s,areObjectsEqual:l,arePrimitiveWrappersEqual:c,areRegExpsEqual:f,areSetsEqual:d,areTypedArraysEqual:p,areUrlsEqual:h,unknownTagComparators:x}){return function(y,g,m){if(y===g)return!0;if(y==null||g==null)return!1;const w=typeof y;if(w!==typeof g)return!1;if(w!=="object")return w==="number"?s(y,g,m):w==="function"?i(y,g,m):!1;const S=y.constructor;if(S!==g.constructor)return!1;if(S===Object)return l(y,g,m);if(Array.isArray(y))return t(y,g,m);if(S===Date)return n(y,g,m);if(S===RegExp)return f(y,g,m);if(S===Map)return o(y,g,m);if(S===Set)return d(y,g,m);const b=wre.call(y);if(b===dre)return n(y,g,m);if(b===vre)return f(y,g,m);if(b===pre)return o(y,g,m);if(b===yre)return d(y,g,m);if(b===mre)return typeof y.then!="function"&&typeof g.then!="function"&&l(y,g,m);if(b===bre)return h(y,g,m);if(b===fre)return a(y,g,m);if(b===lre)return l(y,g,m);if(xre[b])return p(y,g,m);if(b===sre)return e(y,g,m);if(b===cre)return r(y,g,m);if(b===ure||b===hre||b===gre)return c(y,g,m);if(x){let _=x[b];if(!_){const k=Hte(y);k&&(_=x[k])}if(_)return _(y,g,m)}return!1}}function Sre({circular:e,createCustomConfig:t,strict:r}){let n={areArrayBuffersEqual:Yte,areArraysEqual:r?vu:Zte,areDataViewsEqual:Qte,areDatesEqual:Jte,areErrorsEqual:ere,areFunctionsEqual:tre,areMapsEqual:r?wy(o2,vu):o2,areNumbersEqual:rre,areObjectsEqual:r?vu:nre,arePrimitiveWrappersEqual:are,areRegExpsEqual:ire,areSetsEqual:r?wy(s2,vu):s2,areTypedArraysEqual:r?wy(dh,vu):dh,areUrlsEqual:ore,unknownTagComparators:void 0};if(t&&(n=Object.assign({},n,t(n))),e){const a=gf(n.areArraysEqual),i=gf(n.areMapsEqual),o=gf(n.areObjectsEqual),s=gf(n.areSetsEqual);n=Object.assign({},n,{areArraysEqual:a,areMapsEqual:i,areObjectsEqual:o,areSetsEqual:s})}return n}function jre(e){return function(t,r,n,a,i,o,s){return e(t,r,s)}}function kre({circular:e,comparator:t,createState:r,equals:n,strict:a}){if(r)return function(s,l){const{cache:c=e?new WeakMap:void 0,meta:f}=r();return t(s,l,{cache:c,equals:n,meta:f,strict:a})};if(e)return function(s,l){return t(s,l,{cache:new WeakMap,equals:n,meta:void 0,strict:a})};const i={cache:void 0,equals:n,meta:void 0,strict:a};return function(s,l){return t(s,l,i)}}const Ore=eo();eo({strict:!0});eo({circular:!0});eo({circular:!0,strict:!0});eo({createInternalComparator:()=>Jo});eo({strict:!0,createInternalComparator:()=>Jo});eo({circular:!0,createInternalComparator:()=>Jo});eo({circular:!0,createInternalComparator:()=>Jo,strict:!0});function eo(e={}){const{circular:t=!1,createInternalComparator:r,createState:n,strict:a=!1}=e,i=Sre(e),o=_re(i),s=r?r(o):jre(o);return kre({circular:t,comparator:o,createState:n,equals:s,strict:a})}function Are(e){typeof requestAnimationFrame<"u"&&requestAnimationFrame(e)}function l2(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,r=-1,n=function a(i){r<0&&(r=i),i-r>t?(e(i),r=-1):Are(a)};requestAnimationFrame(n)}function Yx(e){"@babel/helpers - typeof";return Yx=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Yx(e)}function Nre(e){return Tre(e)||Cre(e)||Pre(e)||Ere()}function Ere(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Pre(e,t){if(e){if(typeof e=="string")return u2(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return u2(e,t)}}function u2(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);re.length)&&(t=e.length);for(var r=0,n=new Array(t);r1?1:g<0?0:g},v=function(g){for(var m=g>1?1:g,w=m,S=0;S<8;++S){var b=d(w)-m,_=h(w);if(Math.abs(b-m)0&&arguments[0]!==void 0?arguments[0]:{},r=t.stiff,n=r===void 0?100:r,a=t.damping,i=a===void 0?8:a,o=t.dt,s=o===void 0?17:o,l=function(f,d,p){var h=-(f-d)*n,x=p*i,v=p+(h-x)*s/1e3,y=p*s/1e3+f;return Math.abs(y-d)e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function une(e,t){if(e==null)return{};var r={},n=Object.keys(e),a,i;for(i=0;i=0)&&(r[a]=e[a]);return r}function _y(e){return pne(e)||fne(e)||dne(e)||cne()}function cne(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function dne(e,t){if(e){if(typeof e=="string")return t0(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t0(e,t)}}function fne(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function pne(e){if(Array.isArray(e))return t0(e)}function t0(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function hh(e){return hh=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(r){return r.__proto__||Object.getPrototypeOf(r)},hh(e)}var ia=function(e){gne(r,e);var t=xne(r);function r(n,a){var i;hne(this,r),i=t.call(this,n,a);var o=i.props,s=o.isActive,l=o.attributeName,c=o.from,f=o.to,d=o.steps,p=o.children,h=o.duration;if(i.handleStyleChange=i.handleStyleChange.bind(a0(i)),i.changeStyle=i.changeStyle.bind(a0(i)),!s||h<=0)return i.state={style:{}},typeof p=="function"&&(i.state={style:f}),n0(i);if(d&&d.length)i.state={style:d[0].style};else if(c){if(typeof p=="function")return i.state={style:c},n0(i);i.state={style:l?Cu({},l,c):c}}else i.state={style:{}};return i}return vne(r,[{key:"componentDidMount",value:function(){var a=this.props,i=a.isActive,o=a.canBegin;this.mounted=!0,!(!i||!o)&&this.runAnimation(this.props)}},{key:"componentDidUpdate",value:function(a){var i=this.props,o=i.isActive,s=i.canBegin,l=i.attributeName,c=i.shouldReAnimate,f=i.to,d=i.from,p=this.state.style;if(s){if(!o){var h={style:l?Cu({},l,f):f};this.state&&p&&(l&&p[l]!==f||!l&&p!==f)&&this.setState(h);return}if(!(Ore(a.to,f)&&a.canBegin&&a.isActive)){var x=!a.canBegin||!a.isActive;this.manager&&this.manager.stop(),this.stopJSAnimation&&this.stopJSAnimation();var v=x||c?d:a.to;if(this.state&&p){var y={style:l?Cu({},l,v):v};(l&&p[l]!==v||!l&&p!==v)&&this.setState(y)}this.runAnimation(Wn(Wn({},this.props),{},{from:v,begin:0}))}}}},{key:"componentWillUnmount",value:function(){this.mounted=!1;var a=this.props.onAnimationEnd;this.unSubscribe&&this.unSubscribe(),this.manager&&(this.manager.stop(),this.manager=null),this.stopJSAnimation&&this.stopJSAnimation(),a&&a()}},{key:"handleStyleChange",value:function(a){this.changeStyle(a)}},{key:"changeStyle",value:function(a){this.mounted&&this.setState({style:a})}},{key:"runJSAnimation",value:function(a){var i=this,o=a.from,s=a.to,l=a.duration,c=a.easing,f=a.begin,d=a.onAnimationEnd,p=a.onAnimationStart,h=one(o,s,Xre(c),l,this.changeStyle),x=function(){i.stopJSAnimation=h()};this.manager.start([p,f,x,l,d])}},{key:"runStepAnimation",value:function(a){var i=this,o=a.steps,s=a.begin,l=a.onAnimationStart,c=o[0],f=c.style,d=c.duration,p=d===void 0?0:d,h=function(v,y,g){if(g===0)return v;var m=y.duration,w=y.easing,S=w===void 0?"ease":w,b=y.style,_=y.properties,k=y.onAnimationEnd,O=g>0?o[g-1]:y,N=_||Object.keys(b);if(typeof S=="function"||S==="spring")return[].concat(_y(v),[i.runJSAnimation.bind(i,{from:O.style,to:b,duration:m,easing:S}),m]);var T=f2(N,m,S),R=Wn(Wn(Wn({},O.style),b),{},{transition:T});return[].concat(_y(v),[R,m,k]).filter(Dre)};return this.manager.start([l].concat(_y(o.reduce(h,[f,Math.max(p,s)])),[a.onAnimationEnd]))}},{key:"runAnimation",value:function(a){this.manager||(this.manager=$re());var i=a.begin,o=a.duration,s=a.attributeName,l=a.to,c=a.easing,f=a.onAnimationStart,d=a.onAnimationEnd,p=a.steps,h=a.children,x=this.manager;if(this.unSubscribe=x.subscribe(this.handleStyleChange),typeof c=="function"||typeof h=="function"||c==="spring"){this.runJSAnimation(a);return}if(p.length>1){this.runStepAnimation(a);return}var v=s?Cu({},s,l):l,y=f2(Object.keys(v),o,c);x.start([f,i,Wn(Wn({},v),{},{transition:y}),o,d])}},{key:"render",value:function(){var a=this.props,i=a.children;a.begin;var o=a.duration;a.attributeName,a.easing;var s=a.isActive;a.steps,a.from,a.to,a.canBegin,a.onAnimationEnd,a.shouldReAnimate,a.onAnimationReStart;var l=lne(a,sne),c=j.Children.count(i),f=this.state.style;if(typeof i=="function")return i(f);if(!s||c===0||o<=0)return i;var d=function(h){var x=h.props,v=x.style,y=v===void 0?{}:v,g=x.className,m=j.cloneElement(h,Wn(Wn({},l),{},{style:Wn(Wn({},y),f),className:g}));return m};return c===1?d(j.Children.only(i)):E.createElement("div",null,j.Children.map(i,function(p){return d(p)}))}}]),r}(j.PureComponent);ia.displayName="Animate";ia.defaultProps={begin:0,duration:1e3,from:"",to:"",attributeName:"",easing:"ease",isActive:!0,canBegin:!0,steps:[],onAnimationEnd:function(){},onAnimationStart:function(){}};ia.propTypes={from:ut.oneOfType([ut.object,ut.string]),to:ut.oneOfType([ut.object,ut.string]),attributeName:ut.string,duration:ut.number,begin:ut.number,easing:ut.oneOfType([ut.string,ut.func]),steps:ut.arrayOf(ut.shape({duration:ut.number.isRequired,style:ut.object.isRequired,easing:ut.oneOfType([ut.oneOf(["ease","ease-in","ease-out","ease-in-out","linear"]),ut.func]),properties:ut.arrayOf("string"),onAnimationEnd:ut.func})),children:ut.oneOfType([ut.node,ut.func]),isActive:ut.bool,canBegin:ut.bool,onAnimationEnd:ut.func,shouldReAnimate:ut.bool,onAnimationStart:ut.func,onAnimationReStart:ut.func};function ed(e){"@babel/helpers - typeof";return ed=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ed(e)}function mh(){return mh=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0?1:-1,l=n>=0?1:-1,c=a>=0&&n>=0||a<0&&n<0?1:0,f;if(o>0&&i instanceof Array){for(var d=[0,0,0,0],p=0,h=4;po?o:i[p];f="M".concat(t,",").concat(r+s*d[0]),d[0]>0&&(f+="A ".concat(d[0],",").concat(d[0],",0,0,").concat(c,",").concat(t+l*d[0],",").concat(r)),f+="L ".concat(t+n-l*d[1],",").concat(r),d[1]>0&&(f+="A ".concat(d[1],",").concat(d[1],",0,0,").concat(c,`, + `).concat(t+n,",").concat(r+s*d[1])),f+="L ".concat(t+n,",").concat(r+a-s*d[2]),d[2]>0&&(f+="A ".concat(d[2],",").concat(d[2],",0,0,").concat(c,`, + `).concat(t+n-l*d[2],",").concat(r+a)),f+="L ".concat(t+l*d[3],",").concat(r+a),d[3]>0&&(f+="A ".concat(d[3],",").concat(d[3],",0,0,").concat(c,`, + `).concat(t,",").concat(r+a-s*d[3])),f+="Z"}else if(o>0&&i===+i&&i>0){var x=Math.min(o,i);f="M ".concat(t,",").concat(r+s*x,` + A `).concat(x,",").concat(x,",0,0,").concat(c,",").concat(t+l*x,",").concat(r,` + L `).concat(t+n-l*x,",").concat(r,` + A `).concat(x,",").concat(x,",0,0,").concat(c,",").concat(t+n,",").concat(r+s*x,` + L `).concat(t+n,",").concat(r+a-s*x,` + A `).concat(x,",").concat(x,",0,0,").concat(c,",").concat(t+n-l*x,",").concat(r+a,` + L `).concat(t+l*x,",").concat(r+a,` + A `).concat(x,",").concat(x,",0,0,").concat(c,",").concat(t,",").concat(r+a-s*x," Z")}else f="M ".concat(t,",").concat(r," h ").concat(n," v ").concat(a," h ").concat(-n," Z");return f},Ene=function(t,r){if(!t||!r)return!1;var n=t.x,a=t.y,i=r.x,o=r.y,s=r.width,l=r.height;if(Math.abs(s)>0&&Math.abs(l)>0){var c=Math.min(i,i+s),f=Math.max(i,i+s),d=Math.min(o,o+l),p=Math.max(o,o+l);return n>=c&&n<=f&&a>=d&&a<=p}return!1},Pne={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},qw=function(t){var r=b2(b2({},Pne),t),n=j.useRef(),a=j.useState(-1),i=wne(a,2),o=i[0],s=i[1];j.useEffect(function(){if(n.current&&n.current.getTotalLength)try{var S=n.current.getTotalLength();S&&s(S)}catch{}},[]);var l=r.x,c=r.y,f=r.width,d=r.height,p=r.radius,h=r.className,x=r.animationEasing,v=r.animationDuration,y=r.animationBegin,g=r.isAnimationActive,m=r.isUpdateAnimationActive;if(l!==+l||c!==+c||f!==+f||d!==+d||f===0||d===0)return null;var w=He("recharts-rectangle",h);return m?E.createElement(ia,{canBegin:o>0,from:{width:f,height:d,x:l,y:c},to:{width:f,height:d,x:l,y:c},duration:v,animationEasing:x,isActive:m},function(S){var b=S.width,_=S.height,k=S.x,O=S.y;return E.createElement(ia,{canBegin:o>0,from:"0px ".concat(o===-1?1:o,"px"),to:"".concat(o,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:v,isActive:g,easing:x},E.createElement("path",mh({},Oe(r,!0),{className:w,d:w2(k,O,b,_,p),ref:n})))}):E.createElement("path",mh({},Oe(r,!0),{className:w,d:w2(l,c,f,d,p)}))},Cne=["points","className","baseLinePoints","connectNulls"];function Es(){return Es=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function $ne(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function _2(e){return Dne(e)||Mne(e)||Ine(e)||Rne()}function Rne(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Ine(e,t){if(e){if(typeof e=="string")return i0(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return i0(e,t)}}function Mne(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Dne(e){if(Array.isArray(e))return i0(e)}function i0(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&arguments[0]!==void 0?arguments[0]:[],r=[[]];return t.forEach(function(n){S2(n)?r[r.length-1].push(n):r[r.length-1].length>0&&r.push([])}),S2(t[0])&&r[r.length-1].push(t[0]),r[r.length-1].length<=0&&(r=r.slice(0,-1)),r},rc=function(t,r){var n=Lne(t);r&&(n=[n.reduce(function(i,o){return[].concat(_2(i),_2(o))},[])]);var a=n.map(function(i){return i.reduce(function(o,s,l){return"".concat(o).concat(l===0?"M":"L").concat(s.x,",").concat(s.y)},"")}).join("");return n.length===1?"".concat(a,"Z"):a},Fne=function(t,r,n){var a=rc(t,n);return"".concat(a.slice(-1)==="Z"?a.slice(0,-1):a,"L").concat(rc(r.reverse(),n).slice(1))},zne=function(t){var r=t.points,n=t.className,a=t.baseLinePoints,i=t.connectNulls,o=Tne(t,Cne);if(!r||!r.length)return null;var s=He("recharts-polygon",n);if(a&&a.length){var l=o.stroke&&o.stroke!=="none",c=Fne(r,a,i);return E.createElement("g",{className:s},E.createElement("path",Es({},Oe(o,!0),{fill:c.slice(-1)==="Z"?o.fill:"none",stroke:"none",d:c})),l?E.createElement("path",Es({},Oe(o,!0),{fill:"none",d:rc(r,i)})):null,l?E.createElement("path",Es({},Oe(o,!0),{fill:"none",d:rc(a,i)})):null)}var f=rc(r,i);return E.createElement("path",Es({},Oe(o,!0),{fill:f.slice(-1)==="Z"?o.fill:"none",className:s,d:f}))};function o0(){return o0=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function qne(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var Kne=function(t,r,n,a,i,o){return"M".concat(t,",").concat(i,"v").concat(a,"M").concat(o,",").concat(r,"h").concat(n)},Xne=function(t){var r=t.x,n=r===void 0?0:r,a=t.y,i=a===void 0?0:a,o=t.top,s=o===void 0?0:o,l=t.left,c=l===void 0?0:l,f=t.width,d=f===void 0?0:f,p=t.height,h=p===void 0?0:p,x=t.className,v=Gne(t,Bne),y=Une({x:n,y:i,top:s,left:c,width:d,height:h},v);return!ae(n)||!ae(i)||!ae(d)||!ae(h)||!ae(s)||!ae(c)?null:E.createElement("path",s0({},Oe(y,!0),{className:He("recharts-cross",x),d:Kne(n,i,d,h,s,c)}))},Yne=Mm,Zne=hT,Qne=Aa;function Jne(e,t){return e&&e.length?Yne(e,Qne(t),Zne):void 0}var eae=Jne;const tae=dt(eae);var rae=Mm,nae=Aa,aae=mT;function iae(e,t){return e&&e.length?rae(e,nae(t),aae):void 0}var oae=iae;const sae=dt(oae);var lae=["cx","cy","angle","ticks","axisLine"],uae=["ticks","tick","angle","tickFormatter","stroke"];function ml(e){"@babel/helpers - typeof";return ml=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ml(e)}function nc(){return nc=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function cae(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function dae(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function A2(e,t){for(var r=0;rP2?o=a==="outer"?"start":"end":i<-P2?o=a==="outer"?"end":"start":o="middle",o}},{key:"renderAxisLine",value:function(){var n=this.props,a=n.cx,i=n.cy,o=n.radius,s=n.axisLine,l=n.axisLineType,c=oo(oo({},Oe(this.props,!1)),{},{fill:"none"},Oe(s,!1));if(l==="circle")return E.createElement(Td,ho({className:"recharts-polar-angle-axis-line"},c,{cx:a,cy:i,r:o}));var f=this.props.ticks,d=f.map(function(p){return _t(a,i,o,p.coordinate)});return E.createElement(zne,ho({className:"recharts-polar-angle-axis-line"},c,{points:d}))}},{key:"renderTicks",value:function(){var n=this,a=this.props,i=a.ticks,o=a.tick,s=a.tickLine,l=a.tickFormatter,c=a.stroke,f=Oe(this.props,!1),d=Oe(o,!1),p=oo(oo({},f),{},{fill:"none"},Oe(s,!1)),h=i.map(function(x,v){var y=n.getTickLineCoord(x),g=n.getTickTextAnchor(x),m=oo(oo(oo({textAnchor:g},f),{},{stroke:"none",fill:c},d),{},{index:v,payload:x,x:y.x2,y:y.y2});return E.createElement(Qe,ho({className:He("recharts-polar-angle-axis-tick",VT(o)),key:"tick-".concat(x.coordinate)},Bo(n.props,x,v)),s&&E.createElement("line",ho({className:"recharts-polar-angle-axis-tick-line"},p,y)),o&&t.renderTickItem(o,m,l?l(x.value,v):x.value))});return E.createElement(Qe,{className:"recharts-polar-angle-axis-ticks"},h)}},{key:"render",value:function(){var n=this.props,a=n.ticks,i=n.radius,o=n.axisLine;return i<=0||!a||!a.length?null:E.createElement(Qe,{className:He("recharts-polar-angle-axis",this.props.className)},o&&this.renderAxisLine(),this.renderTicks())}}],[{key:"renderTickItem",value:function(n,a,i){var o;return E.isValidElement(n)?o=E.cloneElement(n,a):Te(n)?o=n(a):o=E.createElement(Uo,ho({},a,{className:"recharts-polar-angle-axis-tick-value"}),i),o}}])}(j.PureComponent);Um(Vm,"displayName","PolarAngleAxis");Um(Vm,"axisType","angleAxis");Um(Vm,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var Oae=dC,Aae=Oae(Object.getPrototypeOf,Object),Nae=Aae,Eae=oi,Pae=Nae,Cae=si,Tae="[object Object]",$ae=Function.prototype,Rae=Object.prototype,s$=$ae.toString,Iae=Rae.hasOwnProperty,Mae=s$.call(Object);function Dae(e){if(!Cae(e)||Eae(e)!=Tae)return!1;var t=Pae(e);if(t===null)return!0;var r=Iae.call(t,"constructor")&&t.constructor;return typeof r=="function"&&r instanceof r&&s$.call(r)==Mae}var Lae=Dae;const Fae=dt(Lae);var zae=oi,Bae=si,Uae="[object Boolean]";function Vae(e){return e===!0||e===!1||Bae(e)&&zae(e)==Uae}var Wae=Vae;const Hae=dt(Wae);function rd(e){"@babel/helpers - typeof";return rd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},rd(e)}function gh(){return gh=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r0,from:{upperWidth:0,lowerWidth:0,height:p,x:l,y:c},to:{upperWidth:f,lowerWidth:d,height:p,x:l,y:c},duration:v,animationEasing:x,isActive:g},function(w){var S=w.upperWidth,b=w.lowerWidth,_=w.height,k=w.x,O=w.y;return E.createElement(ia,{canBegin:o>0,from:"0px ".concat(o===-1?1:o,"px"),to:"".concat(o,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:v,easing:x},E.createElement("path",gh({},Oe(r,!0),{className:m,d:R2(k,O,S,b,_),ref:n})))}):E.createElement("g",null,E.createElement("path",gh({},Oe(r,!0),{className:m,d:R2(l,c,f,d,p)})))},rie=["option","shapeType","propTransformer","activeClassName","isActive"];function nd(e){"@babel/helpers - typeof";return nd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},nd(e)}function nie(e,t){if(e==null)return{};var r=aie(e,t),n,a;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function aie(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function I2(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function xh(e){for(var t=1;t0?vn(w,"paddingAngle",0):0;if(b){var k=or(b.endAngle-b.startAngle,w.endAngle-w.startAngle),O=xt(xt({},w),{},{startAngle:m+_,endAngle:m+k(v)+_});y.push(O),m=O.endAngle}else{var N=w.endAngle,T=w.startAngle,R=or(0,N-T),A=R(v),C=xt(xt({},w),{},{startAngle:m+_,endAngle:m+A+_});y.push(C),m=C.endAngle}}),E.createElement(Qe,null,n.renderSectorsStatically(y))})}},{key:"attachKeyboardHandlers",value:function(n){var a=this;n.onkeydown=function(i){if(!i.altKey)switch(i.key){case"ArrowLeft":{var o=++a.state.sectorToFocus%a.sectorRefs.length;a.sectorRefs[o].focus(),a.setState({sectorToFocus:o});break}case"ArrowRight":{var s=--a.state.sectorToFocus<0?a.sectorRefs.length-1:a.state.sectorToFocus%a.sectorRefs.length;a.sectorRefs[s].focus(),a.setState({sectorToFocus:s});break}case"Escape":{a.sectorRefs[a.state.sectorToFocus].blur(),a.setState({sectorToFocus:0});break}}}}},{key:"renderSectors",value:function(){var n=this.props,a=n.sectors,i=n.isAnimationActive,o=this.state.prevSectors;return i&&a&&a.length&&(!o||!Vo(o,a))?this.renderSectorsWithAnimation():this.renderSectorsStatically(a)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var n=this,a=this.props,i=a.hide,o=a.sectors,s=a.className,l=a.label,c=a.cx,f=a.cy,d=a.innerRadius,p=a.outerRadius,h=a.isAnimationActive,x=this.state.isAnimationFinished;if(i||!o||!o.length||!ae(c)||!ae(f)||!ae(d)||!ae(p))return null;var v=He("recharts-pie",s);return E.createElement(Qe,{tabIndex:this.props.rootTabIndex,className:v,ref:function(g){n.pieRef=g}},this.renderSectors(),l&&this.renderLabels(o),mr.renderCallByParent(this.props,null,!1),(!h||x)&&xa.renderCallByParent(this.props,o,!1))}}],[{key:"getDerivedStateFromProps",value:function(n,a){return a.prevIsAnimationActive!==n.isAnimationActive?{prevIsAnimationActive:n.isAnimationActive,prevAnimationId:n.animationId,curSectors:n.sectors,prevSectors:[],isAnimationFinished:!0}:n.isAnimationActive&&n.animationId!==a.prevAnimationId?{prevAnimationId:n.animationId,curSectors:n.sectors,prevSectors:a.curSectors,isAnimationFinished:!0}:n.sectors!==a.curSectors?{curSectors:n.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(n,a){return n>a?"start":n=360?m:m-1)*l,S=y-m*h-w,b=a.reduce(function(O,N){var T=Ht(N,g,0);return O+(ae(T)?T:0)},0),_;if(b>0){var k;_=a.map(function(O,N){var T=Ht(O,g,0),R=Ht(O,f,N),A=(ae(T)?T:0)/b,C;N?C=k.endAngle+Lr(v)*l*(T!==0?1:0):C=o;var M=C+Lr(v)*((T!==0?h:0)+A*S),B=(C+M)/2,V=(x.innerRadius+x.outerRadius)/2,I=[{name:R,value:T,payload:O,dataKey:g,type:p}],D=_t(x.cx,x.cy,V,B);return k=xt(xt(xt({percent:A,cornerRadius:i,name:R,tooltipPayload:I,midAngle:B,middleRadius:V,tooltipPosition:D},O),x),{},{value:Ht(O,g),startAngle:C,endAngle:M,payload:O,paddingAngle:Lr(v)*l}),k})}return xt(xt({},x),{},{sectors:_,data:a})});var kie=Math.ceil,Oie=Math.max;function Aie(e,t,r,n){for(var a=-1,i=Oie(kie((t-e)/(r||1)),0),o=Array(i);i--;)o[n?i:++a]=e,e+=r;return o}var Nie=Aie,Eie=PC,F2=1/0,Pie=17976931348623157e292;function Cie(e){if(!e)return e===0?e:0;if(e=Eie(e),e===F2||e===-F2){var t=e<0?-1:1;return t*Pie}return e===e?e:0}var d$=Cie,Tie=Nie,$ie=Em,Sy=d$;function Rie(e){return function(t,r,n){return n&&typeof n!="number"&&$ie(t,r,n)&&(r=n=void 0),t=Sy(t),r===void 0?(r=t,t=0):r=Sy(r),n=n===void 0?t0&&n.handleDrag(a.changedTouches[0])}),ln(n,"handleDragEnd",function(){n.setState({isTravellerMoving:!1,isSlideMoving:!1},function(){var a=n.props,i=a.endIndex,o=a.onDragEnd,s=a.startIndex;o==null||o({endIndex:i,startIndex:s})}),n.detachDragEndListener()}),ln(n,"handleLeaveWrapper",function(){(n.state.isTravellerMoving||n.state.isSlideMoving)&&(n.leaveTimer=window.setTimeout(n.handleDragEnd,n.props.leaveTimeOut))}),ln(n,"handleEnterSlideOrTraveller",function(){n.setState({isTextActive:!0})}),ln(n,"handleLeaveSlideOrTraveller",function(){n.setState({isTextActive:!1})}),ln(n,"handleSlideDragStart",function(a){var i=W2(a)?a.changedTouches[0]:a;n.setState({isTravellerMoving:!1,isSlideMoving:!0,slideMoveStartX:i.pageX}),n.attachDragEndListener()}),n.travellerDragStartHandlers={startX:n.handleTravellerDragStart.bind(n,"startX"),endX:n.handleTravellerDragStart.bind(n,"endX")},n.state={},n}return Kie(t,e),Wie(t,[{key:"componentWillUnmount",value:function(){this.leaveTimer&&(clearTimeout(this.leaveTimer),this.leaveTimer=null),this.detachDragEndListener()}},{key:"getIndex",value:function(n){var a=n.startX,i=n.endX,o=this.state.scaleValues,s=this.props,l=s.gap,c=s.data,f=c.length-1,d=Math.min(a,i),p=Math.max(a,i),h=t.getIndexInRange(o,d),x=t.getIndexInRange(o,p);return{startIndex:h-h%l,endIndex:x===f?f:x-x%l}}},{key:"getTextOfTick",value:function(n){var a=this.props,i=a.data,o=a.tickFormatter,s=a.dataKey,l=Ht(i[n],s,n);return Te(o)?o(l,n):l}},{key:"attachDragEndListener",value:function(){window.addEventListener("mouseup",this.handleDragEnd,!0),window.addEventListener("touchend",this.handleDragEnd,!0),window.addEventListener("mousemove",this.handleDrag,!0)}},{key:"detachDragEndListener",value:function(){window.removeEventListener("mouseup",this.handleDragEnd,!0),window.removeEventListener("touchend",this.handleDragEnd,!0),window.removeEventListener("mousemove",this.handleDrag,!0)}},{key:"handleSlideDrag",value:function(n){var a=this.state,i=a.slideMoveStartX,o=a.startX,s=a.endX,l=this.props,c=l.x,f=l.width,d=l.travellerWidth,p=l.startIndex,h=l.endIndex,x=l.onChange,v=n.pageX-i;v>0?v=Math.min(v,c+f-d-s,c+f-d-o):v<0&&(v=Math.max(v,c-o,c-s));var y=this.getIndex({startX:o+v,endX:s+v});(y.startIndex!==p||y.endIndex!==h)&&x&&x(y),this.setState({startX:o+v,endX:s+v,slideMoveStartX:n.pageX})}},{key:"handleTravellerDragStart",value:function(n,a){var i=W2(a)?a.changedTouches[0]:a;this.setState({isSlideMoving:!1,isTravellerMoving:!0,movingTravellerId:n,brushMoveStartX:i.pageX}),this.attachDragEndListener()}},{key:"handleTravellerMove",value:function(n){var a=this.state,i=a.brushMoveStartX,o=a.movingTravellerId,s=a.endX,l=a.startX,c=this.state[o],f=this.props,d=f.x,p=f.width,h=f.travellerWidth,x=f.onChange,v=f.gap,y=f.data,g={startX:this.state.startX,endX:this.state.endX},m=n.pageX-i;m>0?m=Math.min(m,d+p-h-c):m<0&&(m=Math.max(m,d-c)),g[o]=c+m;var w=this.getIndex(g),S=w.startIndex,b=w.endIndex,_=function(){var O=y.length-1;return o==="startX"&&(s>l?S%v===0:b%v===0)||sl?b%v===0:S%v===0)||s>l&&b===O};this.setState(ln(ln({},o,c+m),"brushMoveStartX",n.pageX),function(){x&&_()&&x(w)})}},{key:"handleTravellerMoveKeyboard",value:function(n,a){var i=this,o=this.state,s=o.scaleValues,l=o.startX,c=o.endX,f=this.state[a],d=s.indexOf(f);if(d!==-1){var p=d+n;if(!(p===-1||p>=s.length)){var h=s[p];a==="startX"&&h>=c||a==="endX"&&h<=l||this.setState(ln({},a,h),function(){i.props.onChange(i.getIndex({startX:i.state.startX,endX:i.state.endX}))})}}}},{key:"renderBackground",value:function(){var n=this.props,a=n.x,i=n.y,o=n.width,s=n.height,l=n.fill,c=n.stroke;return E.createElement("rect",{stroke:c,fill:l,x:a,y:i,width:o,height:s})}},{key:"renderPanorama",value:function(){var n=this.props,a=n.x,i=n.y,o=n.width,s=n.height,l=n.data,c=n.children,f=n.padding,d=j.Children.only(c);return d?E.cloneElement(d,{x:a,y:i,width:o,height:s,margin:f,compact:!0,data:l}):null}},{key:"renderTravellerLayer",value:function(n,a){var i,o,s=this,l=this.props,c=l.y,f=l.travellerWidth,d=l.height,p=l.traveller,h=l.ariaLabel,x=l.data,v=l.startIndex,y=l.endIndex,g=Math.max(n,this.props.x),m=jy(jy({},Oe(this.props,!1)),{},{x:g,y:c,width:f,height:d}),w=h||"Min value: ".concat((i=x[v])===null||i===void 0?void 0:i.name,", Max value: ").concat((o=x[y])===null||o===void 0?void 0:o.name);return E.createElement(Qe,{tabIndex:0,role:"slider","aria-label":w,"aria-valuenow":n,className:"recharts-brush-traveller",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.travellerDragStartHandlers[a],onTouchStart:this.travellerDragStartHandlers[a],onKeyDown:function(b){["ArrowLeft","ArrowRight"].includes(b.key)&&(b.preventDefault(),b.stopPropagation(),s.handleTravellerMoveKeyboard(b.key==="ArrowRight"?1:-1,a))},onFocus:function(){s.setState({isTravellerFocused:!0})},onBlur:function(){s.setState({isTravellerFocused:!1})},style:{cursor:"col-resize"}},t.renderTraveller(p,m))}},{key:"renderSlide",value:function(n,a){var i=this.props,o=i.y,s=i.height,l=i.stroke,c=i.travellerWidth,f=Math.min(n,a)+c,d=Math.max(Math.abs(a-n)-c,0);return E.createElement("rect",{className:"recharts-brush-slide",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.handleSlideDragStart,onTouchStart:this.handleSlideDragStart,style:{cursor:"move"},stroke:"none",fill:l,fillOpacity:.2,x:f,y:o,width:d,height:s})}},{key:"renderText",value:function(){var n=this.props,a=n.startIndex,i=n.endIndex,o=n.y,s=n.height,l=n.travellerWidth,c=n.stroke,f=this.state,d=f.startX,p=f.endX,h=5,x={pointerEvents:"none",fill:c};return E.createElement(Qe,{className:"recharts-brush-texts"},E.createElement(Uo,_h({textAnchor:"end",verticalAnchor:"middle",x:Math.min(d,p)-h,y:o+s/2},x),this.getTextOfTick(a)),E.createElement(Uo,_h({textAnchor:"start",verticalAnchor:"middle",x:Math.max(d,p)+l+h,y:o+s/2},x),this.getTextOfTick(i)))}},{key:"render",value:function(){var n=this.props,a=n.data,i=n.className,o=n.children,s=n.x,l=n.y,c=n.width,f=n.height,d=n.alwaysShowText,p=this.state,h=p.startX,x=p.endX,v=p.isTextActive,y=p.isSlideMoving,g=p.isTravellerMoving,m=p.isTravellerFocused;if(!a||!a.length||!ae(s)||!ae(l)||!ae(c)||!ae(f)||c<=0||f<=0)return null;var w=He("recharts-brush",i),S=E.Children.count(o)===1,b=Uie("userSelect","none");return E.createElement(Qe,{className:w,onMouseLeave:this.handleLeaveWrapper,onTouchMove:this.handleTouchMove,style:b},this.renderBackground(),S&&this.renderPanorama(),this.renderSlide(h,x),this.renderTravellerLayer(h,"startX"),this.renderTravellerLayer(x,"endX"),(v||y||g||m||d)&&this.renderText())}}],[{key:"renderDefaultTraveller",value:function(n){var a=n.x,i=n.y,o=n.width,s=n.height,l=n.stroke,c=Math.floor(i+s/2)-1;return E.createElement(E.Fragment,null,E.createElement("rect",{x:a,y:i,width:o,height:s,fill:l,stroke:"none"}),E.createElement("line",{x1:a+1,y1:c,x2:a+o-1,y2:c,fill:"none",stroke:"#fff"}),E.createElement("line",{x1:a+1,y1:c+2,x2:a+o-1,y2:c+2,fill:"none",stroke:"#fff"}))}},{key:"renderTraveller",value:function(n,a){var i;return E.isValidElement(n)?i=E.cloneElement(n,a):Te(n)?i=n(a):i=t.renderDefaultTraveller(a),i}},{key:"getDerivedStateFromProps",value:function(n,a){var i=n.data,o=n.width,s=n.x,l=n.travellerWidth,c=n.updateId,f=n.startIndex,d=n.endIndex;if(i!==a.prevData||c!==a.prevUpdateId)return jy({prevData:i,prevTravellerWidth:l,prevUpdateId:c,prevX:s,prevWidth:o},i&&i.length?Yie({data:i,width:o,x:s,travellerWidth:l,startIndex:f,endIndex:d}):{scale:null,scaleValues:null});if(a.scale&&(o!==a.prevWidth||s!==a.prevX||l!==a.prevTravellerWidth)){a.scale.range([s,s+o-l]);var p=a.scale.domain().map(function(h){return a.scale(h)});return{prevData:i,prevTravellerWidth:l,prevUpdateId:c,prevX:s,prevWidth:o,startX:a.scale(n.startIndex),endX:a.scale(n.endIndex),scaleValues:p}}return null}},{key:"getIndexInRange",value:function(n,a){for(var i=n.length,o=0,s=i-1;s-o>1;){var l=Math.floor((o+s)/2);n[l]>a?s=l:o=l}return a>=n[s]?s:o}}])}(j.PureComponent);ln(xl,"displayName","Brush");ln(xl,"defaultProps",{height:40,travellerWidth:5,gap:1,fill:"#fff",stroke:"#666",padding:{top:1,right:1,bottom:1,left:1},leaveTimeOut:1e3,alwaysShowText:!1});var Zie=xw;function Qie(e,t){var r;return Zie(e,function(n,a,i){return r=t(n,a,i),!r}),!!r}var Jie=Qie,eoe=nC,toe=Aa,roe=Jie,noe=nn,aoe=Em;function ioe(e,t,r){var n=noe(e)?eoe:roe;return r&&aoe(e,t,r)&&(t=void 0),n(e,toe(t))}var ooe=ioe;const soe=dt(ooe);var ba=function(t,r){var n=t.alwaysShow,a=t.ifOverflow;return n&&(a="extendDomain"),a===r},H2=kC;function loe(e,t,r){t=="__proto__"&&H2?H2(e,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):e[t]=r}var uoe=loe,coe=uoe,doe=SC,foe=Aa;function poe(e,t){var r={};return t=foe(t),doe(e,function(n,a,i){coe(r,a,t(n,a,i))}),r}var hoe=poe;const moe=dt(hoe);function voe(e,t){for(var r=-1,n=e==null?0:e.length;++r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function $oe(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Roe(e,t){var r=e.x,n=e.y,a=Toe(e,Noe),i="".concat(r),o=parseInt(i,10),s="".concat(n),l=parseInt(s,10),c="".concat(t.height||a.height),f=parseInt(c,10),d="".concat(t.width||a.width),p=parseInt(d,10);return yu(yu(yu(yu(yu({},t),a),o?{x:o}:{}),l?{y:l}:{}),{},{height:f,width:p,name:t.name,radius:t.radius})}function q2(e){return E.createElement(l$,f0({shapeType:"rectangle",propTransformer:Roe,activeClassName:"recharts-active-bar"},e))}var Ioe=function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return function(n,a){if(typeof t=="number")return t;var i=ae(n)||nB(n);return i?t(n,a):(i||Ho(),r)}},Moe=["value","background"],v$;function bl(e){"@babel/helpers - typeof";return bl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},bl(e)}function Doe(e,t){if(e==null)return{};var r=Loe(e,t),n,a;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Loe(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function jh(){return jh=Object.assign?Object.assign.bind():function(e){for(var t=1;t0&&Math.abs(B)0&&Math.abs(M)0&&(C=Math.min((Z||0)-(M[de-1]||0),C))}),Number.isFinite(C)){var B=C/A,V=v.layout==="vertical"?n.height:n.width;if(v.padding==="gap"&&(k=B*V/2),v.padding==="no-gap"){var I=Fr(t.barCategoryGap,B*V),D=B*V/2;k=D-I-(D-I)/V*I}}}a==="xAxis"?O=[n.left+(w.left||0)+(k||0),n.left+n.width-(w.right||0)-(k||0)]:a==="yAxis"?O=l==="horizontal"?[n.top+n.height-(w.bottom||0),n.top+(w.top||0)]:[n.top+(w.top||0)+(k||0),n.top+n.height-(w.bottom||0)-(k||0)]:O=v.range,b&&(O=[O[1],O[0]]);var U=MT(v,i,p),G=U.scale,q=U.realScaleType;G.domain(g).range(O),DT(G);var K=LT(G,Kn(Kn({},v),{},{realScaleType:q}));a==="xAxis"?(R=y==="top"&&!S||y==="bottom"&&S,N=n.left,T=d[_]-R*v.height):a==="yAxis"&&(R=y==="left"&&!S||y==="right"&&S,N=d[_]-R*v.width,T=n.top);var te=Kn(Kn(Kn({},v),K),{},{realScaleType:q,x:N,y:T,scale:G,width:a==="xAxis"?n.width:v.width,height:a==="yAxis"?n.height:v.height});return te.bandSize=lh(te,K),!v.hide&&a==="xAxis"?d[_]+=(R?-1:1)*te.height:v.hide||(d[_]+=(R?-1:1)*te.width),Kn(Kn({},h),{},Gm({},x,te))},{})},b$=function(t,r){var n=t.x,a=t.y,i=r.x,o=r.y;return{x:Math.min(n,i),y:Math.min(a,o),width:Math.abs(i-n),height:Math.abs(o-a)}},Xoe=function(t){var r=t.x1,n=t.y1,a=t.x2,i=t.y2;return b$({x:r,y:n},{x:a,y:i})},w$=function(){function e(t){Goe(this,e),this.scale=t}return qoe(e,[{key:"domain",get:function(){return this.scale.domain}},{key:"range",get:function(){return this.scale.range}},{key:"rangeMin",get:function(){return this.range()[0]}},{key:"rangeMax",get:function(){return this.range()[1]}},{key:"bandwidth",get:function(){return this.scale.bandwidth}},{key:"apply",value:function(r){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=n.bandAware,i=n.position;if(r!==void 0){if(i)switch(i){case"start":return this.scale(r);case"middle":{var o=this.bandwidth?this.bandwidth()/2:0;return this.scale(r)+o}case"end":{var s=this.bandwidth?this.bandwidth():0;return this.scale(r)+s}default:return this.scale(r)}if(a){var l=this.bandwidth?this.bandwidth()/2:0;return this.scale(r)+l}return this.scale(r)}}},{key:"isInRange",value:function(r){var n=this.range(),a=n[0],i=n[n.length-1];return a<=i?r>=a&&r<=i:r>=i&&r<=a}}],[{key:"create",value:function(r){return new e(r)}}])}();Gm(w$,"EPS",1e-4);var Xw=function(t){var r=Object.keys(t).reduce(function(n,a){return Kn(Kn({},n),{},Gm({},a,w$.create(t[a])))},{});return Kn(Kn({},r),{},{apply:function(a){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},o=i.bandAware,s=i.position;return moe(a,function(l,c){return r[c].apply(l,{bandAware:o,position:s})})},isInRange:function(a){return m$(a,function(i,o){return r[o].isInRange(i)})}})};function Yoe(e){return(e%180+180)%180}var Zoe=function(t){var r=t.width,n=t.height,a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,i=Yoe(a),o=i*Math.PI/180,s=Math.atan(n/r),l=o>s&&o-1?a[i?t[o]:o]:void 0}}var rse=tse,nse=d$;function ase(e){var t=nse(e),r=t%1;return t===t?r?t-r:t:0}var ise=ase,ose=yC,sse=Aa,lse=ise,use=Math.max;function cse(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var a=r==null?0:lse(r);return a<0&&(a=use(n+a,0)),ose(e,sse(t),a)}var dse=cse,fse=rse,pse=dse,hse=fse(pse),mse=hse;const vse=dt(mse);var yse=uz(function(e){return{x:e.left,y:e.top,width:e.width,height:e.height}},function(e){return["l",e.left,"t",e.top,"w",e.width,"h",e.height].join("")}),Yw=j.createContext(void 0),Zw=j.createContext(void 0),_$=j.createContext(void 0),S$=j.createContext({}),j$=j.createContext(void 0),k$=j.createContext(0),O$=j.createContext(0),Q2=function(t){var r=t.state,n=r.xAxisMap,a=r.yAxisMap,i=r.offset,o=t.clipPathId,s=t.children,l=t.width,c=t.height,f=yse(i);return E.createElement(Yw.Provider,{value:n},E.createElement(Zw.Provider,{value:a},E.createElement(S$.Provider,{value:i},E.createElement(_$.Provider,{value:f},E.createElement(j$.Provider,{value:o},E.createElement(k$.Provider,{value:c},E.createElement(O$.Provider,{value:l},s)))))))},gse=function(){return j.useContext(j$)},A$=function(t){var r=j.useContext(Yw);r==null&&Ho();var n=r[t];return n==null&&Ho(),n},xse=function(){var t=j.useContext(Yw);return _i(t)},bse=function(){var t=j.useContext(Zw),r=vse(t,function(n){return m$(n.domain,Number.isFinite)});return r||_i(t)},N$=function(t){var r=j.useContext(Zw);r==null&&Ho();var n=r[t];return n==null&&Ho(),n},wse=function(){var t=j.useContext(_$);return t},_se=function(){return j.useContext(S$)},Qw=function(){return j.useContext(O$)},Jw=function(){return j.useContext(k$)};function wl(e){"@babel/helpers - typeof";return wl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},wl(e)}function Sse(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function jse(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);re*a)return!1;var i=r();return e*(t-e*i/2-n)>=0&&e*(t+e*i/2-a)<=0}function ile(e,t){return I$(e,t+1)}function ole(e,t,r,n,a){for(var i=(n||[]).slice(),o=t.start,s=t.end,l=0,c=1,f=o,d=function(){var x=n==null?void 0:n[l];if(x===void 0)return{v:I$(n,c)};var v=l,y,g=function(){return y===void 0&&(y=r(x,v)),y},m=x.coordinate,w=l===0||Eh(e,m,g,f,s);w||(l=0,f=o,c+=1),w&&(f=m+e*(g()/2+a),l+=c)},p;c<=i.length;)if(p=d(),p)return p.v;return[]}function ld(e){"@babel/helpers - typeof";return ld=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ld(e)}function oO(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Nr(e){for(var t=1;t0?h.coordinate-y*e:h.coordinate})}else i[p]=h=Nr(Nr({},h),{},{tickCoord:h.coordinate});var g=Eh(e,h.tickCoord,v,s,l);g&&(l=h.tickCoord-e*(v()/2+a),i[p]=Nr(Nr({},h),{},{isShow:!0}))},f=o-1;f>=0;f--)c(f);return i}function dle(e,t,r,n,a,i){var o=(n||[]).slice(),s=o.length,l=t.start,c=t.end;if(i){var f=n[s-1],d=r(f,s-1),p=e*(f.coordinate+e*d/2-c);o[s-1]=f=Nr(Nr({},f),{},{tickCoord:p>0?f.coordinate-p*e:f.coordinate});var h=Eh(e,f.tickCoord,function(){return d},l,c);h&&(c=f.tickCoord-e*(d/2+a),o[s-1]=Nr(Nr({},f),{},{isShow:!0}))}for(var x=i?s-1:s,v=function(m){var w=o[m],S,b=function(){return S===void 0&&(S=r(w,m)),S};if(m===0){var _=e*(w.coordinate-e*b()/2-l);o[m]=w=Nr(Nr({},w),{},{tickCoord:_<0?w.coordinate-_*e:w.coordinate})}else o[m]=w=Nr(Nr({},w),{},{tickCoord:w.coordinate});var k=Eh(e,w.tickCoord,b,l,c);k&&(l=w.tickCoord+e*(b()/2+a),o[m]=Nr(Nr({},w),{},{isShow:!0}))},y=0;y=2?Lr(a[1].coordinate-a[0].coordinate):1,g=ale(i,y,h);return l==="equidistantPreserveStart"?ole(y,g,v,a,o):(l==="preserveStart"||l==="preserveStartEnd"?p=dle(y,g,v,a,o,l==="preserveStartEnd"):p=cle(y,g,v,a,o),p.filter(function(m){return m.isShow}))}var fle=["viewBox"],ple=["viewBox"],hle=["ticks"];function jl(e){"@babel/helpers - typeof";return jl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},jl(e)}function Cs(){return Cs=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function mle(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function vle(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function lO(e,t){for(var r=0;r0?l(this.props):l(h)),o<=0||s<=0||!x||!x.length?null:E.createElement(Qe,{className:He("recharts-cartesian-axis",c),ref:function(y){n.layerReference=y}},i&&this.renderAxisLine(),this.renderTicks(x,this.state.fontSize,this.state.letterSpacing),mr.renderCallByParent(this.props))}}],[{key:"renderTickItem",value:function(n,a,i){var o,s=He(a.className,"recharts-cartesian-axis-tick-value");return E.isValidElement(n)?o=E.cloneElement(n,rr(rr({},a),{},{className:s})):Te(n)?o=n(rr(rr({},a),{},{className:s})):o=E.createElement(Uo,Cs({},a,{className:"recharts-cartesian-axis-tick-value"}),i),o}}])}(j.Component);n1(Yl,"displayName","CartesianAxis");n1(Yl,"defaultProps",{x:0,y:0,width:0,height:0,viewBox:{x:0,y:0,width:0,height:0},orientation:"bottom",ticks:[],stroke:"#666",tickLine:!0,axisLine:!0,tick:!0,mirror:!1,minTickGap:5,tickSize:6,tickMargin:2,interval:"preserveEnd"});var Sle=["x1","y1","x2","y2","key"],jle=["offset"];function Go(e){"@babel/helpers - typeof";return Go=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Go(e)}function uO(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Cr(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Nle(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var Ele=function(t){var r=t.fill;if(!r||r==="none")return null;var n=t.fillOpacity,a=t.x,i=t.y,o=t.width,s=t.height,l=t.ry;return E.createElement("rect",{x:a,y:i,ry:l,width:o,height:s,stroke:"none",fill:r,fillOpacity:n,className:"recharts-cartesian-grid-bg"})};function L$(e,t){var r;if(E.isValidElement(e))r=E.cloneElement(e,t);else if(Te(e))r=e(t);else{var n=t.x1,a=t.y1,i=t.x2,o=t.y2,s=t.key,l=cO(t,Sle),c=Oe(l,!1);c.offset;var f=cO(c,jle);r=E.createElement("line",So({},f,{x1:n,y1:a,x2:i,y2:o,fill:"none",key:s}))}return r}function Ple(e){var t=e.x,r=e.width,n=e.horizontal,a=n===void 0?!0:n,i=e.horizontalPoints;if(!a||!i||!i.length)return null;var o=i.map(function(s,l){var c=Cr(Cr({},e),{},{x1:t,y1:s,x2:t+r,y2:s,key:"line-".concat(l),index:l});return L$(a,c)});return E.createElement("g",{className:"recharts-cartesian-grid-horizontal"},o)}function Cle(e){var t=e.y,r=e.height,n=e.vertical,a=n===void 0?!0:n,i=e.verticalPoints;if(!a||!i||!i.length)return null;var o=i.map(function(s,l){var c=Cr(Cr({},e),{},{x1:s,y1:t,x2:s,y2:t+r,key:"line-".concat(l),index:l});return L$(a,c)});return E.createElement("g",{className:"recharts-cartesian-grid-vertical"},o)}function Tle(e){var t=e.horizontalFill,r=e.fillOpacity,n=e.x,a=e.y,i=e.width,o=e.height,s=e.horizontalPoints,l=e.horizontal,c=l===void 0?!0:l;if(!c||!t||!t.length)return null;var f=s.map(function(p){return Math.round(p+a-a)}).sort(function(p,h){return p-h});a!==f[0]&&f.unshift(0);var d=f.map(function(p,h){var x=!f[h+1],v=x?a+o-p:f[h+1]-p;if(v<=0)return null;var y=h%t.length;return E.createElement("rect",{key:"react-".concat(h),y:p,x:n,height:v,width:i,stroke:"none",fill:t[y],fillOpacity:r,className:"recharts-cartesian-grid-bg"})});return E.createElement("g",{className:"recharts-cartesian-gridstripes-horizontal"},d)}function $le(e){var t=e.vertical,r=t===void 0?!0:t,n=e.verticalFill,a=e.fillOpacity,i=e.x,o=e.y,s=e.width,l=e.height,c=e.verticalPoints;if(!r||!n||!n.length)return null;var f=c.map(function(p){return Math.round(p+i-i)}).sort(function(p,h){return p-h});i!==f[0]&&f.unshift(0);var d=f.map(function(p,h){var x=!f[h+1],v=x?i+s-p:f[h+1]-p;if(v<=0)return null;var y=h%n.length;return E.createElement("rect",{key:"react-".concat(h),x:p,y:o,width:v,height:l,stroke:"none",fill:n[y],fillOpacity:a,className:"recharts-cartesian-grid-bg"})});return E.createElement("g",{className:"recharts-cartesian-gridstripes-vertical"},d)}var Rle=function(t,r){var n=t.xAxis,a=t.width,i=t.height,o=t.offset;return IT(r1(Cr(Cr(Cr({},Yl.defaultProps),n),{},{ticks:Ha(n,!0),viewBox:{x:0,y:0,width:a,height:i}})),o.left,o.left+o.width,r)},Ile=function(t,r){var n=t.yAxis,a=t.width,i=t.height,o=t.offset;return IT(r1(Cr(Cr(Cr({},Yl.defaultProps),n),{},{ticks:Ha(n,!0),viewBox:{x:0,y:0,width:a,height:i}})),o.top,o.top+o.height,r)},os={horizontal:!0,vertical:!0,stroke:"#ccc",fill:"none",verticalFill:[],horizontalFill:[]};function Gf(e){var t,r,n,a,i,o,s=Qw(),l=Jw(),c=_se(),f=Cr(Cr({},e),{},{stroke:(t=e.stroke)!==null&&t!==void 0?t:os.stroke,fill:(r=e.fill)!==null&&r!==void 0?r:os.fill,horizontal:(n=e.horizontal)!==null&&n!==void 0?n:os.horizontal,horizontalFill:(a=e.horizontalFill)!==null&&a!==void 0?a:os.horizontalFill,vertical:(i=e.vertical)!==null&&i!==void 0?i:os.vertical,verticalFill:(o=e.verticalFill)!==null&&o!==void 0?o:os.verticalFill,x:ae(e.x)?e.x:c.left,y:ae(e.y)?e.y:c.top,width:ae(e.width)?e.width:c.width,height:ae(e.height)?e.height:c.height}),d=f.x,p=f.y,h=f.width,x=f.height,v=f.syncWithTicks,y=f.horizontalValues,g=f.verticalValues,m=xse(),w=bse();if(!ae(h)||h<=0||!ae(x)||x<=0||!ae(d)||d!==+d||!ae(p)||p!==+p)return null;var S=f.verticalCoordinatesGenerator||Rle,b=f.horizontalCoordinatesGenerator||Ile,_=f.horizontalPoints,k=f.verticalPoints;if((!_||!_.length)&&Te(b)){var O=y&&y.length,N=b({yAxis:w?Cr(Cr({},w),{},{ticks:O?y:w.ticks}):void 0,width:s,height:l,offset:c},O?!0:v);ra(Array.isArray(N),"horizontalCoordinatesGenerator should return Array but instead it returned [".concat(Go(N),"]")),Array.isArray(N)&&(_=N)}if((!k||!k.length)&&Te(S)){var T=g&&g.length,R=S({xAxis:m?Cr(Cr({},m),{},{ticks:T?g:m.ticks}):void 0,width:s,height:l,offset:c},T?!0:v);ra(Array.isArray(R),"verticalCoordinatesGenerator should return Array but instead it returned [".concat(Go(R),"]")),Array.isArray(R)&&(k=R)}return E.createElement("g",{className:"recharts-cartesian-grid"},E.createElement(Ele,{fill:f.fill,fillOpacity:f.fillOpacity,x:f.x,y:f.y,width:f.width,height:f.height,ry:f.ry}),E.createElement(Ple,So({},f,{offset:c,horizontalPoints:_,xAxis:m,yAxis:w})),E.createElement(Cle,So({},f,{offset:c,verticalPoints:k,xAxis:m,yAxis:w})),E.createElement(Tle,So({},f,{horizontalPoints:_})),E.createElement($le,So({},f,{verticalPoints:k})))}Gf.displayName="CartesianGrid";var Mle=["type","layout","connectNulls","ref"],Dle=["key"];function kl(e){"@babel/helpers - typeof";return kl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},kl(e)}function dO(e,t){if(e==null)return{};var r=Lle(e,t),n,a;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Lle(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function ac(){return ac=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);rd){h=[].concat(ss(l.slice(0,x)),[d-v]);break}var y=h.length%2===0?[0,p]:[p];return[].concat(ss(t.repeat(l,f)),ss(h),y).map(function(g){return"".concat(g,"px")}).join(", ")}),Xn(r,"id",Yo("recharts-line-")),Xn(r,"pathRef",function(o){r.mainCurve=o}),Xn(r,"handleAnimationEnd",function(){r.setState({isAnimationFinished:!0}),r.props.onAnimationEnd&&r.props.onAnimationEnd()}),Xn(r,"handleAnimationStart",function(){r.setState({isAnimationFinished:!1}),r.props.onAnimationStart&&r.props.onAnimationStart()}),r}return Kle(t,e),Wle(t,[{key:"componentDidMount",value:function(){if(this.props.isAnimationActive){var n=this.getTotalLength();this.setState({totalLength:n})}}},{key:"componentDidUpdate",value:function(){if(this.props.isAnimationActive){var n=this.getTotalLength();n!==this.state.totalLength&&this.setState({totalLength:n})}}},{key:"getTotalLength",value:function(){var n=this.mainCurve;try{return n&&n.getTotalLength&&n.getTotalLength()||0}catch{return 0}}},{key:"renderErrorBar",value:function(n,a){if(this.props.isAnimationActive&&!this.state.isAnimationFinished)return null;var i=this.props,o=i.points,s=i.xAxis,l=i.yAxis,c=i.layout,f=i.children,d=yn(f,Cd);if(!d)return null;var p=function(v,y){return{x:v.x,y:v.y,value:v.value,errorVal:Ht(v.payload,y)}},h={clipPath:n?"url(#clipPath-".concat(a,")"):null};return E.createElement(Qe,h,d.map(function(x){return E.cloneElement(x,{key:"bar-".concat(x.props.dataKey),data:o,xAxis:s,yAxis:l,layout:c,dataPointFormatter:p})}))}},{key:"renderDots",value:function(n,a,i){var o=this.props.isAnimationActive;if(o&&!this.state.isAnimationFinished)return null;var s=this.props,l=s.dot,c=s.points,f=s.dataKey,d=Oe(this.props,!1),p=Oe(l,!0),h=c.map(function(v,y){var g=sn(sn(sn({key:"dot-".concat(y),r:3},d),p),{},{index:y,cx:v.x,cy:v.y,value:v.value,dataKey:f,payload:v.payload,points:c});return t.renderDotItem(l,g)}),x={clipPath:n?"url(#clipPath-".concat(a?"":"dots-").concat(i,")"):null};return E.createElement(Qe,ac({className:"recharts-line-dots",key:"dots"},x),h)}},{key:"renderCurveStatically",value:function(n,a,i,o){var s=this.props,l=s.type,c=s.layout,f=s.connectNulls;s.ref;var d=dO(s,Mle),p=sn(sn(sn({},Oe(d,!0)),{},{fill:"none",className:"recharts-line-curve",clipPath:a?"url(#clipPath-".concat(i,")"):null,points:n},o),{},{type:l,layout:c,connectNulls:f});return E.createElement(Po,ac({},p,{pathRef:this.pathRef}))}},{key:"renderCurveWithAnimation",value:function(n,a){var i=this,o=this.props,s=o.points,l=o.strokeDasharray,c=o.isAnimationActive,f=o.animationBegin,d=o.animationDuration,p=o.animationEasing,h=o.animationId,x=o.animateNewValues,v=o.width,y=o.height,g=this.state,m=g.prevPoints,w=g.totalLength;return E.createElement(ia,{begin:f,duration:d,isActive:c,easing:p,from:{t:0},to:{t:1},key:"line-".concat(h),onAnimationEnd:this.handleAnimationEnd,onAnimationStart:this.handleAnimationStart},function(S){var b=S.t;if(m){var _=m.length/s.length,k=s.map(function(A,C){var M=Math.floor(C*_);if(m[M]){var B=m[M],V=or(B.x,A.x),I=or(B.y,A.y);return sn(sn({},A),{},{x:V(b),y:I(b)})}if(x){var D=or(v*2,A.x),U=or(y/2,A.y);return sn(sn({},A),{},{x:D(b),y:U(b)})}return sn(sn({},A),{},{x:A.x,y:A.y})});return i.renderCurveStatically(k,n,a)}var O=or(0,w),N=O(b),T;if(l){var R="".concat(l).split(/[,\s]+/gim).map(function(A){return parseFloat(A)});T=i.getStrokeDasharray(N,w,R)}else T=i.generateSimpleStrokeDasharray(w,N);return i.renderCurveStatically(s,n,a,{strokeDasharray:T})})}},{key:"renderCurve",value:function(n,a){var i=this.props,o=i.points,s=i.isAnimationActive,l=this.state,c=l.prevPoints,f=l.totalLength;return s&&o&&o.length&&(!c&&f>0||!Vo(c,o))?this.renderCurveWithAnimation(n,a):this.renderCurveStatically(o,n,a)}},{key:"render",value:function(){var n,a=this.props,i=a.hide,o=a.dot,s=a.points,l=a.className,c=a.xAxis,f=a.yAxis,d=a.top,p=a.left,h=a.width,x=a.height,v=a.isAnimationActive,y=a.id;if(i||!s||!s.length)return null;var g=this.state.isAnimationFinished,m=s.length===1,w=He("recharts-line",l),S=c&&c.allowDataOverflow,b=f&&f.allowDataOverflow,_=S||b,k=Fe(y)?this.id:y,O=(n=Oe(o,!1))!==null&&n!==void 0?n:{r:3,strokeWidth:2},N=O.r,T=N===void 0?3:N,R=O.strokeWidth,A=R===void 0?2:R,C=NP(o)?o:{},M=C.clipDot,B=M===void 0?!0:M,V=T*2+A;return E.createElement(Qe,{className:w},S||b?E.createElement("defs",null,E.createElement("clipPath",{id:"clipPath-".concat(k)},E.createElement("rect",{x:S?p:p-h/2,y:b?d:d-x/2,width:S?h:h*2,height:b?x:x*2})),!B&&E.createElement("clipPath",{id:"clipPath-dots-".concat(k)},E.createElement("rect",{x:p-V/2,y:d-V/2,width:h+V,height:x+V}))):null,!m&&this.renderCurve(_,k),this.renderErrorBar(_,k),(m||o)&&this.renderDots(_,B,k),(!v||g)&&xa.renderCallByParent(this.props,s))}}],[{key:"getDerivedStateFromProps",value:function(n,a){return n.animationId!==a.prevAnimationId?{prevAnimationId:n.animationId,curPoints:n.points,prevPoints:a.curPoints}:n.points!==a.curPoints?{curPoints:n.points}:null}},{key:"repeat",value:function(n,a){for(var i=n.length%2!==0?[].concat(ss(n),[0]):n,o=[],s=0;s=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Qle(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function jo(){return jo=Object.assign?Object.assign.bind():function(e){for(var t=1;t0||!Vo(f,o)||!Vo(d,s))?this.renderAreaWithAnimation(n,a):this.renderAreaStatically(o,s,n,a)}},{key:"render",value:function(){var n,a=this.props,i=a.hide,o=a.dot,s=a.points,l=a.className,c=a.top,f=a.left,d=a.xAxis,p=a.yAxis,h=a.width,x=a.height,v=a.isAnimationActive,y=a.id;if(i||!s||!s.length)return null;var g=this.state.isAnimationFinished,m=s.length===1,w=He("recharts-area",l),S=d&&d.allowDataOverflow,b=p&&p.allowDataOverflow,_=S||b,k=Fe(y)?this.id:y,O=(n=Oe(o,!1))!==null&&n!==void 0?n:{r:3,strokeWidth:2},N=O.r,T=N===void 0?3:N,R=O.strokeWidth,A=R===void 0?2:R,C=NP(o)?o:{},M=C.clipDot,B=M===void 0?!0:M,V=T*2+A;return E.createElement(Qe,{className:w},S||b?E.createElement("defs",null,E.createElement("clipPath",{id:"clipPath-".concat(k)},E.createElement("rect",{x:S?f:f-h/2,y:b?c:c-x/2,width:S?h:h*2,height:b?x:x*2})),!B&&E.createElement("clipPath",{id:"clipPath-dots-".concat(k)},E.createElement("rect",{x:f-V/2,y:c-V/2,width:h+V,height:x+V}))):null,m?null:this.renderArea(_,k),(o||m)&&this.renderDots(_,B,k),(!v||g)&&xa.renderCallByParent(this.props,s))}}],[{key:"getDerivedStateFromProps",value:function(n,a){return n.animationId!==a.prevAnimationId?{prevAnimationId:n.animationId,curPoints:n.points,curBaseLine:n.baseLine,prevPoints:a.curPoints,prevBaseLine:a.curBaseLine}:n.points!==a.curPoints||n.baseLine!==a.curBaseLine?{curPoints:n.points,curBaseLine:n.baseLine}:null}}])}(j.PureComponent);B$=to;ha(to,"displayName","Area");ha(to,"defaultProps",{stroke:"#3182bd",fill:"#3182bd",fillOpacity:.6,xAxisId:0,yAxisId:0,legendType:"line",connectNulls:!1,points:[],dot:!1,activeDot:!0,hide:!1,isAnimationActive:!Zi.isSsr,animationBegin:0,animationDuration:1500,animationEasing:"ease"});ha(to,"getBaseValue",function(e,t,r,n){var a=e.layout,i=e.baseValue,o=t.props.baseValue,s=o??i;if(ae(s)&&typeof s=="number")return s;var l=a==="horizontal"?n:r,c=l.scale.domain();if(l.type==="number"){var f=Math.max(c[0],c[1]),d=Math.min(c[0],c[1]);return s==="dataMin"?d:s==="dataMax"||f<0?f:Math.max(Math.min(c[0],c[1]),0)}return s==="dataMin"?c[0]:s==="dataMax"?c[1]:c[0]});ha(to,"getComposedData",function(e){var t=e.props,r=e.item,n=e.xAxis,a=e.yAxis,i=e.xAxisTicks,o=e.yAxisTicks,s=e.bandSize,l=e.dataKey,c=e.stackedData,f=e.dataStartIndex,d=e.displayedData,p=e.offset,h=t.layout,x=c&&c.length,v=B$.getBaseValue(t,r,n,a),y=h==="horizontal",g=!1,m=d.map(function(S,b){var _;x?_=c[f+b]:(_=Ht(S,l),Array.isArray(_)?g=!0:_=[v,_]);var k=_[1]==null||x&&Ht(S,l)==null;return y?{x:sh({axis:n,ticks:i,bandSize:s,entry:S,index:b}),y:k?null:a.scale(_[1]),value:_,payload:S}:{x:k?null:n.scale(_[1]),y:sh({axis:a,ticks:o,bandSize:s,entry:S,index:b}),value:_,payload:S}}),w;return x||g?w=m.map(function(S){var b=Array.isArray(S.value)?S.value[0]:null;return y?{x:S.x,y:b!=null&&S.y!=null?a.scale(b):null}:{x:b!=null?n.scale(b):null,y:S.y}}):w=y?a.scale(v):n.scale(v),pi({points:m,baseLine:w,layout:h,isRange:g},p)});ha(to,"renderDotItem",function(e,t){var r;if(E.isValidElement(e))r=E.cloneElement(e,t);else if(Te(e))r=e(t);else{var n=He("recharts-area-dot",typeof e!="boolean"?e.className:""),a=t.key,i=U$(t,Zle);r=E.createElement(Td,jo({},i,{key:a,className:n}))}return r});function Al(e){"@babel/helpers - typeof";return Al=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Al(e)}function oue(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function sue(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function que(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Kue(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Xue(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r0?o:t&&t.length&&ae(a)&&ae(i)?t.slice(a,i+1):[]};function aR(e){return e==="number"?[0,"auto"]:void 0}var T0=function(t,r,n,a){var i=t.graphicalItems,o=t.tooltipAxis,s=Zm(r,t);return n<0||!i||!i.length||n>=s.length?null:i.reduce(function(l,c){var f,d=(f=c.props.data)!==null&&f!==void 0?f:r;d&&t.dataStartIndex+t.dataEndIndex!==0&&t.dataEndIndex-t.dataStartIndex>=n&&(d=d.slice(t.dataStartIndex,t.dataEndIndex+1));var p;if(o.dataKey&&!o.allowDuplicatedCategory){var h=d===void 0?s:d;p=Tp(h,o.dataKey,a)}else p=d&&d[n]||s[n];return p?[].concat(Pl(l),[zT(c,p)]):l},[])},wO=function(t,r,n,a){var i=a||{x:t.chartX,y:t.chartY},o=sce(i,n),s=t.orderedTooltipTicks,l=t.tooltipAxis,c=t.tooltipTicks,f=mee(o,s,c,l);if(f>=0&&c){var d=c[f]&&c[f].value,p=T0(t,r,f,d),h=lce(n,s,f,i);return{activeTooltipIndex:f,activeLabel:d,activePayload:p,activeCoordinate:h}}return null},uce=function(t,r){var n=r.axes,a=r.graphicalItems,i=r.axisType,o=r.axisIdKey,s=r.stackGroups,l=r.dataStartIndex,c=r.dataEndIndex,f=t.layout,d=t.children,p=t.stackOffset,h=RT(f,i);return n.reduce(function(x,v){var y,g=v.type.defaultProps!==void 0?X(X({},v.type.defaultProps),v.props):v.props,m=g.type,w=g.dataKey,S=g.allowDataOverflow,b=g.allowDuplicatedCategory,_=g.scale,k=g.ticks,O=g.includeHidden,N=g[o];if(x[N])return x;var T=Zm(t.data,{graphicalItems:a.filter(function(K){var te,Z=o in K.props?K.props[o]:(te=K.type.defaultProps)===null||te===void 0?void 0:te[o];return Z===N}),dataStartIndex:l,dataEndIndex:c}),R=T.length,A,C,M;Iue(g.domain,S,m)&&(A=Hx(g.domain,null,S),h&&(m==="number"||_!=="auto")&&(M=ec(T,w,"category")));var B=aR(m);if(!A||A.length===0){var V,I=(V=g.domain)!==null&&V!==void 0?V:B;if(w){if(A=ec(T,w,m),m==="category"&&h){var D=iB(A);b&&D?(C=A,A=wh(0,R)):b||(A=Wk(I,A,v).reduce(function(K,te){return K.indexOf(te)>=0?K:[].concat(Pl(K),[te])},[]))}else if(m==="category")b?A=A.filter(function(K){return K!==""&&!Fe(K)}):A=Wk(I,A,v).reduce(function(K,te){return K.indexOf(te)>=0||te===""||Fe(te)?K:[].concat(Pl(K),[te])},[]);else if(m==="number"){var U=bee(T,a.filter(function(K){var te,Z,de=o in K.props?K.props[o]:(te=K.type.defaultProps)===null||te===void 0?void 0:te[o],fe="hide"in K.props?K.props.hide:(Z=K.type.defaultProps)===null||Z===void 0?void 0:Z.hide;return de===N&&(O||!fe)}),w,i,f);U&&(A=U)}h&&(m==="number"||_!=="auto")&&(M=ec(T,w,"category"))}else h?A=wh(0,R):s&&s[N]&&s[N].hasStack&&m==="number"?A=p==="expand"?[0,1]:FT(s[N].stackGroups,l,c):A=$T(T,a.filter(function(K){var te=o in K.props?K.props[o]:K.type.defaultProps[o],Z="hide"in K.props?K.props.hide:K.type.defaultProps.hide;return te===N&&(O||!Z)}),m,f,!0);if(m==="number")A=E0(d,A,N,i,k),I&&(A=Hx(I,A,S));else if(m==="category"&&I){var G=I,q=A.every(function(K){return G.indexOf(K)>=0});q&&(A=G)}}return X(X({},x),{},Pe({},N,X(X({},g),{},{axisType:i,domain:A,categoricalDomain:M,duplicateDomain:C,originalDomain:(y=g.domain)!==null&&y!==void 0?y:B,isCategorical:h,layout:f})))},{})},cce=function(t,r){var n=r.graphicalItems,a=r.Axis,i=r.axisType,o=r.axisIdKey,s=r.stackGroups,l=r.dataStartIndex,c=r.dataEndIndex,f=t.layout,d=t.children,p=Zm(t.data,{graphicalItems:n,dataStartIndex:l,dataEndIndex:c}),h=p.length,x=RT(f,i),v=-1;return n.reduce(function(y,g){var m=g.type.defaultProps!==void 0?X(X({},g.type.defaultProps),g.props):g.props,w=m[o],S=aR("number");if(!y[w]){v++;var b;return x?b=wh(0,h):s&&s[w]&&s[w].hasStack?(b=FT(s[w].stackGroups,l,c),b=E0(d,b,w,i)):(b=Hx(S,$T(p,n.filter(function(_){var k,O,N=o in _.props?_.props[o]:(k=_.type.defaultProps)===null||k===void 0?void 0:k[o],T="hide"in _.props?_.props.hide:(O=_.type.defaultProps)===null||O===void 0?void 0:O.hide;return N===w&&!T}),"number",f),a.defaultProps.allowDataOverflow),b=E0(d,b,w,i)),X(X({},y),{},Pe({},w,X(X({axisType:i},a.defaultProps),{},{hide:!0,orientation:vn(ice,"".concat(i,".").concat(v%2),null),domain:b,originalDomain:S,isCategorical:x,layout:f})))}return y},{})},dce=function(t,r){var n=r.axisType,a=n===void 0?"xAxis":n,i=r.AxisComp,o=r.graphicalItems,s=r.stackGroups,l=r.dataStartIndex,c=r.dataEndIndex,f=t.children,d="".concat(a,"Id"),p=yn(f,i),h={};return p&&p.length?h=uce(t,{axes:p,graphicalItems:o,axisType:a,axisIdKey:d,stackGroups:s,dataStartIndex:l,dataEndIndex:c}):o&&o.length&&(h=cce(t,{Axis:i,graphicalItems:o,axisType:a,axisIdKey:d,stackGroups:s,dataStartIndex:l,dataEndIndex:c})),h},fce=function(t){var r=_i(t),n=Ha(r,!1,!0);return{tooltipTicks:n,orderedTooltipTicks:bw(n,function(a){return a.coordinate}),tooltipAxis:r,tooltipAxisBandSize:lh(r,n)}},_O=function(t){var r=t.children,n=t.defaultShowTooltip,a=cn(r,xl),i=0,o=0;return t.data&&t.data.length!==0&&(o=t.data.length-1),a&&a.props&&(a.props.startIndex>=0&&(i=a.props.startIndex),a.props.endIndex>=0&&(o=a.props.endIndex)),{chartX:0,chartY:0,dataStartIndex:i,dataEndIndex:o,activeTooltipIndex:-1,isTooltipActive:!!n}},pce=function(t){return!t||!t.length?!1:t.some(function(r){var n=qa(r&&r.type);return n&&n.indexOf("Bar")>=0})},SO=function(t){return t==="horizontal"?{numericAxisName:"yAxis",cateAxisName:"xAxis"}:t==="vertical"?{numericAxisName:"xAxis",cateAxisName:"yAxis"}:t==="centric"?{numericAxisName:"radiusAxis",cateAxisName:"angleAxis"}:{numericAxisName:"angleAxis",cateAxisName:"radiusAxis"}},hce=function(t,r){var n=t.props,a=t.graphicalItems,i=t.xAxisMap,o=i===void 0?{}:i,s=t.yAxisMap,l=s===void 0?{}:s,c=n.width,f=n.height,d=n.children,p=n.margin||{},h=cn(d,xl),x=cn(d,Jn),v=Object.keys(l).reduce(function(b,_){var k=l[_],O=k.orientation;return!k.mirror&&!k.hide?X(X({},b),{},Pe({},O,b[O]+k.width)):b},{left:p.left||0,right:p.right||0}),y=Object.keys(o).reduce(function(b,_){var k=o[_],O=k.orientation;return!k.mirror&&!k.hide?X(X({},b),{},Pe({},O,vn(b,"".concat(O))+k.height)):b},{top:p.top||0,bottom:p.bottom||0}),g=X(X({},y),v),m=g.bottom;h&&(g.bottom+=h.props.height||xl.defaultProps.height),x&&r&&(g=gee(g,a,n,r));var w=c-g.left-g.right,S=f-g.top-g.bottom;return X(X({brushBottom:m},g),{},{width:Math.max(w,0),height:Math.max(S,0)})},mce=function(t,r){if(r==="xAxis")return t[r].width;if(r==="yAxis")return t[r].height},Qm=function(t){var r=t.chartName,n=t.GraphicalChild,a=t.defaultTooltipEventType,i=a===void 0?"axis":a,o=t.validateTooltipEventTypes,s=o===void 0?["axis"]:o,l=t.axisComponents,c=t.legendContent,f=t.formatAxisMap,d=t.defaultProps,p=function(g,m){var w=m.graphicalItems,S=m.stackGroups,b=m.offset,_=m.updateId,k=m.dataStartIndex,O=m.dataEndIndex,N=g.barSize,T=g.layout,R=g.barGap,A=g.barCategoryGap,C=g.maxBarSize,M=SO(T),B=M.numericAxisName,V=M.cateAxisName,I=pce(w),D=[];return w.forEach(function(U,G){var q=Zm(g.data,{graphicalItems:[U],dataStartIndex:k,dataEndIndex:O}),K=U.type.defaultProps!==void 0?X(X({},U.type.defaultProps),U.props):U.props,te=K.dataKey,Z=K.maxBarSize,de=K["".concat(B,"Id")],fe=K["".concat(V,"Id")],Ie={},De=l.reduce(function(Ke,Ze){var P=m["".concat(Ze.axisType,"Map")],L=K["".concat(Ze.axisType,"Id")];P&&P[L]||Ze.axisType==="zAxis"||Ho();var H=P[L];return X(X({},Ke),{},Pe(Pe({},Ze.axisType,H),"".concat(Ze.axisType,"Ticks"),Ha(H)))},Ie),oe=De[V],J=De["".concat(V,"Ticks")],we=S&&S[de]&&S[de].hasStack&&Eee(U,S[de].stackGroups),Y=qa(U.type).indexOf("Bar")>=0,Ne=lh(oe,J),xe=[],$e=I&&vee({barSize:N,stackGroups:S,totalSize:mce(De,V)});if(Y){var Ee,je,Be=Fe(Z)?C:Z,Me=(Ee=(je=lh(oe,J,!0))!==null&&je!==void 0?je:Be)!==null&&Ee!==void 0?Ee:0;xe=yee({barGap:R,barCategoryGap:A,bandSize:Me!==Ne?Me:Ne,sizeList:$e[fe],maxBarSize:Be}),Me!==Ne&&(xe=xe.map(function(Ke){return X(X({},Ke),{},{position:X(X({},Ke.position),{},{offset:Ke.position.offset-Me/2})})}))}var Re=U&&U.type&&U.type.getComposedData;Re&&D.push({props:X(X({},Re(X(X({},De),{},{displayedData:q,props:g,dataKey:te,item:U,bandSize:Ne,barPosition:xe,offset:b,stackedData:we,layout:T,dataStartIndex:k,dataEndIndex:O}))),{},Pe(Pe(Pe({key:U.key||"item-".concat(G)},B,De[B]),V,De[V]),"animationId",_)),childIndex:yB(U,g.children),item:U})}),D},h=function(g,m){var w=g.props,S=g.dataStartIndex,b=g.dataEndIndex,_=g.updateId;if(!LS({props:w}))return null;var k=w.children,O=w.layout,N=w.stackOffset,T=w.data,R=w.reverseStackOrder,A=SO(O),C=A.numericAxisName,M=A.cateAxisName,B=yn(k,n),V=Aee(T,B,"".concat(C,"Id"),"".concat(M,"Id"),N,R),I=l.reduce(function(K,te){var Z="".concat(te.axisType,"Map");return X(X({},K),{},Pe({},Z,dce(w,X(X({},te),{},{graphicalItems:B,stackGroups:te.axisType===C&&V,dataStartIndex:S,dataEndIndex:b}))))},{}),D=hce(X(X({},I),{},{props:w,graphicalItems:B}),m==null?void 0:m.legendBBox);Object.keys(I).forEach(function(K){I[K]=f(w,I[K],D,K.replace("Map",""),r)});var U=I["".concat(M,"Map")],G=fce(U),q=p(w,X(X({},I),{},{dataStartIndex:S,dataEndIndex:b,updateId:_,graphicalItems:B,stackGroups:V,offset:D}));return X(X({formattedGraphicalItems:q,graphicalItems:B,offset:D,stackGroups:V},G),I)},x=function(y){function g(m){var w,S,b;return Kue(this,g),b=Zue(this,g,[m]),Pe(b,"eventEmitterSymbol",Symbol("rechartsEventEmitter")),Pe(b,"accessibilityManager",new Rue),Pe(b,"handleLegendBBoxUpdate",function(_){if(_){var k=b.state,O=k.dataStartIndex,N=k.dataEndIndex,T=k.updateId;b.setState(X({legendBBox:_},h({props:b.props,dataStartIndex:O,dataEndIndex:N,updateId:T},X(X({},b.state),{},{legendBBox:_}))))}}),Pe(b,"handleReceiveSyncEvent",function(_,k,O){if(b.props.syncId===_){if(O===b.eventEmitterSymbol&&typeof b.props.syncMethod!="function")return;b.applySyncEvent(k)}}),Pe(b,"handleBrushChange",function(_){var k=_.startIndex,O=_.endIndex;if(k!==b.state.dataStartIndex||O!==b.state.dataEndIndex){var N=b.state.updateId;b.setState(function(){return X({dataStartIndex:k,dataEndIndex:O},h({props:b.props,dataStartIndex:k,dataEndIndex:O,updateId:N},b.state))}),b.triggerSyncEvent({dataStartIndex:k,dataEndIndex:O})}}),Pe(b,"handleMouseEnter",function(_){var k=b.getMouseInfo(_);if(k){var O=X(X({},k),{},{isTooltipActive:!0});b.setState(O),b.triggerSyncEvent(O);var N=b.props.onMouseEnter;Te(N)&&N(O,_)}}),Pe(b,"triggeredAfterMouseMove",function(_){var k=b.getMouseInfo(_),O=k?X(X({},k),{},{isTooltipActive:!0}):{isTooltipActive:!1};b.setState(O),b.triggerSyncEvent(O);var N=b.props.onMouseMove;Te(N)&&N(O,_)}),Pe(b,"handleItemMouseEnter",function(_){b.setState(function(){return{isTooltipActive:!0,activeItem:_,activePayload:_.tooltipPayload,activeCoordinate:_.tooltipPosition||{x:_.cx,y:_.cy}}})}),Pe(b,"handleItemMouseLeave",function(){b.setState(function(){return{isTooltipActive:!1}})}),Pe(b,"handleMouseMove",function(_){_.persist(),b.throttleTriggeredAfterMouseMove(_)}),Pe(b,"handleMouseLeave",function(_){b.throttleTriggeredAfterMouseMove.cancel();var k={isTooltipActive:!1};b.setState(k),b.triggerSyncEvent(k);var O=b.props.onMouseLeave;Te(O)&&O(k,_)}),Pe(b,"handleOuterEvent",function(_){var k=vB(_),O=vn(b.props,"".concat(k));if(k&&Te(O)){var N,T;/.*touch.*/i.test(k)?T=b.getMouseInfo(_.changedTouches[0]):T=b.getMouseInfo(_),O((N=T)!==null&&N!==void 0?N:{},_)}}),Pe(b,"handleClick",function(_){var k=b.getMouseInfo(_);if(k){var O=X(X({},k),{},{isTooltipActive:!0});b.setState(O),b.triggerSyncEvent(O);var N=b.props.onClick;Te(N)&&N(O,_)}}),Pe(b,"handleMouseDown",function(_){var k=b.props.onMouseDown;if(Te(k)){var O=b.getMouseInfo(_);k(O,_)}}),Pe(b,"handleMouseUp",function(_){var k=b.props.onMouseUp;if(Te(k)){var O=b.getMouseInfo(_);k(O,_)}}),Pe(b,"handleTouchMove",function(_){_.changedTouches!=null&&_.changedTouches.length>0&&b.throttleTriggeredAfterMouseMove(_.changedTouches[0])}),Pe(b,"handleTouchStart",function(_){_.changedTouches!=null&&_.changedTouches.length>0&&b.handleMouseDown(_.changedTouches[0])}),Pe(b,"handleTouchEnd",function(_){_.changedTouches!=null&&_.changedTouches.length>0&&b.handleMouseUp(_.changedTouches[0])}),Pe(b,"handleDoubleClick",function(_){var k=b.props.onDoubleClick;if(Te(k)){var O=b.getMouseInfo(_);k(O,_)}}),Pe(b,"handleContextMenu",function(_){var k=b.props.onContextMenu;if(Te(k)){var O=b.getMouseInfo(_);k(O,_)}}),Pe(b,"triggerSyncEvent",function(_){b.props.syncId!==void 0&&Oy.emit(Ay,b.props.syncId,_,b.eventEmitterSymbol)}),Pe(b,"applySyncEvent",function(_){var k=b.props,O=k.layout,N=k.syncMethod,T=b.state.updateId,R=_.dataStartIndex,A=_.dataEndIndex;if(_.dataStartIndex!==void 0||_.dataEndIndex!==void 0)b.setState(X({dataStartIndex:R,dataEndIndex:A},h({props:b.props,dataStartIndex:R,dataEndIndex:A,updateId:T},b.state)));else if(_.activeTooltipIndex!==void 0){var C=_.chartX,M=_.chartY,B=_.activeTooltipIndex,V=b.state,I=V.offset,D=V.tooltipTicks;if(!I)return;if(typeof N=="function")B=N(D,_);else if(N==="value"){B=-1;for(var U=0;U=0){var we,Y;if(C.dataKey&&!C.allowDuplicatedCategory){var Ne=typeof C.dataKey=="function"?J:"payload.".concat(C.dataKey.toString());we=Tp(U,Ne,B),Y=G&&q&&Tp(q,Ne,B)}else we=U==null?void 0:U[M],Y=G&&q&&q[M];if(fe||de){var xe=_.props.activeIndex!==void 0?_.props.activeIndex:M;return[j.cloneElement(_,X(X(X({},N.props),De),{},{activeIndex:xe})),null,null]}if(!Fe(we))return[oe].concat(Pl(b.renderActivePoints({item:N,activePoint:we,basePoint:Y,childIndex:M,isRange:G})))}else{var $e,Ee=($e=b.getItemByXY(b.state.activeCoordinate))!==null&&$e!==void 0?$e:{graphicalItem:oe},je=Ee.graphicalItem,Be=je.item,Me=Be===void 0?_:Be,Re=je.childIndex,Ke=X(X(X({},N.props),De),{},{activeIndex:Re});return[j.cloneElement(Me,Ke),null,null]}return G?[oe,null,null]:[oe,null]}),Pe(b,"renderCustomized",function(_,k,O){return j.cloneElement(_,X(X({key:"recharts-customized-".concat(O)},b.props),b.state))}),Pe(b,"renderMap",{CartesianGrid:{handler:bf,once:!0},ReferenceArea:{handler:b.renderReferenceElement},ReferenceLine:{handler:bf},ReferenceDot:{handler:b.renderReferenceElement},XAxis:{handler:bf},YAxis:{handler:bf},Brush:{handler:b.renderBrush,once:!0},Bar:{handler:b.renderGraphicChild},Line:{handler:b.renderGraphicChild},Area:{handler:b.renderGraphicChild},Radar:{handler:b.renderGraphicChild},RadialBar:{handler:b.renderGraphicChild},Scatter:{handler:b.renderGraphicChild},Pie:{handler:b.renderGraphicChild},Funnel:{handler:b.renderGraphicChild},Tooltip:{handler:b.renderCursor,once:!0},PolarGrid:{handler:b.renderPolarGrid,once:!0},PolarAngleAxis:{handler:b.renderPolarAxis},PolarRadiusAxis:{handler:b.renderPolarAxis},Customized:{handler:b.renderCustomized}}),b.clipPathId="".concat((w=m.id)!==null&&w!==void 0?w:Yo("recharts"),"-clip"),b.throttleTriggeredAfterMouseMove=CC(b.triggeredAfterMouseMove,(S=m.throttleDelay)!==null&&S!==void 0?S:1e3/60),b.state={},b}return ece(g,y),Yue(g,[{key:"componentDidMount",value:function(){var w,S;this.addListener(),this.accessibilityManager.setDetails({container:this.container,offset:{left:(w=this.props.margin.left)!==null&&w!==void 0?w:0,top:(S=this.props.margin.top)!==null&&S!==void 0?S:0},coordinateList:this.state.tooltipTicks,mouseHandlerCallback:this.triggeredAfterMouseMove,layout:this.props.layout}),this.displayDefaultTooltip()}},{key:"displayDefaultTooltip",value:function(){var w=this.props,S=w.children,b=w.data,_=w.height,k=w.layout,O=cn(S,wr);if(O){var N=O.props.defaultIndex;if(!(typeof N!="number"||N<0||N>this.state.tooltipTicks.length-1)){var T=this.state.tooltipTicks[N]&&this.state.tooltipTicks[N].value,R=T0(this.state,b,N,T),A=this.state.tooltipTicks[N].coordinate,C=(this.state.offset.top+_)/2,M=k==="horizontal",B=M?{x:A,y:C}:{y:A,x:C},V=this.state.formattedGraphicalItems.find(function(D){var U=D.item;return U.type.name==="Scatter"});V&&(B=X(X({},B),V.props.points[N].tooltipPosition),R=V.props.points[N].tooltipPayload);var I={activeTooltipIndex:N,isTooltipActive:!0,activeLabel:T,activePayload:R,activeCoordinate:B};this.setState(I),this.renderCursor(O),this.accessibilityManager.setIndex(N)}}}},{key:"getSnapshotBeforeUpdate",value:function(w,S){if(!this.props.accessibilityLayer)return null;if(this.state.tooltipTicks!==S.tooltipTicks&&this.accessibilityManager.setDetails({coordinateList:this.state.tooltipTicks}),this.props.layout!==w.layout&&this.accessibilityManager.setDetails({layout:this.props.layout}),this.props.margin!==w.margin){var b,_;this.accessibilityManager.setDetails({offset:{left:(b=this.props.margin.left)!==null&&b!==void 0?b:0,top:(_=this.props.margin.top)!==null&&_!==void 0?_:0}})}return null}},{key:"componentDidUpdate",value:function(w){ox([cn(w.children,wr)],[cn(this.props.children,wr)])||this.displayDefaultTooltip()}},{key:"componentWillUnmount",value:function(){this.removeListener(),this.throttleTriggeredAfterMouseMove.cancel()}},{key:"getTooltipEventType",value:function(){var w=cn(this.props.children,wr);if(w&&typeof w.props.shared=="boolean"){var S=w.props.shared?"axis":"item";return s.indexOf(S)>=0?S:i}return i}},{key:"getMouseInfo",value:function(w){if(!this.container)return null;var S=this.container,b=S.getBoundingClientRect(),_=VX(b),k={chartX:Math.round(w.pageX-_.left),chartY:Math.round(w.pageY-_.top)},O=b.width/S.offsetWidth||1,N=this.inRange(k.chartX,k.chartY,O);if(!N)return null;var T=this.state,R=T.xAxisMap,A=T.yAxisMap,C=this.getTooltipEventType(),M=wO(this.state,this.props.data,this.props.layout,N);if(C!=="axis"&&R&&A){var B=_i(R).scale,V=_i(A).scale,I=B&&B.invert?B.invert(k.chartX):null,D=V&&V.invert?V.invert(k.chartY):null;return X(X({},k),{},{xValue:I,yValue:D},M)}return M?X(X({},k),M):null}},{key:"inRange",value:function(w,S){var b=arguments.length>2&&arguments[2]!==void 0?arguments[2]:1,_=this.props.layout,k=w/b,O=S/b;if(_==="horizontal"||_==="vertical"){var N=this.state.offset,T=k>=N.left&&k<=N.left+N.width&&O>=N.top&&O<=N.top+N.height;return T?{x:k,y:O}:null}var R=this.state,A=R.angleAxisMap,C=R.radiusAxisMap;if(A&&C){var M=_i(A);return qk({x:k,y:O},M)}return null}},{key:"parseEventsOfWrapper",value:function(){var w=this.props.children,S=this.getTooltipEventType(),b=cn(w,wr),_={};b&&S==="axis"&&(b.props.trigger==="click"?_={onClick:this.handleClick}:_={onMouseEnter:this.handleMouseEnter,onDoubleClick:this.handleDoubleClick,onMouseMove:this.handleMouseMove,onMouseLeave:this.handleMouseLeave,onTouchMove:this.handleTouchMove,onTouchStart:this.handleTouchStart,onTouchEnd:this.handleTouchEnd,onContextMenu:this.handleContextMenu});var k=$p(this.props,this.handleOuterEvent);return X(X({},k),_)}},{key:"addListener",value:function(){Oy.on(Ay,this.handleReceiveSyncEvent)}},{key:"removeListener",value:function(){Oy.removeListener(Ay,this.handleReceiveSyncEvent)}},{key:"filterFormatItem",value:function(w,S,b){for(var _=this.state.formattedGraphicalItems,k=0,O=_.length;kEt(`/routing/list/active/${e}`)),a=r==null?void 0:r.find(A=>{var C;return((C=A.algorithm_data||A.algorithm)==null?void 0:C.type)==="volume_split"}),[i,o]=j.useState([{id:gu(),name:"",split:50},{id:gu(),name:"",split:50}]),[s,l]=j.useState(""),[c,f]=j.useState(!1),[d,p]=j.useState(null),[h,x]=j.useState(null),[v,y]=j.useState(!1),[g,m]=j.useState(new Set),w=i.reduce((A,C)=>A+C.split,0);function S(A,C,M){o(B=>B.map(V=>V.id===A?{...V,[C]:M}:V))}function b(){o(A=>[...A,{id:gu(),name:"",split:0}])}function _(A){o(C=>C.filter(M=>M.id!==A))}async function k(){if(!e)return p("Set a merchant ID first");if(!s.trim())return p("Enter a rule name");if(w!==100)return p(`Splits must sum to 100 (currently ${w})`);if(i.some(A=>!A.name.trim()))return p("All gateways must have names");f(!0),p(null),x(null);try{await Et("/routing/create",{rule_id:null,name:s,description:"",created_by:e,algorithm_for:"payment",metadata:null,algorithm:{type:"volume_split",data:i.map(A=>({split:A.split,output:{gateway_name:A.name.trim(),gateway_id:null}}))}}),await Promise.all([n(),t(["routing-list",e])]),x(`Rule "${s}" created successfully. Find it in the list below to activate.`),l(""),o([{id:gu(),name:"",split:50},{id:gu(),name:"",split:50}])}catch(A){p(A instanceof Error?A.message:"Failed to create rule")}finally{f(!1)}}async function O(A){if(e)try{await Et("/routing/activate",{created_by:e,routing_algorithm_id:A}),await Promise.all([n(),t(["routing-list",e])]),x("Rule activated.")}catch(C){p(C instanceof Error?C.message:"Failed to activate")}}function N(A){m(C=>{const M=new Set(C);return M.has(A)?M.delete(A):M.add(A),M})}const T=a?a.algorithm_data||a.algorithm:null,R=T&&"data"in T?T.data.map(A=>{var C;return{name:((C=A.output)==null?void 0:C.gateway_name)??"?",value:A.split}}):[];return u.jsxs("div",{className:"space-y-6 max-w-4xl",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-bold text-slate-900",children:"Volume Split Routing"}),u.jsx("p",{className:"text-slate-500 mt-1 text-sm",children:"Distribute payment traffic across gateways by percentage."})]}),a&&u.jsxs(Ae,{children:[u.jsxs(nt,{className:"flex flex-row items-center justify-between",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800",children:"Active Volume Split"}),u.jsx("p",{className:"text-xs text-slate-500 mt-0.5",children:a.name})]}),u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx(Ue,{variant:"green",children:"Active"}),u.jsxs(Ce,{type:"button",variant:"ghost",size:"sm",onClick:()=>y(!v),children:[u.jsx(em,{size:14,className:"mr-1"}),v?"Hide":"View"]})]})]}),v&&u.jsxs(Ge,{children:[u.jsx(Oi,{width:"100%",height:220,children:u.jsxs(a1,{children:[u.jsx(sa,{data:R,dataKey:"value",nameKey:"name",cx:"50%",cy:"50%",outerRadius:80,label:({name:A,value:C})=>`${A}: ${C}%`,labelLine:{stroke:"#45454f"},children:R.map((A,C)=>u.jsx(Li,{fill:jO[C%jO.length]},C))}),u.jsx(wr,{formatter:A=>`${A}%`,contentStyle:{backgroundColor:"#0d0d12",border:"1px solid #1c1c24",borderRadius:"8px",color:"#e8e8f4"}}),u.jsx(Jn,{wrapperStyle:{color:"#8e8ea0"}})]})}),u.jsxs("div",{className:"mt-4 text-xs text-slate-600",children:[u.jsxs("p",{children:[u.jsx("strong",{children:"Rule ID:"})," ",a.id]}),u.jsxs("p",{children:[u.jsx("strong",{children:"Created:"})," ",a.created_at?new Date(a.created_at).toLocaleString():"Unknown"]})]})]})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"font-medium text-slate-800",children:"Create Volume Split Rule"})}),u.jsxs(Ge,{className:"space-y-4",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-sm font-medium text-slate-700 mb-1",children:"Rule Name"}),u.jsx("input",{value:s,onChange:A=>l(A.target.value),placeholder:"e.g. ab-test-split",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm w-64 focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{className:"space-y-2",children:[u.jsxs("div",{className:"grid grid-cols-[1fr_100px_32px] gap-2 text-xs font-medium text-slate-500 px-1",children:[u.jsx("span",{children:"Gateway Name"}),u.jsx("span",{children:"Split %"}),u.jsx("span",{})]}),i.map(A=>u.jsxs("div",{className:"grid grid-cols-[1fr_100px_32px] gap-2 items-center",children:[u.jsx("input",{value:A.name,onChange:C=>S(A.id,"name",C.target.value),placeholder:"e.g. stripe",className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("input",{type:"number",min:0,max:100,value:A.split,onChange:C=>S(A.id,"split",Number(C.target.value)),className:"border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("button",{onClick:()=>_(A.id),className:"text-slate-400 hover:text-red-500",children:u.jsx(Ja,{size:15})})]},A.id)),u.jsxs("div",{className:"flex items-center gap-3",children:[u.jsxs("button",{onClick:b,className:"flex items-center gap-1 text-sm text-brand-500 hover:text-brand-600",children:[u.jsx(Ui,{size:14})," Add Gateway"]}),u.jsxs("span",{className:`text-xs font-medium ${w===100?"text-emerald-400":"text-red-400"}`,children:["Total: ",w,"%",w!==100&&" (must be 100)"]})]})]}),u.jsx(Xr,{error:d}),h&&u.jsx("p",{className:"text-sm text-emerald-400",children:h}),u.jsx(Ce,{onClick:k,disabled:c||!e,children:c?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Creating…"]}):"Create Rule"})]})]}),u.jsx(xce,{merchantId:e,onActivate:O,expandedRuleIds:g,onToggleExpand:N})]})}function xce({merchantId:e,onActivate:t,expandedRuleIds:r,onToggleExpand:n}){const{data:a,isLoading:i}=Bt(e?["routing-list",e]:null,()=>Et(`/routing/list/${e}`)),o=(a==null?void 0:a.filter(s=>{var l;return((l=s.algorithm_data||s.algorithm)==null?void 0:l.type)==="volume_split"}))??[];return e?i?u.jsx("div",{className:"flex justify-center py-4",children:u.jsx(pr,{})}):o.length?u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"font-medium text-slate-800",children:"Saved Volume Split Rules"})}),u.jsx(Ge,{className:"p-0",children:u.jsxs("table",{className:"w-full text-sm",children:[u.jsx("thead",{className:"bg-slate-50 dark:bg-[#0a0a0f] text-xs text-slate-500 uppercase tracking-wider",children:u.jsxs("tr",{children:[u.jsx("th",{className:"text-left px-4 py-2",children:"Name"}),u.jsx("th",{className:"text-left px-4 py-2",children:"Split"}),u.jsx("th",{className:"px-4 py-2"})]})}),u.jsx("tbody",{className:"divide-y divide-[#1c1c24]",children:o.map(s=>{const l=s.algorithm_data||s.algorithm,c=(l==null?void 0:l.data)||[],f=r.has(s.id);return u.jsxs(u.Fragment,{children:[u.jsxs("tr",{className:"hover:bg-slate-100 dark:bg-[#0f0f16] transition-colors",children:[u.jsx("td",{className:"px-4 py-2 font-medium text-slate-800",children:s.name}),u.jsx("td",{className:"px-4 py-2 text-slate-600 text-xs",children:c.map(d=>{var p;return`${(p=d.output)==null?void 0:p.gateway_name}:${d.split}%`}).join(" | ")}),u.jsx("td",{className:"px-4 py-2 text-right",children:u.jsxs("div",{className:"flex items-center justify-end gap-2",children:[u.jsxs(Ce,{size:"sm",variant:"ghost",onClick:()=>n(s.id),children:[u.jsx(em,{size:14,className:"mr-1"}),f?"Hide":"View"]}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>t(s.id),children:"Activate"})]})})]},s.id),f&&u.jsx("tr",{children:u.jsx("td",{colSpan:3,className:"px-4 py-3 bg-slate-50 dark:bg-[#151518]",children:u.jsxs("div",{className:"text-xs text-slate-600 space-y-2",children:[u.jsxs("p",{children:[u.jsx("strong",{children:"ID:"})," ",s.id]}),u.jsxs("p",{children:[u.jsx("strong",{children:"Description:"})," ",s.description||"N/A"]}),s.created_at&&u.jsxs("p",{children:[u.jsx("strong",{children:"Created:"})," ",new Date(s.created_at).toLocaleString()]}),u.jsxs("div",{children:[u.jsx("strong",{children:"Configuration:"}),u.jsx("pre",{className:"mt-1 p-2 bg-slate-100 dark:bg-[#0f0f11] border border-transparent dark:border-[#222226] rounded text-xs overflow-auto max-h-48",children:JSON.stringify(l,null,2)})]})]})})})]})})})]})})]}):null:null}function bce(){var m;const{merchantId:e}=ka(),{data:t,mutate:r,isLoading:n}=Bt(e?["rule-debit",e]:null,()=>Et("/rule/get",{merchant_id:e,config:{type:"debitRouting"}})),[a,i]=j.useState(""),[o,s]=j.useState(""),[l,c]=j.useState(!1),[f,d]=j.useState(null),[p,h]=j.useState(null),x=(m=t==null?void 0:t.config)==null?void 0:m.data,v=a||(x==null?void 0:x.merchant_category_code)||"",y=o||(x==null?void 0:x.acquirer_country)||"";async function g(){if(!e)return d("Set a merchant ID first");const w={merchant_id:e,config:{type:"debitRouting",data:{merchant_category_code:v.trim(),acquirer_country:y.trim()}}};c(!0),d(null);try{await Et(t?"/rule/update":"/rule/create",w),h("Debit routing config saved."),r()}catch(S){d(S instanceof Error?S.message:"Failed to save")}finally{c(!1)}}return u.jsxs("div",{className:"space-y-6 max-w-2xl",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-bold text-slate-900",children:"Network / Debit Routing"}),u.jsx("p",{className:"text-slate-500 mt-1 text-sm",children:"Configure network-based routing to optimise processing fees for debit card transactions. The engine selects the cheapest eligible network (Visa, Mastercard, ACCEL, NYCE, PULSE, STAR)."})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx(sE,{size:16,className:"text-brand-500"}),u.jsx("h2",{className:"font-medium text-slate-800",children:"Debit Routing Configuration"})]})}),u.jsx(Ge,{className:"space-y-4",children:n?u.jsx("div",{className:"flex justify-center py-6",children:u.jsx(pr,{})}):u.jsxs(u.Fragment,{children:[!e&&u.jsx("p",{className:"text-sm text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-2",children:"Set a merchant ID in the top bar to load configuration."}),u.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-sm font-medium text-slate-700 mb-1",children:"Merchant Category Code (MCC)"}),u.jsx("input",{value:v,onChange:w=>i(w.target.value),placeholder:"e.g. 5411",className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("p",{className:"text-xs text-slate-400 mt-1",children:"4-digit ISO MCC for your business type"})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-sm font-medium text-slate-700 mb-1",children:"Acquirer Country"}),u.jsx("input",{value:y,onChange:w=>s(w.target.value),placeholder:"e.g. US",className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("p",{className:"text-xs text-slate-400 mt-1",children:"ISO 3166-1 alpha-2 country code"})]})]}),u.jsx(Xr,{error:f}),p&&u.jsx("p",{className:"text-sm text-emerald-400",children:p}),u.jsx(Ce,{onClick:g,disabled:l||!e,children:l?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Saving…"]}):t?"Update Config":"Save Config"})]})})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h2",{className:"font-medium text-slate-800",children:"How Network Routing Works"})}),u.jsxs(Ge,{className:"text-sm text-slate-600 space-y-2",children:[u.jsx("p",{children:"For co-badged debit cards (e.g. Visa/NYCE, Mastercard/PULSE), the engine evaluates all eligible networks and routes to the one with the lowest processing fee."}),u.jsxs("p",{children:["Supported networks: ",["VISA","MASTERCARD","ACCEL","NYCE","PULSE","STAR"].map(w=>u.jsx("span",{className:"font-mono text-xs bg-slate-100 dark:bg-[#111118] border border-slate-200 dark:border-[#1c1c24] px-1.5 py-0.5 rounded-md mr-1 text-slate-700",children:w},w))]}),u.jsxs("p",{children:["Use the ",u.jsx("strong",{className:"text-slate-800",children:"Decision Explorer"})," to test network routing decisions with ",u.jsx("code",{className:"text-xs bg-slate-100 dark:bg-[#111118] border border-slate-200 dark:border-[#1c1c24] px-1.5 py-0.5 rounded-md text-brand-500",children:"NtwBasedRouting"})," algorithm."]})]})]})]})}const wce=["SR_BASED_ROUTING","PL_BASED_ROUTING","NTW_BASED_ROUTING"],_ce={SR_BASED_ROUTING:"Success Rate Based",PL_BASED_ROUTING:"Priority List Based",NTW_BASED_ROUTING:"Network Based"};function Sce(e){for(const[t,r]of Object.entries(A4))if(e.includes(t)||t.includes(e))return r;return"bg-white/5 text-slate-600 ring-1 ring-inset ring-white/8"}const Hr=["#0069ED","#10b981","#f59e0b","#ef4444","#8b5cf6","#ec4899","#06b6d4","#84cc16"];function qf(e=[]){return e.map(t=>t.trim()).filter(Boolean).map(t=>t.toUpperCase())}function Ey(e=[]){return Array.from(new Set(qf(e)))}function jce(e){var t,r,n,a,i;return((r=(t=e.evaluated_output)==null?void 0:t[0])==null?void 0:r.gateway_name)||((n=e.output.connector)==null?void 0:n.gateway_name)||((i=(a=e.output.connectors)==null?void 0:a[0])==null?void 0:i.gateway_name)||null}function Py(e){return e==="enum"?"enum_variant":e==="integer"?"number":e==="udf"||e==="global_ref"?"metadata_variant":"str_value"}function iR(e){const t=new URLSearchParams;return Object.entries(e).forEach(([r,n])=>{n!==void 0&&n!==""&&t.set(r,String(n))}),t.toString()}function Tu(e){return new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(new Date(e))}function An(e){return e?e.replace(/[_-]+/g," ").replace(/\s+/g," ").trim().toLowerCase().replace(/\b\w/g,r=>r.toUpperCase()):""}function $u(e){return e?e==="decision_gateway"||e==="decide_gateway"?"Decide Gateway":e==="update_gateway_score"?"Update Gateway":e==="routing_evaluate"?"Rule Evaluate":An(e):"Unknown route"}function kO(e){return e?e==="decision"?"Decide Gateway":e==="gateway_update"?"Update Gateway":e==="rule_hit"?"Rule Evaluate":e==="rule_evaluation_preview"?"Preview Result":e==="error"?"Errors":An(e):"Unknown event"}function Ru(e){return e.event_stage==="gateway_decided"?"Decide Gateway":e.event_stage==="score_updated"?"Update Gateway":e.event_stage==="rule_applied"?"Rule Evaluate":e.event_stage==="preview_evaluated"||e.event_type==="rule_evaluation_preview"?"Preview Result":e.event_type==="error"?"Errors":An(e.event_stage||e.event_type)}function R0(e){return e.event_type==="decision"||e.event_stage==="gateway_decided"?"Decide Gateway":e.event_type==="rule_hit"||e.event_stage==="rule_applied"?"Rule Evaluate":e.event_type==="gateway_update"||e.event_stage==="score_updated"?"Update Gateway":e.event_type==="rule_evaluation_preview"||e.event_stage==="preview_evaluated"?"Preview":"Errors"}function wf(e){const t=(e.status||"").toUpperCase();return e.event_type==="error"||t==="FAILURE"||t.includes("FAILED")||t.includes("DECLINED")?"red":e.event_type==="rule_hit"?"purple":t==="CHARGED"||t==="AUTHORIZED"||t==="SUCCESS"?"green":e.event_type==="rule_evaluation_preview"?"purple":e.event_type==="gateway_update"?"green":e.event_type==="decision"?"blue":"orange"}function OO(e){const t=(e||"").toUpperCase();return t==="FAILURE"||t.includes("FAILED")||t.includes("DECLINED")?"red":t==="SUCCESS"||t==="CHARGED"||t==="AUTHORIZED"?"green":"gray"}function AO(e){return e==="Decide Gateway"?"blue":e==="Rule Evaluate"||e==="Preview"?"purple":e==="Update Gateway"?"green":e==="Errors"?"red":"gray"}function xu(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function Cy(e){return Object.fromEntries(Object.entries(e).filter(([,t])=>t!=null&&t!==""))}function oR(e){return typeof e=="string"?e:JSON.stringify(e,null,2)}function kce(e,t){return`/analytics/payment-audit?${iR({scope:"current",range:"24h",page:1,page_size:25,merchant_id:e,payment_id:t})}`}function Oce(e,t){return`/analytics/preview-trace?${iR({scope:"current",range:"24h",page:1,page_size:25,merchant_id:e,payment_id:t})}`}function NO(e){if(!e)return null;const t=xu(e.details_json)?e.details_json:{},r=t.response??t.response_payload??t.result??t.output??null,n=t.request??t.request_payload??t.input??t.payload??Cy({payment_id:e.payment_id,request_id:e.request_id,payment_method_type:e.payment_method_type,payment_method:e.payment_method,gateway:e.gateway}),a=r??Cy({event_type:e.event_type,status:e.status,error_code:e.error_code,error_message:e.error_message,score_value:e.score_value,sigma_factor:e.sigma_factor,average_latency:e.average_latency,tp99_latency:e.tp99_latency,transaction_count:e.transaction_count,rule_name:e.rule_name,routing_approach:e.routing_approach}),i=xu(r)?r:null,o=xu(i==null?void 0:i.decided_gateway)?i.decided_gateway:null,s=t.score_context??(o?o.gateway_priority_map:null)??(i?i.gateway_priority_map:null)??null,l=t.selection_reason??null,c=[{label:"Phase",value:R0(e)},{label:"Stage",value:Ru(e)},{label:"Route",value:$u(e.route)},{label:"Timestamp",value:Tu(e.created_at_ms)},...e.merchant_id?[{label:"Merchant",value:e.merchant_id}]:[],...e.payment_id?[{label:"Payment ID",value:e.payment_id}]:[],...e.request_id?[{label:"Request ID",value:e.request_id}]:[],...e.gateway?[{label:"Gateway",value:e.gateway}]:[],...e.status?[{label:"Status",value:An(e.status)}]:[]],f=Cy(Object.fromEntries(Object.entries(t).filter(([d])=>!["request","request_payload","input","payload","response","response_payload","result","output","score_context","selection_reason"].includes(d))));return{summaryRows:c,requestPayload:xu(n)&&!Object.keys(n).length?null:n,responsePayload:xu(a)&&!Object.keys(a).length?null:a,scoreContext:s,selectionReason:l,signalRecord:Object.keys(f).length?f:null,rawEvent:{...e,details_json:e.details_json}}}function ls(e){return e?"!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white":"!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white"}function Iu({title:e,body:t}){return u.jsxs("div",{className:"rounded-[22px] border border-dashed border-slate-200 bg-slate-50/80 px-6 py-12 text-center dark:border-[#2a303a] dark:bg-[#161b24]/80",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e}),u.jsx("p",{className:"mt-2 text-sm text-slate-500 dark:text-[#b2bdd1]",children:t})]})}function EO({rows:e}){return e.length?u.jsx("div",{className:"grid gap-3 md:grid-cols-2",children:e.map(t=>u.jsxs("div",{className:"rounded-[22px] border border-slate-200 bg-white/80 px-4 py-3 shadow-[0_14px_30px_-28px_rgba(15,23,42,0.18)] dark:border-[#2a303a] dark:bg-[#161b24] dark:shadow-none",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8390a7]",children:t.label}),u.jsx("p",{className:"mt-2 break-words text-sm text-slate-900 dark:text-white",children:t.value})]},`${t.label}-${t.value}`))}):null}function $a({title:e,value:t,emptyMessage:r}){return u.jsxs("div",{className:"space-y-3",children:[u.jsx("div",{children:u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e})}),t?u.jsx("pre",{className:"overflow-x-auto rounded-[22px] border border-slate-200 bg-slate-950/95 px-4 py-4 text-xs leading-6 text-slate-200 shadow-[0_16px_30px_-28px_rgba(15,23,42,0.4)] dark:border-[#2a303a] dark:bg-[#0b1017] dark:text-[#d8e1ef] dark:shadow-none",children:oR(t)}):u.jsx(Iu,{title:`No ${e.toLowerCase()} captured`,body:r})]})}function Ace(){var Le,ze,gt,ro,Rd,i1,o1,s1;const{merchantId:e}=ka(),{routingKeysConfig:t,isLoading:r,error:n}=fP(),a=Object.keys(t).length>0,i=!r&&(!a||!!n),[o,s]=j.useState("single"),[l,c]=j.useState({amount:"1000",currency:"",payment_method_type:"",payment_method:"",card_brand:"",auth_type:"",eligible_gateways:"stripe, adyen",ranking_algorithm:"SR_BASED_ROUTING",elimination_enabled:!1}),[f,d]=j.useState({totalPayments:"10",successCount:"7",failureCount:"3"}),[p,h]=j.useState([{key:"payment_method_type",type:"enum_variant",value:"",metadataKey:""},{key:"currency",type:"enum_variant",value:"",metadataKey:""}]),[x,v]=j.useState([{gateway_name:"stripe",gateway_id:"gateway_001"},{gateway_name:"adyen",gateway_id:"gateway_002"}]),[y,g]=j.useState("100"),[m,w]=j.useState(null),[S,b]=j.useState(null),[_,k]=j.useState("CHARGED"),[O,N]=j.useState(null),[T,R]=j.useState([]),[A,C]=j.useState([]),[M,B]=j.useState(0),[V,I]=j.useState([]),[D,U]=j.useState(!1),[G,q]=j.useState(null),[K,te]=j.useState(!1),[Z,de]=j.useState(!1),[fe,Ie]=j.useState(!1),[De,oe]=j.useState(!1),[J,we]=j.useState(null),[Y,Ne]=j.useState(null),[xe,$e]=j.useState("summary"),[Ee,je]=j.useState(null),[Be,Me]=j.useState(null),[Re,Ke]=j.useState("summary"),[Ze,P]=j.useState("Rule Evaluation Preview"),L=j.useMemo(()=>Object.keys(t).sort(),[t]),H=j.useMemo(()=>{var $;return qf((($=t.payment_method)==null?void 0:$.values)||[])},[t]),se=j.useMemo(()=>{var F;const $=l.payment_method_type.toLowerCase();return qf(((F=t[$])==null?void 0:F.values)||[])},[l.payment_method_type,t]),re=j.useMemo(()=>{var $;return Ey((($=t.currency)==null?void 0:$.values)||[])},[t]),Q=j.useMemo(()=>{var $;return Ey((($=t.card_network)==null?void 0:$.values)||[])},[t]),ne=j.useMemo(()=>{var $;return Ey((($=t.authentication_type)==null?void 0:$.values)||[])},[t]),ke=e&&J?kce(e,J):null,W=Bt(ke,pn,{refreshInterval:J?12e3:0,revalidateOnFocus:!0}),le=e&&Ee?Oce(e,Ee):null,_e=Bt(le,pn,{refreshInterval:Ee?12e3:0,revalidateOnFocus:!0});j.useEffect(()=>{i||r||(c($=>{var me;const F={...$};re.length>0&&!re.includes(F.currency)&&(F.currency=re[0]),H.length>0&&!H.includes(F.payment_method_type)&&(F.payment_method_type=H[0]);const ce=qf(((me=t[F.payment_method_type.toLowerCase()])==null?void 0:me.values)||[]);return ce.length>0&&!ce.includes(F.payment_method)&&(F.payment_method=ce[0]),ne.length>0&&!ne.includes(F.auth_type)&&(F.auth_type=ne[0]),Q.length>0&&!Q.includes(F.card_brand)&&(F.card_brand=Q[0]),F}),h($=>$.map(F=>{if(!F.key||!t[F.key])return F;const ce=t[F.key],me=Py(ce.type),ht=ce.values||[],qt=me==="enum_variant"?ht.includes(F.value)?F.value:ht[0]||"":F.value;return{...F,type:me,value:qt}})))},[i,r,t,re,H,ne,Q]),j.useEffect(()=>{if(!J&&!Ee)return;const $=document.body.style.overflow,F=ce=>{ce.key==="Escape"&&(we(null),Ne(null),$e("summary"),je(null),Me(null),Ke("summary"))};return document.body.style.overflow="hidden",window.addEventListener("keydown",F),()=>{document.body.style.overflow=$,window.removeEventListener("keydown",F)}},[J,Ee]);function rt($,F){c(ce=>({...ce,[$]:F}))}function $r(){var ht;if(L.length===0)return;const $=L[0],F=t[$],ce=Py(F==null?void 0:F.type),me=ce==="enum_variant"&&((ht=F==null?void 0:F.values)==null?void 0:ht[0])||"";h([...p,{key:$,type:ce,value:me,metadataKey:""}])}function Jt($){h(p.filter((F,ce)=>ce!==$))}function Rr($,F,ce){h(p.map((me,ht)=>ht===$?{...me,[F]:ce}:me))}function Na($,F){h(p.map((ce,me)=>me===$?{...ce,metadataKey:F}:ce))}function Ea($,F){var qt;const ce=t[F],me=Py(ce==null?void 0:ce.type),ht=me==="enum_variant"&&((qt=ce==null?void 0:ce.values)==null?void 0:qt[0])||"";h(p.map((jt,Wr)=>Wr===$?{...jt,key:F,type:me,value:ht,metadataKey:""}:jt))}function ui(){v([...x,{gateway_name:"",gateway_id:""}])}function Pa($){v(x.filter((F,ce)=>ce!==$))}function Mt($,F,ce){v(x.map((me,ht)=>ht===$?{...me,[F]:ce}:me))}async function er(){if(!e)return q("Set a merchant ID in the top bar");if(i)return q("Routing key config unavailable. Fix /config/routing-keys and retry.");te(!0),q(null),b(null);const $=l.eligible_gateways.split(",").map(ce=>ce.trim()).filter(Boolean),F=`explorer_${Date.now()}`;try{const ce=await Et("/decide-gateway",{merchantId:e,paymentInfo:{paymentId:F,amount:parseFloat(l.amount)||1e3,currency:l.currency,paymentType:"ORDER_PAYMENT",paymentMethodType:l.payment_method_type,paymentMethod:l.payment_method,authType:l.auth_type,cardBrand:l.card_brand},eligibleGatewayList:$,rankingAlgorithm:l.ranking_algorithm,eliminationEnabled:l.elimination_enabled});await Et("/update-gateway-score",{merchantId:e,gateway:ce.decided_gateway,gatewayReferenceId:null,status:_,paymentId:F,enforceDynamicRoutingFailure:null}),w(ce),b(F)}catch(ce){q(ce instanceof Error?ce.message:"Request failed")}finally{te(!1)}}async function cr(){if(!e)return q("Set a merchant ID in the top bar");if(i)return q("Routing key config unavailable. Fix /config/routing-keys and retry.");const $=parseInt(f.totalPayments)||0,F=parseInt(f.successCount)||0,ce=parseInt(f.failureCount)||0;if($<=0)return q("Total Payments must be greater than 0");if(F+ce!==$)return q("Success + Failure count must equal Total Payments");U(!0),q(null),I([]);const me=l.eligible_gateways.split(",").map(jt=>jt.trim()).filter(Boolean),ht=[],qt=[...Array(F).fill("CHARGED"),...Array(ce).fill("FAILURE")];for(let jt=qt.length-1;jt>0;jt--){const Wr=Math.floor(Math.random()*(jt+1));[qt[jt],qt[Wr]]=[qt[Wr],qt[jt]]}try{for(let jt=0;jt<$;jt++){const Wr=`sim_${Date.now()}_${jt}`,Ql=(await Et("/decide-gateway",{merchantId:e,paymentInfo:{paymentId:Wr,amount:parseFloat(l.amount)||1e3,currency:l.currency,paymentType:"ORDER_PAYMENT",paymentMethodType:l.payment_method_type,paymentMethod:l.payment_method,authType:l.auth_type,cardBrand:l.card_brand},eligibleGatewayList:me,rankingAlgorithm:l.ranking_algorithm,eliminationEnabled:l.elimination_enabled})).decided_gateway,ci=qt[jt];await Et("/update-gateway-score",{merchantId:e,gateway:Ql,gatewayReferenceId:null,status:ci,paymentId:Wr,enforceDynamicRoutingFailure:null}),ht.push({paymentId:Wr,decidedGateway:Ql,status:ci,timestamp:new Date().toISOString()}),I([...ht])}}catch(jt){q(jt instanceof Error?jt.message:"Simulation failed")}finally{U(!1)}}async function an(){if(i)return q("Routing key config unavailable. Fix /config/routing-keys and retry.");te(!0),q(null),N(null),R([]),C([]),B(0);const $=`rule_preview_${Date.now()}`;try{const F={};p.forEach(me=>{me.key&&(me.type==="metadata_variant"?F[me.key]={type:me.type,value:{key:me.metadataKey||me.key,value:me.value}}:me.type==="number"?F[me.key]={type:me.type,value:parseFloat(me.value)||0}:F[me.key]={type:me.type,value:me.value})});const ce=await Et("/routing/evaluate",{created_by:e||"test_user",payment_id:$,fallback_output:x.filter(me=>me.gateway_name),parameters:F});if(N(ce),ce.output.type==="volume_split"&&ce.output.splits){const me=parseInt(y)||100,ht=ce.output.splits.map(qt=>({name:qt.connector.gateway_name,count:Math.round(qt.split/100*me),percentage:qt.split}));R(ht)}}catch(F){q(F instanceof Error?F.message:"Request failed")}finally{te(!1)}}async function gr(){if(!e)return q("Set a merchant ID in the top bar");te(!0),q(null),N(null),R([]),C([]),B(0);const $=parseInt(y)||0;if($<=0)return te(!1),q("Total Payments must be greater than 0");try{const ce=`volume_preview_${Date.now()}`,me=[],ht=new Map;let qt=null;for(let jt=0;jt<$;jt+=10){const Wr=Math.min(10,$-jt),no=await Promise.all(Array.from({length:Wr},async(Ql,ci)=>{const es=jt+ci,l1=`${ce}_${es}`,dR=await Et("/routing/evaluate",{created_by:e,payment_id:l1,fallback_output:[{gateway_name:"stripe",gateway_id:"gateway_001"},{gateway_name:"adyen",gateway_id:"gateway_002"}],parameters:{}});return{paymentId:l1,response:dR}}));for(const{paymentId:Ql,response:ci}of no){if(ci.output.type!=="volume_split")throw new Error("Active routing algorithm is not a volume split rule.");const es=jce(ci);if(!es)throw new Error("Volume split evaluation did not return a connector.");ht.set(es,(ht.get(es)||0)+1),me.push({paymentId:Ql,connector:es}),qt=ci}B(me.length)}if(qt){const jt=Array.from(ht.entries()).map(([Wr,no])=>({name:Wr,count:no,percentage:Number((no/$*100).toFixed(1))})).sort((Wr,no)=>no.count-Wr.count);N(qt),C(me),R(jt),je(qt.payment_id)}}catch(F){q(F instanceof Error?F.message:"Request failed")}finally{te(!1)}}const Ut=m!=null&&m.gateway_priority_map?Object.entries(m.gateway_priority_map).sort(([,$],[,F])=>F-$).map(([$,F])=>({name:$,score:Math.round(F*1e3)/10})):[],jr=V.reduce(($,F)=>($[F.decidedGateway]||($[F.decidedGateway]={total:0,success:0,failure:0}),$[F.decidedGateway].total++,F.status==="CHARGED"?$[F.decidedGateway].success++:$[F.decidedGateway].failure++,$),{}),_n=T.map($=>({name:$.name,value:$.count})),Dt=j.useMemo(()=>new Map(T.map(($,F)=>[$.name,F])),[T]),St=j.useMemo(()=>{var F;const $=((F=W.data)==null?void 0:F.results)||[];return $.find(ce=>ce.payment_id===J)||$[0]||null},[(Le=W.data)==null?void 0:Le.results,J]),qe=j.useMemo(()=>{var F;const $=((F=W.data)==null?void 0:F.timeline)||[];return $.find(ce=>ce.id===Y)||$[0]||null},[(ze=W.data)==null?void 0:ze.timeline,Y]);j.useEffect(()=>{var F,ce;if(qe!=null&&qe.id){Ne(qe.id);return}const $=(ce=(F=W.data)==null?void 0:F.timeline)==null?void 0:ce[0];$!=null&&$.id&&Ne($.id)},[(gt=W.data)==null?void 0:gt.timeline,qe==null?void 0:qe.id]);const Ir=j.useMemo(()=>{var F;const $=[];for(const ce of((F=W.data)==null?void 0:F.timeline)||[]){const me=R0(ce),ht=$[$.length-1];!ht||ht.phase!==me?$.push({phase:me,events:[ce]}):ht.events.push(ce)}return $},[(ro=W.data)==null?void 0:ro.timeline]),tr=j.useMemo(()=>NO(qe),[qe]),Lt=j.useMemo(()=>{var F;const $=((F=_e.data)==null?void 0:F.results)||[];return $.find(ce=>ce.payment_id===Ee)||$[0]||null},[(Rd=_e.data)==null?void 0:Rd.results,Ee]),Xe=j.useMemo(()=>{var F;const $=((F=_e.data)==null?void 0:F.timeline)||[];return $.find(ce=>ce.id===Be)||$[0]||null},[(i1=_e.data)==null?void 0:i1.timeline,Be]);j.useEffect(()=>{var F,ce;if(Xe!=null&&Xe.id){Me(Xe.id);return}const $=(ce=(F=_e.data)==null?void 0:F.timeline)==null?void 0:ce[0];$!=null&&$.id&&Me($.id)},[(o1=_e.data)==null?void 0:o1.timeline,Xe==null?void 0:Xe.id]);const Un=j.useMemo(()=>{var F;const $=[];for(const ce of((F=_e.data)==null?void 0:F.timeline)||[]){const me=R0(ce),ht=$[$.length-1];!ht||ht.phase!==me?$.push({phase:me,events:[ce]}):ht.events.push(ce)}return $},[(s1=_e.data)==null?void 0:s1.timeline]),on=j.useMemo(()=>NO(Xe),[Xe]);function Zl($){je(null),Me(null),Ke("summary"),we($),Ne(null),$e("summary")}function z(){we(null),Ne(null),$e("summary")}function ee($,F){we(null),Ne(null),$e("summary"),P(F),je($),Me(null),Ke("summary")}function ve(){je(null),Me(null),Ke("summary")}return u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900 dark:text-white",children:"Decision Explorer"}),u.jsx("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#b2bdd1]",children:"Test payment routing with different algorithms: Success Rate, Priority List, Rule-Based, or Volume Split."})]}),u.jsxs("div",{className:"flex flex-wrap gap-2",children:[u.jsx("button",{onClick:()=>s("single"),className:`rounded-full border px-4 py-2 text-sm font-medium transition ${ls(o==="single")}`,children:"Single Test"}),u.jsx("button",{onClick:()=>s("batch"),className:`rounded-full border px-4 py-2 text-sm font-medium transition ${ls(o==="batch")}`,children:"Batch Simulation"}),u.jsx("button",{onClick:()=>s("rule"),className:`rounded-full border px-4 py-2 text-sm font-medium transition ${ls(o==="rule")}`,children:"Rule-Based"}),u.jsx("button",{onClick:()=>s("volume"),className:`rounded-full border px-4 py-2 text-sm font-medium transition ${ls(o==="volume")}`,children:"Volume Split"})]}),u.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{children:[u.jsx(hn,{children:o==="rule"?"Rule Evaluation":o==="volume"?"Volume Split":"Payment Setup"}),u.jsx("h2",{className:"mt-3 font-medium text-slate-800 dark:text-white",children:o==="rule"?"Rule Evaluation Parameters":o==="volume"?"Volume Split Configuration":"Payment Parameters"})]})}),u.jsxs(Ge,{className:"space-y-3",children:[!e&&u.jsx("p",{className:"text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-2",children:"Set a merchant ID in the top bar first."}),o!=="volume"&&r&&u.jsx("p",{className:"text-xs text-slate-600 bg-slate-50 border border-slate-200 rounded px-3 py-2",children:"Loading routing config from backend..."}),o!=="volume"&&i&&u.jsx(Xr,{error:"Routing config unavailable from /config/routing-keys. Parameter forms are disabled."}),o==="rule"?u.jsxs(u.Fragment,{children:[r&&u.jsx("p",{className:"text-sm text-slate-500",children:"Loading routing keys from backend..."}),i&&u.jsx(Xr,{error:"Routing keys are unavailable from backend (/config/routing-keys). Rule Evaluation is disabled."}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Parameters"}),u.jsx("div",{className:"space-y-2",children:p.map(($,F)=>{var ce;return u.jsxs("div",{className:"space-y-2",children:[u.jsxs("div",{className:"flex gap-2 items-center",children:[u.jsx("select",{value:$.key,onChange:me=>Ea(F,me.target.value),disabled:i||r,className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:L.length===0?u.jsx("option",{value:"",children:"No keys available"}):L.map(me=>u.jsx("option",{value:me,children:me},me))}),u.jsx("input",{value:$.type,readOnly:!0,className:"w-36 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("button",{onClick:()=>Jt(F),className:"p-1.5 text-slate-400 hover:text-red-500",children:u.jsx(Ja,{size:14})})]}),$.type==="metadata_variant"?u.jsxs("div",{className:"flex gap-2 items-center pl-1",children:[u.jsx("input",{placeholder:"Metadata Key",value:$.metadataKey||"",onChange:me=>Na(F,me.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("input",{placeholder:"Metadata Value",value:$.value,onChange:me=>Rr(F,"value",me.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]}):$.type==="enum_variant"?u.jsx("div",{className:"flex gap-2 items-center pl-1",children:u.jsx("select",{value:$.value,onChange:me=>Rr(F,"value",me.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:(((ce=t[$.key])==null?void 0:ce.values)||[]).map(me=>u.jsx("option",{value:me,children:me},me))})}):$.type==="number"?u.jsx("div",{className:"flex gap-2 items-center pl-1",children:u.jsx("input",{type:"number",placeholder:"Value",value:$.value,onChange:me=>Rr(F,"value",me.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})}):u.jsx("div",{className:"flex gap-2 items-center pl-1",children:u.jsx("input",{placeholder:"Value",value:$.value,onChange:me=>Rr(F,"value",me.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})})]},F)})}),u.jsxs("button",{onClick:$r,disabled:i||r||L.length===0,className:"mt-2 flex items-center gap-1 text-xs text-brand-500 hover:text-brand-600",children:[u.jsx(Ui,{size:12})," Add Parameter"]})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Fallback gateway_name/gateway_id"}),u.jsx("div",{className:"space-y-2",children:x.map(($,F)=>u.jsxs("div",{className:"flex gap-2 items-center",children:[u.jsx("input",{placeholder:"gateway_name",value:$.gateway_name,onChange:ce=>Mt(F,"gateway_name",ce.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("input",{placeholder:"gateway_id",value:$.gateway_id||"",onChange:ce=>Mt(F,"gateway_id",ce.target.value),className:"flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("button",{onClick:()=>Pa(F),className:"p-1.5 text-slate-400 hover:text-red-500",children:u.jsx(Ja,{size:14})})]},F))}),u.jsxs("button",{onClick:ui,className:"mt-2 flex items-center gap-1 text-xs text-brand-500 hover:text-brand-600",children:[u.jsx(Ui,{size:12})," Add Gateway"]})]})]}):o==="volume"?u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Number of Payments"}),u.jsx("input",{type:"text",value:y,onChange:$=>g($.target.value),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"}),u.jsx("p",{className:"text-xs text-slate-500 mt-1",children:"Enter how many preview evaluations to run against the active volume split rule."})]}):u.jsxs(u.Fragment,{children:[u.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Amount"}),u.jsx("input",{value:l.amount,onChange:$=>rt("amount",$.target.value),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Currency"}),u.jsx("select",{value:l.currency,onChange:$=>rt("currency",$.target.value),disabled:i||r,className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:re.map($=>u.jsx("option",{children:$},$))})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Payment Method Type"}),u.jsx("select",{value:l.payment_method_type,onChange:$=>rt("payment_method_type",$.target.value),disabled:i||r,className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:H.map($=>u.jsx("option",{children:$},$))})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Payment Method"}),u.jsx("select",{value:l.payment_method,onChange:$=>rt("payment_method",$.target.value),disabled:i||r,className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:se.map($=>u.jsx("option",{children:$},$))})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Card Brand"}),u.jsx("select",{value:l.card_brand,onChange:$=>rt("card_brand",$.target.value),disabled:i||r,className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:Q.map($=>u.jsx("option",{children:$},$))})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Auth Type"}),u.jsx("select",{value:l.auth_type,onChange:$=>rt("auth_type",$.target.value),disabled:i||r,className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:ne.map($=>u.jsx("option",{children:$},$))})]})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Eligible Gateways (comma-separated)"}),u.jsx("input",{value:l.eligible_gateways,onChange:$=>rt("eligible_gateways",$.target.value),placeholder:"stripe, adyen",className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Algorithm"}),u.jsx("select",{value:l.ranking_algorithm,onChange:$=>rt("ranking_algorithm",$.target.value),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:wce.map($=>u.jsx("option",{value:$,children:_ce[$]},$))})]}),u.jsx("div",{className:"flex items-end pb-1",children:u.jsxs("label",{className:"flex items-center gap-2 text-sm text-slate-700 cursor-pointer",children:[u.jsx("input",{type:"checkbox",checked:l.elimination_enabled,onChange:$=>rt("elimination_enabled",$.target.checked),className:"rounded"}),"Elimination enabled"]})})]}),o==="single"&&u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Transaction Outcome"}),u.jsxs("select",{value:_,onChange:$=>k($.target.value),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500",children:[u.jsx("option",{value:"CHARGED",children:"Success (CHARGED)"}),u.jsx("option",{value:"FAILURE",children:"Failure (FAILURE)"})]}),u.jsx("p",{className:"mt-1 text-xs text-slate-500",children:"After deciding the gateway, single test will post feedback with this outcome so the payment appears in Decision Audit."})]}),o==="batch"&&u.jsxs("div",{className:"border-t border-slate-200 dark:border-[#1c1c24] pt-4 mt-4 space-y-3",children:[u.jsxs("h3",{className:"text-sm font-medium text-slate-800 flex items-center gap-2",children:[u.jsx(zs,{size:14}),"Simulation Configuration"]}),u.jsxs("div",{className:"grid grid-cols-3 gap-3",children:[u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Total Payments"}),u.jsx("input",{type:"text",value:f.totalPayments,onChange:$=>d(F=>({...F,totalPayments:$.target.value})),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Success Count"}),u.jsx("input",{type:"text",value:f.successCount,onChange:$=>d(F=>({...F,successCount:$.target.value})),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]}),u.jsxs("div",{children:[u.jsx("label",{className:"block text-xs font-medium text-slate-600 mb-1",children:"Failure Count"}),u.jsx("input",{type:"text",value:f.failureCount,onChange:$=>d(F=>({...F,failureCount:$.target.value})),className:"w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"})]})]}),u.jsxs("p",{className:"text-xs text-slate-500",children:["Will run ",f.totalPayments||0," payments: ",f.successCount||0," SUCCESS, ",f.failureCount||0," FAILURE"]})]})]}),u.jsx(Xr,{error:G}),o==="rule"?u.jsx(Ce,{onClick:an,disabled:K||i,className:"w-full justify-center",children:K?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Evaluating…"]}):u.jsxs(u.Fragment,{children:[u.jsx(Jd,{size:14})," Evaluate Rules"]})}):o==="volume"?u.jsx(Ce,{onClick:gr,disabled:K||!e,className:"w-full justify-center",children:K?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Running ",M,"/",y||0," previews…"]}):u.jsxs(u.Fragment,{children:[u.jsx(bp,{size:14})," Run Volume Evaluation"]})}):o==="batch"?u.jsx(Ce,{onClick:cr,disabled:D||!e||i,className:"w-full justify-center",children:D?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14}),"Simulating ",V.length,"/",f.totalPayments||0,"..."]}):u.jsxs(u.Fragment,{children:[u.jsx(zs,{size:14})," Run Batch Simulation"]})}):u.jsx(Ce,{onClick:er,disabled:K||!e||i,className:"w-full justify-center",children:K?u.jsxs(u.Fragment,{children:[u.jsx(pr,{size:14})," Running…"]}):u.jsxs(u.Fragment,{children:[u.jsx(Jd,{size:14})," Run Single Transaction"]})})]})]}),u.jsx("div",{className:"space-y-4",children:o==="volume"?T.length>0?u.jsxs(u.Fragment,{children:[u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Volume Distribution Overview"}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500",children:["Actual distribution from ",A.length," calls to ",u.jsx("code",{children:"/routing/evaluate"})," using the active volume split rule."]})]}),O!=null&&O.payment_id?u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>ee(O.payment_id,"Volume Split Preview"),children:"View latest preview trace"}):null]})}),u.jsxs(Ge,{children:[u.jsxs("div",{className:"text-center mb-4",children:[u.jsx("p",{className:"text-3xl font-bold text-slate-900",children:A.length}),u.jsx("p",{className:"text-xs text-slate-500",children:"Evaluations completed"})]}),u.jsx("div",{className:"grid grid-cols-2 gap-4",children:T.map(($,F)=>u.jsxs("div",{className:"bg-slate-50 dark:bg-[#111114] rounded-lg p-3",children:[u.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[u.jsx("div",{className:"w-3 h-3 rounded",style:{backgroundColor:Hr[F%Hr.length]}}),u.jsx("span",{className:"font-medium text-sm",children:$.name})]}),u.jsxs("div",{className:"flex justify-between text-xs text-slate-500",children:[u.jsxs("span",{children:[$.percentage,"%"]}),u.jsxs("span",{className:"font-medium text-slate-700",children:[$.count," payments"]})]})]},F))})]})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Pie Chart"})}),u.jsx(Ge,{children:u.jsx(Oi,{width:"100%",height:250,children:u.jsxs(a1,{children:[u.jsx(sa,{data:_n,cx:"50%",cy:"50%",innerRadius:60,outerRadius:100,paddingAngle:3,dataKey:"value",label:({name:$,percent:F})=>`${$} ${(F*100).toFixed(0)}%`,labelLine:!1,children:_n.map(($,F)=>u.jsx(Li,{fill:Hr[F%Hr.length]},`cell-${F}`))}),u.jsx(wr,{formatter:$=>[`${$} payments`,"Count"],contentStyle:document.documentElement.classList.contains("dark")?{backgroundColor:"#111114",border:"1px solid #222226",borderRadius:"8px",color:"#fff"}:{backgroundColor:"#fff",border:"1px solid #e5e7eb",borderRadius:"8px",color:"#1f2937"}})]})})})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Bar Chart"})}),u.jsx(Ge,{children:u.jsx(Oi,{width:"100%",height:T.length*50+40,children:u.jsxs($0,{data:T,layout:"vertical",margin:{left:20,right:40},children:[u.jsx(wa,{type:"number",tick:{fontSize:12,fill:"#666"},axisLine:{stroke:"#e5e7eb"},tickLine:!1}),u.jsx(_a,{type:"category",dataKey:"name",tick:{fontSize:12,fill:"#666"},width:80,axisLine:!1,tickLine:!1}),u.jsx(wr,{formatter:$=>[`${$} payments`,"Count"],contentStyle:document.documentElement.classList.contains("dark")?{backgroundColor:"#111114",border:"1px solid #222226",borderRadius:"8px",color:"#fff"}:{backgroundColor:"#fff",border:"1px solid #e5e7eb",borderRadius:"8px",color:"#1f2937"}}),u.jsx(ai,{dataKey:"count",radius:[0,6,6,0],children:T.map(($,F)=>u.jsx(Li,{fill:Hr[F%Hr.length]},`cell-${F}`))})]})})})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Percentage Distribution"})}),u.jsxs(Ge,{children:[u.jsx("div",{className:"h-4 rounded-full overflow-hidden flex",children:T.map(($,F)=>u.jsx("div",{style:{width:`${$.percentage}%`,backgroundColor:Hr[F%Hr.length]},className:"h-full transition-all duration-300",title:`${$.name}: ${$.percentage}%`},F))}),u.jsx("div",{className:"flex flex-wrap gap-3 mt-3",children:T.map(($,F)=>u.jsxs("div",{className:"flex items-center gap-1.5 text-xs",children:[u.jsx("div",{className:"w-2.5 h-2.5 rounded-sm",style:{backgroundColor:Hr[F%Hr.length]}}),u.jsx("span",{className:"text-slate-600",children:$.name}),u.jsxs("span",{className:"font-medium",children:[$.percentage,"%"]})]},F))})]})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Gateway Summary"})}),u.jsx(Ge,{className:"p-0",children:u.jsxs("table",{className:"w-full text-sm",children:[u.jsx("thead",{className:"bg-slate-50 dark:bg-[#111114] text-xs text-slate-500",children:u.jsxs("tr",{children:[u.jsx("th",{className:"text-left px-4 py-2",children:"gateway_name"}),u.jsx("th",{className:"text-right px-4 py-2",children:"Payments"}),u.jsx("th",{className:"text-right px-4 py-2",children:"Percentage"})]})}),u.jsxs("tbody",{className:"divide-y divide-slate-100 dark:divide-[#222226]",children:[T.map(($,F)=>u.jsxs("tr",{className:"hover:bg-slate-50 dark:bg-[#111114]",children:[u.jsx("td",{className:"px-4 py-2",children:u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx("div",{className:"w-3 h-3 rounded",style:{backgroundColor:Hr[F%Hr.length]}}),u.jsx("span",{className:"font-medium",children:$.name})]})}),u.jsx("td",{className:"px-4 py-2 text-right font-medium",children:$.count}),u.jsxs("td",{className:"px-4 py-2 text-right text-slate-500",children:[$.percentage,"%"]})]},F)),u.jsxs("tr",{className:"bg-slate-50 dark:bg-[#111114] font-medium",children:[u.jsx("td",{className:"px-4 py-2",children:"Total"}),u.jsx("td",{className:"px-4 py-2 text-right",children:A.length}),u.jsx("td",{className:"px-4 py-2 text-right",children:"100%"})]})]})]})})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{children:[u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Evaluation Sequence"}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500",children:["Actual connector sequence returned by repeated ",u.jsx("code",{children:"/routing/evaluate"})," calls."]})]})}),u.jsx(Ge,{className:"p-0 max-h-80 overflow-auto",children:u.jsxs("table",{className:"w-full text-sm",children:[u.jsx("thead",{className:"bg-slate-50 dark:bg-[#111114] text-xs text-slate-500 sticky top-0",children:u.jsxs("tr",{children:[u.jsx("th",{className:"text-left px-4 py-2 w-20",children:"#"}),u.jsx("th",{className:"text-left px-4 py-2",children:"gateway_name"})]})}),u.jsx("tbody",{className:"divide-y divide-slate-100 dark:divide-[#222226]",children:A.map(($,F)=>u.jsxs("tr",{className:"hover:bg-slate-50 dark:bg-[#111114]",children:[u.jsx("td",{className:"px-4 py-1.5 text-slate-500 font-mono text-xs",children:F+1}),u.jsx("td",{className:"px-4 py-1.5",children:u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx("div",{className:"w-2 h-2 rounded",style:{backgroundColor:Hr[(Dt.get($.connector)||0)%Hr.length]}}),u.jsx("span",{className:"font-medium",children:$.connector})]})})]},$.paymentId))})]})})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("button",{onClick:()=>oe($=>!$),className:"flex items-center justify-between w-full text-sm font-medium text-slate-800",children:[u.jsxs("span",{className:"flex items-center gap-2",children:[u.jsx(Ov,{size:14}),"API Response"]}),De?u.jsx(Nu,{size:14}):u.jsx(Au,{size:14})]})}),De&&O&&u.jsx(Ge,{className:"p-0",children:u.jsx("pre",{className:"text-xs text-slate-600 bg-slate-50 dark:bg-[#0a0a0f] p-4 overflow-auto max-h-96 font-mono",children:JSON.stringify(O,null,2)})})]})]}):u.jsx(Ae,{children:u.jsxs(Ge,{className:"py-16 text-center",children:[u.jsx(bp,{size:32,className:"text-gray-300 mx-auto mb-3"}),u.jsxs("p",{className:"text-slate-400 text-sm",children:['Enter the number of payments and click "Run Volume Evaluation" to execute repeated ',u.jsx("code",{children:"/routing/evaluate"})," calls against the active volume rule."]})]})}):o==="rule"?O?u.jsxs(u.Fragment,{children:[u.jsx(Ae,{children:u.jsxs(Ge,{children:[u.jsxs("div",{className:"flex items-start justify-between mb-3",children:[u.jsxs("div",{children:[u.jsx("p",{className:"text-xs text-slate-500 uppercase tracking-wide mb-1",children:"Status"}),u.jsx("p",{className:"text-2xl font-bold text-slate-900",children:O.status}),u.jsxs("p",{className:"text-xs text-slate-500 mt-1",children:["output_type: ",O.output.type]})]}),O.payment_id?u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>ee(O.payment_id,"Rule Evaluation Preview"),children:"View preview trace"}):null]}),O.output.type==="single"&&O.output.connector&&u.jsxs("div",{className:"border-t border-slate-200 dark:border-[#1c1c24] pt-3",children:[u.jsx("p",{className:"text-xs text-slate-400 mb-1",children:"Selected gateway_name"}),u.jsx("p",{className:"text-lg font-semibold",children:O.output.connector.gateway_name}),O.output.connector.gateway_id&&u.jsxs("p",{className:"text-xs text-slate-500",children:["gateway_id: ",O.output.connector.gateway_id]})]}),O.output.type==="priority"&&O.output.connectors&&u.jsxs("div",{className:"border-t border-slate-200 dark:border-[#1c1c24] pt-3",children:[u.jsx("p",{className:"text-xs text-slate-400 mb-2",children:"Priority gateway_name list"}),u.jsx("div",{className:"space-y-1",children:O.output.connectors.map(($,F)=>u.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[u.jsx("span",{className:"w-5 h-5 rounded-full bg-brand-500 text-white text-xs flex items-center justify-center",children:F+1}),u.jsx("span",{className:"font-medium",children:$.gateway_name}),$.gateway_id&&u.jsxs("span",{className:"text-xs text-slate-500",children:["(",$.gateway_id,")"]})]},F))})]}),O.output.type==="volume_split"&&u.jsxs("div",{className:"border-t border-slate-200 dark:border-[#1c1c24] pt-3",children:[u.jsx("p",{className:"text-xs text-slate-400 mb-2",children:"Volume Split Result"}),u.jsx("p",{className:"text-sm text-slate-600",children:"See Volume Split tab for detailed visualization."})]})]})}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("button",{onClick:()=>Ie($=>!$),className:"flex items-center justify-between w-full text-sm font-medium text-slate-800",children:[u.jsxs("span",{className:"flex items-center gap-2",children:[u.jsx(Ov,{size:14}),"API Response"]}),fe?u.jsx(Nu,{size:14}):u.jsx(Au,{size:14})]})}),fe&&u.jsx(Ge,{className:"p-0",children:u.jsx("pre",{className:"text-xs text-slate-600 bg-slate-50 dark:bg-[#0a0a0f] p-4 overflow-auto max-h-96 font-mono",children:JSON.stringify(O,null,2)})})]})]}):u.jsx(Ae,{children:u.jsxs(Ge,{className:"py-16 text-center",children:[u.jsx(Jd,{size:32,className:"text-gray-300 mx-auto mb-3"}),u.jsx("p",{className:"text-slate-400 text-sm",children:'Configure rule parameters and click "Evaluate Rules" to test routing.'})]})}):o==="batch"?V.length>0?u.jsxs(u.Fragment,{children:[u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Simulation Progress"})}),u.jsxs(Ge,{children:[u.jsxs("div",{className:"mb-4",children:[u.jsxs("div",{className:"flex justify-between text-xs text-slate-600 mb-1",children:[u.jsx("span",{children:"Progress"}),u.jsxs("span",{children:[Math.round(V.length/(parseInt(f.totalPayments)||1)*100),"%"]})]}),u.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:u.jsx("div",{className:"bg-brand-500 h-2 rounded-full transition-all duration-300",style:{width:`${V.length/(parseInt(f.totalPayments)||1)*100}%`}})})]}),Object.keys(jr).length>0&&u.jsxs("div",{className:"space-y-2",children:[u.jsx("h4",{className:"text-xs font-medium text-slate-700",children:"Gateway Selection Summary"}),Object.entries(jr).map(([$,F])=>u.jsxs("div",{className:"flex items-center justify-between text-sm",children:[u.jsx("span",{className:"font-medium",children:$}),u.jsxs("div",{className:"flex gap-3 text-xs",children:[u.jsxs("span",{className:"text-emerald-600",children:[F.success," ✓"]}),u.jsxs("span",{className:"text-red-500",children:[F.failure," ✗"]}),u.jsxs("span",{className:"text-slate-500",children:["(",F.total," total)"]})]})]},$))]})]})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Transaction Log"})}),u.jsx(Ge,{className:"p-0 max-h-96 overflow-auto",children:u.jsxs("table",{className:"w-full text-sm",children:[u.jsx("thead",{className:"bg-slate-50 dark:bg-[#0a0a0f] text-xs text-slate-500 sticky top-0",children:u.jsxs("tr",{children:[u.jsx("th",{className:"text-left px-3 py-2",children:"#"}),u.jsx("th",{className:"text-left px-3 py-2",children:"Payment ID"}),u.jsx("th",{className:"text-left px-3 py-2",children:"Gateway"}),u.jsx("th",{className:"text-left px-3 py-2",children:"Outcome"})]})}),u.jsx("tbody",{className:"divide-y divide-[#1c1c24]",children:V.map(($,F)=>u.jsxs("tr",{className:"hover:bg-slate-100 dark:bg-[#0f0f16]",children:[u.jsx("td",{className:"px-3 py-2 text-slate-500",children:F+1}),u.jsx("td",{className:"px-3 py-2",children:u.jsxs("button",{type:"button",title:$.paymentId,onClick:()=>Zl($.paymentId),className:"group flex items-start gap-3 text-left",children:[u.jsx("span",{className:"inline-flex h-8 w-8 items-center justify-center rounded-full bg-brand-500/10 text-[11px] font-semibold uppercase tracking-[0.16em] text-brand-600 dark:text-brand-300",children:F+1}),u.jsxs("span",{className:"min-w-0",children:[u.jsx("span",{className:"block truncate font-mono text-xs font-semibold text-slate-900 transition group-hover:text-brand-600 dark:text-white",children:$.paymentId}),u.jsx("span",{className:"mt-1 block text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400 transition group-hover:text-brand-500",children:"View audit"})]})]})}),u.jsx("td",{className:"px-3 py-2 font-medium",children:$.decidedGateway}),u.jsx("td",{className:"px-3 py-2",children:u.jsx(Ue,{variant:$.status==="CHARGED"?"green":"red",children:$.status})})]},$.paymentId))})]})})]})]}):u.jsx(Ae,{children:u.jsxs(Ge,{className:"py-16 text-center",children:[u.jsx(zs,{size:32,className:"text-gray-300 mx-auto mb-3"}),u.jsx("p",{className:"text-slate-400 text-sm",children:'Configure simulation parameters and click "Run Batch Simulation" to test Success Rate routing.'})]})}):m?u.jsxs(u.Fragment,{children:[u.jsx(Ae,{children:u.jsxs(Ge,{children:[u.jsxs("div",{className:"flex items-start justify-between mb-3",children:[u.jsxs("div",{children:[u.jsx("p",{className:"text-xs text-slate-500 uppercase tracking-wide mb-1",children:"Decided Gateway"}),u.jsx("p",{className:"text-3xl font-bold text-slate-900",children:m.decided_gateway})]}),u.jsxs("div",{className:"text-right space-y-2",children:[u.jsx("div",{children:u.jsx("span",{className:`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${Sce(m.routing_approach)}`,children:m.routing_approach})}),S?u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>Zl(S),children:"View audit"}):null,m.is_scheduled_outage&&u.jsx(Ue,{variant:"red",children:"Scheduled Outage"}),S?u.jsx(Ue,{variant:_==="CHARGED"?"green":"red",children:_}):null,m.latency!=null&&u.jsxs("p",{className:"text-xs text-slate-400",children:[m.latency,"ms"]})]})]}),S?u.jsxs("div",{className:"mb-3 rounded-[18px] border border-slate-200 bg-slate-50/80 px-4 py-3 dark:border-[#1c1c23] dark:bg-[#0b0b10]",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8a8a93]",children:"Payment ID"}),u.jsx("p",{className:"mt-2 font-mono text-sm text-slate-900 dark:text-white",children:S}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:["Feedback recorded as ",_,". Open audit to inspect the full decide and update flow."]})]}):null,m.routing_dimension&&u.jsxs("div",{className:"flex gap-4 text-sm text-slate-600 border-t border-slate-200 dark:border-[#1c1c24] pt-3",children:[u.jsxs("div",{children:[u.jsx("span",{className:"text-xs text-slate-400",children:"Dimension"}),u.jsx("p",{className:"font-medium",children:m.routing_dimension})]}),m.routing_dimension_level&&u.jsxs("div",{children:[u.jsx("span",{className:"text-xs text-slate-400",children:"Level"}),u.jsx("p",{className:"font-medium",children:m.routing_dimension_level})]}),u.jsxs("div",{children:[u.jsx("span",{className:"text-xs text-slate-400",children:"Reset"}),u.jsx("p",{className:"font-medium",children:m.reset_approach})]})]})]})}),Ut.length>0&&u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-center justify-between",children:[u.jsx("h3",{className:"text-sm font-medium text-slate-800",children:"Gateway Scores"}),u.jsxs(Ce,{size:"sm",variant:"ghost",onClick:er,className:"text-xs",children:[u.jsx(Av,{size:12})," Refresh"]})]})}),u.jsx(Ge,{children:u.jsx(Oi,{width:"100%",height:Ut.length*40+20,children:u.jsxs($0,{data:Ut,layout:"vertical",margin:{left:10,right:30},children:[u.jsx(wa,{type:"number",domain:[0,100],tickFormatter:$=>`${$}%`,tick:{fontSize:11,fill:"#66667a"},axisLine:{stroke:"#1c1c24"},tickLine:!1}),u.jsx(_a,{type:"category",dataKey:"name",tick:{fontSize:12,fill:"#8e8ea0"},width:60,axisLine:!1,tickLine:!1}),u.jsx(wr,{formatter:$=>`${$}%`,contentStyle:{backgroundColor:"#0d0d12",border:"1px solid #1c1c24",borderRadius:"8px",color:"#e8e8f4"}}),u.jsx(ai,{dataKey:"score",radius:[0,4,4,0],children:Ut.map(($,F)=>u.jsx(Li,{fill:$.name===m.decided_gateway?"#0069ED":$.score<30?"#ef4444":$.score<60?"#f59e0b":"#10b981"},F))})]})})})]}),m.filter_wise_gateways&&u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("button",{onClick:()=>de($=>!$),className:"flex items-center justify-between w-full text-sm font-medium text-slate-800",children:["Filter Chain",Z?u.jsx(Nu,{size:14}):u.jsx(Au,{size:14})]})}),Z&&u.jsx(Ge,{className:"space-y-2",children:Object.entries(m.filter_wise_gateways).map(([$,F])=>u.jsxs("div",{className:"flex items-start gap-3",children:[u.jsx("span",{className:"text-xs font-mono bg-slate-100 dark:bg-[#111118] text-slate-600 rounded-md px-2 py-0.5 mt-0.5 shrink-0 border border-slate-200 dark:border-[#1c1c24]",children:$}),u.jsx("div",{className:"flex flex-wrap gap-1",children:Array.isArray(F)?F.map(ce=>u.jsx("span",{className:"text-xs bg-blue-500/10 text-blue-400 ring-1 ring-inset ring-blue-500/20 rounded-md px-2 py-0.5",children:ce},ce)):u.jsx("span",{className:"text-xs text-slate-400",children:"—"})})]},$))})]}),u.jsxs(Ae,{children:[u.jsx(nt,{children:u.jsxs("button",{onClick:()=>Ie($=>!$),className:"flex items-center justify-between w-full text-sm font-medium text-slate-800",children:[u.jsxs("span",{className:"flex items-center gap-2",children:[u.jsx(Ov,{size:14}),"API Response"]}),fe?u.jsx(Nu,{size:14}):u.jsx(Au,{size:14})]})}),fe&&u.jsx(Ge,{className:"p-0",children:u.jsx("pre",{className:"text-xs text-slate-600 bg-slate-50 dark:bg-[#0a0a0f] p-4 overflow-auto max-h-96 font-mono",children:JSON.stringify(m,null,2)})})]})]}):u.jsx(Ae,{children:u.jsxs(Ge,{className:"py-16 text-center",children:[u.jsx(Jd,{size:32,className:"text-gray-300 mx-auto mb-3"}),u.jsx("p",{className:"text-slate-400 text-sm",children:'Fill in the parameters and click "Run Single Transaction" to decide a gateway, post feedback, and inspect the audit trail.'})]})})})]}),J&&u.jsxs("div",{className:"fixed bottom-0 left-64 right-0 top-[76px] z-[130] p-8",children:[u.jsx("button",{type:"button","aria-label":"Close payment audit",className:"absolute inset-0 bg-slate-950/70 backdrop-blur-sm",onClick:z}),u.jsxs("div",{role:"dialog","aria-modal":"true","aria-labelledby":"decision-explorer-audit-title",className:"relative mx-auto flex h-full w-full max-w-7xl flex-col overflow-hidden rounded-[30px] border border-slate-200 bg-white shadow-2xl dark:border-[#1c1c23] dark:bg-[#09090d]",children:[u.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-4 border-b border-slate-200 bg-slate-50/90 px-6 py-5 dark:border-[#1c1c23] dark:bg-[#0b0b10]",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-[#8a8a93]",children:"Batch Simulation Audit"}),u.jsx("h2",{id:"decision-explorer-audit-title",className:"mt-2 truncate text-2xl font-semibold text-slate-900 dark:text-white",children:J}),u.jsx("p",{className:"mt-2 max-w-3xl text-sm text-slate-500 dark:text-[#8a8a93]",children:"Inspect the exact decision trail for this simulated payment, including request payloads, API responses, score context, and the final transaction outcome."})]}),u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[St!=null&&St.latest_gateway?u.jsx(Ue,{variant:"green",children:St.latest_gateway}):null,St!=null&&St.latest_status?u.jsx(Ue,{variant:OO(St.latest_status),children:An(St.latest_status)}):null,St!=null&&St.event_count?u.jsxs(Ue,{variant:"gray",children:[St.event_count," events"]}):null,u.jsxs(Ce,{size:"sm",variant:"secondary",onClick:()=>W.mutate(),children:[u.jsx(Av,{size:12}),"Refresh"]}),u.jsxs(Ce,{size:"sm",variant:"ghost",onClick:z,children:[u.jsx(E_,{size:14}),"Close"]})]})]}),u.jsxs("div",{className:"grid min-h-0 flex-1 gap-0 xl:grid-cols-[340px_minmax(0,1fr)]",children:[u.jsxs("div",{className:"flex min-h-0 flex-col border-b border-slate-200 bg-slate-50/70 xl:border-b-0 xl:border-r dark:border-[#1c1c23] dark:bg-[#08080b]",children:[u.jsxs("div",{className:"border-b border-slate-200 px-6 py-4 dark:border-[#1c1c23]",children:[u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:"Audit Timeline"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Choose a step to inspect its request, response, and scoring context."})]}),u.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-4",children:W.isLoading&&!W.data?u.jsxs("div",{className:"flex items-center gap-2 px-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading payment audit…"]}):W.error?u.jsx(Xr,{error:W.error.message}):Ir.length?u.jsx("div",{className:"space-y-4",children:Ir.map($=>u.jsxs("section",{className:"space-y-2",children:[u.jsx("div",{className:"px-2",children:u.jsx(Ue,{variant:AO($.phase),children:$.phase})}),u.jsx("div",{className:"space-y-2",children:$.events.map(F=>u.jsxs("button",{type:"button",onClick:()=>{Ne(F.id),$e("summary")},className:`w-full rounded-[22px] border px-4 py-3 text-left transition ${(qe==null?void 0:qe.id)===F.id?"border-brand-500/50 bg-brand-500/8":"border-slate-200 bg-white hover:border-slate-300 dark:border-[#1d1d23] dark:bg-[#0c0c10] dark:hover:border-[#2a2a31]"}`,children:[u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"truncate text-sm font-semibold text-slate-900 dark:text-white",children:Ru(F)}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:Tu(F.created_at_ms)})]}),u.jsx(Ue,{variant:wf(F),children:An(F.status)||kO(F.event_type)})]}),u.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[u.jsx(Ue,{variant:"gray",children:$u(F.route)}),F.gateway?u.jsx(Ue,{variant:"green",children:F.gateway}):null,F.request_id?u.jsx(Ue,{variant:"blue",children:"Request"}):null]})]},F.id))})]},$.phase))}):u.jsx(Iu,{title:"No audit trail captured yet",body:"Run a simulated payment and gateway update first, then reopen the row once the audit payload is available."})})]}),u.jsxs("div",{className:"flex min-h-0 flex-col",children:[u.jsxs("div",{className:"border-b border-slate-200 px-6 py-4 dark:border-[#1c1c23]",children:[u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:qe?Ru(qe):"Audit Inspector"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:qe?`${$u(qe.route)} · ${Tu(qe.created_at_ms)}`:"Select an event from the left to inspect payloads."})]}),u.jsxs("div",{className:"flex flex-wrap gap-2",children:[qe!=null&&qe.gateway?u.jsx(Ue,{variant:"green",children:qe.gateway}):null,qe!=null&&qe.status?u.jsx(Ue,{variant:wf(qe),children:An(qe.status)}):null]})]}),u.jsx("div",{className:"mt-4 flex flex-wrap gap-2",children:["summary","input","response","raw"].map($=>u.jsx("button",{type:"button",onClick:()=>$e($),className:`rounded-full px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] transition ${ls(xe===$)}`,children:$==="raw"?"Raw JSON":An($)},$))})]}),u.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-6 py-5",children:W.isLoading&&!W.data?u.jsxs("div",{className:"flex items-center gap-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading inspector…"]}):tr?u.jsxs("div",{className:"space-y-5",children:[xe==="summary"?u.jsxs(u.Fragment,{children:[u.jsx(EO,{rows:tr.summaryRows}),tr.selectionReason?u.jsxs("div",{className:"rounded-[22px] border border-slate-200 bg-slate-50/80 px-5 py-4 dark:border-[#1d1d23] dark:bg-[#0b0b10]",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8a8a93]",children:"Selection Reason"}),u.jsx("p",{className:"mt-3 text-sm leading-6 text-slate-700 dark:text-slate-200",children:oR(tr.selectionReason)})]}):null,u.jsx($a,{title:"Score Context",value:tr.scoreContext,emptyMessage:"No scoring context was captured for this event."}),tr.signalRecord?u.jsx($a,{title:"Additional Signals",value:tr.signalRecord,emptyMessage:"No additional signals were captured for this event."}):null]}):null,xe==="input"?u.jsx($a,{title:"Request Payload",value:tr.requestPayload,emptyMessage:"This step did not persist a request payload."}):null,xe==="response"?u.jsx($a,{title:"Response Payload",value:tr.responsePayload,emptyMessage:"This step did not persist a response payload."}):null,xe==="raw"?u.jsx($a,{title:"Raw Event JSON",value:tr.rawEvent,emptyMessage:"No raw event payload is available."}):null]}):u.jsx(Iu,{title:"Select a timeline step",body:"Choose one of the audit events on the left to inspect its request, response, and score context."})})]})]})]})]}),Ee&&u.jsxs("div",{className:"fixed bottom-0 left-64 right-0 top-[76px] z-[130] p-8",children:[u.jsx("button",{type:"button","aria-label":"Close preview trace",className:"absolute inset-0 bg-slate-950/70 backdrop-blur-sm",onClick:ve}),u.jsxs("div",{role:"dialog","aria-modal":"true","aria-labelledby":"decision-explorer-preview-title",className:"relative mx-auto flex h-full w-full max-w-7xl flex-col overflow-hidden rounded-[30px] border border-slate-200 bg-white shadow-2xl dark:border-[#1c1c23] dark:bg-[#09090d]",children:[u.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-4 border-b border-slate-200 bg-slate-50/90 px-6 py-5 dark:border-[#1c1c23] dark:bg-[#0b0b10]",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-[#8a8a93]",children:"Preview Trace"}),u.jsx("h2",{id:"decision-explorer-preview-title",className:"mt-2 truncate text-2xl font-semibold text-slate-900 dark:text-white",children:Ee}),u.jsxs("p",{className:"mt-2 max-w-3xl text-sm text-slate-500 dark:text-[#8a8a93]",children:[Ze,". This is a preview-only trace captured from ",u.jsx("code",{className:"font-mono text-xs",children:"/routing/evaluate"}),", not a transaction outcome."]})]}),u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[Lt!=null&&Lt.latest_gateway?u.jsx(Ue,{variant:"green",children:Lt.latest_gateway}):null,Lt!=null&&Lt.latest_status?u.jsx(Ue,{variant:OO(Lt.latest_status),children:An(Lt.latest_status)}):null,Lt!=null&&Lt.event_count?u.jsxs(Ue,{variant:"gray",children:[Lt.event_count," events"]}):null,u.jsxs(Ce,{size:"sm",variant:"secondary",onClick:()=>_e.mutate(),children:[u.jsx(Av,{size:12}),"Refresh"]}),u.jsxs(Ce,{size:"sm",variant:"ghost",onClick:ve,children:[u.jsx(E_,{size:14}),"Close"]})]})]}),u.jsxs("div",{className:"grid min-h-0 flex-1 gap-0 xl:grid-cols-[340px_minmax(0,1fr)]",children:[u.jsxs("div",{className:"flex min-h-0 flex-col border-b border-slate-200 bg-slate-50/70 xl:border-b-0 xl:border-r dark:border-[#1c1c23] dark:bg-[#08080b]",children:[u.jsxs("div",{className:"border-b border-slate-200 px-6 py-4 dark:border-[#1c1c23]",children:[u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:"Preview Timeline"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Choose a preview step to inspect its request, response, and routing output."})]}),u.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-4",children:_e.isLoading&&!_e.data?u.jsxs("div",{className:"flex items-center gap-2 px-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading preview trace…"]}):_e.error?u.jsx(Xr,{error:_e.error.message}):Un.length?u.jsx("div",{className:"space-y-4",children:Un.map($=>u.jsxs("section",{className:"space-y-2",children:[u.jsx("div",{className:"px-2",children:u.jsx(Ue,{variant:AO($.phase),children:$.phase})}),u.jsx("div",{className:"space-y-2",children:$.events.map(F=>u.jsxs("button",{type:"button",onClick:()=>{Me(F.id),Ke("summary")},className:`w-full rounded-[22px] border px-4 py-3 text-left transition ${(Xe==null?void 0:Xe.id)===F.id?"border-brand-500/50 bg-brand-500/8":"border-slate-200 bg-white hover:border-slate-300 dark:border-[#1d1d23] dark:bg-[#0c0c10] dark:hover:border-[#2a2a31]"}`,children:[u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"truncate text-sm font-semibold text-slate-900 dark:text-white",children:Ru(F)}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:Tu(F.created_at_ms)})]}),u.jsx(Ue,{variant:wf(F),children:An(F.status)||kO(F.event_type)})]}),u.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[u.jsx(Ue,{variant:"gray",children:$u(F.route)}),F.gateway?u.jsx(Ue,{variant:"green",children:F.gateway}):null]})]},F.id))})]},$.phase))}):u.jsx(Iu,{title:"No preview trace captured yet",body:"Run Rule-Based or Volume Split evaluation first, then open the preview trace once the request has been logged."})})]}),u.jsxs("div",{className:"flex min-h-0 flex-col",children:[u.jsxs("div",{className:"border-b border-slate-200 px-6 py-4 dark:border-[#1c1c23]",children:[u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:Xe?Ru(Xe):"Preview Inspector"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:Xe?`${$u(Xe.route)} · ${Tu(Xe.created_at_ms)}`:"Select an event from the left to inspect the preview payload."})]}),u.jsxs("div",{className:"flex flex-wrap gap-2",children:[Xe!=null&&Xe.gateway?u.jsx(Ue,{variant:"green",children:Xe.gateway}):null,Xe!=null&&Xe.status?u.jsx(Ue,{variant:wf(Xe),children:An(Xe.status)}):null]})]}),u.jsx("div",{className:"mt-4 flex flex-wrap gap-2",children:["summary","input","response","raw"].map($=>u.jsx("button",{type:"button",onClick:()=>Ke($),className:`rounded-full px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] transition ${ls(Re===$)}`,children:$==="raw"?"Raw JSON":An($)},$))})]}),u.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-6 py-5",children:_e.isLoading&&!_e.data?u.jsxs("div",{className:"flex items-center gap-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading preview inspector…"]}):on?u.jsxs("div",{className:"space-y-5",children:[Re==="summary"?u.jsxs(u.Fragment,{children:[u.jsx(EO,{rows:on.summaryRows}),u.jsx($a,{title:"Preview Signals",value:on.signalRecord,emptyMessage:"No extra preview metadata was captured for this evaluation."})]}):null,Re==="input"?u.jsx($a,{title:"Request Payload",value:on.requestPayload,emptyMessage:"No request payload was captured for this preview."}):null,Re==="response"?u.jsx($a,{title:"Response Payload",value:on.responsePayload,emptyMessage:"No response payload was captured for this preview."}):null,Re==="raw"?u.jsx($a,{title:"Raw Event JSON",value:on.rawEvent,emptyMessage:"No raw event payload is available for this preview."}):null]}):u.jsx(Iu,{title:"Select a preview step",body:"Choose one of the preview events on the left to inspect its request and response payload."})})]})]})]})]})]})}const PO=[{value:"15m",label:"Last 15 mins"},{value:"1h",label:"Last 1 hour"},{value:"24h",label:"Last 1 day"},{value:"custom",label:"Custom window"}],Gr=["#0069ED","#14b8a6","#f97316","#e11d48","#8b5cf6","#22c55e"],_f={backgroundColor:"#0d0d12",border:"1px solid #1c1c24",borderRadius:"14px",color:"#e8e8f4",boxShadow:"0 16px 40px rgba(0, 0, 0, 0.35)"},Sf={color:"#f8fafc",fontWeight:600,marginBottom:8},jf={color:"#e2e8f0"},kf={zIndex:30,outline:"none"},CO={dimensions:{},gateways:[]},bu=3,Ty=50,Nce=5,$y=10,us={hits:{title:"API call counts",purpose:"Use these cards to see how much traffic each major decision-engine API handled in the selected window.",calculation:"Each request records one lightweight API-call event. The cards count those recorded calls for the endpoints surfaced in the current view.",source:"Counts come from analytics rows persisted in `analytics_event` in Postgres."},share:{title:"Gateway share over time",purpose:"Use this to see when traffic shifted from one connector to another for the selected merchant.",calculation:"Decision events are bucketed by time and grouped by chosen connector. The chart shows how many decisions each gateway captured in each bucket.",source:"Reads persisted `decision` rows from `analytics_event` in Postgres."},sr:{title:"Connector success rate over time",purpose:"Use this to explain why a connector won routing at a given time, based on the recorded historical score trail.",calculation:"Stored `score_snapshot` events are bucketed over the selected window and averaged per connector. The line values are displayed as percentages.",source:"Reads persisted `score_snapshot` rows from `analytics_event` in Postgres. The current score state originates from Redis-backed scoring flows."},preview_hits:{title:"Rule-based summary",purpose:"Use these cards to distinguish preview request volume from the connector coverage produced by rule-based routing.",calculation:"Rule Evaluate counts come from request-hit analytics for `/routing/evaluate`. Gateway coverage counts the unique connectors selected in the fetched preview sample.",source:"Reads `request_hit` and `rule_evaluation_preview` analytics associated with preview routing activity."},preview_activity:{title:"Connector selections over time",purpose:"Use this to see which connectors were selected in each time bucket inside the selected preview window.",calculation:"Returned preview traces are bucketed by time using each trace's latest activity timestamp, then grouped by latest selected connector. The chart shows connector counts per bucket.",source:"Reads `rule_evaluation_preview` activity through `/analytics/preview-trace`."},preview_share:{title:"Rule-based gateway selection mix",purpose:"Use this to see which connectors dominate the fetched rule-preview sample, separate from real transaction decisions.",calculation:"Returned preview traces are grouped by latest selected connector and displayed as share of the fetched preview sample.",source:"Reads `rule_evaluation_preview` activity through `/analytics/preview-trace`."}};function sR(e){const t=new URLSearchParams;return Object.entries(e).forEach(([r,n])=>{n!==void 0&&n!==""&&t.set(r,String(n))}),t.toString()}function Ry(e,t,r,n,a){const i={scope:"current",range:t==="custom"?"1h":t,start_ms:n==null?void 0:n.start_ms,end_ms:n==null?void 0:n.end_ms,merchant_id:r,gateway:a!=null&&a.gateways.length?a.gateways.join(","):void 0};Object.entries((a==null?void 0:a.dimensions)||{}).forEach(([s,l])=>{l&&(i[s]=l)});const o=sR(i);return o?`${e}?${o}`:e}function I0(e,t,r,n,a){const i={scope:"current",range:e==="custom"?"1h":e,start_ms:a==null?void 0:a.start_ms,end_ms:a==null?void 0:a.end_ms,merchant_id:t,page:r,page_size:n},o=sR(i);return o?`/analytics/preview-trace?${o}`:"/analytics/preview-trace"}async function Ece(e,t,r){const n=await pn(I0(e,t,1,Ty,r)),a=Math.min(Math.ceil(n.total_results/Ty),Nce);if(a<=1)return n;const i=await Promise.all(Array.from({length:a-1},(o,s)=>pn(I0(e,t,s+2,Ty,r))));return{...n,results:[n.results,...i.map(o=>o.results)].flat()}}function Mh(e,t=2){if(e==null||Number.isNaN(Number(e)))return"0";const r=Number(e);return Number.isInteger(r)?r.toString():r.toFixed(t)}function lR(e){return Number.isFinite(e)?e<=1?e*100:e:0}function Of(e,t=1){return e==null||Number.isNaN(Number(e))?"0%":`${Mh(lR(Number(e)),t)}%`}function Iy(e){return new Intl.DateTimeFormat(void 0,{hour:"2-digit",minute:"2-digit"}).format(new Date(e))}function so(e){return new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(new Date(e))}function Pce(e,t){const r=t?t.end_ms-t.start_ms:e==="15m"?9e5:e==="1h"?36e5:864e5;return r<=15*60*1e3?60*1e3:r<=60*60*1e3?5*60*1e3:r<=24*60*60*1e3?15*60*1e3:r<=72*60*60*1e3?60*60*1e3:3*60*60*1e3}function M0(e,t){return e-e%Math.max(1,t)}function Cce(e,t){const r=[],n=Math.max(1,t),a=M0(e.start_ms,n),i=M0(e.end_ms,n);for(let o=a;o<=i;o+=n)r.push(o);return r}function TO(e){const t=Date.now(),r=e==="15m"?15*60*1e3:e==="1h"?60*60*1e3:24*60*60*1e3;return{start_ms:t-r,end_ms:t}}function Af(e){const t=new Date(e),r=n=>n.toString().padStart(2,"0");return`${t.getFullYear()}-${r(t.getMonth()+1)}-${r(t.getDate())}T${r(t.getHours())}:${r(t.getMinutes())}`}function $O(e){const t=new Date(e).getTime();return Number.isFinite(t)?t:null}function Ra({title:e,body:t}){return u.jsxs("div",{className:"rounded-[24px] border border-dashed border-slate-200 bg-white/60 px-6 py-12 text-center dark:border-[#222227] dark:bg-[#0b0b0d]",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e}),u.jsx("p",{className:"mt-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:t})]})}function Nf(){return"h-11 w-full rounded-2xl border border-slate-200 bg-white px-4 text-sm text-slate-700 shadow-sm outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:border-[#27272a] dark:bg-[#121214] dark:text-[#e5e7eb]"}function cs({content:e}){const[t,r]=j.useState(!1),n=j.useRef(null),[a,i]=j.useState({top:0,left:0,width:320});return j.useEffect(()=>{if(!t)return;function o(s){var l;(l=n.current)!=null&&l.contains(s.target)||r(!1)}return document.addEventListener("mousedown",o),()=>document.removeEventListener("mousedown",o)},[t]),j.useLayoutEffect(()=>{if(!t||!n.current)return;const o=320,s=280,l=16,c=12;function f(){if(!n.current)return;const d=n.current.getBoundingClientRect(),p=Math.min(o,window.innerWidth-l*2),h=Math.min(Math.max(d.right-p,l),window.innerWidth-p-l),v=d.bottom+c+s>window.innerHeight-l?Math.max(d.top-s-c,l):d.bottom+c;i({top:v,left:h,width:p})}return f(),window.addEventListener("resize",f),window.addEventListener("scroll",f,!0),()=>{window.removeEventListener("resize",f),window.removeEventListener("scroll",f,!0)}},[t]),u.jsxs("div",{ref:n,className:"relative shrink-0",children:[u.jsx("button",{type:"button","aria-label":`About ${e.title}`,onClick:()=>r(o=>!o),className:`flex h-7 w-7 items-center justify-center rounded-full border text-xs font-semibold transition ${t?"border-brand-500/50 bg-brand-500/10 text-brand-700 dark:text-brand-200":"border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-900 dark:border-[#27272a] dark:bg-[#121214] dark:text-[#8a8a93] dark:hover:text-white"}`,children:"i"}),t?u.jsxs("div",{style:{position:"fixed",top:a.top,left:a.left,width:a.width},className:"z-[120] rounded-[24px] border border-slate-200 bg-white/95 p-4 shadow-2xl backdrop-blur dark:border-[#1d1d23] dark:bg-[#09090d]/95",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e.title}),u.jsxs("div",{className:"mt-3 space-y-3 text-xs leading-6 text-slate-600 dark:text-[#b3b3bd]",children:[u.jsxs("div",{children:[u.jsx("p",{className:"font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8a8a93]",children:"Why it matters"}),u.jsx("p",{className:"mt-1",children:e.purpose})]}),u.jsxs("div",{children:[u.jsx("p",{className:"font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8a8a93]",children:"How it is calculated"}),u.jsx("p",{className:"mt-1",children:e.calculation})]}),u.jsxs("div",{children:[u.jsx("p",{className:"font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8a8a93]",children:"Data source"}),u.jsx("p",{className:"mt-1",children:e.source})]})]})]}):null]})}function My({label:e,value:t,subtitle:r,eyebrow:n="Endpoint hits"}){return u.jsx(Ae,{className:"h-full overflow-hidden",children:u.jsxs(Ge,{className:"flex h-full min-h-[150px] flex-col justify-between",children:[u.jsxs("div",{className:"space-y-2",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:n}),u.jsx("p",{className:"text-lg font-semibold text-slate-900 dark:text-white",children:e})]}),u.jsxs("div",{className:"flex items-end justify-between gap-4",children:[u.jsx("p",{className:"text-5xl font-semibold tracking-tight text-slate-950 dark:text-white",children:Mh(t,0)}),u.jsx(Ue,{variant:"blue",children:r})]})]})})}function Tce(e){return e==="/decide_gateway"?"Decide Gateway":e==="/update_gateway"?"Update Gateway":e==="/rule_evaluate"?"Rule Evaluate":e}function $ce(){var Rr,Na,Ea,ui,Pa,Mt,er,cr,an,gr,Ut,jr,_n,Dt,St,qe,Ir,tr,Lt,Xe,Un,on,Zl;const{merchantId:e}=ka(),[t,r]=j.useState("1h"),[n,a]=j.useState("transactions"),[i,o]=j.useState(CO),[s,l]=j.useState(!1),[c,f]=j.useState(1),[d,p]=j.useState(()=>Af(Date.now()-2*60*60*1e3)),[h,x]=j.useState(()=>Af(Date.now())),v=!!e,y=j.useMemo(()=>{if(t!=="custom")return;const z=$O(d),ee=$O(h);if(!(z===null||ee===null||ee<=z))return{start_ms:z,end_ms:ee}},[h,d,t]),g=v&&e&&(t!=="custom"||y)?Ry("/analytics/overview",t,e,y):null,m=v&&e&&(t!=="custom"||y)?Ry("/analytics/routing-stats",t,e,y):null,w=v&&e&&(t!=="custom"||y)?Ry("/analytics/routing-stats",t,e,y,i):null,S=v&&e&&(t!=="custom"||y)?["preview-trace-analytics",t,e,(y==null?void 0:y.start_ms)??null,(y==null?void 0:y.end_ms)??null]:null,b=v&&e&&(t!=="custom"||y)?I0(t,e,c,$y,y):null,_={refreshInterval:1e4,revalidateOnFocus:!0,revalidateIfStale:!1},k={refreshInterval:12e3,revalidateOnFocus:!0,revalidateIfStale:!1},O={...k,keepPreviousData:!0},N={...k,keepPreviousData:!0},T=Bt(g,pn,_),R=Bt(m,pn,k),A=Bt(w,pn,O),C=Bt(S,async z=>{const[,ee,ve,Le,ze]=z;return Ece(ee,ve,Le!==null&&ze!==null?{start_ms:Number(Le),end_ms:Number(ze)}:void 0)},k),M=Bt(b,pn,N),B=!T.data&&T.isLoading||!R.data&&R.isLoading||!A.data&&A.isLoading,V=!T.data&&T.isLoading||!C.data&&C.isLoading,I=((Rr=T.error)==null?void 0:Rr.message)||((Na=R.error)==null?void 0:Na.message)||((Ea=A.error)==null?void 0:Ea.message)||null,D=((ui=T.error)==null?void 0:ui.message)||((Pa=C.error)==null?void 0:Pa.message)||((Mt=M.error)==null?void 0:Mt.message)||null,U=n==="transactions"?B:V,G=n==="transactions"?I:D,q={dimensions:((cr=(er=R.data)==null?void 0:er.available_filters)==null?void 0:cr.dimensions)||((gr=(an=A.data)==null?void 0:an.available_filters)==null?void 0:gr.dimensions)||[],missing_dimensions:((jr=(Ut=R.data)==null?void 0:Ut.available_filters)==null?void 0:jr.missing_dimensions)||((Dt=(_n=A.data)==null?void 0:_n.available_filters)==null?void 0:Dt.missing_dimensions)||[],gateways:((qe=(St=R.data)==null?void 0:St.available_filters)==null?void 0:qe.gateways)||((tr=(Ir=A.data)==null?void 0:Ir.available_filters)==null?void 0:tr.gateways)||[]},K=j.useMemo(()=>new Map(q.dimensions.map(z=>[z.key,z])),[q.dimensions]);j.useEffect(()=>{o(z=>{const ee=Object.fromEntries(Object.entries(z.dimensions).filter(([Le,ze])=>{if(!ze)return!1;const gt=K.get(Le);return gt?gt.values.includes(ze):!1})),ve=z.gateways.filter(Le=>q.gateways.includes(Le));return Object.keys(ee).length===Object.keys(z.dimensions).length&&Object.entries(ee).every(([Le,ze])=>z.dimensions[Le]===ze)&&ve.length===z.gateways.length&&ve.every((Le,ze)=>Le===z.gateways[ze])?z:{dimensions:ee,gateways:ve}})},[K,q.gateways]),j.useEffect(()=>{q.dimensions.length<=bu&&s&&l(!1)},[q.dimensions.length,s]),j.useEffect(()=>{f(1)},[e,t,y==null?void 0:y.start_ms,y==null?void 0:y.end_ms]);const te=j.useMemo(()=>{var z;return t!=="custom"?((z=PO.find(ee=>ee.value===t))==null?void 0:z.label)||"Selected window":y?`${so(y.start_ms)} to ${so(y.end_ms)}`:"Custom window"},[y,t]),Z=j.useMemo(()=>y||TO(t),[y,t]),de=j.useMemo(()=>{var ee,ve;const z=[{route:"/decide_gateway",count:0},{route:"/update_gateway",count:0},{route:"/rule_evaluate",count:0}];return(ve=(ee=T.data)==null?void 0:ee.route_hits)!=null&&ve.length?z.map(Le=>{var ze,gt;return{...Le,count:((gt=(ze=T.data)==null?void 0:ze.route_hits.find(ro=>ro.route===Le.route))==null?void 0:gt.count)||0}}):z},[T.data]),fe=j.useMemo(()=>de.filter(z=>z.route!=="/rule_evaluate"),[de]),Ie=j.useMemo(()=>{var z;return((z=de.find(ee=>ee.route==="/rule_evaluate"))==null?void 0:z.count)||0},[de]),De=((Lt=C.data)==null?void 0:Lt.results)||[],oe=((Xe=M.data)==null?void 0:Xe.results)||[],J=j.useMemo(()=>{const z=new Map;for(const ee of De){const ve=ee.latest_gateway||"No gateway selected";z.set(ve,(z.get(ve)||0)+1)}return Array.from(z.entries()).map(([ee,ve])=>({gateway:ee,count:ve})).sort((ee,ve)=>ve.count-ee.count).slice(0,6)},[De]),we=j.useMemo(()=>{const z=new Map;for(const ee of De){const ve=ee.latest_status||"unknown";z.set(ve,(z.get(ve)||0)+1)}return Array.from(z.entries()).map(([ee,ve])=>({status:ee,count:ve})).sort((ee,ve)=>ve.count-ee.count)},[De]),Y=j.useMemo(()=>Pce(t,y),[y,t]),Ne=j.useMemo(()=>{const z=J.map(ve=>ve.gateway).slice(0,6),ee=new Map;for(const ve of Cce(Z,Y))ee.set(ve,z.reduce((Le,ze)=>(Le[ze]=0,Le),{bucket_ms:ve}));for(const ve of De){const Le=ve.latest_gateway||"No gateway selected";if(!z.includes(Le))continue;const ze=M0(ve.last_seen_ms,Y),gt=ee.get(ze)||z.reduce((ro,Rd)=>(ro[Rd]=0,ro),{bucket_ms:ze});gt[Le]=(gt[Le]||0)+1,ee.set(ze,gt)}return{gateways:z,rows:Array.from(ee.values()).sort((ve,Le)=>ve.bucket_ms-Le.bucket_ms)}},[Z,Y,De,J]),xe=(Un=De[0])==null?void 0:Un.last_seen_ms,$e=((on=M.data)==null?void 0:on.total_results)||0,Ee=Math.max(1,Math.ceil($e/$y)),je=$e?(c-1)*$y+1:0,Be=$e?je+oe.length-1:0,Me=J.filter(z=>z.gateway!=="No gateway selected").length,Re=((Zl=J[0])==null?void 0:Zl.count)||1,Ke=j.useMemo(()=>{const z=J.reduce((ee,ve)=>ee+ve.count,0);return J.map((ee,ve)=>({name:ee.gateway,value:ee.count,percentage:z?ee.count/z*100:0,color:ee.gateway==="No gateway selected"?"#64748b":Gr[ve%Gr.length]}))},[J]);j.useEffect(()=>{if(!$e&&c!==1){f(1);return}c>Ee&&f(Ee)},[c,Ee,$e]);const Ze=j.useMemo(()=>{var ve,Le;const z=Array.from(new Set((((ve=R.data)==null?void 0:ve.gateway_share)||[]).map(ze=>ze.gateway))).slice(0,6),ee=new Map;for(const ze of((Le=R.data)==null?void 0:Le.gateway_share)||[]){if(!z.includes(ze.gateway))continue;const gt=ee.get(ze.bucket_ms)||{bucket_ms:ze.bucket_ms};gt[ze.gateway]=ze.count,ee.set(ze.bucket_ms,gt)}return{gateways:z,rows:Array.from(ee.values()).sort((ze,gt)=>ze.bucket_ms-gt.bucket_ms)}},[R.data]),P=j.useMemo(()=>{var ve,Le;const z=Array.from(new Set((((ve=A.data)==null?void 0:ve.sr_trend)||[]).map(ze=>ze.gateway))).slice(0,6),ee=new Map;for(const ze of((Le=A.data)==null?void 0:Le.sr_trend)||[]){if(!z.includes(ze.gateway))continue;const gt=ee.get(ze.bucket_ms)||{bucket_ms:ze.bucket_ms};gt[ze.gateway]=lR(ze.score_value),ee.set(ze.bucket_ms,gt)}return{gateways:z,rows:Array.from(ee.values()).sort((ze,gt)=>ze.bucket_ms-gt.bucket_ms)}},[A.data]),L=j.useMemo(()=>{if(!P.rows.length)return[];const z=P.rows[P.rows.length-1];return P.gateways.map(ee=>({gateway:ee,value:typeof z[ee]=="number"?z[ee]:null})).filter(ee=>ee.value!==null)},[P]),H=j.useMemo(()=>{const z=P.rows.flatMap(ze=>P.gateways.map(gt=>ze[gt]).filter(gt=>typeof gt=="number"));if(!z.length)return[0,100];const ee=Math.min(...z),ve=Math.max(...z),Le=ee===ve?5:Math.max(2,(ve-ee)*.35);return[Math.max(0,Math.floor(ee-Le)),Math.min(100,Math.ceil(ve+Le))]},[P]),se=j.useMemo(()=>{const z=q.dimensions.flatMap(ee=>{const ve=i.dimensions[ee.key];return ve?[`${ee.label}: ${ve}`]:[]});return i.gateways.length&&z.push(i.gateways.join(", ")),z.length?z.join(" / "):"All routing dimensions"},[q.dimensions,i]),re=j.useMemo(()=>s||q.dimensions.length<=bu?q.dimensions:q.dimensions.slice(0,bu),[q.dimensions,s]),Q=q.dimensions.length>bu,ne=Q?q.dimensions.length-bu:0,ke=j.useMemo(()=>{const z=q.dimensions.flatMap(ve=>{const Le=i.dimensions[ve.key];return Le?[{key:`dimension:${ve.key}`,label:`${ve.label}: ${Le}`}]:[]}),ee=i.gateways.map(ve=>({key:`gateway:${ve}`,label:`Connector: ${ve}`}));return[...z,...ee]},[q.dimensions,i]);function W(z){if(r(z),z!=="custom"){const ee=TO(z);p(Af(ee.start_ms)),x(Af(ee.end_ms))}}function le(){T.mutate(),R.mutate(),A.mutate(),C.mutate(),M.mutate()}function _e(z){o(ee=>{const ve=ee.gateways.includes(z);return{...ee,gateways:ve?ee.gateways.filter(Le=>Le!==z):[...ee.gateways,z]}})}function rt(){o(CO)}function $r(z){if(z.startsWith("dimension:")){Jt(z.replace("dimension:",""),"");return}z.startsWith("gateway:")&&_e(z.replace("gateway:",""))}function Jt(z,ee){o(ve=>{const Le={...ve.dimensions};return ee?Le[z]=ee:delete Le[z],{...ve,dimensions:Le}})}return v?u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-4",children:[u.jsxs("div",{className:"space-y-2",children:[u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900 dark:text-white",children:"Analytics"}),u.jsx(Ue,{variant:"green",children:e||"Current merchant"})]}),u.jsx("p",{className:"text-sm text-slate-500 dark:text-[#8a8a93]",children:n==="transactions"?"One working surface for route volume, connector share, and historical connector success rate.":"Preview-only activity for rule-based routing, separate from transaction decisions and score updates."})]}),u.jsx("div",{className:"flex flex-wrap items-center gap-2",children:u.jsx(Ce,{size:"sm",variant:"ghost",onClick:le,children:"Refresh"})})]}),u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[u.jsx(Ce,{size:"sm",variant:"secondary",className:n==="transactions"?"!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white":"!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white",onClick:()=>a("transactions"),children:"Transactions"}),u.jsx(Ce,{size:"sm",variant:"secondary",className:n==="rule_based"?"!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white":"!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white",onClick:()=>a("rule_based"),children:"Rule-Based"})]}),u.jsx(Ae,{className:"overflow-visible",children:u.jsxs(Ge,{className:"flex flex-wrap items-end gap-4",children:[u.jsxs("label",{className:"min-w-[220px] flex-1 space-y-2",children:[u.jsx("span",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Time window"}),u.jsx("select",{value:t,onChange:z=>W(z.target.value),className:Nf(),children:PO.map(z=>u.jsx("option",{value:z.value,children:z.label},z.value))})]}),t==="custom"?u.jsxs(u.Fragment,{children:[u.jsxs("label",{className:"min-w-[220px] flex-1 space-y-2",children:[u.jsx("span",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Start time"}),u.jsx("input",{type:"datetime-local",value:d,onChange:z=>p(z.target.value),className:Nf()})]}),u.jsxs("label",{className:"min-w-[220px] flex-1 space-y-2",children:[u.jsx("span",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"End time"}),u.jsx("input",{type:"datetime-local",value:h,onChange:z=>x(z.target.value),className:Nf()})]})]}):null,u.jsxs("div",{className:"min-w-[220px] flex-1 rounded-[24px] border border-slate-200 bg-white px-4 py-3 dark:border-[#1d1d23] dark:bg-[#0c0c0e]",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Active window"}),u.jsx("p",{className:"mt-1 text-sm font-medium text-slate-900 dark:text-white",children:te}),t==="custom"&&!y?u.jsx("p",{className:"mt-1 text-xs text-red-500",children:"Choose an end time after the start time."}):null]})]})}),u.jsx(Xr,{error:G}),U?u.jsxs("div",{className:"flex items-center gap-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading analytics…"]}):null,n==="transactions"?u.jsxs(u.Fragment,{children:[u.jsxs("section",{className:"space-y-4",children:[u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-lg font-semibold text-slate-900 dark:text-white",children:"API calls"}),u.jsx("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#8a8a93]",children:"Counts for the decision and feedback surfaces tied to real transaction flow."})]}),u.jsx(cs,{content:us.hits})]}),u.jsx("div",{className:"grid gap-4 lg:grid-cols-2",children:fe.map(z=>u.jsx(My,{label:Tce(z.route),value:z.count,subtitle:t==="custom"?"Custom window":te},z.route))})]}),u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Gateway share over time"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"How decision volume moved across connectors inside the selected merchant window."})]}),u.jsx(cs,{content:us.share})]})}),u.jsx(Ge,{children:Ze.rows.length?u.jsx("div",{className:"h-80",children:u.jsx(Oi,{width:"100%",height:"100%",children:u.jsxs(yce,{data:Ze.rows,children:[u.jsx(Gf,{strokeDasharray:"3 3",stroke:"#e2e8f0"}),u.jsx(wa,{dataKey:"bucket_ms",tickFormatter:Iy,tick:{fontSize:11}}),u.jsx(_a,{tick:{fontSize:11}}),u.jsx(wr,{labelFormatter:z=>so(Number(z)),contentStyle:_f,labelStyle:Sf,itemStyle:jf,wrapperStyle:kf}),u.jsx(Jn,{}),Ze.gateways.map((z,ee)=>u.jsx(to,{type:"monotone",dataKey:z,stackId:"1",stroke:Gr[ee%Gr.length],fill:Gr[ee%Gr.length],fillOpacity:.24,name:z},z))]})})}):u.jsx(Ra,{title:"No gateway share history yet",body:"Send real decide-gateway traffic in the selected window to populate connector share."})})]}),u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Connector success rate over time"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Historical connector score trend for the selected merchant window."}),u.jsxs("p",{className:"mt-2 text-xs font-medium text-slate-600 dark:text-[#b3b3bd]",children:["Active filters: ",se]})]}),u.jsx(cs,{content:us.sr})]})}),u.jsxs(Ge,{className:"space-y-4",children:[u.jsxs("div",{className:"rounded-[24px] border border-slate-200 bg-white p-4 dark:border-[#1d1d23] dark:bg-[#0c0c0e]",children:[u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Connector filters"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Narrow the success-rate line chart by the routing dimensions present for this merchant."})]}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:rt,disabled:!Object.values(i.dimensions).some(Boolean)&&!i.gateways.length,children:"Clear filters"})]}),q.dimensions.length?u.jsxs("div",{className:"mt-4 space-y-3",children:[u.jsx("div",{className:"grid gap-3 md:grid-cols-2 xl:grid-cols-3",children:re.map(z=>u.jsxs("label",{className:"space-y-2",children:[u.jsx("span",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:z.label}),u.jsxs("select",{value:i.dimensions[z.key]||"",onChange:ee=>Jt(z.key,ee.target.value),className:Nf(),disabled:!z.values.length,children:[u.jsxs("option",{value:"",children:["All ",z.label.toLowerCase()]}),z.values.map(ee=>u.jsx("option",{value:ee,children:ee},ee))]})]},z.key))}),Q?u.jsxs("div",{className:"flex items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-3 dark:border-[#1d1d23] dark:bg-[#09090b]",children:[u.jsx("p",{className:"text-xs text-slate-500 dark:text-[#8a8a93]",children:s?"Showing all routing dimensions available for this merchant.":`${ne} more routing dimension${ne===1?"":"s"} available for this merchant.`}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>l(z=>!z),children:s?"Show fewer filters":"More filters"})]}):null]}):q.missing_dimensions.length?u.jsx(Ra,{title:"No populated routing dimensions in this window",body:"The merchant has score history, but none of the dynamic routing dimensions have values recorded in the selected time window yet."}):null,q.missing_dimensions.length?u.jsxs("div",{className:"mt-4 rounded-2xl border border-dashed border-slate-200 bg-white px-4 py-3 dark:border-[#1d1d23] dark:bg-[#09090b]",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"No values in this window yet"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:q.missing_dimensions.map(z=>z.label).join(", ")})]}):null,ke.length?u.jsxs("div",{className:"mt-4 space-y-2",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Active filters"}),u.jsx("div",{className:"flex flex-wrap gap-2",children:ke.map(z=>u.jsxs("button",{type:"button",onClick:()=>$r(z.key),className:"inline-flex items-center gap-2 rounded-full border border-brand-500/30 bg-brand-500/10 px-3 py-1.5 text-xs font-semibold text-brand-700 transition hover:bg-brand-500/15 dark:text-brand-200",children:[u.jsx("span",{children:z.label}),u.jsx("span",{"aria-hidden":"true",children:"×"})]},z.key))})]}):null,u.jsxs("div",{className:"mt-4 space-y-2",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Connectors"}),u.jsx("div",{className:"flex flex-wrap gap-2",children:q.gateways.length?q.gateways.map(z=>{const ee=i.gateways.includes(z);return u.jsx("button",{type:"button",onClick:()=>_e(z),className:`rounded-full border px-3 py-1.5 text-xs font-semibold transition ${ee?"border-brand-500/50 bg-brand-500/10 text-brand-700 dark:text-brand-200":"border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-[#27272a] dark:bg-[#121214] dark:text-[#a1a1aa] dark:hover:text-white"}`,children:z},z)}):u.jsx("p",{className:"text-xs text-slate-500 dark:text-[#8a8a93]",children:"No connector history yet for the selected window."})})]})]}),L.length?u.jsx("div",{className:"flex flex-wrap gap-2",children:L.map(z=>u.jsxs(Ue,{variant:"blue",children:[z.gateway,": ",Of(z.value)]},z.gateway))}):null,P.rows.length?u.jsx("div",{className:"h-80",children:u.jsx(Oi,{width:"100%",height:"100%",children:u.jsxs(vce,{data:P.rows,children:[u.jsx(Gf,{strokeDasharray:"3 3",stroke:"#e2e8f0"}),u.jsx(wa,{dataKey:"bucket_ms",tickFormatter:Iy,tick:{fontSize:11}}),u.jsx(_a,{domain:H,tick:{fontSize:11},tickFormatter:z=>`${Mh(Number(z),0)}%`}),u.jsx(wr,{labelFormatter:z=>so(Number(z)),formatter:(z,ee)=>[Of(z),String(ee)],contentStyle:_f,labelStyle:Sf,itemStyle:jf,wrapperStyle:kf}),u.jsx(Jn,{}),P.gateways.map((z,ee)=>u.jsx($d,{type:"monotone",dataKey:z,stroke:Gr[ee%Gr.length],strokeWidth:3,dot:{r:3,strokeWidth:1,fill:Gr[ee%Gr.length]},activeDot:{r:5},name:z},z))]})})}):u.jsx(Ra,{title:"No connector score history yet",body:"Send decide-gateway and update-gateway-score traffic in the selected window to populate connector history."})]})]})]}):u.jsxs(u.Fragment,{children:[u.jsxs("section",{className:"space-y-4",children:[u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-lg font-semibold text-slate-900 dark:text-white",children:"Rule-based activity"}),u.jsxs("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#8a8a93]",children:["Preview-only routing activity from ",u.jsx("code",{children:"/routing/evaluate"}),", kept separate from transaction routing and gateway scoring."]})]}),u.jsx(cs,{content:us.preview_hits})]}),u.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[u.jsx(My,{label:"Rule Evaluate",value:Ie,subtitle:t==="custom"?"Custom window":te}),u.jsx(My,{label:"Gateways touched",value:Me,subtitle:"Across recent preview selections",eyebrow:"Preview coverage"})]})]}),u.jsxs("div",{className:"grid gap-4 xl:grid-cols-2",children:[u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Connector selections over time"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Time-bucketed connector counts from the fetched rule-preview sample."})]}),u.jsx(cs,{content:us.preview_activity})]})}),u.jsx(Ge,{children:Ne.gateways.length?u.jsx("div",{className:"h-80",children:u.jsx(Oi,{width:"100%",height:"100%",children:u.jsxs($0,{data:Ne.rows,barGap:6,children:[u.jsx(Gf,{strokeDasharray:"3 3",stroke:"#e2e8f0"}),u.jsx(wa,{dataKey:"bucket_ms",tickFormatter:Iy,tick:{fontSize:11}}),u.jsx(_a,{tick:{fontSize:11}}),u.jsx(wr,{labelFormatter:z=>so(Number(z)),contentStyle:_f,labelStyle:Sf,itemStyle:jf,wrapperStyle:kf}),u.jsx(Jn,{}),Ne.gateways.map((z,ee)=>u.jsx(ai,{dataKey:z,stackId:"preview-connectors",fill:z==="No gateway selected"?"#64748b":Gr[ee%Gr.length],radius:[6,6,0,0],name:z},z))]})})}):u.jsx(Ra,{title:"No connector selections yet",body:"Send /routing/evaluate preview traffic in the selected window to populate connector time-series."})})]}),u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Gateway selection mix"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Connector share across the fetched rule-preview sample."})]}),u.jsx(cs,{content:us.preview_share})]})}),u.jsx(Ge,{children:Ke.length?u.jsxs("div",{className:"grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]",children:[u.jsxs("div",{className:"relative h-80",children:[u.jsx(Oi,{width:"100%",height:"100%",children:u.jsxs(a1,{children:[u.jsx(wr,{formatter:(z,ee,ve)=>{var Le;return[`${Mh(z,0)} previews`,`${String(ee)} (${Of(((Le=ve.payload)==null?void 0:Le.percentage)||0)})`]},contentStyle:_f,labelStyle:Sf,itemStyle:jf,wrapperStyle:kf}),u.jsx(Jn,{}),u.jsx(sa,{data:Ke,dataKey:"value",nameKey:"name",innerRadius:72,outerRadius:108,paddingAngle:3,children:Ke.map(z=>u.jsx(Li,{fill:z.color},z.name))})]})}),u.jsxs("div",{className:"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Sample size"}),u.jsx("p",{className:"mt-2 text-3xl font-semibold tracking-tight text-slate-950 dark:text-white",children:De.length}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"preview groups"})]})]}),u.jsx("div",{className:"space-y-3",children:Ke.map(z=>u.jsxs("div",{className:"rounded-[20px] border border-slate-200 bg-white/80 px-4 py-3 dark:border-[#1d1d23] dark:bg-[#0c0c0e]",children:[u.jsxs("div",{className:"flex items-center justify-between gap-3",children:[u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx("span",{className:"h-2.5 w-2.5 rounded-full",style:{backgroundColor:z.color}}),u.jsx("p",{className:"text-sm font-medium text-slate-900 dark:text-white",children:z.name})]}),u.jsx("p",{className:"text-xs font-semibold text-slate-500 dark:text-[#8a8a93]",children:z.value})]}),u.jsxs("p",{className:"mt-2 text-xs text-slate-500 dark:text-[#8a8a93]",children:[Of(z.percentage)," of fetched previews"]})]},z.name))})]}):u.jsx(Ra,{title:"No preview connector mix yet",body:"Rule previews need to return gateway selections before the mix chart can render."})})]})]}),u.jsxs("div",{className:"grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]",children:[u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Recent rule previews"}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:["Preview-only evaluations captured from ",u.jsx("code",{children:"/routing/evaluate"}),". This does not affect transaction scoring."]})]}),u.jsx(Ue,{variant:"purple",children:xe?`Latest ${so(xe)}`:"No activity"})]})}),u.jsx(Ge,{children:!M.data&&M.isLoading?u.jsxs("div",{className:"flex items-center gap-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading rule previews…"]}):M.error&&!M.data?u.jsx(Xr,{error:M.error.message}):oe.length?u.jsxs("div",{className:"space-y-4",children:[u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("p",{className:"text-xs text-slate-500 dark:text-[#8a8a93]",children:["Showing ",je,"-",Be," of ",$e]}),M.isLoading?u.jsxs("div",{className:"flex items-center gap-2 text-xs text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:14}),"Loading page…"]}):null]}),u.jsx("div",{className:"space-y-3",children:oe.map(z=>u.jsxs("div",{className:"rounded-[22px] border border-slate-200 bg-white/90 px-4 py-4 dark:border-[#1d1d23] dark:bg-[#0c0c0e]",children:[u.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"truncate text-sm font-semibold text-slate-900 dark:text-white",children:z.payment_id||z.request_id||z.lookup_key}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:[z.merchant_id||"unknown merchant"," · ",so(z.last_seen_ms)]})]}),u.jsx(Ue,{variant:"purple",children:z.latest_status||"preview"})]}),u.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[u.jsx(Ue,{variant:"blue",children:"Rule Evaluate"}),z.latest_gateway?u.jsx(Ue,{variant:"green",children:z.latest_gateway}):null,u.jsxs(Ue,{variant:"gray",children:[z.event_count," events"]})]})]},z.lookup_key))}),Ee>1?u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4 dark:border-[#1d1d23]",children:[u.jsxs("p",{className:"text-xs text-slate-500 dark:text-[#8a8a93]",children:["Page ",c," of ",Ee]}),u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>f(z=>Math.max(1,z-1)),disabled:c===1||M.isLoading,children:"Previous"}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>f(z=>Math.min(Ee,z+1)),disabled:c>=Ee||M.isLoading,children:"Next"})]})]}):null]}):u.jsx(Ra,{title:"No rule-based activity yet",body:"Send /routing/evaluate preview traffic in the selected window to populate rule-based activity."})})]}),u.jsxs("div",{className:"space-y-4",children:[u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Gateway activity"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Recent preview selections grouped by latest chosen gateway."})]})}),u.jsx(Ge,{children:J.length?u.jsx("div",{className:"space-y-3",children:J.map((z,ee)=>u.jsxs("div",{className:"space-y-2",children:[u.jsxs("div",{className:"flex items-center justify-between gap-3",children:[u.jsx("p",{className:"text-sm font-medium text-slate-900 dark:text-white",children:z.gateway}),u.jsx("p",{className:"text-xs font-semibold text-slate-500 dark:text-[#8a8a93]",children:z.count})]}),u.jsx("div",{className:"h-2 overflow-hidden rounded-full bg-slate-100 dark:bg-[#141822]",children:u.jsx("div",{className:"h-full rounded-full",style:{width:`${z.count/Re*100}%`,backgroundColor:Gr[ee%Gr.length]}})})]},z.gateway))}):u.jsx(Ra,{title:"No gateway activity yet",body:"Once rule previews are captured, this panel will show which connectors are being selected."})})]}),u.jsxs(Ae,{className:"overflow-visible",children:[u.jsx(nt,{children:u.jsxs("div",{children:[u.jsx("h2",{className:"text-sm font-semibold text-slate-800 dark:text-white",children:"Recent preview outcomes"}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:"Status mix from the loaded preview sample."})]})}),u.jsx(Ge,{children:we.length?u.jsx("div",{className:"flex flex-wrap gap-2",children:we.map(z=>u.jsxs(Ue,{variant:z.status.toLowerCase().includes("fail")?"red":z.status==="default_selection"?"orange":"purple",children:[z.status," · ",z.count]},z.status))}):u.jsx(Ra,{title:"No preview outcomes yet",body:"Recent rule preview results will appear here once preview traffic is recorded."})})]})]})]})]})]}):u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900 dark:text-white",children:"Analytics"}),u.jsx("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#8a8a93]",children:"Set a merchant in the top bar to load merchant-scoped analytics."})]}),u.jsx(Ra,{title:"Select a merchant first",body:"The analytics surface is merchant-scoped. Use the merchant selector in the top bar to load data."})]})}const Rce=["15m","1h","24h"],Ice=[{value:"",label:"Any status"},{value:"success",label:"Success"},{value:"failure",label:"Failure"}],Mce=[{value:"",label:"Any route"},{value:"decide_gateway",label:"Decide Gateway"},{value:"update_gateway_score",label:"Update Gateway"},{value:"routing_evaluate",label:"Rule Evaluate"}],Dce=["summary","input","response","raw"],RO={paymentId:"",requestId:"",gateway:"",route:"",status:"",eventType:"",errorCode:""};function $s(e){const t=e.paymentId.trim(),r=t?"":e.requestId.trim();return{paymentId:t,requestId:r,gateway:e.gateway.trim(),route:e.route,status:e.status,eventType:e.eventType,errorCode:e.errorCode.trim()}}function uR(e){const t=new URLSearchParams;return Object.entries(e).forEach(([r,n])=>{n!==void 0&&n!==""&&t.set(r,String(n))}),t.toString()}function IO(e,t,r,n,a,i){const o=$s(i),s={scope:"current",range:t,page:n,page_size:a,merchant_id:r,payment_id:o.paymentId||void 0,request_id:o.requestId||void 0,gateway:o.gateway||void 0,route:o.route||void 0,status:o.status||void 0,event_type:o.eventType||void 0,error_code:o.errorCode||void 0},l=uR(s);return l?`${e}?${l}`:e}function Lce(e){return e==="15m"||e==="24h"?e:"24h"}function Fce(e){return e==="rule_based"?"rule_based":"transactions"}function zce(e){return $s({paymentId:e.get("payment_id")||"",requestId:e.get("request_id")||"",gateway:e.get("gateway")||"",route:e.get("route")||"",status:e.get("status")||"",eventType:e.get("event_type")||"",errorCode:e.get("error_code")||""})}function Kf(e){return new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(new Date(e))}function Bce(e){const t=Math.max(0,Math.round((Date.now()-e)/6e4));if(t<1)return"just now";if(t<60)return`${t}m ago`;const r=Math.round(t/60);return r<24?`${r}h ago`:`${Math.round(r/24)}d ago`}function mo(e){return e?e.replace(/[_-]+/g," ").replace(/\s+/g," ").trim().toLowerCase().replace(/\b\w/g,r=>r.toUpperCase()):""}function wu(e){return e.filter(Boolean).join(" · ")}function cR(e){return e?e==="decision_gateway"||e==="decide_gateway"?"Decide Gateway":e==="update_gateway_score"?"Update Gateway":e==="routing_evaluate"?"Rule Evaluate":mo(e):"Unknown route"}function D0(e){return e.event_stage==="gateway_decided"?"Decide Gateway":e.event_stage==="score_updated"?"Update Gateway":e.event_stage==="rule_applied"?"Rule Evaluate":e.event_stage==="preview_evaluated"||e.event_type==="rule_evaluation_preview"?"Preview Result":e.event_type==="error"?"Errors":mo(e.event_stage||e.event_type)}function L0(e){return e.event_type==="decision"||e.event_stage==="gateway_decided"?"Decide Gateway":e.event_type==="rule_hit"||e.event_stage==="rule_applied"?"Rule Evaluate":e.event_type==="rule_evaluation_preview"||e.event_stage==="preview_evaluated"?"Rule Preview":e.event_type==="gateway_update"||e.event_stage==="score_updated"?"Update Gateway":"Errors"}function Ef(e){const t=(e.status||"").toUpperCase();return e.event_type==="error"||t==="FAILURE"||t.includes("FAILED")||t.includes("DECLINED")?"red":e.event_type==="rule_evaluation_preview"||e.event_type==="rule_hit"?"purple":t==="CHARGED"||t==="AUTHORIZED"||t==="SUCCESS"||e.event_type==="gateway_update"?"green":e.event_type==="decision"?"blue":"orange"}function Dy(e){const t=(e||"").toUpperCase();return t==="FAILURE"||t.includes("FAILED")||t.includes("DECLINED")?"red":t==="SUCCESS"||t==="CHARGED"||t==="AUTHORIZED"?"green":t==="HIT"?"purple":"gray"}function _u(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function Ly(e){return Object.fromEntries(Object.entries(e).filter(([,t])=>t!=null&&t!==""))}function Uce(e){return typeof e=="string"?e:JSON.stringify(e,null,2)}function Pf(e){return e?"!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white":"!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white"}function ds(){return"h-11 rounded-2xl border border-slate-200 bg-white/90 px-4 text-sm text-slate-700 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.2)] outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:border-[#2a303a] dark:bg-[#161b24] dark:text-[#e5ecf7] dark:shadow-none"}function Cf({label:e,value:t,helper:r}){return u.jsxs(Ae,{className:"p-5",children:[u.jsx(hn,{children:e}),u.jsx("p",{className:"mt-4 text-3xl font-semibold tracking-tight text-slate-950 dark:text-white",children:t}),u.jsx("p",{className:"mt-2 text-sm text-slate-500 dark:text-[#b2bdd1]",children:r})]})}function Mu({title:e,body:t}){return u.jsxs(jE,{className:"border-dashed border-slate-200 bg-slate-50/70 px-6 py-12 text-center dark:border-[#2a303a] dark:bg-[#161b24]/80",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e}),u.jsx("p",{className:"mt-2 text-sm text-slate-500 dark:text-[#b2bdd1]",children:t})]})}function Vce({rows:e}){return e.length?u.jsx("div",{className:"grid gap-3 md:grid-cols-2",children:e.map(t=>u.jsxs(jE,{className:"px-4 py-3",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-[#8390a7]",children:t.label}),u.jsx("p",{className:"mt-2 text-sm text-slate-900 dark:text-white break-words",children:t.value})]},`${t.label}-${t.value}`))}):null}function fs({title:e,value:t,emptyMessage:r}){return u.jsxs("div",{className:"space-y-3",children:[u.jsx("div",{children:u.jsx("h3",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:e})}),t?u.jsx("pre",{className:"overflow-x-auto rounded-[22px] border border-slate-200 bg-slate-950/95 px-4 py-4 text-xs leading-6 text-slate-200 shadow-[0_16px_30px_-28px_rgba(15,23,42,0.4)] dark:border-[#2a303a] dark:bg-[#0b1017] dark:text-[#d8e1ef] dark:shadow-none",children:Uce(t)}):u.jsx(Mu,{title:`No ${e.toLowerCase()} captured`,body:r})]})}function Wce(e){if(!e)return null;const t=_u(e.details_json)?e.details_json:{},r=t.response??t.response_payload??t.result??t.output??null,n=t.request??t.request_payload??t.input??t.payload??Ly({payment_id:e.payment_id,request_id:e.request_id,payment_method_type:e.payment_method_type,payment_method:e.payment_method,gateway:e.gateway}),a=r??Ly({event_type:e.event_type,status:e.status,error_code:e.error_code,error_message:e.error_message,score_value:e.score_value,sigma_factor:e.sigma_factor,average_latency:e.average_latency,tp99_latency:e.tp99_latency,transaction_count:e.transaction_count,rule_name:e.rule_name,routing_approach:e.routing_approach}),i=_u(r)?r:null,o=_u(i==null?void 0:i.decided_gateway)?i.decided_gateway:null,s=t.score_context??(o?o.gateway_priority_map:null)??(i?i.gateway_priority_map:null)??null,l=t.selection_reason??null,c=[{label:"Phase",value:L0(e)},{label:"Stage",value:D0(e)},{label:"Route",value:cR(e.route)},{label:"Timestamp",value:Kf(e.created_at_ms)},{label:"Merchant",value:e.merchant_id||"unknown merchant"},...e.payment_id?[{label:"Payment ID",value:e.payment_id}]:[],...e.request_id?[{label:"Request ID",value:e.request_id}]:[],...e.gateway?[{label:"Gateway",value:e.gateway}]:[],...e.status?[{label:"Status",value:e.status}]:[]],f=Ly(Object.fromEntries(Object.entries(t).filter(([d])=>!["request","request_payload","input","payload","response","response_payload","result","output","score_context","selection_reason"].includes(d))));return{summaryRows:c,requestPayload:_u(n)&&!Object.keys(n).length?null:n,responsePayload:_u(a)&&!Object.keys(a).length?null:a,scoreContext:s,selectionReason:l,signalRecord:Object.keys(f).length?f:null,rawEvent:{...e,details_json:e.details_json}}}function Hce(){var Ee,je,Be,Me,Re,Ke,Ze,P,L,H,se,re,Q,ne,ke;const{merchantId:e}=ka(),[t,r]=zD(),n=Fce(t.get("mode")),a=Lce(t.get("range")),i=zce(t),o=Math.max(1,Number(t.get("page")||"1")),s=t.get("selected")||"",[l,c]=j.useState(n),[f,d]=j.useState(a),[p,h]=j.useState(i),[x,v]=j.useState(i),[y,g]=j.useState(o),[m,w]=j.useState(s),[S,b]=j.useState(null),[_,k]=j.useState("summary"),O=12,N=!!e,T=l==="rule_based"?"/analytics/preview-trace":"/analytics/payment-audit",R=N&&e?IO(T,f,e,y,O,x):null,A=Bt(R,pn,{refreshInterval:12e3,revalidateOnFocus:!0}),C=j.useMemo(()=>{var le;const W=((le=A.data)==null?void 0:le.results)||[];return W.find(_e=>_e.lookup_key===m)||W[0]||null},[(Ee=A.data)==null?void 0:Ee.results,m]);j.useEffect(()=>{var le,_e;if(C!=null&&C.lookup_key){w(C.lookup_key);return}const W=(_e=(le=A.data)==null?void 0:le.results)==null?void 0:_e[0];W!=null&&W.lookup_key&&w(W.lookup_key)},[(je=A.data)==null?void 0:je.results,C==null?void 0:C.lookup_key]);const M=j.useMemo(()=>{if(!C)return null;const W=C.payment_id||"";return{paymentId:W,requestId:W?"":C.request_id||"",gateway:"",route:"",status:"",eventType:"",errorCode:""}},[C]),B=N&&e&&M?IO(T,f,e,1,50,M):null,V=Bt(B,pn,{refreshInterval:12e3,revalidateOnFocus:!0}),I=j.useMemo(()=>{var le;const W=((le=V.data)==null?void 0:le.timeline)||[];return W.find(_e=>_e.id===S)||W[0]||null},[(Be=V.data)==null?void 0:Be.timeline,S]);j.useEffect(()=>{var le,_e;if(I!=null&&I.id){b(I.id);return}const W=(_e=(le=V.data)==null?void 0:le.timeline)==null?void 0:_e[0];W!=null&&W.id&&b(W.id)},[(Me=V.data)==null?void 0:Me.timeline,I==null?void 0:I.id]);const D=j.useMemo(()=>{var le;const W=[];for(const _e of((le=V.data)==null?void 0:le.timeline)||[]){const rt=L0(_e),$r=W[W.length-1];!$r||$r.phase!==rt?W.push({phase:rt,events:[_e]}):$r.events.push(_e)}return W},[(Re=V.data)==null?void 0:Re.timeline]),U=j.useMemo(()=>Wce(I),[I]),G=((Ke=A.error)==null?void 0:Ke.message)||((Ze=V.error)==null?void 0:Ze.message)||null,q=A.isLoading||V.isLoading,K=((L=(P=V.data)==null?void 0:P.timeline)==null?void 0:L.length)||0,te=((H=C==null?void 0:C.gateways)==null?void 0:H.length)||0,Z=C?Bce(C.last_seen_ms):"No activity",de=l==="rule_based"?{title:"Decision Audit",description:"Inspect preview-only rule activity from /routing/evaluate without mixing it into transaction outcomes.",merchantPrompt:"Use the merchant selector in the top bar to load the preview trace for a merchant.",searchTitle:"Search Rule Preview Trail",searchDescription:"Use preview payment IDs or request IDs when you have them. Gateway, status, and error code help narrow rule-preview activity quickly.",matchingLabel:"Matching previews",matchingDescription:"Scan the current result set and pick a preview to open its full trace.",summaryLabel:"Selected Preview Timeline",summaryEmpty:"Pick a preview from the left column to see the full rule evaluation trace.",noMatchesTitle:"No matching previews found",noMatchesBody:"Try widening the time range or searching by a preview payment ID, request ID, or gateway."}:{title:"Decision Audit",description:"Search by payment or request, then inspect gateway decisions, gateway updates, rule evaluations, and errors with the exact payload captured at each step.",merchantPrompt:"Use the merchant selector in the top bar to load the decision trail for a merchant.",searchTitle:"Search Decision Trail",searchDescription:"Use payment or request IDs when you have them. Error code, gateway, route, and status narrow operational noise quickly.",matchingLabel:"Matching payments",matchingDescription:"Scan the current result set and pick a payment to open its full event trail.",summaryLabel:"Selected Payment Timeline",summaryEmpty:"Pick a payment from the left column to see the full transaction trail.",noMatchesTitle:"No matching payments found",noMatchesBody:"Try widening the time range or searching by a single payment ID, request ID, or error code."};function fe(W,le,_e,rt,$r){const Jt=$s(rt),Rr=uR({mode:W==="rule_based"?W:void 0,range:le,page:_e>1?_e:void 0,payment_id:Jt.paymentId||void 0,request_id:Jt.requestId||void 0,gateway:Jt.gateway||void 0,route:Jt.route||void 0,status:Jt.status||void 0,event_type:Jt.eventType||void 0,error_code:Jt.errorCode||void 0,selected:$r||void 0});r(Rr)}function Ie(W,le){h(_e=>$s({..._e,[W]:le}))}function De(){const le=$s({...p,route:l==="rule_based"?"":p.route});g(1),h(le),v(le),fe(l,f,1,le)}function oe(){const le={...RO,route:l==="rule_based"?"":RO.route};g(1),h(le),v(le),fe(l,f,1,le)}function J(){A.mutate(),V.mutate()}function we(W){d(W),g(1),fe(l,W,1,x,m)}function Y(W){w(W),fe(l,f,y,x,W)}function Ne(W){const _e=$s({...p,route:W==="rule_based"?"":p.route});c(W),g(1),w(""),b(null),h(_e),v(_e),fe(W,f,1,_e)}async function xe(W){if(W)try{await navigator.clipboard.writeText(W)}catch{}}function $e(){if(!I)return;const W=I.payment_id||"",le={paymentId:W,requestId:W?"":I.request_id||"",gateway:I.gateway||"",route:"",status:"",eventType:"",errorCode:""};h(le),v(le),g(1),fe(l,f,1,le,m)}return N?u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-4",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900 dark:text-white",children:de.title}),u.jsx("p",{className:"mt-1 max-w-3xl text-sm text-slate-500 dark:text-[#8a8a93]",children:de.description})]}),u.jsx("div",{className:"flex flex-wrap items-center gap-2",children:u.jsx(Ce,{size:"sm",variant:"ghost",onClick:J,children:"Refresh"})})]}),u.jsxs("div",{className:"space-y-3",children:[u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[u.jsx(Ce,{size:"sm",variant:"secondary",className:Pf(l==="transactions"),onClick:()=>Ne("transactions"),children:"Transactions"}),u.jsx(Ce,{size:"sm",variant:"secondary",className:Pf(l==="rule_based"),onClick:()=>Ne("rule_based"),children:"Rule-Based"}),u.jsx(Ue,{variant:"green",children:e||"Current merchant"})]}),u.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[u.jsx("p",{className:"text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-[#8a8a93]",children:"Time window"}),Rce.map(W=>u.jsx(Ce,{size:"sm",variant:"secondary",className:Pf(f===W),onClick:()=>we(W),children:W},W))]})]}),u.jsxs(Ae,{className:"p-6",children:[u.jsxs("div",{className:"border-b border-slate-200 pb-5 dark:border-[#2a303a]",children:[u.jsx(hn,{children:de.searchTitle}),u.jsx("p",{className:"mt-3 text-sm text-slate-500 dark:text-[#b2bdd1]",children:de.searchDescription})]}),u.jsxs("div",{className:"space-y-4 pt-5",children:[u.jsxs("div",{className:`grid gap-3 md:grid-cols-2 ${l==="rule_based"?"xl:grid-cols-3":"xl:grid-cols-4"}`,children:[u.jsx("input",{className:ds(),value:p.paymentId,onChange:W=>Ie("paymentId",W.target.value),placeholder:"Payment ID"}),u.jsx("input",{className:ds(),value:p.requestId,onChange:W=>Ie("requestId",W.target.value),placeholder:"Request ID"}),u.jsx("input",{className:ds(),value:p.gateway,onChange:W=>Ie("gateway",W.target.value),placeholder:"Gateway"}),l==="transactions"?u.jsx("select",{className:ds(),value:p.route,onChange:W=>Ie("route",W.target.value),children:Mce.map(W=>u.jsx("option",{value:W.value,children:W.label},W.value||"all"))}):null,u.jsx("input",{className:ds(),value:p.errorCode,onChange:W=>Ie("errorCode",W.target.value),placeholder:"Error code"}),u.jsx("select",{className:ds(),value:p.status,onChange:W=>Ie("status",W.target.value),children:Ice.map(W=>u.jsx("option",{value:W.value,children:W.label},W.value||"all"))})]}),u.jsxs("div",{className:"flex flex-wrap gap-2",children:[u.jsx(Ce,{size:"sm",onClick:De,children:"Search"}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:oe,children:"Clear"})]})]})]}),u.jsx(Xr,{error:G}),q&&u.jsxs("div",{className:"flex items-center gap-2 text-sm text-slate-500 dark:text-[#8a8a93]",children:[u.jsx(pr,{size:16}),"Loading decision audit data…"]}),u.jsxs("section",{className:"grid gap-4 md:grid-cols-2 xl:grid-cols-4",children:[u.jsx(Cf,{label:de.matchingLabel,value:String(((se=A.data)==null?void 0:se.total_results)||0),helper:"Results within the selected time window"}),u.jsx(Cf,{label:"Timeline events",value:String(K),helper:l==="rule_based"?"Captured for the selected preview":"Captured for the selected payment"}),u.jsx(Cf,{label:"Active gateways",value:String(te),helper:l==="rule_based"?"Distinct gateways seen on the selected preview":"Distinct gateways seen on the selected payment"}),u.jsx(Cf,{label:"Latest activity",value:Z,helper:l==="rule_based"?"Most recent event on the selected preview":"Most recent event on the selected payment"})]}),u.jsxs("div",{className:"grid gap-4 xl:grid-cols-[360px_minmax(0,1fr)]",children:[u.jsxs(Ae,{className:"p-6",children:[u.jsxs("div",{className:"flex items-center justify-between gap-3 border-b border-slate-200 pb-5 dark:border-[#2a303a]",children:[u.jsxs("div",{children:[u.jsx(hn,{children:de.matchingLabel}),u.jsx("p",{className:"mt-3 text-sm text-slate-500 dark:text-[#b2bdd1]",children:de.matchingDescription})]}),u.jsx("div",{className:"flex items-center justify-between gap-3",children:u.jsxs("div",{className:"flex items-center gap-2",children:[u.jsx(Ce,{size:"sm",variant:"secondary",disabled:y<=1,onClick:()=>{const W=Math.max(1,y-1);g(W),fe(l,f,W,x,m)},children:"Prev"}),u.jsx(Ce,{size:"sm",variant:"secondary",disabled:(((Q=(re=A.data)==null?void 0:re.results)==null?void 0:Q.length)||0){const W=y+1;g(W),fe(l,f,W,x,m)},children:"Next"})]})})]}),u.jsx("div",{className:"space-y-3 pt-5",children:(ke=(ne=A.data)==null?void 0:ne.results)!=null&&ke.length?A.data.results.map(W=>u.jsxs("button",{type:"button",onClick:()=>Y(W.lookup_key),className:`w-full rounded-[24px] border p-4 text-left transition-all ${(C==null?void 0:C.lookup_key)===W.lookup_key?"border-slate-200 bg-slate-50 shadow-[0_14px_30px_-28px_rgba(15,23,42,0.25)] dark:border-[#2a303a] dark:bg-[#161b24] dark:shadow-none":"border-transparent bg-transparent hover:border-slate-200 hover:bg-slate-50/80 dark:hover:border-[#2a303a] dark:hover:bg-[#161b24]/75"}`,children:[u.jsxs("div",{className:"flex items-start justify-between gap-3",children:[u.jsxs("div",{className:"min-w-0",children:[u.jsx("p",{className:"truncate text-sm font-semibold text-slate-900 dark:text-white",children:W.payment_id||W.request_id||W.lookup_key}),u.jsxs("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:[W.merchant_id||"unknown merchant"," · ",Kf(W.last_seen_ms)]})]}),u.jsx(Ue,{variant:Dy(W.latest_status),children:mo(W.latest_status)||"Unknown"})]}),u.jsx("p",{className:"mt-3 text-xs text-slate-500 dark:text-[#9dabc0]",children:wu([W.latest_stage?mo(W.latest_stage):null,W.latest_gateway?`gateway ${W.latest_gateway}`:null,`${W.event_count} events`])}),W.request_id?u.jsxs("p",{className:"mt-3 truncate text-[11px] text-slate-500 dark:text-[#8a8a93]",children:["request ",W.request_id]}):null]},W.lookup_key)):u.jsx(Mu,{title:de.noMatchesTitle,body:de.noMatchesBody})})]}),u.jsxs("div",{className:"grid gap-4 xl:grid-cols-[minmax(0,1fr)_380px]",children:[u.jsxs(Ae,{className:"overflow-visible p-6",children:[u.jsx("div",{className:"flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 pb-5 dark:border-[#2a303a]",children:u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx(hn,{children:de.summaryLabel}),u.jsx("p",{className:"mt-3 text-sm text-slate-500 dark:text-[#b2bdd1]",children:(C==null?void 0:C.payment_id)||(C==null?void 0:C.request_id)||(l==="rule_based"?"Choose a preview from the result list to inspect the timeline.":"Choose a payment from the result list to inspect the timeline.")})]}),u.jsxs("div",{className:"flex flex-wrap items-center gap-3",children:[u.jsx("p",{className:"text-xs text-slate-500 dark:text-[#9dabc0]",children:wu([C!=null&&C.latest_stage?mo(C.latest_stage):null,C!=null&&C.latest_gateway?`gateway ${C.latest_gateway}`:null])}),C!=null&&C.latest_status?u.jsx(Ue,{variant:Dy(C.latest_status),children:mo(C.latest_status)}):null]})]})}),u.jsx("div",{className:"pt-5",children:D.length?u.jsx("div",{className:"space-y-6",children:D.map(W=>u.jsxs("div",{className:"space-y-3",children:[u.jsxs("div",{className:"flex items-center gap-3",children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:W.phase}),u.jsxs("p",{className:"text-xs text-slate-500 dark:text-[#8a8a93]",children:[W.events.length," event",W.events.length===1?"":"s"]})]}),u.jsx("div",{className:"relative space-y-3 pl-6 before:absolute before:left-2 before:top-2 before:bottom-2 before:w-px before:bg-slate-200 dark:before:bg-[#23232a]",children:W.events.map(le=>{const _e=(I==null?void 0:I.id)===le.id;return u.jsxs("button",{type:"button",onClick:()=>{b(le.id),k("summary")},className:`relative w-full rounded-[24px] border p-5 text-left transition ${_e?"border-slate-200 bg-slate-50 shadow-[0_16px_30px_-28px_rgba(15,23,42,0.28)] dark:border-[#2a303a] dark:bg-[#161b24] dark:shadow-none":"border-slate-200/70 bg-white/70 hover:border-slate-300 hover:bg-white dark:border-[#2a303a]/70 dark:bg-[#131923] dark:hover:border-[#2a303a] dark:hover:bg-[#161b24]"}`,children:[u.jsx("span",{className:`absolute -left-[25px] top-6 h-3 w-3 rounded-full ${Ef(le)==="red"?"bg-red-400":Ef(le)==="green"?"bg-emerald-400":Ef(le)==="purple"?"bg-purple-400":Ef(le)==="orange"?"bg-orange-400":"bg-blue-400"}`}),u.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx("p",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:D0(le)}),u.jsx("p",{className:"mt-1 text-xs text-slate-500 dark:text-[#8a8a93]",children:wu([cR(le.route),Kf(le.created_at_ms),le.gateway?`gateway ${le.gateway}`:null])})]}),u.jsx("div",{className:"flex flex-wrap gap-2",children:le.status?u.jsx(Ue,{variant:Dy(le.status),children:mo(le.status)}):null})]}),u.jsx("p",{className:"mt-4 text-xs text-slate-500 dark:text-[#8a8a93]",children:wu([le.request_id?`request ${le.request_id}`:null,le.routing_approach?`approach ${le.routing_approach}`:null,le.rule_name?`rule ${le.rule_name}`:null,le.payment_method_type||le.payment_method?wu([le.payment_method_type?`PMT ${le.payment_method_type}`:null,le.payment_method?`method ${le.payment_method}`:null]):null,le.error_code?`error ${le.error_code}`:null])}),le.error_message?u.jsx("p",{className:"mt-4 rounded-2xl border border-red-500/20 bg-red-500/[0.08] px-4 py-3 text-sm text-red-600 dark:text-red-300",children:le.error_message}):null]},le.id)})})]},W.phase))}):u.jsx(Mu,{title:"No timeline selected yet",body:de.summaryEmpty})})]}),u.jsxs(Ae,{className:"overflow-visible p-6 xl:sticky xl:top-6 xl:self-start",children:[u.jsx("div",{className:"space-y-4 border-b border-slate-200 pb-5 dark:border-[#2a303a]",children:u.jsxs("div",{className:"space-y-3",children:[u.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[u.jsxs("div",{children:[u.jsx(hn,{children:"Event Inspector"}),u.jsx("p",{className:"mt-3 text-sm text-slate-500 dark:text-[#b2bdd1]",children:I?`${D0(I)} · ${Kf(I.created_at_ms)}`:"Select a timeline event to inspect the captured payload."})]}),I?u.jsx("p",{className:"text-xs text-slate-500 dark:text-[#9dabc0]",children:L0(I)}):null]}),u.jsx("div",{className:"flex flex-wrap gap-2",children:Dce.map(W=>u.jsx(Ce,{size:"sm",variant:"secondary",className:Pf(_===W),onClick:()=>k(W),children:W==="summary"?"Summary":W==="input"?"Input":W==="response"?"Response":"Raw JSON"},W))})]})}),u.jsx("div",{className:"space-y-4 pt-5",children:I&&U?u.jsxs(u.Fragment,{children:[u.jsxs("div",{className:"flex flex-wrap gap-2",children:[u.jsx(Ce,{size:"sm",variant:"secondary",onClick:()=>k("raw"),children:"View payload"}),u.jsx(Ce,{size:"sm",variant:"secondary",disabled:!I.request_id,onClick:()=>xe(I.request_id),children:"Copy request ID"}),u.jsx(Ce,{size:"sm",variant:"secondary",disabled:!I.payment_id,onClick:()=>xe(I.payment_id),children:"Copy payment ID"}),u.jsx(Ce,{size:"sm",variant:"secondary",onClick:$e,children:"Open related events"})]}),_==="summary"?u.jsxs("div",{className:"space-y-4",children:[u.jsx(Vce,{rows:U.summaryRows}),u.jsx(fs,{title:"Connector score context",value:U.scoreContext,emptyMessage:"No connector score map was captured for this event."}),u.jsx(fs,{title:"Selection reason",value:U.selectionReason,emptyMessage:"No explicit selection reason was captured for this event."}),u.jsx(fs,{title:"Score / rule details",value:U.signalRecord,emptyMessage:"This event did not capture additional scoring or rule metadata."})]}):null,_==="input"?u.jsx(fs,{title:"Input",value:U.requestPayload,emptyMessage:"No dedicated request payload was captured for this event."}):null,_==="response"?u.jsx(fs,{title:"Response",value:U.responsePayload,emptyMessage:"No dedicated response payload was captured for this event."}):null,_==="raw"?u.jsx(fs,{title:"Raw JSON",value:U.rawEvent,emptyMessage:"No raw payload is available for this event."}):null]}):u.jsx(Mu,{title:"No event selected",body:"Pick a timeline step to see the request, response, transaction context, and raw payload."})})]})]})]})]}):u.jsxs("div",{className:"space-y-6",children:[u.jsxs("div",{children:[u.jsx("h1",{className:"text-2xl font-semibold text-slate-900 dark:text-white",children:de.title}),u.jsx("p",{className:"mt-1 text-sm text-slate-500 dark:text-[#8a8a93]",children:de.description})]}),u.jsx(Mu,{title:l==="rule_based"?"Select a merchant to start auditing previews":"Select a merchant to start auditing payments",body:de.merchantPrompt})]})}function Gce(){return u.jsx(ID,{children:u.jsxs(On,{element:u.jsx(U3,{}),children:[u.jsx(On,{index:!0,element:u.jsx(_L,{})}),u.jsx(On,{path:"routing",element:u.jsx(SL,{})}),u.jsx(On,{path:"routing/sr",element:u.jsx(T4,{})}),u.jsx(On,{path:"routing/rules",element:u.jsx(O6,{})}),u.jsx(On,{path:"routing/volume",element:u.jsx(gce,{})}),u.jsx(On,{path:"routing/debit",element:u.jsx(bce,{})}),u.jsx(On,{path:"decisions",element:u.jsx(Ace,{})}),u.jsx(On,{path:"analytics",element:u.jsx($ce,{})}),u.jsx(On,{path:"audit",element:u.jsx(Hce,{})}),u.jsx(On,{path:"*",element:u.jsx(TD,{to:".",replace:!0})})]})})}class qce extends j.Component{constructor(){super(...arguments);u1(this,"state",{error:null,errorInfo:null})}static getDerivedStateFromError(r){return{error:r,errorInfo:null}}componentDidCatch(r,n){console.log(` +`+"!".repeat(80)),console.log("[ERROR BOUNDARY] Component Error Caught"),console.log(`Timestamp: ${new Date().toISOString()}`),console.log("Error Message:",r.message),console.log("Error Stack:",r.stack),console.log("Component Stack:",n.componentStack),console.log("!".repeat(80)+` +`),this.setState({errorInfo:n})}render(){return this.state.error?u.jsxs("div",{style:{padding:32,fontFamily:"monospace",color:"red"},children:[u.jsx("h2",{children:"Dashboard Error"}),u.jsx("pre",{children:this.state.error.message}),u.jsx("pre",{children:this.state.error.stack}),this.state.errorInfo&&u.jsxs("pre",{style:{marginTop:16,color:"darkred"},children:["Component Stack:",this.state.errorInfo.componentStack]})]}):this.props.children}}const Kce="/dashboard/".endsWith("/")?"/dashboard/".slice(0,-1):"/dashboard/";console.log(` +`+"=".repeat(80));console.log("[APP STARTUP] Dashboard initializing...");console.log(`Timestamp: ${new Date().toISOString()}`);console.log("Environment: production");console.log("Base URL: /dashboard/");console.log("=".repeat(80)+` +`);window.onerror=(e,t,r,n,a)=>{console.log(` +`+"!".repeat(80)),console.log("[WINDOW ERROR]"),console.log("Message:",e),console.log("Source:",t),console.log("Line:",r,"Column:",n),a&&(console.log("Error:",a.message),console.log("Stack:",a.stack)),console.log("!".repeat(80)+` +`)};window.onunhandledrejection=e=>{console.log(` +`+"!".repeat(80)),console.log("[UNHANDLED PROMISE REJECTION]"),console.log("Reason:",e.reason),e.reason instanceof Error&&console.log("Stack:",e.reason.stack),console.log("!".repeat(80)+` +`)};Fy.createRoot(document.getElementById("root")).render(u.jsx(E.StrictMode,{children:u.jsx(qce,{children:u.jsx(FD,{basename:Kce,children:u.jsx(Gce,{})})})})); diff --git a/website/dist/assets/index-CMNgPdXY.css b/website/dist/assets/index-CMNgPdXY.css new file mode 100644 index 00000000..a7adebd7 --- /dev/null +++ b/website/dist/assets/index-CMNgPdXY.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Outfit,Inter,system-ui,-apple-system,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Menlo,Monaco,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{color-scheme:light}html.dark{color-scheme:dark}html,body{font-family:Outfit,Inter,system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark html,.dark body{color:#f1f5f9}html,body{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}html:is(.dark *),body:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.dark p,.dark span,.dark h1,.dark h2,.dark h3,.dark h4,.dark h5,.dark h6{color:#f1f5f9}p,span,h1,h2,h3,h4,h5,h6{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}p:is(.dark *),span:is(.dark *),h1:is(.dark *),h2:is(.dark *),h3:is(.dark *),h4:is(.dark *),h5:is(.dark *),h6:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark p.text-slate-500:is(.dark *),.dark span.text-slate-500:is(.dark *),.dark div.text-slate-500:is(.dark *){color:#64748b}p.text-slate-500:is(.dark *),span.text-slate-500:is(.dark *),div.text-slate-500:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}p.text-slate-600:is(.dark *),span.text-slate-600:is(.dark *),div.text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}p.\!text-slate-600:is(.dark *),span.\!text-slate-600:is(.dark *),div.\!text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark .text-slate-900,.dark .text-slate-800{color:#f1f5f9!important}.dark .text-slate-700{color:#e2e8f0!important}.dark .text-slate-600,.dark .\!text-slate-600{color:#cbd5e1!important}.dark .text-slate-500{color:#94a3b8!important}.dark .text-slate-400{color:#64748b!important}.dark .text-blue-800,.dark .text-blue-700{color:#93c5fd!important}.dark .text-blue-600{color:#60a5fa!important}.dark .text-brand-500{color:#818cf8!important}.dark .bg-slate-50{--tw-bg-opacity: 1 !important;background-color:rgb(26 26 29 / var(--tw-bg-opacity, 1))!important}.dark .bg-slate-100,.dark .\!bg-slate-100{--tw-bg-opacity: 1 !important;background-color:rgb(34 34 38 / var(--tw-bg-opacity, 1))!important}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1));-webkit-transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(51 51 51 / var(--tw-bg-opacity, 1))}.dark :where(input:not([type=checkbox]):not([type=radio]):not([type=range])),.dark :where(select),.dark :where(textarea){color:#f1f5f9}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])),:where(select),:where(textarea){border-radius:9999px;--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding-left:1rem;padding-right:1rem;--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]))::-moz-placeholder,:where(select)::-moz-placeholder,:where(textarea)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]))::placeholder,:where(select)::placeholder,:where(textarea)::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])),:where(select),:where(textarea){transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])):focus,:where(select):focus,:where(textarea):focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: rgb(99 102 241 / .2)}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])):is(.dark *),:where(select):is(.dark *),:where(textarea):is(.dark *){border-color:transparent;--tw-bg-opacity: 1;background-color:rgb(15 15 17 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])):is(.dark *)::-moz-placeholder,:where(select):is(.dark *)::-moz-placeholder,:where(textarea):is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(102 102 110 / var(--tw-placeholder-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])):is(.dark *)::placeholder,:where(select):is(.dark *)::placeholder,:where(textarea):is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(102 102 110 / var(--tw-placeholder-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])):focus:is(.dark *),:where(select):focus:is(.dark *),:where(textarea):focus:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 51 56 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(21 21 24 / var(--tw-bg-opacity, 1))}:where(input:not([type=checkbox]):not([type=radio]):not([type=range])),:where(select),:where(textarea){box-shadow:0 1px 2px #0000000d!important;font-family:Outfit,sans-serif}.dark input:not([type=checkbox]):not([type=radio]):not([type=range]),.dark select,.dark textarea{box-shadow:none!important}.dark select option{color:#f1f5f9}select option{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}select option:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 15 17 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}input[type=range]{accent-color:#6366f1}input[type=range]:is(.dark *){accent-color:#fff}input[type=radio],input[type=checkbox]{height:1rem;width:1rem;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));accent-color:#6366f1;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}input[type=radio]:is(.dark *),input[type=checkbox]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(34 34 38 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(15 15 17 / var(--tw-bg-opacity, 1));accent-color:#fff}input[type=radio]{border-radius:50%}input[type=checkbox]{border-radius:4px}input[type=radio]:checked,input[type=checkbox]:checked{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}input[type=radio]:checked:is(.dark *),input[type=checkbox]:checked:is(.dark *){--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.-left-16{left:-4rem}.-left-\[25px\]{left:-25px}.bottom-0{bottom:0}.left-1{left:.25rem}.left-64{left:16rem}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-12{top:3rem}.top-6{top:1.5rem}.top-\[76px\]{top:76px}.-z-10{z-index:-10}.z-10{z-index:10}.z-20{z-index:20}.z-\[120\]{z-index:120}.z-\[130\]{z-index:130}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-11{height:2.75rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-72{height:18rem}.h-8{height:2rem}.h-80{height:20rem}.h-\[78px\]{height:78px}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.min-h-0{min-height:0px}.min-h-\[150px\]{min-height:150px}.min-h-\[158px\]{min-height:158px}.w-10{width:2.5rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[3px\]{width:3px}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[220px\]{min-width:220px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-\[1380px\]{max-width:1380px}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[1fr_100px_32px\]{grid-template-columns:1fr 100px 32px}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-\[\#1c1c24\]>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(28 28 36 / var(--tw-divide-opacity, 1))}.divide-slate-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(241 245 249 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[14px\]{border-radius:14px}.rounded-\[16px\]{border-radius:16px}.rounded-\[18px\]{border-radius:18px}.rounded-\[20px\]{border-radius:20px}.rounded-\[22px\]{border-radius:22px}.rounded-\[24px\]{border-radius:24px}.rounded-\[30px\]{border-radius:30px}.rounded-\[40px\]{border-radius:40px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.\!border-slate-200{--tw-border-opacity: 1 !important;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))!important}.\!border-transparent{border-color:transparent!important}.border-\[\#1c2d50\]{--tw-border-opacity: 1;border-color:rgb(28 45 80 / var(--tw-border-opacity, 1))}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-brand-500\/30{border-color:#6366f14d}.border-brand-500\/50{border-color:#6366f180}.border-emerald-500\/20{border-color:#10b98133}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-red-500\/20{border-color:#ef444433}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-200\/70{border-color:#e2e8f0b3}.border-transparent{border-color:transparent}.border-white\/10{border-color:#ffffff1a}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.\!bg-slate-100{--tw-bg-opacity: 1 !important;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))!important}.\!bg-white{--tw-bg-opacity: 1 !important;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))!important}.bg-\[\#07070b\]{--tw-bg-opacity: 1;background-color:rgb(7 7 11 / var(--tw-bg-opacity, 1))}.bg-\[\#0d0d12\]{--tw-bg-opacity: 1;background-color:rgb(13 13 18 / var(--tw-bg-opacity, 1))}.bg-\[\#ffffff\]{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-brand-50{--tw-bg-opacity: 1;background-color:rgb(238 242 255 / var(--tw-bg-opacity, 1))}.bg-brand-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}.bg-brand-500\/10{background-color:#6366f11a}.bg-brand-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity, 1))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-400{--tw-bg-opacity: 1;background-color:rgb(251 146 60 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-400{--tw-bg-opacity: 1;background-color:rgb(192 132 252 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/\[0\.08\]{background-color:#ef444414}.bg-sky-500\/10{background-color:#0ea5e91a}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.bg-slate-50\/70{background-color:#f8fafcb3}.bg-slate-50\/80{background-color:#f8fafccc}.bg-slate-50\/90{background-color:#f8fafce6}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-slate-900\/\[0\.04\]{background-color:#0f172a0a}.bg-slate-950\/70{background-color:#020617b3}.bg-slate-950\/95{background-color:#020617f2}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/5{background-color:#ffffff0d}.bg-white\/60{background-color:#fff9}.bg-white\/70{background-color:#ffffffb3}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-white\/95{background-color:#fffffff2}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-\[linear-gradient\(180deg\,rgba\(255\,255\,255\,0\.55\)\,transparent_26\%\)\]{background-image:linear-gradient(180deg,rgba(255,255,255,.55),transparent 26%)}.bg-\[radial-gradient\(circle_at_top_left\,_rgba\(59\,130\,246\,0\.05\)\,_transparent_22\%\)\,radial-gradient\(circle_at_top_right\,_rgba\(14\,165\,233\,0\.04\)\,_transparent_20\%\)\,linear-gradient\(180deg\,_rgba\(255\,255\,255\,1\)\,_rgba\(255\,255\,255\,1\)\)\]{background-image:radial-gradient(circle at top left,rgba(59,130,246,.05),transparent 22%),radial-gradient(circle at top right,rgba(14,165,233,.04),transparent 20%),linear-gradient(180deg,#fff,#fff)}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-sky-400{--tw-gradient-from: #38bdf8 var(--tw-gradient-from-position);--tw-gradient-to: rgb(56 189 248 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-\[\#3b82f6\]\/25{--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(59 130 246 / .25) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-blue-500{--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #3b82f6 var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-cyan-300{--tw-gradient-to: #67e8f9 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to: transparent var(--tw-gradient-to-position)}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-5{padding-bottom:1.25rem}.pl-1{padding-left:.25rem}.pl-6{padding-left:1.5rem}.pl-8{padding-left:2rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Menlo,Monaco,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[11px\]{font-size:11px}.text-\[14px\]{font-size:14px}.text-\[2\.5rem\]{font-size:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.tracking-\[-0\.05em\]{letter-spacing:-.05em}.tracking-\[0\.16em\]{letter-spacing:.16em}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.\!text-slate-600{--tw-text-opacity: 1 !important;color:rgb(71 85 105 / var(--tw-text-opacity, 1))!important}.\!text-slate-950{--tw-text-opacity: 1 !important;color:rgb(2 6 23 / var(--tw-text-opacity, 1))!important}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-brand-500{--tw-text-opacity: 1;color:rgb(99 102 241 / var(--tw-text-opacity, 1))}.text-brand-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity, 1))}.text-brand-700{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity, 1))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-800{--tw-text-opacity: 1;color:rgb(107 33 168 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-slate-950{--tw-text-opacity: 1;color:rgb(2 6 23 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.placeholder-slate-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.placeholder-slate-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.accent-brand-500{accent-color:#6366f1}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_12px_30px_-24px_rgba\(15\,23\,42\,0\.2\)\]{--tw-shadow: 0 12px 30px -24px rgba(15,23,42,.2);--tw-shadow-colored: 0 12px 30px -24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_12px_30px_-24px_rgba\(15\,23\,42\,0\.28\)\]{--tw-shadow: 0 12px 30px -24px rgba(15,23,42,.28);--tw-shadow-colored: 0 12px 30px -24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_14px_30px_-28px_rgba\(15\,23\,42\,0\.18\)\]{--tw-shadow: 0 14px 30px -28px rgba(15,23,42,.18);--tw-shadow-colored: 0 14px 30px -28px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_14px_30px_-28px_rgba\(15\,23\,42\,0\.25\)\]{--tw-shadow: 0 14px 30px -28px rgba(15,23,42,.25);--tw-shadow-colored: 0 14px 30px -28px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_16px_30px_-28px_rgba\(15\,23\,42\,0\.28\)\]{--tw-shadow: 0 16px 30px -28px rgba(15,23,42,.28);--tw-shadow-colored: 0 16px 30px -28px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_16px_30px_-28px_rgba\(15\,23\,42\,0\.4\)\]{--tw-shadow: 0 16px 30px -28px rgba(15,23,42,.4);--tw-shadow-colored: 0 16px 30px -28px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_18px_60px_-42px_rgba\(15\,23\,42\,0\.15\)\]{--tw-shadow: 0 18px 60px -42px rgba(15,23,42,.15);--tw-shadow-colored: 0 18px 60px -42px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_28px_90px_-56px_rgba\(15\,23\,42\,0\.16\)\]{--tw-shadow: 0 28px 90px -56px rgba(15,23,42,.16);--tw-shadow-colored: 0 28px 90px -56px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_6px_20px_-16px_rgba\(15\,23\,42\,0\.18\)\]{--tw-shadow: 0 6px 20px -16px rgba(15,23,42,.18);--tw-shadow-colored: 0 6px 20px -16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-inset{--tw-ring-inset: inset}.ring-blue-500\/20{--tw-ring-color: rgb(59 130 246 / .2)}.ring-emerald-500\/20{--tw-ring-color: rgb(16 185 129 / .2)}.ring-orange-500\/20{--tw-ring-color: rgb(249 115 22 / .2)}.ring-purple-500\/20{--tw-ring-color: rgb(168 85 247 / .2)}.ring-red-500\/20{--tw-ring-color: rgb(239 68 68 / .2)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur: blur(64px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.glass-panel{position:relative;overflow:hidden;border-radius:1rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 18px 50px -30px rgba(15,23,42,.12);--tw-shadow-colored: 0 18px 50px -30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.glass-panel:is(.dark *){--tw-border-opacity: 1;border-color:rgb(26 26 29 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(12 12 14 / var(--tw-bg-opacity, 1));--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.glass-panel-hover:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.glass-panel-hover:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(34 34 38 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(17 17 20 / var(--tw-bg-opacity, 1))}.text-gradient{background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: #4f46e5 var(--tw-gradient-from-position);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: #a855f7 var(--tw-gradient-to-position);-webkit-background-clip:text;background-clip:text;color:transparent}.text-gradient:is(.dark *){--tw-gradient-from: #9b51e0 var(--tw-gradient-from-position);--tw-gradient-to: rgb(155 81 224 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #4f46e5 var(--tw-gradient-via-position), var(--tw-gradient-to);--tw-gradient-to: #0ea5e9 var(--tw-gradient-to-position)}.aurora-top{position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,#9b51e0,#4f46e5,#0ea5e9,transparent);opacity:.8;z-index:100}.dark .hover\:\!text-slate-900:hover,.dark .hover\:text-slate-900:hover{color:#f1f5f9!important}.dark .hover\:text-slate-700:hover{color:#e2e8f0!important}.dark .hover\:bg-slate-50:hover{--tw-bg-opacity: 1 !important;background-color:rgb(26 26 29 / var(--tw-bg-opacity, 1))!important}.dark .hover\:bg-slate-100:hover{--tw-bg-opacity: 1 !important;background-color:rgb(34 34 38 / var(--tw-bg-opacity, 1))!important}.dark .group:hover .group-hover\:text-slate-700{color:#e2e8f0!important}.dark .group:hover .group-hover\:text-brand-500{color:#818cf8!important}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:bottom-2:before{content:var(--tw-content);bottom:.5rem}.before\:left-2:before{content:var(--tw-content);left:.5rem}.before\:top-2:before{content:var(--tw-content);top:.5rem}.before\:w-px:before{content:var(--tw-content);width:1px}.before\:bg-slate-200:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-\[\#3b82f6\]\/35:hover{border-color:#3b82f659}.hover\:border-slate-200:hover{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.hover\:border-slate-300:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.hover\:\!bg-slate-200:hover{--tw-bg-opacity: 1 !important;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))!important}.hover\:bg-brand-500\/15:hover{background-color:#6366f126}.hover\:bg-brand-700:hover{--tw-bg-opacity: 1;background-color:rgb(67 56 202 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-50\/80:hover{background-color:#f8fafccc}.hover\:bg-slate-900\/\[0\.025\]:hover{background-color:#0f172a06}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:\!text-slate-900:hover{--tw-text-opacity: 1 !important;color:rgb(15 23 42 / var(--tw-text-opacity, 1))!important}.hover\:text-brand-600:hover{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.hover\:text-slate-700:hover{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.hover\:text-slate-900:hover{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-\[\#28282f\]:focus{--tw-border-opacity: 1;border-color:rgb(40 40 47 / var(--tw-border-opacity, 1))}.focus\:border-\[\#3b82f6\]\/30:focus{border-color:#3b82f64d}.focus\:border-brand-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-brand-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity, 1))}.focus\:ring-brand-500\/20:focus{--tw-ring-color: rgb(99 102 241 / .2)}.focus\:ring-brand-500\/50:focus{--tw-ring-color: rgb(99 102 241 / .5)}.focus\:ring-offset-1:focus{--tw-ring-offset-width: 1px}.focus\:ring-offset-transparent:focus{--tw-ring-offset-color: transparent}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-\[\#3b82f6\]\/40:focus-visible{--tw-ring-color: rgb(59 130 246 / .4)}.focus-visible\:ring-offset-0:focus-visible{--tw-ring-offset-width: 0px}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-brand-500{--tw-text-opacity: 1;color:rgb(99 102 241 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-brand-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.dark\:block:is(.dark *){display:block}.dark\:hidden:is(.dark *){display:none}.dark\:divide-\[\#222226\]:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(34 34 38 / var(--tw-divide-opacity, 1))}.dark\:\!border-\[\#2a303a\]:is(.dark *){--tw-border-opacity: 1 !important;border-color:rgb(42 48 58 / var(--tw-border-opacity, 1))!important}.dark\:border-\[\#1c1c23\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(28 28 35 / var(--tw-border-opacity, 1))}.dark\:border-\[\#1c1c24\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(28 28 36 / var(--tw-border-opacity, 1))}.dark\:border-\[\#1d1d23\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(29 29 35 / var(--tw-border-opacity, 1))}.dark\:border-\[\#222226\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(34 34 38 / var(--tw-border-opacity, 1))}.dark\:border-\[\#222227\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(34 34 39 / var(--tw-border-opacity, 1))}.dark\:border-\[\#22262f\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(34 38 47 / var(--tw-border-opacity, 1))}.dark\:border-\[\#232933\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(35 41 51 / var(--tw-border-opacity, 1))}.dark\:border-\[\#27272a\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(39 39 42 / var(--tw-border-opacity, 1))}.dark\:border-\[\#2a303a\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(42 48 58 / var(--tw-border-opacity, 1))}.dark\:border-\[\#2a303a\]\/70:is(.dark *){border-color:#2a303ab3}.dark\:border-\[\#5c1c1c\]:is(.dark *){--tw-border-opacity: 1;border-color:rgb(92 28 28 / var(--tw-border-opacity, 1))}.dark\:\!bg-\[\#161b24\]:is(.dark *){--tw-bg-opacity: 1 !important;background-color:rgb(22 27 36 / var(--tw-bg-opacity, 1))!important}.dark\:bg-\[\#030507\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 5 7 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#06080d\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(6 8 13 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#08080b\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(8 8 11 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#09090b\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#09090d\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(9 9 13 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#09090d\]\/95:is(.dark *){background-color:#09090df2}.dark\:bg-\[\#090c12\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(9 12 18 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0a0a0f\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(10 10 15 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0a0d12\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(10 13 18 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0b0b0d\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 11 13 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0b0b10\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 11 16 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0b1017\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 16 23 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0c0c0e\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(12 12 14 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0c0c10\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(12 12 16 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0f0f11\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 15 17 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#0f0f16\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 15 22 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#111114\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 17 20 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#111118\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 17 24 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#11141b\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 20 27 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#11151d\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 21 29 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#121214\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(18 18 20 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#121720\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(18 23 32 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#131923\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(19 25 35 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#141822\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 24 34 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#151518\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(21 21 24 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#161b24\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(22 27 36 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#161b24\]\/80:is(.dark *){background-color:#161b24cc}.dark\:bg-\[\#1a2332\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(26 35 50 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#232933\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(35 41 51 / var(--tw-bg-opacity, 1))}.dark\:bg-\[\#2a0505\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(42 5 5 / var(--tw-bg-opacity, 1))}.dark\:bg-brand-500\/10:is(.dark *){background-color:#6366f11a}.dark\:bg-sky-300:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(125 211 252 / var(--tw-bg-opacity, 1))}.dark\:bg-white:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.dark\:bg-white\/\[0\.05\]:is(.dark *){background-color:#ffffff0d}.dark\:bg-\[linear-gradient\(180deg\,rgba\(255\,255\,255\,0\.02\)\,transparent_26\%\)\]:is(.dark *){background-image:linear-gradient(180deg,rgba(255,255,255,.02),transparent 26%)}.dark\:bg-\[radial-gradient\(circle_at_top_left\,_rgba\(56\,189\,248\,0\.06\)\,_transparent_22\%\)\,linear-gradient\(180deg\,_rgba\(3\,5\,7\,1\)\,_rgba\(5\,8\,12\,1\)\)\]:is(.dark *){background-image:radial-gradient(circle at top left,rgba(56,189,248,.06),transparent 22%),linear-gradient(180deg,#030507,#05080c)}.dark\:via-\[\#3b82f6\]\/30:is(.dark *){--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(59 130 246 / .3) var(--tw-gradient-via-position), var(--tw-gradient-to)}.dark\:via-\[\#3b82f6\]\/35:is(.dark *){--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(59 130 246 / .35) var(--tw-gradient-via-position), var(--tw-gradient-to)}.dark\:\!text-\[\#a7b2c6\]:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(167 178 198 / var(--tw-text-opacity, 1))!important}.dark\:\!text-white:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(255 255 255 / var(--tw-text-opacity, 1))!important}.dark\:text-\[\#697387\]:is(.dark *){--tw-text-opacity: 1;color:rgb(105 115 135 / var(--tw-text-opacity, 1))}.dark\:text-\[\#6d768a\]:is(.dark *){--tw-text-opacity: 1;color:rgb(109 118 138 / var(--tw-text-opacity, 1))}.dark\:text-\[\#7a8397\]:is(.dark *){--tw-text-opacity: 1;color:rgb(122 131 151 / var(--tw-text-opacity, 1))}.dark\:text-\[\#7d879b\]:is(.dark *){--tw-text-opacity: 1;color:rgb(125 135 155 / var(--tw-text-opacity, 1))}.dark\:text-\[\#8390a7\]:is(.dark *){--tw-text-opacity: 1;color:rgb(131 144 167 / var(--tw-text-opacity, 1))}.dark\:text-\[\#8a8a93\]:is(.dark *){--tw-text-opacity: 1;color:rgb(138 138 147 / var(--tw-text-opacity, 1))}.dark\:text-\[\#8d96aa\]:is(.dark *){--tw-text-opacity: 1;color:rgb(141 150 170 / var(--tw-text-opacity, 1))}.dark\:text-\[\#8ea0bb\]:is(.dark *){--tw-text-opacity: 1;color:rgb(142 160 187 / var(--tw-text-opacity, 1))}.dark\:text-\[\#98a3b8\]:is(.dark *){--tw-text-opacity: 1;color:rgb(152 163 184 / var(--tw-text-opacity, 1))}.dark\:text-\[\#9dabc0\]:is(.dark *){--tw-text-opacity: 1;color:rgb(157 171 192 / var(--tw-text-opacity, 1))}.dark\:text-\[\#a1a1aa\]:is(.dark *){--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.dark\:text-\[\#a6b0c3\]:is(.dark *){--tw-text-opacity: 1;color:rgb(166 176 195 / var(--tw-text-opacity, 1))}.dark\:text-\[\#aeb6c7\]:is(.dark *){--tw-text-opacity: 1;color:rgb(174 182 199 / var(--tw-text-opacity, 1))}.dark\:text-\[\#b2bdd1\]:is(.dark *){--tw-text-opacity: 1;color:rgb(178 189 209 / var(--tw-text-opacity, 1))}.dark\:text-\[\#b3b3bd\]:is(.dark *){--tw-text-opacity: 1;color:rgb(179 179 189 / var(--tw-text-opacity, 1))}.dark\:text-\[\#d5dded\]:is(.dark *){--tw-text-opacity: 1;color:rgb(213 221 237 / var(--tw-text-opacity, 1))}.dark\:text-\[\#d8e1ef\]:is(.dark *){--tw-text-opacity: 1;color:rgb(216 225 239 / var(--tw-text-opacity, 1))}.dark\:text-\[\#e5e7eb\]:is(.dark *){--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.dark\:text-\[\#e5ecf7\]:is(.dark *){--tw-text-opacity: 1;color:rgb(229 236 247 / var(--tw-text-opacity, 1))}.dark\:text-black:is(.dark *){--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-orange-300:is(.dark *){--tw-text-opacity: 1;color:rgb(253 186 116 / var(--tw-text-opacity, 1))}.dark\:text-purple-300:is(.dark *){--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:text-red-500:is(.dark *){--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.dark\:text-sky-200:is(.dark *){--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.dark\:text-sky-300:is(.dark *){--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:placeholder-\[\#6c7486\]:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(108 116 134 / var(--tw-placeholder-opacity, 1))}.dark\:placeholder-\[\#6c7486\]:is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(108 116 134 / var(--tw-placeholder-opacity, 1))}.dark\:shadow-\[0_18px_60px_-42px_rgba\(0\,0\,0\,0\.7\)\]:is(.dark *){--tw-shadow: 0 18px 60px -42px rgba(0,0,0,.7);--tw-shadow-colored: 0 18px 60px -42px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:shadow-\[0_28px_90px_-56px_rgba\(0\,0\,0\,0\.72\)\]:is(.dark *){--tw-shadow: 0 28px 90px -56px rgba(0,0,0,.72);--tw-shadow-colored: 0 28px 90px -56px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:shadow-none:is(.dark *){--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:before\:bg-\[\#23232a\]:is(.dark *):before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(35 35 42 / var(--tw-bg-opacity, 1))}.dark\:hover\:border-\[\#2a2a31\]:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(42 42 49 / var(--tw-border-opacity, 1))}.dark\:hover\:border-\[\#2a303a\]:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(42 48 58 / var(--tw-border-opacity, 1))}.dark\:hover\:\!bg-\[\#1c2330\]:hover:is(.dark *){--tw-bg-opacity: 1 !important;background-color:rgb(28 35 48 / var(--tw-bg-opacity, 1))!important}.dark\:hover\:bg-\[\#121214\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(18 18 20 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-\[\#141923\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 25 35 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-\[\#161b24\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(22 27 36 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-\[\#161b24\]\/75:hover:is(.dark *){background-color:#161b24bf}.dark\:hover\:bg-\[\#171b23\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(23 27 35 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-\[\#18181b\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-\[\#380808\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(56 8 8 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-200:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-white\/\[0\.035\]:hover:is(.dark *){background-color:#ffffff09}.dark\:hover\:\!text-white:hover:is(.dark *){--tw-text-opacity: 1 !important;color:rgb(255 255 255 / var(--tw-text-opacity, 1))!important}.dark\:hover\:text-\[\#dbe7ff\]:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(219 231 255 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.group:hover .dark\:group-hover\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-start{align-items:flex-start}.sm\:justify-end{justify-content:flex-end}.sm\:justify-between{justify-content:space-between}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:p-7{padding:1.75rem}.md\:p-8{padding:2rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.md\:text-\[3rem\]{font-size:3rem}.md\:text-\[4rem\]{font-size:4rem}}@media (min-width: 1024px){.lg\:col-span-1{grid-column:span 1 / span 1}.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:grid-cols-\[minmax\(0\,1fr\)_240px\]{grid-template-columns:minmax(0,1fr) 240px}}@media (min-width: 1280px){.xl\:sticky{position:sticky}.xl\:top-6{top:1.5rem}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-\[0\.86fr_1\.14fr\]{grid-template-columns:.86fr 1.14fr}.xl\:grid-cols-\[1\.02fr_0\.98fr\]{grid-template-columns:1.02fr .98fr}.xl\:grid-cols-\[1\.15fr_0\.85fr\]{grid-template-columns:1.15fr .85fr}.xl\:grid-cols-\[340px_minmax\(0\,1fr\)\]{grid-template-columns:340px minmax(0,1fr)}.xl\:grid-cols-\[360px_minmax\(0\,1fr\)\]{grid-template-columns:360px minmax(0,1fr)}.xl\:grid-cols-\[minmax\(0\,1\.2fr\)_minmax\(320px\,0\.8fr\)\]{grid-template-columns:minmax(0,1.2fr) minmax(320px,.8fr)}.xl\:grid-cols-\[minmax\(0\,1fr\)_380px\]{grid-template-columns:minmax(0,1fr) 380px}.xl\:self-start{align-self:flex-start}.xl\:border-b-0{border-bottom-width:0px}.xl\:border-r{border-right-width:1px}} diff --git a/website/dist/favicon.svg b/website/dist/favicon.svg new file mode 100644 index 00000000..5ae09a6b --- /dev/null +++ b/website/dist/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/website/dist/index.html b/website/dist/index.html new file mode 100644 index 00000000..244618f8 --- /dev/null +++ b/website/dist/index.html @@ -0,0 +1,16 @@ + + + + + + + + + Juspay Decision Engine Dashboard + + + + +
+ + diff --git a/website/dist/logo/dark.svg b/website/dist/logo/dark.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/website/dist/logo/dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/dist/logo/decision-engine-dark.svg b/website/dist/logo/decision-engine-dark.svg new file mode 100644 index 00000000..d96f4aed --- /dev/null +++ b/website/dist/logo/decision-engine-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/website/dist/logo/decision-engine-light.svg b/website/dist/logo/decision-engine-light.svg new file mode 100644 index 00000000..5c18998b --- /dev/null +++ b/website/dist/logo/decision-engine-light.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/website/dist/logo/light.svg b/website/dist/logo/light.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/website/dist/logo/light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/index.html b/website/index.html new file mode 100644 index 00000000..b42af4e4 --- /dev/null +++ b/website/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Juspay Decision Engine Dashboard + + +
+ + + diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 00000000..c51b183d --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,3239 @@ +{ + "name": "decision-engine-dashboard", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decision-engine-dashboard", + "version": "0.0.1", + "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^3.3.4", + "lucide-react": "^0.363.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.51.0", + "react-router-dom": "^6.22.3", + "recharts": "^2.12.2", + "swr": "^2.2.5", + "zod": "^3.22.4", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.332", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", + "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.363.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.363.0.tgz", + "integrity": "sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", + "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..db3e5889 --- /dev/null +++ b/website/package.json @@ -0,0 +1,36 @@ +{ + "name": "decision-engine-dashboard", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^3.3.4", + "lucide-react": "^0.363.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.51.0", + "react-router-dom": "^6.22.3", + "recharts": "^2.12.2", + "swr": "^2.2.5", + "zod": "^3.22.4", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/website/postcss.config.js b/website/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/website/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/website/public/favicon.svg b/website/public/favicon.svg new file mode 100644 index 00000000..5ae09a6b --- /dev/null +++ b/website/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/website/public/logo/dark.svg b/website/public/logo/dark.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/website/public/logo/dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/public/logo/decision-engine-dark.svg b/website/public/logo/decision-engine-dark.svg new file mode 100644 index 00000000..d96f4aed --- /dev/null +++ b/website/public/logo/decision-engine-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/website/public/logo/decision-engine-light.svg b/website/public/logo/decision-engine-light.svg new file mode 100644 index 00000000..5c18998b --- /dev/null +++ b/website/public/logo/decision-engine-light.svg @@ -0,0 +1,10 @@ + + + + + + + + JUSPAY + Decision Engine + diff --git a/website/public/logo/light.svg b/website/public/logo/light.svg new file mode 100644 index 00000000..57d3d833 --- /dev/null +++ b/website/public/logo/light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/src/App.tsx b/website/src/App.tsx new file mode 100644 index 00000000..34fc43df --- /dev/null +++ b/website/src/App.tsx @@ -0,0 +1,30 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import { AppShell } from './components/layout/AppShell' +import { OverviewPage } from './components/pages/OverviewPage' +import { RoutingHubPage } from './components/pages/RoutingHubPage' +import { SRRoutingPage } from './components/pages/SRRoutingPage' +import { EuclidRulesPage } from './components/pages/EuclidRulesPage' +import { VolumeSplitPage } from './components/pages/VolumeSplitPage' +import { DebitRoutingPage } from './components/pages/DebitRoutingPage' +import { DecisionExplorerPage } from './components/pages/DecisionExplorerPage' +import { AnalyticsPage } from './components/pages/AnalyticsPage' +import { PaymentAuditPage } from './components/pages/PaymentAuditPage' + +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} diff --git a/website/src/ErrorBoundary.tsx b/website/src/ErrorBoundary.tsx new file mode 100644 index 00000000..da6fd2bc --- /dev/null +++ b/website/src/ErrorBoundary.tsx @@ -0,0 +1,41 @@ +import { Component, ReactNode } from 'react' + +interface Props { children: ReactNode } +interface State { error: Error | null; errorInfo: React.ErrorInfo | null } + +export class ErrorBoundary extends Component { + state: State = { error: null, errorInfo: null } + + static getDerivedStateFromError(error: Error): State { + return { error, errorInfo: null } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.log('\n' + '!'.repeat(80)) + console.log('[ERROR BOUNDARY] Component Error Caught') + console.log(`Timestamp: ${new Date().toISOString()}`) + console.log('Error Message:', error.message) + console.log('Error Stack:', error.stack) + console.log('Component Stack:', errorInfo.componentStack) + console.log('!'.repeat(80) + '\n') + this.setState({ errorInfo }) + } + + render() { + if (this.state.error) { + return ( +
+

Dashboard Error

+
{this.state.error.message}
+
{this.state.error.stack}
+ {this.state.errorInfo && ( +
+              Component Stack:{this.state.errorInfo.componentStack}
+            
+ )} +
+ ) + } + return this.props.children + } +} diff --git a/website/src/components/layout/AppShell.tsx b/website/src/components/layout/AppShell.tsx new file mode 100644 index 00000000..c7bb5ff7 --- /dev/null +++ b/website/src/components/layout/AppShell.tsx @@ -0,0 +1,19 @@ +import { Outlet } from 'react-router-dom' +import { Sidebar } from './Sidebar' +import { TopBar } from './TopBar' + +export function AppShell() { + return ( +
+
+
+ +
+ +
+ +
+
+
+ ) +} diff --git a/website/src/components/layout/Sidebar.tsx b/website/src/components/layout/Sidebar.tsx new file mode 100644 index 00000000..d6ebff14 --- /dev/null +++ b/website/src/components/layout/Sidebar.tsx @@ -0,0 +1,138 @@ +import { useLayoutEffect, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { + LayoutDashboard, + GitBranch, + Search, + TrendingUp, + BookOpen, + PieChart, + Network, + BarChart3, + Activity, +} from 'lucide-react' + +export function Sidebar() { + const location = useLocation() + const [pendingPath, setPendingPath] = useState(null) + const selectedPath = pendingPath ?? location.pathname + const assetBaseUrl = import.meta.env.BASE_URL + + useLayoutEffect(() => { + if (!pendingPath) { + return + } + + const navigationSettled = + location.pathname === pendingPath || + location.pathname.startsWith(`${pendingPath}/`) + + if (navigationSettled) { + setPendingPath(null) + } + }, [location.pathname, pendingPath]) + + return ( + + ) +} + +function SideLink({ + to, + icon: Icon, + children, + end, + indent, + selectedPath, + onNavigate, +}: { + to: string + icon: React.ElementType + children: React.ReactNode + end?: boolean + indent?: boolean + selectedPath: string + onNavigate?: (path: string) => void +}) { + const navigate = useNavigate() + const isHighlighted = end + ? selectedPath === to + : selectedPath === to || selectedPath.startsWith(`${to}/`) + + return ( + + ) +} diff --git a/website/src/components/layout/TopBar.tsx b/website/src/components/layout/TopBar.tsx new file mode 100644 index 00000000..5fae4eb4 --- /dev/null +++ b/website/src/components/layout/TopBar.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { Building2, ArrowRight, Loader2, Moon, Sun } from 'lucide-react' + +export function TopBar() { + const { merchantId, setMerchantId } = useMerchantStore() + const [draft, setDraft] = useState(merchantId) + const [creating, setCreating] = useState(false) + + // Theme management + const [isDark, setIsDark] = useState(() => localStorage.getItem('theme') === 'dark') + + useEffect(() => { + const root = window.document.documentElement + if (isDark) { + root.classList.add('dark') + localStorage.setItem('theme', 'dark') + } else { + root.classList.remove('dark') + localStorage.setItem('theme', 'light') + } + }, [isDark]) + + async function handleSetMerchant() { + const id = draft.trim() + if (!id) return + + setMerchantId(id) + setCreating(true) + + try { + await apiPost('/merchant-account/create', { + merchant_id: id, + gateway_success_rate_based_decider_input: null, + }) + } catch { + // Merchant may already exist - that's fine + } finally { + setCreating(false) + } + } + + return ( +
+
+
+
+ setDraft(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSetMerchant()} + placeholder="Set Merchant ID" + className="w-72 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 shadow-[0_6px_20px_-16px_rgba(15,23,42,0.18)] transition-all placeholder-slate-400 focus:outline-none focus:border-[#3b82f6]/30 dark:border-[#22262f] dark:bg-[#11141b] dark:text-white dark:placeholder-[#6c7486] dark:shadow-none" + /> + +
+ + {merchantId && ( +
+ + + {merchantId} + +
+ )} + + {/* Theme Toggle */} + +
+
+ ) +} diff --git a/website/src/components/pages/AnalyticsPage.tsx b/website/src/components/pages/AnalyticsPage.tsx new file mode 100644 index 00000000..0ec24d63 --- /dev/null +++ b/website/src/components/pages/AnalyticsPage.tsx @@ -0,0 +1,1736 @@ +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import useSWR from 'swr' +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Cell, + Legend, + Line, + LineChart, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' +import { useMerchantStore } from '../../store/merchantStore' +import { fetcher } from '../../lib/api' +import { + AnalyticsOverviewResponse, + AnalyticsRange, + AnalyticsRangeValue, + AnalyticsRoutingStatsResponse, + PaymentAuditResponse, + RoutingFilterOptions, +} from '../../types/api' +import { Button } from '../ui/Button' +import { Card, CardBody, CardHeader } from '../ui/Card' +import { Badge } from '../ui/Badge' +import { Spinner } from '../ui/Spinner' +import { ErrorMessage } from '../ui/ErrorMessage' + +type TimeWindow = { + start_ms: number + end_ms: number +} + +type RoutingFilters = { + dimensions: Record + gateways: string[] +} + +type AnalyticsView = 'transactions' | 'rule_based' +type PreviewTraceKey = readonly [ + 'preview-trace-analytics', + AnalyticsRangeValue, + string, + number | null, + number | null, +] + +type InfoContent = { + title: string + purpose: string + calculation: string + source: string +} + +const PRESET_OPTIONS: { value: AnalyticsRangeValue; label: string }[] = [ + { value: '15m', label: 'Last 15 mins' }, + { value: '1h', label: 'Last 1 hour' }, + { value: '24h', label: 'Last 1 day' }, + { value: 'custom', label: 'Custom window' }, +] + +const CHART_COLORS = ['#0069ED', '#14b8a6', '#f97316', '#e11d48', '#8b5cf6', '#22c55e'] +const CHART_TOOLTIP_STYLE = { + backgroundColor: '#0d0d12', + border: '1px solid #1c1c24', + borderRadius: '14px', + color: '#e8e8f4', + boxShadow: '0 16px 40px rgba(0, 0, 0, 0.35)', +} +const CHART_TOOLTIP_LABEL_STYLE = { + color: '#f8fafc', + fontWeight: 600, + marginBottom: 8, +} +const CHART_TOOLTIP_ITEM_STYLE = { + color: '#e2e8f0', +} +const CHART_TOOLTIP_WRAPPER_STYLE = { + zIndex: 30, + outline: 'none', +} + +const EMPTY_ROUTING_FILTERS: RoutingFilters = { + dimensions: {}, + gateways: [], +} +const MAX_VISIBLE_DIMENSIONS = 3 +const PREVIEW_TRACE_PAGE_SIZE = 50 +const MAX_PREVIEW_TRACE_PAGES = 5 +const PREVIEW_LIST_PAGE_SIZE = 10 + +const CARD_INFO: Record<'hits' | 'share' | 'sr' | 'preview_hits' | 'preview_activity' | 'preview_share', InfoContent> = { + hits: { + title: 'API call counts', + purpose: 'Use these cards to see how much traffic each major decision-engine API handled in the selected window.', + calculation: 'Each request records one lightweight API-call event. The cards count those recorded calls for the endpoints surfaced in the current view.', + source: 'Counts come from analytics rows persisted in `analytics_event` in Postgres.', + }, + share: { + title: 'Gateway share over time', + purpose: 'Use this to see when traffic shifted from one connector to another for the selected merchant.', + calculation: 'Decision events are bucketed by time and grouped by chosen connector. The chart shows how many decisions each gateway captured in each bucket.', + source: 'Reads persisted `decision` rows from `analytics_event` in Postgres.', + }, + sr: { + title: 'Connector success rate over time', + purpose: 'Use this to explain why a connector won routing at a given time, based on the recorded historical score trail.', + calculation: 'Stored `score_snapshot` events are bucketed over the selected window and averaged per connector. The line values are displayed as percentages.', + source: 'Reads persisted `score_snapshot` rows from `analytics_event` in Postgres. The current score state originates from Redis-backed scoring flows.', + }, + preview_hits: { + title: 'Rule-based summary', + purpose: 'Use these cards to distinguish preview request volume from the connector coverage produced by rule-based routing.', + calculation: 'Rule Evaluate counts come from request-hit analytics for `/routing/evaluate`. Gateway coverage counts the unique connectors selected in the fetched preview sample.', + source: 'Reads `request_hit` and `rule_evaluation_preview` analytics associated with preview routing activity.', + }, + preview_activity: { + title: 'Connector selections over time', + purpose: 'Use this to see which connectors were selected in each time bucket inside the selected preview window.', + calculation: 'Returned preview traces are bucketed by time using each trace\'s latest activity timestamp, then grouped by latest selected connector. The chart shows connector counts per bucket.', + source: 'Reads `rule_evaluation_preview` activity through `/analytics/preview-trace`.', + }, + preview_share: { + title: 'Rule-based gateway selection mix', + purpose: 'Use this to see which connectors dominate the fetched rule-preview sample, separate from real transaction decisions.', + calculation: 'Returned preview traces are grouped by latest selected connector and displayed as share of the fetched preview sample.', + source: 'Reads `rule_evaluation_preview` activity through `/analytics/preview-trace`.', + }, +} + +function queryString(params: Record) { + const search = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + search.set(key, String(value)) + } + }) + return search.toString() +} + +function buildAnalyticsUrl( + path: string, + range: AnalyticsRangeValue, + merchantId: string, + customWindow?: TimeWindow, + routingFilters?: RoutingFilters, +) { + const params: Record = { + scope: 'current', + range: range === 'custom' ? '1h' : range, + start_ms: customWindow?.start_ms, + end_ms: customWindow?.end_ms, + merchant_id: merchantId, + gateway: routingFilters?.gateways.length ? routingFilters.gateways.join(',') : undefined, + } + + Object.entries(routingFilters?.dimensions || {}).forEach(([key, value]) => { + if (value) { + params[key] = value + } + }) + + const qs = queryString(params) + return qs ? `${path}?${qs}` : path +} + +function buildPreviewTraceUrl( + range: AnalyticsRangeValue, + merchantId: string, + page: number, + pageSize: number, + customWindow?: TimeWindow, +) { + const params: Record = { + scope: 'current', + range: range === 'custom' ? '1h' : range, + start_ms: customWindow?.start_ms, + end_ms: customWindow?.end_ms, + merchant_id: merchantId, + page, + page_size: pageSize, + } + + const qs = queryString(params) + return qs ? `/analytics/preview-trace?${qs}` : '/analytics/preview-trace' +} + +async function loadPreviewTraceSample( + range: AnalyticsRangeValue, + merchantId: string, + customWindow?: TimeWindow, +) { + const firstPage = await fetcher( + buildPreviewTraceUrl(range, merchantId, 1, PREVIEW_TRACE_PAGE_SIZE, customWindow), + ) + const totalPages = Math.min( + Math.ceil(firstPage.total_results / PREVIEW_TRACE_PAGE_SIZE), + MAX_PREVIEW_TRACE_PAGES, + ) + + if (totalPages <= 1) { + return firstPage + } + + const remainingPages = await Promise.all( + Array.from({ length: totalPages - 1 }, (_, index) => + fetcher( + buildPreviewTraceUrl( + range, + merchantId, + index + 2, + PREVIEW_TRACE_PAGE_SIZE, + customWindow, + ), + ), + ), + ) + + return { + ...firstPage, + results: [firstPage.results, ...remainingPages.map((page) => page.results)].flat(), + } +} + +function formatNumber(value: number | string | undefined, digits = 2) { + if (value === undefined || value === null || Number.isNaN(Number(value))) { + return '0' + } + const numericValue = Number(value) + if (Number.isInteger(numericValue)) return numericValue.toString() + return numericValue.toFixed(digits) +} + +function toPercent(value: number) { + if (!Number.isFinite(value)) return 0 + return value <= 1 ? value * 100 : value +} + +function formatPercent(value: number | string | undefined, digits = 1) { + if (value === undefined || value === null || Number.isNaN(Number(value))) { + return '0%' + } + return `${formatNumber(toPercent(Number(value)), digits)}%` +} + +function formatBucket(ms: number) { + return new Intl.DateTimeFormat(undefined, { + hour: '2-digit', + minute: '2-digit', + }).format(new Date(ms)) +} + +function formatDateTime(ms: number) { + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(ms)) +} + +function bucketSizeForWindow(range: AnalyticsRangeValue, customWindow?: TimeWindow) { + const windowMs = customWindow + ? customWindow.end_ms - customWindow.start_ms + : range === '15m' + ? 15 * 60 * 1000 + : range === '1h' + ? 60 * 60 * 1000 + : 24 * 60 * 60 * 1000 + + if (windowMs <= 15 * 60 * 1000) return 60 * 1000 + if (windowMs <= 60 * 60 * 1000) return 5 * 60 * 1000 + if (windowMs <= 24 * 60 * 60 * 1000) return 15 * 60 * 1000 + if (windowMs <= 72 * 60 * 60 * 1000) return 60 * 60 * 1000 + return 3 * 60 * 60 * 1000 +} + +function bucketTimestamp(ms: number, bucketSize: number) { + return ms - (ms % Math.max(1, bucketSize)) +} + +function buildBucketTimeline(window: TimeWindow, bucketSize: number) { + const buckets: number[] = [] + const safeBucketSize = Math.max(1, bucketSize) + const startBucket = bucketTimestamp(window.start_ms, safeBucketSize) + const endBucket = bucketTimestamp(window.end_ms, safeBucketSize) + + for (let bucket = startBucket; bucket <= endBucket; bucket += safeBucketSize) { + buckets.push(bucket) + } + + return buckets +} + +function presetWindow(range: AnalyticsRange) { + const now = Date.now() + const duration = + range === '15m' + ? 15 * 60 * 1000 + : range === '1h' + ? 60 * 60 * 1000 + : 24 * 60 * 60 * 1000 + + return { + start_ms: now - duration, + end_ms: now, + } +} + +function toDateTimeInputValue(timestampMs: number) { + const date = new Date(timestampMs) + const pad = (value: number) => value.toString().padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad( + date.getHours(), + )}:${pad(date.getMinutes())}` +} + +function fromDateTimeInputValue(value: string) { + const timestamp = new Date(value).getTime() + return Number.isFinite(timestamp) ? timestamp : null +} + +function EmptyState({ title, body }: { title: string; body: string }) { + return ( +
+

{title}

+

{body}

+
+ ) +} + +function controlClassName() { + return 'h-11 w-full rounded-2xl border border-slate-200 bg-white px-4 text-sm text-slate-700 shadow-sm outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:border-[#27272a] dark:bg-[#121214] dark:text-[#e5e7eb]' +} + +function InfoButton({ content }: { content: InfoContent }) { + const [open, setOpen] = useState(false) + const containerRef = useRef(null) + const [position, setPosition] = useState<{ top: number; left: number; width: number }>({ + top: 0, + left: 0, + width: 320, + }) + + useEffect(() => { + if (!open) return + + function handlePointerDown(event: MouseEvent) { + if (!containerRef.current?.contains(event.target as Node)) { + setOpen(false) + } + } + + document.addEventListener('mousedown', handlePointerDown) + return () => document.removeEventListener('mousedown', handlePointerDown) + }, [open]) + + useLayoutEffect(() => { + if (!open || !containerRef.current) return + + const POPOVER_WIDTH = 320 + const POPOVER_HEIGHT = 280 + const VIEWPORT_GUTTER = 16 + const GAP = 12 + + function updatePosition() { + if (!containerRef.current) return + + const rect = containerRef.current.getBoundingClientRect() + const width = Math.min(POPOVER_WIDTH, window.innerWidth - VIEWPORT_GUTTER * 2) + const left = Math.min( + Math.max(rect.right - width, VIEWPORT_GUTTER), + window.innerWidth - width - VIEWPORT_GUTTER, + ) + const showAbove = rect.bottom + GAP + POPOVER_HEIGHT > window.innerHeight - VIEWPORT_GUTTER + const top = showAbove + ? Math.max(rect.top - POPOVER_HEIGHT - GAP, VIEWPORT_GUTTER) + : rect.bottom + GAP + + setPosition({ top, left, width }) + } + + updatePosition() + window.addEventListener('resize', updatePosition) + window.addEventListener('scroll', updatePosition, true) + + return () => { + window.removeEventListener('resize', updatePosition) + window.removeEventListener('scroll', updatePosition, true) + } + }, [open]) + + return ( +
+ + {open ? ( +
+

{content.title}

+
+
+

Why it matters

+

{content.purpose}

+
+
+

How it is calculated

+

{content.calculation}

+
+
+

Data source

+

{content.source}

+
+
+
+ ) : null} +
+ ) +} + +function HitsCard({ + label, + value, + subtitle, + eyebrow = 'Endpoint hits', +}: { + label: string + value: number + subtitle: string + eyebrow?: string +}) { + return ( + + +
+

+ {eyebrow} +

+

{label}

+
+
+

+ {formatNumber(value, 0)} +

+ {subtitle} +
+
+
+ ) +} + +function analyticsRouteLabel(route: string) { + if (route === '/decide_gateway') return 'Decide Gateway' + if (route === '/update_gateway') return 'Update Gateway' + if (route === '/rule_evaluate') return 'Rule Evaluate' + return route +} + +export function AnalyticsPage() { + const { merchantId } = useMerchantStore() + const [range, setRange] = useState('1h') + const [view, setView] = useState('transactions') + const [routingFilters, setRoutingFilters] = useState(EMPTY_ROUTING_FILTERS) + const [showAllFilters, setShowAllFilters] = useState(false) + const [previewListPage, setPreviewListPage] = useState(1) + const [customStart, setCustomStart] = useState(() => + toDateTimeInputValue(Date.now() - 2 * 60 * 60 * 1000), + ) + const [customEnd, setCustomEnd] = useState(() => toDateTimeInputValue(Date.now())) + + const canQueryCurrent = Boolean(merchantId) + + const customWindow = useMemo(() => { + if (range !== 'custom') return undefined + const start_ms = fromDateTimeInputValue(customStart) + const end_ms = fromDateTimeInputValue(customEnd) + if (start_ms === null || end_ms === null || end_ms <= start_ms) { + return undefined + } + return { start_ms, end_ms } + }, [customEnd, customStart, range]) + + const overviewUrl = + canQueryCurrent && merchantId && (range !== 'custom' || customWindow) + ? buildAnalyticsUrl('/analytics/overview', range, merchantId, customWindow) + : null + const routingUrl = + canQueryCurrent && merchantId && (range !== 'custom' || customWindow) + ? buildAnalyticsUrl('/analytics/routing-stats', range, merchantId, customWindow) + : null + const filteredRoutingUrl = + canQueryCurrent && merchantId && (range !== 'custom' || customWindow) + ? buildAnalyticsUrl('/analytics/routing-stats', range, merchantId, customWindow, routingFilters) + : null + const previewTraceKey = + canQueryCurrent && merchantId && (range !== 'custom' || customWindow) + ? ([ + 'preview-trace-analytics', + range, + merchantId, + customWindow?.start_ms ?? null, + customWindow?.end_ms ?? null, + ] as const) + : null + const previewListUrl = + canQueryCurrent && merchantId && (range !== 'custom' || customWindow) + ? buildPreviewTraceUrl( + range, + merchantId, + previewListPage, + PREVIEW_LIST_PAGE_SIZE, + customWindow, + ) + : null + + const overviewSwrOptions = { + refreshInterval: 10000, + revalidateOnFocus: true, + revalidateIfStale: false, + } as const + const routingSwrOptions = { + refreshInterval: 12000, + revalidateOnFocus: true, + revalidateIfStale: false, + } as const + const filteredRoutingSwrOptions = { + ...routingSwrOptions, + keepPreviousData: true, + } as const + const previewListSwrOptions = { + ...routingSwrOptions, + keepPreviousData: true, + } as const + + const overview = useSWR(overviewUrl, fetcher, overviewSwrOptions) + const routing = useSWR(routingUrl, fetcher, routingSwrOptions) + const filteredRouting = useSWR( + filteredRoutingUrl, + fetcher, + filteredRoutingSwrOptions, + ) + const previewTrace = useSWR( + previewTraceKey, + async (key) => { + const [, selectedRange, selectedMerchantId, startMs, endMs] = key as PreviewTraceKey + return loadPreviewTraceSample( + selectedRange, + selectedMerchantId, + startMs !== null && endMs !== null + ? { start_ms: Number(startMs), end_ms: Number(endMs) } + : undefined, + ) + }, + routingSwrOptions, + ) + const previewList = useSWR( + previewListUrl, + fetcher, + previewListSwrOptions, + ) + + const transactionLoading = + (!overview.data && overview.isLoading) || + (!routing.data && routing.isLoading) || + (!filteredRouting.data && filteredRouting.isLoading) + const ruleBasedLoading = + (!overview.data && overview.isLoading) || + (!previewTrace.data && previewTrace.isLoading) + const transactionError = + overview.error?.message || + routing.error?.message || + filteredRouting.error?.message || + null + const ruleBasedError = + overview.error?.message || + previewTrace.error?.message || + previewList.error?.message || + null + const loading = view === 'transactions' ? transactionLoading : ruleBasedLoading + const error = view === 'transactions' ? transactionError : ruleBasedError + + const availableFilters: RoutingFilterOptions = { + dimensions: + routing.data?.available_filters?.dimensions || + filteredRouting.data?.available_filters?.dimensions || + [], + missing_dimensions: + routing.data?.available_filters?.missing_dimensions || + filteredRouting.data?.available_filters?.missing_dimensions || + [], + gateways: + routing.data?.available_filters?.gateways || + filteredRouting.data?.available_filters?.gateways || + [], + } + const availableFilterMap = useMemo( + () => + new Map( + availableFilters.dimensions.map((dimension) => [dimension.key, dimension] as const), + ), + [availableFilters.dimensions], + ) + + useEffect(() => { + setRoutingFilters((current) => { + const nextDimensions = Object.fromEntries( + Object.entries(current.dimensions).filter(([key, value]) => { + if (!value) return false + const dimension = availableFilterMap.get(key) + return dimension ? dimension.values.includes(value) : false + }), + ) + const nextGateways = current.gateways.filter((gateway) => + availableFilters.gateways.includes(gateway), + ) + + if ( + Object.keys(nextDimensions).length === Object.keys(current.dimensions).length && + Object.entries(nextDimensions).every( + ([key, value]) => current.dimensions[key] === value, + ) && + nextGateways.length === current.gateways.length && + nextGateways.every((gateway, index) => gateway === current.gateways[index]) + ) { + return current + } + + return { + dimensions: nextDimensions, + gateways: nextGateways, + } + }) + }, [availableFilterMap, availableFilters.gateways]) + + useEffect(() => { + if (availableFilters.dimensions.length <= MAX_VISIBLE_DIMENSIONS && showAllFilters) { + setShowAllFilters(false) + } + }, [availableFilters.dimensions.length, showAllFilters]) + + useEffect(() => { + setPreviewListPage(1) + }, [merchantId, range, customWindow?.start_ms, customWindow?.end_ms]) + + const activeWindowLabel = useMemo(() => { + if (range !== 'custom') { + return PRESET_OPTIONS.find((option) => option.value === range)?.label || 'Selected window' + } + if (!customWindow) return 'Custom window' + return `${formatDateTime(customWindow.start_ms)} to ${formatDateTime(customWindow.end_ms)}` + }, [customWindow, range]) + const effectiveWindow = useMemo(() => { + if (customWindow) return customWindow + return presetWindow(range as AnalyticsRange) + }, [customWindow, range]) + + const routeHits = useMemo(() => { + const fallback = [ + { route: '/decide_gateway', count: 0 }, + { route: '/update_gateway', count: 0 }, + { route: '/rule_evaluate', count: 0 }, + ] + if (!overview.data?.route_hits?.length) return fallback + return fallback.map((item) => ({ + ...item, + count: overview.data?.route_hits.find((row) => row.route === item.route)?.count || 0, + })) + }, [overview.data]) + const transactionRouteHits = useMemo( + () => routeHits.filter((item) => item.route !== '/rule_evaluate'), + [routeHits], + ) + const ruleEvaluateHits = useMemo( + () => routeHits.find((item) => item.route === '/rule_evaluate')?.count || 0, + [routeHits], + ) + const previewRows = previewTrace.data?.results || [] + const previewListRows = previewList.data?.results || [] + const previewGatewaySummary = useMemo(() => { + const counts = new Map() + + for (const row of previewRows) { + const gateway = row.latest_gateway || 'No gateway selected' + counts.set(gateway, (counts.get(gateway) || 0) + 1) + } + + return Array.from(counts.entries()) + .map(([gateway, count]) => ({ gateway, count })) + .sort((left, right) => right.count - left.count) + .slice(0, 6) + }, [previewRows]) + const previewStatusSummary = useMemo(() => { + const counts = new Map() + + for (const row of previewRows) { + const status = row.latest_status || 'unknown' + counts.set(status, (counts.get(status) || 0) + 1) + } + + return Array.from(counts.entries()) + .map(([status, count]) => ({ status, count })) + .sort((left, right) => right.count - left.count) + }, [previewRows]) + const previewBucketSize = useMemo( + () => bucketSizeForWindow(range, customWindow), + [customWindow, range], + ) + const previewConnectorSeriesData = useMemo(() => { + const gateways = previewGatewaySummary.map((item) => item.gateway).slice(0, 6) + const buckets = new Map>() + + for (const bucket_ms of buildBucketTimeline(effectiveWindow, previewBucketSize)) { + buckets.set( + bucket_ms, + gateways.reduce>( + (row, gateway) => { + row[gateway] = 0 + return row + }, + { bucket_ms }, + ), + ) + } + + for (const row of previewRows) { + const gateway = row.latest_gateway || 'No gateway selected' + if (!gateways.includes(gateway)) continue + const bucket_ms = bucketTimestamp(row.last_seen_ms, previewBucketSize) + const bucket = + buckets.get(bucket_ms) || + gateways.reduce>( + (seriesRow, seriesGateway) => { + seriesRow[seriesGateway] = 0 + return seriesRow + }, + { bucket_ms }, + ) + bucket[gateway] = (bucket[gateway] || 0) + 1 + buckets.set(bucket_ms, bucket) + } + + return { + gateways, + rows: Array.from(buckets.values()).sort((left, right) => left.bucket_ms - right.bucket_ms), + } + }, [effectiveWindow, previewBucketSize, previewRows, previewGatewaySummary]) + const latestPreviewActivity = previewRows[0]?.last_seen_ms + const previewListTotalResults = previewList.data?.total_results || 0 + const previewListTotalPages = Math.max( + 1, + Math.ceil(previewListTotalResults / PREVIEW_LIST_PAGE_SIZE), + ) + const previewListStart = previewListTotalResults + ? (previewListPage - 1) * PREVIEW_LIST_PAGE_SIZE + 1 + : 0 + const previewListEnd = previewListTotalResults + ? previewListStart + previewListRows.length - 1 + : 0 + const previewGatewaysTouched = previewGatewaySummary.filter( + (item) => item.gateway !== 'No gateway selected', + ).length + const previewGatewayMaxCount = previewGatewaySummary[0]?.count || 1 + const previewGatewayMixData = useMemo(() => { + const total = previewGatewaySummary.reduce((sum, item) => sum + item.count, 0) + + return previewGatewaySummary.map((item, index) => ({ + name: item.gateway, + value: item.count, + percentage: total ? (item.count / total) * 100 : 0, + color: + item.gateway === 'No gateway selected' + ? '#64748b' + : CHART_COLORS[index % CHART_COLORS.length], + })) + }, [previewGatewaySummary]) + + useEffect(() => { + if (!previewListTotalResults && previewListPage !== 1) { + setPreviewListPage(1) + return + } + + if (previewListPage > previewListTotalPages) { + setPreviewListPage(previewListTotalPages) + } + }, [previewListPage, previewListTotalPages, previewListTotalResults]) + + const gatewayShareData = useMemo(() => { + const gateways = Array.from(new Set((routing.data?.gateway_share || []).map((point) => point.gateway))).slice(0, 6) + const buckets = new Map>() + + for (const point of routing.data?.gateway_share || []) { + if (!gateways.includes(point.gateway)) continue + const row = buckets.get(point.bucket_ms) || { bucket_ms: point.bucket_ms } + row[point.gateway] = point.count + buckets.set(point.bucket_ms, row) + } + + return { + gateways, + rows: Array.from(buckets.values()).sort((left, right) => left.bucket_ms - right.bucket_ms), + } + }, [routing.data]) + + const connectorTrendData = useMemo(() => { + const gateways = Array.from(new Set((filteredRouting.data?.sr_trend || []).map((point) => point.gateway))).slice(0, 6) + const buckets = new Map>() + + for (const point of filteredRouting.data?.sr_trend || []) { + if (!gateways.includes(point.gateway)) continue + const row = buckets.get(point.bucket_ms) || { bucket_ms: point.bucket_ms } + row[point.gateway] = toPercent(point.score_value) + buckets.set(point.bucket_ms, row) + } + + return { + gateways, + rows: Array.from(buckets.values()).sort((left, right) => left.bucket_ms - right.bucket_ms), + } + }, [filteredRouting.data]) + + const latestConnectorSummary = useMemo(() => { + if (!connectorTrendData.rows.length) return [] + const latestRow = connectorTrendData.rows[connectorTrendData.rows.length - 1] + return connectorTrendData.gateways + .map((gateway) => ({ + gateway, + value: typeof latestRow[gateway] === 'number' ? latestRow[gateway] : null, + })) + .filter((item): item is { gateway: string; value: number } => item.value !== null) + }, [connectorTrendData]) + + const connectorTrendDomain = useMemo(() => { + const values = connectorTrendData.rows.flatMap((row) => + connectorTrendData.gateways + .map((gateway) => row[gateway]) + .filter((value): value is number => typeof value === 'number'), + ) + + if (!values.length) return [0, 100] as const + + const min = Math.min(...values) + const max = Math.max(...values) + const padding = min === max ? 5 : Math.max(2, (max - min) * 0.35) + + return [ + Math.max(0, Math.floor(min - padding)), + Math.min(100, Math.ceil(max + padding)), + ] as const + }, [connectorTrendData]) + + const activeFilterSummary = useMemo(() => { + const parts = availableFilters.dimensions.flatMap((dimension) => { + const value = routingFilters.dimensions[dimension.key] + return value ? [`${dimension.label}: ${value}`] : [] + }) + if (routingFilters.gateways.length) parts.push(routingFilters.gateways.join(', ')) + return parts.length ? parts.join(' / ') : 'All routing dimensions' + }, [availableFilters.dimensions, routingFilters]) + + const visibleDimensions = useMemo(() => { + if (showAllFilters || availableFilters.dimensions.length <= MAX_VISIBLE_DIMENSIONS) { + return availableFilters.dimensions + } + return availableFilters.dimensions.slice(0, MAX_VISIBLE_DIMENSIONS) + }, [availableFilters.dimensions, showAllFilters]) + + const hasExtraDimensions = availableFilters.dimensions.length > MAX_VISIBLE_DIMENSIONS + const hiddenDimensionCount = hasExtraDimensions + ? availableFilters.dimensions.length - MAX_VISIBLE_DIMENSIONS + : 0 + + const activeFilterChips = useMemo(() => { + const dimensionChips = availableFilters.dimensions.flatMap((dimension) => { + const value = routingFilters.dimensions[dimension.key] + return value + ? [{ key: `dimension:${dimension.key}`, label: `${dimension.label}: ${value}` }] + : [] + }) + const gatewayChips = routingFilters.gateways.map((gateway) => ({ + key: `gateway:${gateway}`, + label: `Connector: ${gateway}`, + })) + return [...dimensionChips, ...gatewayChips] + }, [availableFilters.dimensions, routingFilters]) + + function handleRangeChange(value: AnalyticsRangeValue) { + setRange(value) + if (value !== 'custom') { + const preset = presetWindow(value) + setCustomStart(toDateTimeInputValue(preset.start_ms)) + setCustomEnd(toDateTimeInputValue(preset.end_ms)) + } + } + + function refreshAll() { + overview.mutate() + routing.mutate() + filteredRouting.mutate() + previewTrace.mutate() + previewList.mutate() + } + + function toggleGatewayFilter(gateway: string) { + setRoutingFilters((current) => { + const exists = current.gateways.includes(gateway) + return { + ...current, + gateways: exists + ? current.gateways.filter((value) => value !== gateway) + : [...current.gateways, gateway], + } + }) + } + + function clearRoutingFilters() { + setRoutingFilters(EMPTY_ROUTING_FILTERS) + } + + function removeRoutingFilterChip(chipKey: string) { + if (chipKey.startsWith('dimension:')) { + updateDimensionFilter(chipKey.replace('dimension:', ''), '') + return + } + if (chipKey.startsWith('gateway:')) { + toggleGatewayFilter(chipKey.replace('gateway:', '')) + } + } + + function updateDimensionFilter(dimensionKey: string, value: string) { + setRoutingFilters((current) => { + const nextDimensions = { ...current.dimensions } + if (value) { + nextDimensions[dimensionKey] = value + } else { + delete nextDimensions[dimensionKey] + } + + return { + ...current, + dimensions: nextDimensions, + } + }) + } + + if (!canQueryCurrent) { + return ( +
+
+

Analytics

+

+ Set a merchant in the top bar to load merchant-scoped analytics. +

+
+ +
+ ) + } + + return ( +
+
+
+
+

Analytics

+ {merchantId || 'Current merchant'} +
+

+ {view === 'transactions' + ? 'One working surface for route volume, connector share, and historical connector success rate.' + : 'Preview-only activity for rule-based routing, separate from transaction decisions and score updates.'} +

+
+ +
+ +
+
+ +
+ + +
+ + + + + + {range === 'custom' ? ( + <> + + + + + ) : null} + +
+

+ Active window +

+

{activeWindowLabel}

+ {range === 'custom' && !customWindow ? ( +

Choose an end time after the start time.

+ ) : null} +
+
+
+ + + + {loading ? ( +
+ + Loading analytics… +
+ ) : null} + + {view === 'transactions' ? ( + <> +
+
+
+

API calls

+

+ Counts for the decision and feedback surfaces tied to real transaction flow. +

+
+ +
+ +
+ {transactionRouteHits.map((item) => ( + + ))} +
+
+ + + +
+
+

Gateway share over time

+

+ How decision volume moved across connectors inside the selected merchant window. +

+
+ +
+
+ + {gatewayShareData.rows.length ? ( +
+ + + + + + formatDateTime(Number(label))} + contentStyle={CHART_TOOLTIP_STYLE} + labelStyle={CHART_TOOLTIP_LABEL_STYLE} + itemStyle={CHART_TOOLTIP_ITEM_STYLE} + wrapperStyle={CHART_TOOLTIP_WRAPPER_STYLE} + /> + + {gatewayShareData.gateways.map((gateway, index) => ( + + ))} + + +
+ ) : ( + + )} +
+
+ + + +
+
+

+ Connector success rate over time +

+

+ Historical connector score trend for the selected merchant window. +

+

+ Active filters: {activeFilterSummary} +

+
+ +
+
+ +
+
+
+

+ Connector filters +

+

+ Narrow the success-rate line chart by the routing dimensions present for this merchant. +

+
+ +
+ + {availableFilters.dimensions.length ? ( +
+
+ {visibleDimensions.map((dimension) => ( + + ))} +
+ {hasExtraDimensions ? ( +
+

+ {showAllFilters + ? 'Showing all routing dimensions available for this merchant.' + : `${hiddenDimensionCount} more routing dimension${hiddenDimensionCount === 1 ? '' : 's'} available for this merchant.`} +

+ +
+ ) : null} +
+ ) : availableFilters.missing_dimensions.length ? ( + + ) : null} + + {availableFilters.missing_dimensions.length ? ( +
+

+ No values in this window yet +

+

+ {availableFilters.missing_dimensions.map((dimension) => dimension.label).join(', ')} +

+
+ ) : null} + + {activeFilterChips.length ? ( +
+

+ Active filters +

+
+ {activeFilterChips.map((chip) => ( + + ))} +
+
+ ) : null} + +
+

+ Connectors +

+
+ {availableFilters.gateways.length ? ( + availableFilters.gateways.map((gateway) => { + const active = routingFilters.gateways.includes(gateway) + return ( + + ) + }) + ) : ( +

+ No connector history yet for the selected window. +

+ )} +
+
+
+ + {latestConnectorSummary.length ? ( +
+ {latestConnectorSummary.map((item) => ( + + {item.gateway}: {formatPercent(item.value)} + + ))} +
+ ) : null} + + {connectorTrendData.rows.length ? ( +
+ + + + + `${formatNumber(Number(value), 0)}%`} + /> + formatDateTime(Number(label))} + formatter={(value: unknown, name: string | number) => [formatPercent(value as number), String(name)]} + contentStyle={CHART_TOOLTIP_STYLE} + labelStyle={CHART_TOOLTIP_LABEL_STYLE} + itemStyle={CHART_TOOLTIP_ITEM_STYLE} + wrapperStyle={CHART_TOOLTIP_WRAPPER_STYLE} + /> + + {connectorTrendData.gateways.map((gateway, index) => ( + + ))} + + +
+ ) : ( + + )} +
+
+ + ) : ( + <> +
+
+
+

Rule-based activity

+

+ Preview-only routing activity from /routing/evaluate, kept separate from transaction routing and gateway scoring. +

+
+ +
+ +
+ + +
+
+ +
+ + +
+
+

+ Connector selections over time +

+

+ Time-bucketed connector counts from the fetched rule-preview sample. +

+
+ +
+
+ + {previewConnectorSeriesData.gateways.length ? ( +
+ + + + + + formatDateTime(Number(label))} + contentStyle={CHART_TOOLTIP_STYLE} + labelStyle={CHART_TOOLTIP_LABEL_STYLE} + itemStyle={CHART_TOOLTIP_ITEM_STYLE} + wrapperStyle={CHART_TOOLTIP_WRAPPER_STYLE} + /> + + {previewConnectorSeriesData.gateways.map((gateway, index) => ( + + ))} + + +
+ ) : ( + + )} +
+
+ + + +
+
+

+ Gateway selection mix +

+

+ Connector share across the fetched rule-preview sample. +

+
+ +
+
+ + {previewGatewayMixData.length ? ( +
+
+ + + [ + `${formatNumber(value as number, 0)} previews`, + `${String(name)} (${formatPercent(item.payload?.percentage || 0)})`, + ]} + contentStyle={CHART_TOOLTIP_STYLE} + labelStyle={CHART_TOOLTIP_LABEL_STYLE} + itemStyle={CHART_TOOLTIP_ITEM_STYLE} + wrapperStyle={CHART_TOOLTIP_WRAPPER_STYLE} + /> + + + {previewGatewayMixData.map((entry) => ( + + ))} + + + +
+

+ Sample size +

+

+ {previewRows.length} +

+

+ preview groups +

+
+
+ +
+ {previewGatewayMixData.map((item) => ( +
+
+
+ +

+ {item.name} +

+
+

+ {item.value} +

+
+

+ {formatPercent(item.percentage)} of fetched previews +

+
+ ))} +
+
+ ) : ( + + )} +
+
+
+ +
+ + +
+
+

Recent rule previews

+

+ Preview-only evaluations captured from /routing/evaluate. This does not affect transaction scoring. +

+
+ + {latestPreviewActivity ? `Latest ${formatDateTime(latestPreviewActivity)}` : 'No activity'} + +
+
+ + {!previewList.data && previewList.isLoading ? ( +
+ + Loading rule previews… +
+ ) : previewList.error && !previewList.data ? ( + + ) : previewListRows.length ? ( +
+
+

+ Showing {previewListStart}-{previewListEnd} of {previewListTotalResults} +

+ {previewList.isLoading ? ( +
+ + Loading page… +
+ ) : null} +
+
+ {previewListRows.map((row) => ( +
+
+
+

+ {row.payment_id || row.request_id || row.lookup_key} +

+

+ {(row.merchant_id || 'unknown merchant')} · {formatDateTime(row.last_seen_ms)} +

+
+ + {row.latest_status || 'preview'} + +
+
+ Rule Evaluate + {row.latest_gateway ? {row.latest_gateway} : null} + {row.event_count} events +
+
+ ))} +
+ {previewListTotalPages > 1 ? ( +
+

+ Page {previewListPage} of {previewListTotalPages} +

+
+ + +
+
+ ) : null} +
+ ) : ( + + )} +
+
+ +
+ + +
+

Gateway activity

+

+ Recent preview selections grouped by latest chosen gateway. +

+
+
+ + {previewGatewaySummary.length ? ( +
+ {previewGatewaySummary.map((item, index) => ( +
+
+

{item.gateway}

+

{item.count}

+
+
+
+
+
+ ))} +
+ ) : ( + + )} + + + + + +
+

Recent preview outcomes

+

+ Status mix from the loaded preview sample. +

+
+
+ + {previewStatusSummary.length ? ( +
+ {previewStatusSummary.map((item) => ( + + {item.status} · {item.count} + + ))} +
+ ) : ( + + )} +
+
+
+
+ + )} +
+ ) +} diff --git a/website/src/components/pages/DebitRoutingPage.tsx b/website/src/components/pages/DebitRoutingPage.tsx new file mode 100644 index 00000000..cea9ba24 --- /dev/null +++ b/website/src/components/pages/DebitRoutingPage.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import useSWR from 'swr' +import { Card, CardBody, CardHeader } from '../ui/Card' +import { Button } from '../ui/Button' +import { ErrorMessage } from '../ui/ErrorMessage' +import { Spinner } from '../ui/Spinner' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { DebitRoutingData, CreateRuleRequest } from '../../types/api' +import { Network } from 'lucide-react' + +interface RuleConfigResponse { + merchant_id: string + config: { type: string; data: DebitRoutingData } +} + +export function DebitRoutingPage() { + const { merchantId } = useMerchantStore() + + const { data: existing, mutate, isLoading } = useSWR( + merchantId ? ['rule-debit', merchantId] : null, + () => apiPost('/rule/get', { merchant_id: merchantId, config: { type: 'debitRouting' } }) + ) + + const [mcc, setMcc] = useState('') + const [country, setCountry] = useState('') + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + + // Pre-fill from fetched config + const current = existing?.config?.data + const displayMcc = mcc || current?.merchant_category_code || '' + const displayCountry = country || current?.acquirer_country || '' + + async function handleSave() { + if (!merchantId) return setError('Set a merchant ID first') + const payload: CreateRuleRequest = { + merchant_id: merchantId, + config: { + type: 'debitRouting', + data: { + merchant_category_code: displayMcc.trim(), + acquirer_country: displayCountry.trim(), + } as DebitRoutingData, + }, + } + setSaving(true); setError(null) + try { + await apiPost(existing ? '/rule/update' : '/rule/create', payload) + setSuccess('Debit routing config saved.') + mutate() + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to save') + } finally { + setSaving(false) + } + } + + return ( +
+
+

Network / Debit Routing

+

+ Configure network-based routing to optimise processing fees for debit card transactions. + The engine selects the cheapest eligible network (Visa, Mastercard, ACCEL, NYCE, PULSE, STAR). +

+
+ + + +
+ +

Debit Routing Configuration

+
+
+ + {isLoading ? ( +
+ ) : ( + <> + {!merchantId && ( +

+ Set a merchant ID in the top bar to load configuration. +

+ )} + +
+
+ + setMcc(e.target.value)} + placeholder="e.g. 5411" + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +

4-digit ISO MCC for your business type

+
+ +
+ + setCountry(e.target.value)} + placeholder="e.g. US" + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +

ISO 3166-1 alpha-2 country code

+
+
+ + + {success &&

{success}

} + + + + )} +
+
+ + + +

How Network Routing Works

+
+ +

For co-badged debit cards (e.g. Visa/NYCE, Mastercard/PULSE), the engine evaluates all eligible networks and routes to the one with the lowest processing fee.

+

Supported networks: {['VISA','MASTERCARD','ACCEL','NYCE','PULSE','STAR'].map(n => {n})}

+

Use the Decision Explorer to test network routing decisions with NtwBasedRouting algorithm.

+
+
+
+ ) +} diff --git a/website/src/components/pages/DecisionExplorerPage.tsx b/website/src/components/pages/DecisionExplorerPage.tsx new file mode 100644 index 00000000..f38ec1c3 --- /dev/null +++ b/website/src/components/pages/DecisionExplorerPage.tsx @@ -0,0 +1,2439 @@ +import { useEffect, useMemo, useState } from 'react' +import useSWR from 'swr' +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, PieChart, Pie } from 'recharts' +import { Button } from '../ui/Button' +import { Badge } from '../ui/Badge' +import { Card, CardBody, CardHeader, SurfaceLabel } from '../ui/Card' +import { ErrorMessage } from '../ui/ErrorMessage' +import { Spinner } from '../ui/Spinner' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost, fetcher } from '../../lib/api' +import { DecideGatewayResponse, GatewayConnector, PaymentAuditEvent, PaymentAuditResponse } from '../../types/api' +import { ROUTING_APPROACH_COLORS } from '../../lib/constants' +import { useDynamicRoutingConfig } from '../../hooks/useDynamicRoutingConfig' +import { Play, RefreshCw, ChevronDown, ChevronUp, Activity, Code, Plus, Trash2, PieChart as PieChartIcon, X } from 'lucide-react' + +const ALGORITHMS = ['SR_BASED_ROUTING', 'PL_BASED_ROUTING', 'NTW_BASED_ROUTING'] + +const ALGORITHM_LABELS: Record = { + 'SR_BASED_ROUTING': 'Success Rate Based', + 'PL_BASED_ROUTING': 'Priority List Based', + 'NTW_BASED_ROUTING': 'Network Based' +} + +type TabType = 'single' | 'batch' | 'rule' | 'volume' + +interface FormState { + amount: string + currency: string + payment_method_type: string + payment_method: string + card_brand: string + auth_type: string + eligible_gateways: string + ranking_algorithm: string + elimination_enabled: boolean +} + +interface SimulationConfig { + totalPayments: string + successCount: string + failureCount: string +} + +interface SimulationResult { + paymentId: string + decidedGateway: string + status: 'CHARGED' | 'FAILURE' + timestamp: string +} + +type TransactionOutcome = 'CHARGED' | 'FAILURE' + +type AuditInspectorTab = 'summary' | 'input' | 'response' | 'raw' + +interface RuleEvaluateParams { + key: string + type: 'enum_variant' | 'str_value' | 'number' | 'metadata_variant' + value: string + metadataKey?: string +} + +interface RuleEvaluateResponse { + payment_id: string | null + status: string + output: { + type: 'single' | 'priority' | 'volume_split' + connector?: GatewayConnector + connectors?: GatewayConnector[] + splits?: { connector: GatewayConnector; split: number }[] + } + evaluated_output?: GatewayConnector[] + eligible_connectors?: GatewayConnector[] +} + +function approachColor(approach: string): string { + for (const [k, v] of Object.entries(ROUTING_APPROACH_COLORS)) { + if (approach.includes(k) || k.includes(approach)) return v + } + return 'bg-white/5 text-slate-600 ring-1 ring-inset ring-white/8' +} + +const COLORS = ['#0069ED', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'] + +type VolumePaymentEntry = { + paymentId: string + connector: string +} + +function toUpperOptions(values: string[] = []): string[] { + return values.map(v => v.trim()).filter(Boolean).map(v => v.toUpperCase()) +} + +function uniqueUpperOptions(values: string[] = []): string[] { + return Array.from(new Set(toUpperOptions(values))) +} + +function extractVolumeConnector(response: RuleEvaluateResponse) { + return ( + response.evaluated_output?.[0]?.gateway_name || + response.output.connector?.gateway_name || + response.output.connectors?.[0]?.gateway_name || + null + ) +} + +function mapRoutingTypeToRuleParamType( + keyType?: 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' +): RuleEvaluateParams['type'] { + if (keyType === 'enum') return 'enum_variant' + if (keyType === 'integer') return 'number' + if (keyType === 'udf' || keyType === 'global_ref') return 'metadata_variant' + return 'str_value' +} + +function queryString(params: Record) { + const search = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + search.set(key, String(value)) + } + }) + return search.toString() +} + +function formatDateTime(ms: number) { + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(ms)) +} + +function humanizeAuditValue(value?: string | null) { + if (!value) return '' + const normalized = value + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase() + + return normalized.replace(/\b\w/g, (char) => char.toUpperCase()) +} + +function routeLabel(route?: string | null) { + if (!route) return 'Unknown route' + if (route === 'decision_gateway' || route === 'decide_gateway') return 'Decide Gateway' + if (route === 'update_gateway_score') return 'Update Gateway' + if (route === 'routing_evaluate') return 'Rule Evaluate' + return humanizeAuditValue(route) +} + +function eventTypeLabel(eventType?: string | null) { + if (!eventType) return 'Unknown event' + if (eventType === 'decision') return 'Decide Gateway' + if (eventType === 'gateway_update') return 'Update Gateway' + if (eventType === 'rule_hit') return 'Rule Evaluate' + if (eventType === 'rule_evaluation_preview') return 'Preview Result' + if (eventType === 'error') return 'Errors' + return humanizeAuditValue(eventType) +} + +function stageLabel(event: PaymentAuditEvent) { + if (event.event_stage === 'gateway_decided') return 'Decide Gateway' + if (event.event_stage === 'score_updated') return 'Update Gateway' + if (event.event_stage === 'rule_applied') return 'Rule Evaluate' + if (event.event_stage === 'preview_evaluated' || event.event_type === 'rule_evaluation_preview') return 'Preview Result' + if (event.event_type === 'error') return 'Errors' + return humanizeAuditValue(event.event_stage || event.event_type) +} + +function eventPhase(event: PaymentAuditEvent) { + if (event.event_type === 'decision' || event.event_stage === 'gateway_decided') return 'Decide Gateway' + if (event.event_type === 'rule_hit' || event.event_stage === 'rule_applied') return 'Rule Evaluate' + if (event.event_type === 'gateway_update' || event.event_stage === 'score_updated') return 'Update Gateway' + if (event.event_type === 'rule_evaluation_preview' || event.event_stage === 'preview_evaluated') return 'Preview' + return 'Errors' +} + +function badgeVariantForEvent(event: PaymentAuditEvent): 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' { + const normalizedStatus = (event.status || '').toUpperCase() + if ( + event.event_type === 'error' || + normalizedStatus === 'FAILURE' || + normalizedStatus.includes('FAILED') || + normalizedStatus.includes('DECLINED') + ) return 'red' + if (event.event_type === 'rule_hit') return 'purple' + if ( + normalizedStatus === 'CHARGED' || + normalizedStatus === 'AUTHORIZED' || + normalizedStatus === 'SUCCESS' + ) return 'green' + if (event.event_type === 'rule_evaluation_preview') return 'purple' + if (event.event_type === 'gateway_update') return 'green' + if (event.event_type === 'decision') return 'blue' + return 'orange' +} + +function summaryBadgeVariant(status?: string | null): 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' { + const normalizedStatus = (status || '').toUpperCase() + if ( + normalizedStatus === 'FAILURE' || + normalizedStatus.includes('FAILED') || + normalizedStatus.includes('DECLINED') + ) return 'red' + if ( + normalizedStatus === 'SUCCESS' || + normalizedStatus === 'CHARGED' || + normalizedStatus === 'AUTHORIZED' + ) return 'green' + return 'gray' +} + +function phaseBadgeVariant(phase: string): 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' { + if (phase === 'Decide Gateway') return 'blue' + if (phase === 'Rule Evaluate') return 'purple' + if (phase === 'Preview') return 'purple' + if (phase === 'Update Gateway') return 'green' + if (phase === 'Errors') return 'red' + return 'gray' +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + +function cleanRecord(record: Record) { + return Object.fromEntries( + Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== ''), + ) +} + +function stringifyValue(value: unknown) { + if (typeof value === 'string') return value + return JSON.stringify(value, null, 2) +} + +function buildAuditUrl(merchantId: string, paymentId: string) { + const qs = queryString({ + scope: 'current', + range: '24h', + page: 1, + page_size: 25, + merchant_id: merchantId, + payment_id: paymentId, + }) + return `/analytics/payment-audit?${qs}` +} + +function buildPreviewTraceUrl(merchantId: string, paymentId: string) { + const qs = queryString({ + scope: 'current', + range: '24h', + page: 1, + page_size: 25, + merchant_id: merchantId, + payment_id: paymentId, + }) + return `/analytics/preview-trace?${qs}` +} + +function buildInspectorModel(event: PaymentAuditEvent | null) { + if (!event) return null + + const details = isRecord(event.details_json) ? event.details_json : {} + const explicitResponse = + details.response ?? + details.response_payload ?? + details.result ?? + details.output ?? + null + const requestPayload = + details.request ?? + details.request_payload ?? + details.input ?? + details.payload ?? + cleanRecord({ + payment_id: event.payment_id, + request_id: event.request_id, + payment_method_type: event.payment_method_type, + payment_method: event.payment_method, + gateway: event.gateway, + }) + const responsePayload = + explicitResponse ?? + cleanRecord({ + event_type: event.event_type, + status: event.status, + error_code: event.error_code, + error_message: event.error_message, + score_value: event.score_value, + sigma_factor: event.sigma_factor, + average_latency: event.average_latency, + tp99_latency: event.tp99_latency, + transaction_count: event.transaction_count, + rule_name: event.rule_name, + routing_approach: event.routing_approach, + }) + const responseRecord = isRecord(explicitResponse) ? explicitResponse : null + const decidedGatewayRecord = isRecord(responseRecord?.['decided_gateway']) ? responseRecord['decided_gateway'] : null + const scoreContext = + details.score_context ?? + (decidedGatewayRecord ? decidedGatewayRecord['gateway_priority_map'] : null) ?? + (responseRecord ? responseRecord['gateway_priority_map'] : null) ?? + null + const selectionReason = details.selection_reason ?? null + + const summaryRows = [ + { label: 'Phase', value: eventPhase(event) }, + { label: 'Stage', value: stageLabel(event) }, + { label: 'Route', value: routeLabel(event.route) }, + { label: 'Timestamp', value: formatDateTime(event.created_at_ms) }, + ...(event.merchant_id ? [{ label: 'Merchant', value: event.merchant_id }] : []), + ...(event.payment_id ? [{ label: 'Payment ID', value: event.payment_id }] : []), + ...(event.request_id ? [{ label: 'Request ID', value: event.request_id }] : []), + ...(event.gateway ? [{ label: 'Gateway', value: event.gateway }] : []), + ...(event.status ? [{ label: 'Status', value: humanizeAuditValue(event.status) }] : []), + ] + + const signalRecord = cleanRecord( + Object.fromEntries( + Object.entries(details).filter(([key]) => ![ + 'request', + 'request_payload', + 'input', + 'payload', + 'response', + 'response_payload', + 'result', + 'output', + 'score_context', + 'selection_reason', + ].includes(key)), + ), + ) + + return { + summaryRows, + requestPayload: isRecord(requestPayload) && !Object.keys(requestPayload).length ? null : requestPayload, + responsePayload: isRecord(responsePayload) && !Object.keys(responsePayload).length ? null : responsePayload, + scoreContext, + selectionReason, + signalRecord: Object.keys(signalRecord).length ? signalRecord : null, + rawEvent: { + ...event, + details_json: event.details_json, + }, + } +} + +function sectionButtonClass(active: boolean) { + return active + ? '!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white' + : '!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white' +} + +function EmptyAuditState({ title, body }: { title: string; body: string }) { + return ( +
+

{title}

+

{body}

+
+ ) +} + +function InspectorKeyValueGrid({ rows }: { rows: Array<{ label: string; value: string }> }) { + if (!rows.length) return null + + return ( +
+ {rows.map((row) => ( +
+

+ {row.label} +

+

{row.value}

+
+ ))} +
+ ) +} + +function InspectorJsonPanel({ + title, + value, + emptyMessage, +}: { + title: string + value: unknown + emptyMessage: string +}) { + return ( +
+
+

{title}

+
+ {value ? ( +
+          {stringifyValue(value)}
+        
+ ) : ( + + )} +
+ ) +} + +export function DecisionExplorerPage() { + const { merchantId } = useMerchantStore() + const { routingKeysConfig, isLoading: routingKeysLoading, error: routingKeysError } = useDynamicRoutingConfig() + const hasRoutingKeys = Object.keys(routingKeysConfig).length > 0 + const routingConfigUnavailable = !routingKeysLoading && (!hasRoutingKeys || Boolean(routingKeysError)) + const [activeTab, setActiveTab] = useState('single') + + const [form, setForm] = useState({ + amount: '1000', + currency: '', + payment_method_type: '', + payment_method: '', + card_brand: '', + auth_type: '', + eligible_gateways: 'stripe, adyen', + ranking_algorithm: 'SR_BASED_ROUTING', + elimination_enabled: false, + }) + + const [simulationConfig, setSimulationConfig] = useState({ + totalPayments: '10', + successCount: '7', + failureCount: '3', + }) + + const [ruleParams, setRuleParams] = useState([ + { key: 'payment_method_type', type: 'enum_variant', value: '', metadataKey: '' }, + { key: 'currency', type: 'enum_variant', value: '', metadataKey: '' }, + ]) + + const [fallbackConnectors, setFallbackConnectors] = useState([ + { gateway_name: 'stripe', gateway_id: 'gateway_001' }, + { gateway_name: 'adyen', gateway_id: 'gateway_002' }, + ]) + + const [volumePayments, setVolumePayments] = useState('100') + + const [result, setResult] = useState(null) + const [singleRunPaymentId, setSingleRunPaymentId] = useState(null) + const [singleRunOutcome, setSingleRunOutcome] = useState('CHARGED') + const [ruleResult, setRuleResult] = useState(null) + const [volumeDistribution, setVolumeDistribution] = useState<{ name: string; count: number; percentage: number }[]>([]) + const [volumeEvaluationLog, setVolumeEvaluationLog] = useState([]) + const [volumeProgress, setVolumeProgress] = useState(0) + const [simulationResults, setSimulationResults] = useState([]) + const [isSimulating, setIsSimulating] = useState(false) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + const [filterOpen, setFilterOpen] = useState(false) + const [responseOpen, setResponseOpen] = useState(false) + const [volumeResponseOpen, setVolumeResponseOpen] = useState(false) + const [selectedAuditPaymentId, setSelectedAuditPaymentId] = useState(null) + const [selectedAuditEventId, setSelectedAuditEventId] = useState(null) + const [auditInspectorTab, setAuditInspectorTab] = useState('summary') + const [selectedPreviewPaymentId, setSelectedPreviewPaymentId] = useState(null) + const [selectedPreviewEventId, setSelectedPreviewEventId] = useState(null) + const [previewInspectorTab, setPreviewInspectorTab] = useState('summary') + const [previewTraceLabel, setPreviewTraceLabel] = useState('Rule Evaluation Preview') + + const routingKeyNames = useMemo( + () => Object.keys(routingKeysConfig).sort(), + [routingKeysConfig] + ) + + const paymentMethodTypeOptions = useMemo( + () => toUpperOptions(routingKeysConfig.payment_method?.values || []), + [routingKeysConfig] + ) + + const paymentMethodOptions = useMemo(() => { + const methodTypeKey = form.payment_method_type.toLowerCase() + return toUpperOptions(routingKeysConfig[methodTypeKey]?.values || []) + }, [form.payment_method_type, routingKeysConfig]) + + const currencyOptions = useMemo( + () => uniqueUpperOptions(routingKeysConfig.currency?.values || []), + [routingKeysConfig] + ) + + const cardBrandOptions = useMemo( + () => uniqueUpperOptions(routingKeysConfig.card_network?.values || []), + [routingKeysConfig] + ) + + const authTypeOptions = useMemo( + () => uniqueUpperOptions(routingKeysConfig.authentication_type?.values || []), + [routingKeysConfig] + ) + + const auditUrl = merchantId && selectedAuditPaymentId + ? buildAuditUrl(merchantId, selectedAuditPaymentId) + : null + + const auditDetail = useSWR(auditUrl, fetcher, { + refreshInterval: selectedAuditPaymentId ? 12000 : 0, + revalidateOnFocus: true, + }) + + const previewTraceUrl = merchantId && selectedPreviewPaymentId + ? buildPreviewTraceUrl(merchantId, selectedPreviewPaymentId) + : null + + const previewTraceDetail = useSWR(previewTraceUrl, fetcher, { + refreshInterval: selectedPreviewPaymentId ? 12000 : 0, + revalidateOnFocus: true, + }) + + useEffect(() => { + if (routingConfigUnavailable || routingKeysLoading) return + + setForm(prev => { + const next = { ...prev } + + if (currencyOptions.length > 0 && !currencyOptions.includes(next.currency)) { + next.currency = currencyOptions[0] + } + + if (paymentMethodTypeOptions.length > 0 && !paymentMethodTypeOptions.includes(next.payment_method_type)) { + next.payment_method_type = paymentMethodTypeOptions[0] + } + + const methodsForType = toUpperOptions( + routingKeysConfig[next.payment_method_type.toLowerCase()]?.values || [] + ) + if (methodsForType.length > 0 && !methodsForType.includes(next.payment_method)) { + next.payment_method = methodsForType[0] + } + + if (authTypeOptions.length > 0 && !authTypeOptions.includes(next.auth_type)) { + next.auth_type = authTypeOptions[0] + } + + if (cardBrandOptions.length > 0 && !cardBrandOptions.includes(next.card_brand)) { + next.card_brand = cardBrandOptions[0] + } + + return next + }) + + setRuleParams(prev => + prev.map(param => { + if (!param.key || !routingKeysConfig[param.key]) return param + const keyConfig = routingKeysConfig[param.key] + const mappedType = mapRoutingTypeToRuleParamType(keyConfig.type) + const enumValues = keyConfig.values || [] + const nextValue = mappedType === 'enum_variant' + ? (enumValues.includes(param.value) ? param.value : (enumValues[0] || '')) + : param.value + return { ...param, type: mappedType, value: nextValue } + }) + ) + }, [ + routingConfigUnavailable, + routingKeysLoading, + routingKeysConfig, + currencyOptions, + paymentMethodTypeOptions, + authTypeOptions, + cardBrandOptions, + ]) + + useEffect(() => { + if (!selectedAuditPaymentId && !selectedPreviewPaymentId) return + + const previousOverflow = document.body.style.overflow + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setSelectedAuditPaymentId(null) + setSelectedAuditEventId(null) + setAuditInspectorTab('summary') + setSelectedPreviewPaymentId(null) + setSelectedPreviewEventId(null) + setPreviewInspectorTab('summary') + } + } + + document.body.style.overflow = 'hidden' + window.addEventListener('keydown', onKeyDown) + + return () => { + document.body.style.overflow = previousOverflow + window.removeEventListener('keydown', onKeyDown) + } + }, [selectedAuditPaymentId, selectedPreviewPaymentId]) + + function set(field: keyof FormState, value: string | boolean) { + setForm(f => ({ ...f, [field]: value })) + } + + function addRuleParam() { + if (routingKeyNames.length === 0) return + const firstKey = routingKeyNames[0] + const firstConfig = routingKeysConfig[firstKey] + const mappedType = mapRoutingTypeToRuleParamType(firstConfig?.type) + const firstValue = mappedType === 'enum_variant' ? (firstConfig?.values?.[0] || '') : '' + setRuleParams([...ruleParams, { key: firstKey, type: mappedType, value: firstValue, metadataKey: '' }]) + } + + function removeRuleParam(index: number) { + setRuleParams(ruleParams.filter((_, i) => i !== index)) + } + + function updateRuleParam(index: number, field: keyof RuleEvaluateParams, value: string) { + setRuleParams(ruleParams.map((p, i) => i === index ? { ...p, [field]: value } : p)) + } + + function updateRuleParamMetadataKey(index: number, value: string) { + setRuleParams(ruleParams.map((p, i) => i === index ? { ...p, metadataKey: value } : p)) + } + + function updateRuleParamKey(index: number, key: string) { + const keyConfig = routingKeysConfig[key] + const mappedType = mapRoutingTypeToRuleParamType(keyConfig?.type) + const nextValue = mappedType === 'enum_variant' ? (keyConfig?.values?.[0] || '') : '' + setRuleParams(ruleParams.map((p, i) => ( + i === index ? { ...p, key, type: mappedType, value: nextValue, metadataKey: '' } : p + ))) + } + + function addFallbackConnector() { + setFallbackConnectors([...fallbackConnectors, { gateway_name: '', gateway_id: '' }]) + } + + function removeFallbackConnector(index: number) { + setFallbackConnectors(fallbackConnectors.filter((_, i) => i !== index)) + } + + function updateFallbackConnector(index: number, field: keyof GatewayConnector, value: string) { + setFallbackConnectors(fallbackConnectors.map((c, i) => i === index ? { ...c, [field]: value } : c)) + } + + async function run() { + if (!merchantId) return setError('Set a merchant ID in the top bar') + if (routingConfigUnavailable) return setError('Routing key config unavailable. Fix /config/routing-keys and retry.') + setLoading(true); setError(null) + setSingleRunPaymentId(null) + const gateways = form.eligible_gateways.split(',').map(s => s.trim()).filter(Boolean) + const paymentId = `explorer_${Date.now()}` + try { + const res = await apiPost('/decide-gateway', { + merchantId: merchantId, + paymentInfo: { + paymentId: paymentId, + amount: parseFloat(form.amount) || 1000, + currency: form.currency, + paymentType: 'ORDER_PAYMENT', + paymentMethodType: form.payment_method_type, + paymentMethod: form.payment_method, + authType: form.auth_type, + cardBrand: form.card_brand, + }, + eligibleGatewayList: gateways, + rankingAlgorithm: form.ranking_algorithm, + eliminationEnabled: form.elimination_enabled, + }) + await apiPost('/update-gateway-score', { + merchantId: merchantId, + gateway: res.decided_gateway, + gatewayReferenceId: null, + status: singleRunOutcome, + paymentId: paymentId, + enforceDynamicRoutingFailure: null, + }) + setResult(res) + setSingleRunPaymentId(paymentId) + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Request failed') + } finally { + setLoading(false) + } + } + + async function runSimulation() { + if (!merchantId) return setError('Set a merchant ID in the top bar') + if (routingConfigUnavailable) return setError('Routing key config unavailable. Fix /config/routing-keys and retry.') + + const total = parseInt(simulationConfig.totalPayments) || 0 + const success = parseInt(simulationConfig.successCount) || 0 + const failure = parseInt(simulationConfig.failureCount) || 0 + + if (total <= 0) return setError('Total Payments must be greater than 0') + if (success + failure !== total) { + return setError('Success + Failure count must equal Total Payments') + } + + setIsSimulating(true) + setError(null) + setSimulationResults([]) + + const gateways = form.eligible_gateways.split(',').map(s => s.trim()).filter(Boolean) + const results: SimulationResult[] = [] + + const outcomes: ('CHARGED' | 'FAILURE')[] = [ + ...Array(success).fill('CHARGED'), + ...Array(failure).fill('FAILURE'), + ] + + for (let i = outcomes.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [outcomes[i], outcomes[j]] = [outcomes[j], outcomes[i]] + } + + try { + for (let i = 0; i < total; i++) { + const paymentId = `sim_${Date.now()}_${i}` + + const decideRes = await apiPost('/decide-gateway', { + merchantId: merchantId, + paymentInfo: { + paymentId: paymentId, + amount: parseFloat(form.amount) || 1000, + currency: form.currency, + paymentType: 'ORDER_PAYMENT', + paymentMethodType: form.payment_method_type, + paymentMethod: form.payment_method, + authType: form.auth_type, + cardBrand: form.card_brand, + }, + eligibleGatewayList: gateways, + rankingAlgorithm: form.ranking_algorithm, + eliminationEnabled: form.elimination_enabled, + }) + + const decidedGateway = decideRes.decided_gateway + const outcome = outcomes[i] + + await apiPost('/update-gateway-score', { + merchantId: merchantId, + gateway: decidedGateway, + gatewayReferenceId: null, + status: outcome, + paymentId: paymentId, + enforceDynamicRoutingFailure: null, + }) + + results.push({ + paymentId, + decidedGateway, + status: outcome, + timestamp: new Date().toISOString(), + }) + + setSimulationResults([...results]) + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Simulation failed') + } finally { + setIsSimulating(false) + } + } + + async function runRuleEvaluation() { + if (routingConfigUnavailable) return setError('Routing key config unavailable. Fix /config/routing-keys and retry.') + setLoading(true) + setError(null) + setRuleResult(null) + setVolumeDistribution([]) + setVolumeEvaluationLog([]) + setVolumeProgress(0) + const previewPaymentId = `rule_preview_${Date.now()}` + + try { + const parameters: Record = {} + ruleParams.forEach(p => { + if (p.key) { + if (p.type === 'metadata_variant') { + parameters[p.key] = { + type: p.type, + value: { key: p.metadataKey || p.key, value: p.value } + } + } else if (p.type === 'number') { + parameters[p.key] = { type: p.type, value: parseFloat(p.value) || 0 } + } else { + parameters[p.key] = { type: p.type, value: p.value } + } + } + }) + + const res = await apiPost('/routing/evaluate', { + created_by: merchantId || 'test_user', + payment_id: previewPaymentId, + fallback_output: fallbackConnectors.filter(c => c.gateway_name), + parameters, + }) + + setRuleResult(res) + + if (res.output.type === 'volume_split' && res.output.splits) { + const totalPayments = parseInt(volumePayments) || 100 + const distribution = res.output.splits.map(item => ({ + name: item.connector.gateway_name, + count: Math.round((item.split / 100) * totalPayments), + percentage: item.split, + })) + setVolumeDistribution(distribution) + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Request failed') + } finally { + setLoading(false) + } + } + + async function runVolumeSplit() { + if (!merchantId) return setError('Set a merchant ID in the top bar') + setLoading(true) + setError(null) + setRuleResult(null) + setVolumeDistribution([]) + setVolumeEvaluationLog([]) + setVolumeProgress(0) + const totalPayments = parseInt(volumePayments) || 0 + + if (totalPayments <= 0) { + setLoading(false) + return setError('Total Payments must be greater than 0') + } + + try { + const batchSize = 10 + const basePaymentId = `volume_preview_${Date.now()}` + const logEntries: VolumePaymentEntry[] = [] + const counts = new Map() + let latestResponse: RuleEvaluateResponse | null = null + + for (let start = 0; start < totalPayments; start += batchSize) { + const chunkSize = Math.min(batchSize, totalPayments - start) + const chunkResponses = await Promise.all( + Array.from({ length: chunkSize }, async (_, offset) => { + const index = start + offset + const paymentId = `${basePaymentId}_${index}` + const response = await apiPost('/routing/evaluate', { + created_by: merchantId, + payment_id: paymentId, + fallback_output: [ + { gateway_name: 'stripe', gateway_id: 'gateway_001' }, + { gateway_name: 'adyen', gateway_id: 'gateway_002' }, + ], + parameters: {}, + }) + + return { paymentId, response } + }), + ) + + for (const { paymentId, response } of chunkResponses) { + if (response.output.type !== 'volume_split') { + throw new Error('Active routing algorithm is not a volume split rule.') + } + + const connector = extractVolumeConnector(response) + if (!connector) { + throw new Error('Volume split evaluation did not return a connector.') + } + + counts.set(connector, (counts.get(connector) || 0) + 1) + logEntries.push({ paymentId, connector }) + latestResponse = response + } + + setVolumeProgress(logEntries.length) + } + + if (latestResponse) { + const distribution = Array.from(counts.entries()) + .map(([name, count]) => ({ + name, + count, + percentage: Number(((count / totalPayments) * 100).toFixed(1)), + })) + .sort((left, right) => right.count - left.count) + + setRuleResult(latestResponse) + setVolumeEvaluationLog(logEntries) + setVolumeDistribution(distribution) + setSelectedPreviewPaymentId(latestResponse.payment_id) + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Request failed') + } finally { + setLoading(false) + } + } + + const scoreData = result?.gateway_priority_map + ? Object.entries(result.gateway_priority_map) + .sort(([, a], [, b]) => b - a) + .map(([name, score]) => ({ name, score: Math.round(score * 1000) / 10 })) + : [] + + const gatewayStats = simulationResults.reduce((acc, curr) => { + if (!acc[curr.decidedGateway]) { + acc[curr.decidedGateway] = { total: 0, success: 0, failure: 0 } + } + acc[curr.decidedGateway].total++ + if (curr.status === 'CHARGED') acc[curr.decidedGateway].success++ + else acc[curr.decidedGateway].failure++ + return acc + }, {} as Record) + + const pieData = volumeDistribution.map(d => ({ name: d.name, value: d.count })) + const volumeColorIndex = useMemo( + () => new Map(volumeDistribution.map((item, index) => [item.name, index] as const)), + [volumeDistribution], + ) + + const auditSummary = useMemo(() => { + const results = auditDetail.data?.results || [] + return results.find((row) => row.payment_id === selectedAuditPaymentId) || results[0] || null + }, [auditDetail.data?.results, selectedAuditPaymentId]) + + const selectedAuditEvent = useMemo(() => { + const timeline = auditDetail.data?.timeline || [] + return timeline.find((event) => event.id === selectedAuditEventId) || timeline[0] || null + }, [auditDetail.data?.timeline, selectedAuditEventId]) + + useEffect(() => { + if (selectedAuditEvent?.id) { + setSelectedAuditEventId(selectedAuditEvent.id) + return + } + const first = auditDetail.data?.timeline?.[0] + if (first?.id) { + setSelectedAuditEventId(first.id) + } + }, [auditDetail.data?.timeline, selectedAuditEvent?.id]) + + const groupedAuditTimeline = useMemo(() => { + const groups: Array<{ phase: string; events: PaymentAuditEvent[] }> = [] + for (const event of auditDetail.data?.timeline || []) { + const phase = eventPhase(event) + const current = groups[groups.length - 1] + if (!current || current.phase !== phase) { + groups.push({ phase, events: [event] }) + } else { + current.events.push(event) + } + } + return groups + }, [auditDetail.data?.timeline]) + + const auditInspectorModel = useMemo(() => buildInspectorModel(selectedAuditEvent), [selectedAuditEvent]) + + const previewSummary = useMemo(() => { + const results = previewTraceDetail.data?.results || [] + return results.find((row) => row.payment_id === selectedPreviewPaymentId) || results[0] || null + }, [previewTraceDetail.data?.results, selectedPreviewPaymentId]) + + const selectedPreviewEvent = useMemo(() => { + const timeline = previewTraceDetail.data?.timeline || [] + return timeline.find((event) => event.id === selectedPreviewEventId) || timeline[0] || null + }, [previewTraceDetail.data?.timeline, selectedPreviewEventId]) + + useEffect(() => { + if (selectedPreviewEvent?.id) { + setSelectedPreviewEventId(selectedPreviewEvent.id) + return + } + const first = previewTraceDetail.data?.timeline?.[0] + if (first?.id) { + setSelectedPreviewEventId(first.id) + } + }, [previewTraceDetail.data?.timeline, selectedPreviewEvent?.id]) + + const groupedPreviewTimeline = useMemo(() => { + const groups: Array<{ phase: string; events: PaymentAuditEvent[] }> = [] + for (const event of previewTraceDetail.data?.timeline || []) { + const phase = eventPhase(event) + const current = groups[groups.length - 1] + if (!current || current.phase !== phase) { + groups.push({ phase, events: [event] }) + } else { + current.events.push(event) + } + } + return groups + }, [previewTraceDetail.data?.timeline]) + + const previewInspectorModel = useMemo(() => buildInspectorModel(selectedPreviewEvent), [selectedPreviewEvent]) + + function openAuditModal(paymentId: string) { + setSelectedPreviewPaymentId(null) + setSelectedPreviewEventId(null) + setPreviewInspectorTab('summary') + setSelectedAuditPaymentId(paymentId) + setSelectedAuditEventId(null) + setAuditInspectorTab('summary') + } + + function closeAuditModal() { + setSelectedAuditPaymentId(null) + setSelectedAuditEventId(null) + setAuditInspectorTab('summary') + } + + function openPreviewModal(paymentId: string, label: string) { + setSelectedAuditPaymentId(null) + setSelectedAuditEventId(null) + setAuditInspectorTab('summary') + setPreviewTraceLabel(label) + setSelectedPreviewPaymentId(paymentId) + setSelectedPreviewEventId(null) + setPreviewInspectorTab('summary') + } + + function closePreviewModal() { + setSelectedPreviewPaymentId(null) + setSelectedPreviewEventId(null) + setPreviewInspectorTab('summary') + } + + return ( +
+
+

Decision Explorer

+

+ Test payment routing with different algorithms: Success Rate, Priority List, Rule-Based, or Volume Split. +

+
+ +
+ + + + +
+ +
+ + +
+ + {activeTab === 'rule' ? 'Rule Evaluation' : + activeTab === 'volume' ? 'Volume Split' : + 'Payment Setup'} + +

+ {activeTab === 'rule' ? 'Rule Evaluation Parameters' : + activeTab === 'volume' ? 'Volume Split Configuration' : + 'Payment Parameters'} +

+
+
+ + {!merchantId && ( +

+ Set a merchant ID in the top bar first. +

+ )} + {activeTab !== 'volume' && routingKeysLoading && ( +

+ Loading routing config from backend... +

+ )} + {activeTab !== 'volume' && routingConfigUnavailable && ( + + )} + + {activeTab === 'rule' ? ( + <> + {routingKeysLoading && ( +

Loading routing keys from backend...

+ )} + {routingConfigUnavailable && ( + + )} +
+ +
+ {ruleParams.map((param, idx) => ( +
+
+ + + +
+ {param.type === 'metadata_variant' ? ( +
+ updateRuleParamMetadataKey(idx, e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + updateRuleParam(idx, 'value', e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+ ) : param.type === 'enum_variant' ? ( +
+ +
+ ) : param.type === 'number' ? ( +
+ updateRuleParam(idx, 'value', e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+ ) : ( +
+ updateRuleParam(idx, 'value', e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+ )} +
+ ))} +
+ +
+ +
+ +
+ {fallbackConnectors.map((connector, idx) => ( +
+ updateFallbackConnector(idx, 'gateway_name', e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + updateFallbackConnector(idx, 'gateway_id', e.target.value)} + className="flex-1 border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + +
+ ))} +
+ +
+ + ) : activeTab === 'volume' ? ( +
+ + setVolumePayments(e.target.value)} + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +

+ Enter how many preview evaluations to run against the active volume split rule. +

+
+ ) : ( + <> +
+
+ + set('amount', e.target.value)} + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + set('eligible_gateways', e.target.value)} + placeholder="stripe, adyen" + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" /> +
+ +
+
+ + +
+
+ +
+
+ + {activeTab === 'single' && ( +
+ + +

+ After deciding the gateway, single test will post feedback with this outcome so the payment appears in Decision Audit. +

+
+ )} + + {activeTab === 'batch' && ( +
+

+ + Simulation Configuration +

+
+
+ + setSimulationConfig(s => ({ ...s, totalPayments: e.target.value }))} + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+
+ + setSimulationConfig(s => ({ ...s, successCount: e.target.value }))} + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+
+ + setSimulationConfig(s => ({ ...s, failureCount: e.target.value }))} + className="w-full border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+
+

+ Will run {simulationConfig.totalPayments || 0} payments: {simulationConfig.successCount || 0} SUCCESS, {simulationConfig.failureCount || 0} FAILURE +

+
+ )} + + )} + + + + {activeTab === 'rule' ? ( + + ) : activeTab === 'volume' ? ( + + ) : activeTab === 'batch' ? ( + + ) : ( + + )} +
+
+ +
+ {activeTab === 'volume' ? ( + volumeDistribution.length > 0 ? ( + <> + + +
+
+

Volume Distribution Overview

+

+ Actual distribution from {volumeEvaluationLog.length} calls to /routing/evaluate using the active volume split rule. +

+
+ {ruleResult?.payment_id ? ( + + ) : null} +
+
+ +
+

{volumeEvaluationLog.length}

+

Evaluations completed

+
+
+ {volumeDistribution.map((item, idx) => ( +
+
+
+ {item.name} +
+
+ {item.percentage}% + {item.count} payments +
+
+ ))} +
+ + + + + +

Pie Chart

+
+ + + + `${name} ${(percent * 100).toFixed(0)}%`} + labelLine={false} + > + {pieData.map((_, index) => ( + + ))} + + [`${value} payments`, 'Count']} + contentStyle={document.documentElement.classList.contains('dark') ? { backgroundColor: '#111114', border: '1px solid #222226', borderRadius: '8px', color: '#fff' } : { backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: '8px', color: '#1f2937' }} + /> + + + +
+ + + +

Bar Chart

+
+ + + + + + [`${value} payments`, 'Count']} + contentStyle={document.documentElement.classList.contains('dark') ? { backgroundColor: '#111114', border: '1px solid #222226', borderRadius: '8px', color: '#fff' } : { backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: '8px', color: '#1f2937' }} + /> + + {volumeDistribution.map((_, index) => ( + + ))} + + + + +
+ + + +

Percentage Distribution

+
+ +
+ {volumeDistribution.map((item, idx) => ( +
+ ))} +
+
+ {volumeDistribution.map((item, idx) => ( +
+
+ {item.name} + {item.percentage}% +
+ ))} +
+ + + + + +

Gateway Summary

+
+ + + + + + + + + + + {volumeDistribution.map((item, idx) => ( + + + + + + ))} + + + + + + +
gateway_namePaymentsPercentage
+
+
+ {item.name} +
+
{item.count}{item.percentage}%
Total{volumeEvaluationLog.length}100%
+
+
+ + + +
+

Evaluation Sequence

+

+ Actual connector sequence returned by repeated /routing/evaluate calls. +

+
+
+ + + + + + + + + + {volumeEvaluationLog.map((entry, idx) => ( + + + + + ))} + +
#gateway_name
{idx + 1} +
+
+ {entry.connector} +
+
+
+
+ + + + + + {volumeResponseOpen && ruleResult && ( + +
+                        {JSON.stringify(ruleResult, null, 2)}
+                      
+
+ )} +
+ + ) : ( + + + +

Enter the number of payments and click "Run Volume Evaluation" to execute repeated /routing/evaluate calls against the active volume rule.

+
+
+ ) + ) : activeTab === 'rule' ? ( + ruleResult ? ( + <> + + +
+
+

Status

+

{ruleResult.status}

+

output_type: {ruleResult.output.type}

+
+ {ruleResult.payment_id ? ( + + ) : null} +
+ + {ruleResult.output.type === 'single' && ruleResult.output.connector && ( +
+

Selected gateway_name

+

{ruleResult.output.connector.gateway_name}

+ {ruleResult.output.connector.gateway_id && ( +

gateway_id: {ruleResult.output.connector.gateway_id}

+ )} +
+ )} + + {ruleResult.output.type === 'priority' && ruleResult.output.connectors && ( +
+

Priority gateway_name list

+
+ {ruleResult.output.connectors.map((gw, idx) => ( +
+ {idx + 1} + {gw.gateway_name} + {gw.gateway_id && ({gw.gateway_id})} +
+ ))} +
+
+ )} + + {ruleResult.output.type === 'volume_split' && ( +
+

Volume Split Result

+

See Volume Split tab for detailed visualization.

+
+ )} +
+
+ + + + + + {responseOpen && ( + +
+                        {JSON.stringify(ruleResult, null, 2)}
+                      
+
+ )} +
+ + ) : ( + + + +

Configure rule parameters and click "Evaluate Rules" to test routing.

+
+
+ ) + ) : activeTab === 'batch' ? ( + simulationResults.length > 0 ? ( + <> + + +

Simulation Progress

+
+ +
+
+ Progress + {Math.round((simulationResults.length / (parseInt(simulationConfig.totalPayments) || 1)) * 100)}% +
+
+
+
+
+ + {Object.keys(gatewayStats).length > 0 && ( +
+

Gateway Selection Summary

+ {Object.entries(gatewayStats).map(([gateway, stats]) => ( +
+ {gateway} +
+ {stats.success} ✓ + {stats.failure} ✗ + ({stats.total} total) +
+
+ ))} +
+ )} + + + + + +

Transaction Log

+
+ + + + + + + + + + + + {simulationResults.map((res, idx) => ( + + + + + + + ))} + +
#Payment IDGatewayOutcome
{idx + 1} + + {res.decidedGateway} + + {res.status} + +
+
+
+ + ) : ( + + + +

Configure simulation parameters and click "Run Batch Simulation" to test Success Rate routing.

+
+
+ ) + ) : ( + result ? ( + <> + + +
+
+

Decided Gateway

+

{result.decided_gateway}

+
+
+
+ + {result.routing_approach} + +
+ {singleRunPaymentId ? ( + + ) : null} + {result.is_scheduled_outage && Scheduled Outage} + {singleRunPaymentId ? ( + + {singleRunOutcome} + + ) : null} + {result.latency != null && ( +

{result.latency}ms

+ )} +
+
+ {singleRunPaymentId ? ( +
+

+ Payment ID +

+

{singleRunPaymentId}

+

+ Feedback recorded as {singleRunOutcome}. Open audit to inspect the full decide and update flow. +

+
+ ) : null} + {result.routing_dimension && ( +
+
+ Dimension +

{result.routing_dimension}

+
+ {result.routing_dimension_level && ( +
+ Level +

{result.routing_dimension_level}

+
+ )} +
+ Reset +

{result.reset_approach}

+
+
+ )} +
+
+ + {scoreData.length > 0 && ( + + +
+

Gateway Scores

+ +
+
+ + + + `${v}%`} tick={{ fontSize: 11, fill: '#66667a' }} axisLine={{ stroke: '#1c1c24' }} tickLine={false} /> + + `${v}%`} contentStyle={{ backgroundColor: '#0d0d12', border: '1px solid #1c1c24', borderRadius: '8px', color: '#e8e8f4' }} /> + + {scoreData.map((entry, i) => ( + + ))} + + + + +
+ )} + + {result.filter_wise_gateways && ( + + + + + {filterOpen && ( + + {Object.entries(result.filter_wise_gateways).map(([filter, gateways]) => ( +
+ {filter} +
+ {Array.isArray(gateways) + ? gateways.map(gw => ( + {gw} + )) + : + } +
+
+ ))} +
+ )} +
+ )} + + + + + + {responseOpen && ( + +
+                        {JSON.stringify(result, null, 2)}
+                      
+
+ )} +
+ + ) : ( + + + +

Fill in the parameters and click "Run Single Transaction" to decide a gateway, post feedback, and inspect the audit trail.

+
+
+ ) + )} +
+
+ + {selectedAuditPaymentId && ( +
+ + +
+
+ +
+
+
+

Audit Timeline

+

+ Choose a step to inspect its request, response, and scoring context. +

+
+
+ {auditDetail.isLoading && !auditDetail.data ? ( +
+ + Loading payment audit… +
+ ) : auditDetail.error ? ( + + ) : groupedAuditTimeline.length ? ( +
+ {groupedAuditTimeline.map((group) => ( +
+
+ {group.phase} +
+
+ {group.events.map((event) => ( + + ))} +
+
+ ))} +
+ ) : ( + + )} +
+
+ +
+
+
+
+

+ {selectedAuditEvent ? stageLabel(selectedAuditEvent) : 'Audit Inspector'} +

+

+ {selectedAuditEvent + ? `${routeLabel(selectedAuditEvent.route)} · ${formatDateTime(selectedAuditEvent.created_at_ms)}` + : 'Select an event from the left to inspect payloads.'} +

+
+
+ {selectedAuditEvent?.gateway ? {selectedAuditEvent.gateway} : null} + {selectedAuditEvent?.status ? ( + + {humanizeAuditValue(selectedAuditEvent.status)} + + ) : null} +
+
+
+ {(['summary', 'input', 'response', 'raw'] as AuditInspectorTab[]).map((tab) => ( + + ))} +
+
+ +
+ {auditDetail.isLoading && !auditDetail.data ? ( +
+ + Loading inspector… +
+ ) : auditInspectorModel ? ( +
+ {auditInspectorTab === 'summary' ? ( + <> + + {auditInspectorModel.selectionReason ? ( +
+

+ Selection Reason +

+

+ {stringifyValue(auditInspectorModel.selectionReason)} +

+
+ ) : null} + + {auditInspectorModel.signalRecord ? ( + + ) : null} + + ) : null} + + {auditInspectorTab === 'input' ? ( + + ) : null} + + {auditInspectorTab === 'response' ? ( + + ) : null} + + {auditInspectorTab === 'raw' ? ( + + ) : null} +
+ ) : ( + + )} +
+
+
+
+
+ )} + + {selectedPreviewPaymentId && ( +
+ + +
+
+ +
+
+
+

Preview Timeline

+

+ Choose a preview step to inspect its request, response, and routing output. +

+
+
+ {previewTraceDetail.isLoading && !previewTraceDetail.data ? ( +
+ + Loading preview trace… +
+ ) : previewTraceDetail.error ? ( + + ) : groupedPreviewTimeline.length ? ( +
+ {groupedPreviewTimeline.map((group) => ( +
+
+ {group.phase} +
+
+ {group.events.map((event) => ( + + ))} +
+
+ ))} +
+ ) : ( + + )} +
+
+ +
+
+
+
+

+ {selectedPreviewEvent ? stageLabel(selectedPreviewEvent) : 'Preview Inspector'} +

+

+ {selectedPreviewEvent + ? `${routeLabel(selectedPreviewEvent.route)} · ${formatDateTime(selectedPreviewEvent.created_at_ms)}` + : 'Select an event from the left to inspect the preview payload.'} +

+
+
+ {selectedPreviewEvent?.gateway ? {selectedPreviewEvent.gateway} : null} + {selectedPreviewEvent?.status ? ( + + {humanizeAuditValue(selectedPreviewEvent.status)} + + ) : null} +
+
+
+ {(['summary', 'input', 'response', 'raw'] as AuditInspectorTab[]).map((tab) => ( + + ))} +
+
+ +
+ {previewTraceDetail.isLoading && !previewTraceDetail.data ? ( +
+ + Loading preview inspector… +
+ ) : previewInspectorModel ? ( +
+ {previewInspectorTab === 'summary' ? ( + <> + + + + ) : null} + + {previewInspectorTab === 'input' ? ( + + ) : null} + + {previewInspectorTab === 'response' ? ( + + ) : null} + + {previewInspectorTab === 'raw' ? ( + + ) : null} +
+ ) : ( + + )} +
+
+
+
+
+ )} +
+ ) +} diff --git a/website/src/components/pages/EuclidRulesPage.tsx b/website/src/components/pages/EuclidRulesPage.tsx new file mode 100644 index 00000000..4667e373 --- /dev/null +++ b/website/src/components/pages/EuclidRulesPage.tsx @@ -0,0 +1,881 @@ +import { useState } from 'react' +import useSWR from 'swr' +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { Card, CardBody, CardHeader } from '../ui/Card' +import { Badge } from '../ui/Badge' +import { Button } from '../ui/Button' +import { ErrorMessage } from '../ui/ErrorMessage' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { RoutingAlgorithm } from '../../types/api' +import { useDynamicRoutingConfig, RoutingKeyConfig } from '../../hooks/useDynamicRoutingConfig' +import { Plus, Trash2, GripVertical, ChevronDown, ChevronUp, Eye } from 'lucide-react' + +const OPERATOR_TO_API: Record = { + '==': 'equal', + '!=': 'not_equal', + '>': 'greater_than', + '<': 'less_than', + '>=': 'greater_than_equal', + '<=': 'less_than_equal', +} + +// ---- Types for builder ---- +interface GatewayEntry { + id: string + gatewayName: string + gatewayId: string +} + +interface VolSplitEntry { + id: string + gatewayName: string + gatewayId: string + split: number +} + +interface ConditionRow { + id: string + lhs: string + operator: string + value: string +} + +interface RuleBlock { + id: string + name: string + conditions: ConditionRow[] + outputType: 'priority' | 'volume_split' + priorityGateways: GatewayEntry[] + volumeGateways: VolSplitEntry[] +} + +type DefaultOutput = { + type: 'priority' | 'volume_split' + priorityGateways: GatewayEntry[] + volumeGateways: VolSplitEntry[] +} + +// ---- Sortable gateway item ---- +function SortableGatewayItem({ + id, + name, + onRemove, +}: { + id: string + name: string + onRemove: () => void +}) { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }) + const style = { transform: CSS.Transform.toString(transform), transition } + return ( +
+ + + + {name} + +
+ ) +} + +// ---- Priority output editor ---- +function PriorityEditor({ + gateways, + onChange, +}: { + gateways: GatewayEntry[] + onChange: (gws: GatewayEntry[]) => void +}) { + const [newGatewayName, setNewGatewayName] = useState('') + const [newGatewayId, setNewGatewayId] = useState('') + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) + ) + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event + if (over && active.id !== over.id) { + const oldIndex = gateways.findIndex((g) => g.id === active.id) + const newIndex = gateways.findIndex((g) => g.id === over.id) + onChange(arrayMove(gateways, oldIndex, newIndex)) + } + } + + function add() { + if (!newGatewayName.trim()) return + onChange([ + ...gateways, + { + id: crypto.randomUUID(), + gatewayName: newGatewayName.trim(), + gatewayId: newGatewayId.trim(), + }, + ]) + setNewGatewayName('') + setNewGatewayId('') + } + + return ( +
+ + g.id)} strategy={verticalListSortingStrategy}> + {gateways.map((gw, idx) => ( + onChange(gateways.filter((g) => g.id !== gw.id))} + /> + ))} + + +
+ setNewGatewayName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} + placeholder="gateway_name" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + setNewGatewayId(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} + placeholder="gateway_id" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + +
+
+ ) +} + +// ---- Volume split output editor ---- +function VolumeSplitEditor({ + gateways, + onChange, +}: { + gateways: VolSplitEntry[] + onChange: (gws: VolSplitEntry[]) => void +}) { + const [newGatewayName, setNewGatewayName] = useState('') + const [newGatewayId, setNewGatewayId] = useState('') + const total = gateways.reduce((s, g) => s + g.split, 0) + + function add() { + if (!newGatewayName.trim()) return + onChange([ + ...gateways, + { + id: crypto.randomUUID(), + gatewayName: newGatewayName.trim(), + gatewayId: newGatewayId.trim(), + split: 0, + }, + ]) + setNewGatewayName('') + setNewGatewayId('') + } + + return ( +
+ {gateways.map((gw) => ( +
+ + onChange(gateways.map((g) => (g.id === gw.id ? { ...g, gatewayName: e.target.value } : g))) + } + placeholder="gateway_name" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-32 focus:outline-none" + /> + + onChange(gateways.map((g) => (g.id === gw.id ? { ...g, gatewayId: e.target.value } : g))) + } + placeholder="gateway_id" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-28 focus:outline-none" + /> + + onChange( + gateways.map((g) => + g.id === gw.id ? { ...g, split: Number(e.target.value) } : g + ) + ) + } + className="flex-1 accent-brand-500" + /> + {gw.split}% + +
+ ))} +
+ Total: {total}% {total !== 100 && '(must equal 100)'} +
+
+ setNewGatewayName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} + placeholder="gateway_name" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + setNewGatewayId(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} + placeholder="gateway_id" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + +
+
+ ) +} + +// ---- Condition row ---- +function ConditionRowEditor({ + row, + onChange, + onRemove, + routingKeys, +}: { + row: ConditionRow + onChange: (r: ConditionRow) => void + onRemove: () => void + routingKeys: Record +}) { + const keyInfo = routingKeys[row.lhs] + const isEnum = keyInfo?.type === 'enum' + const isInt = keyInfo?.type === 'integer' + + const operators = isInt + ? ['>', '<', '>=', '<=', '==', '!='] + : ['==', '!='] + + return ( +
+ + + {isEnum ? ( + + ) : ( + onChange({ ...row, value: e.target.value })} + placeholder="value" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-xs w-24 focus:outline-none" + /> + )} + +
+ ) +} + +// ---- Rule block ---- +function RuleBlockEditor({ + block, + onChange, + onRemove, + routingKeys, +}: { + block: RuleBlock + onChange: (b: RuleBlock) => void + onRemove: () => void + routingKeys: Record +}) { + const [collapsed, setCollapsed] = useState(false) + + // Get the first available key from routing keys for defaults + const firstKey = Object.keys(routingKeys)[0] || 'payment_method' + const firstKeyValues = routingKeys[firstKey]?.values || [] + const defaultValue = firstKeyValues[0] || '' + + function addCondition() { + onChange({ + ...block, + conditions: [ + ...block.conditions, + { + id: crypto.randomUUID(), + lhs: firstKey, + operator: '==', + value: defaultValue, + }, + ], + }) + } + + return ( +
+
setCollapsed(!collapsed)} + > + { + e.stopPropagation() + onChange({ ...block, name: e.target.value }) + }} + onClick={(e) => e.stopPropagation()} + placeholder="Rule name" + className="bg-transparent text-sm font-medium focus:outline-none border-b border-transparent focus:border-[#28282f] text-slate-900" + /> +
+ + {collapsed ? : } +
+
+ {!collapsed && ( +
+ {/* Conditions */} +
+

CONDITIONS

+
+ {block.conditions.map((cond) => ( + + onChange({ + ...block, + conditions: block.conditions.map((c) => + c.id === cond.id ? updated : c + ), + }) + } + onRemove={() => + onChange({ + ...block, + conditions: block.conditions.filter((c) => c.id !== cond.id), + }) + } + /> + ))} + +
+
+ + {/* Output */} +
+

OUTPUT

+
+ {(['priority', 'volume_split'] as const).map((t) => ( + + ))} +
+ {block.outputType === 'priority' ? ( + onChange({ ...block, priorityGateways: gws })} + /> + ) : ( + onChange({ ...block, volumeGateways: gws })} + /> + )} +
+
+ )} +
+ ) +} + +// ---- Build Euclid payload ---- +function buildAlgorithmData(rules: RuleBlock[], defaultOutput: DefaultOutput, routingKeys: Record) { + function buildOutput(type: 'priority' | 'volume_split', pg: GatewayEntry[], vg: VolSplitEntry[]): Record { + if (type === 'priority') { + return { + priority: pg.map((g) => ({ gateway_name: g.gatewayName, gateway_id: g.gatewayId || null })), + } + } + return { + volume_split: vg.map((g) => ({ + split: g.split, + output: { gateway_name: g.gatewayName, gateway_id: g.gatewayId || null }, + })), + } + } + + function getRoutingType(type: 'priority' | 'volume_split'): string { + return type === 'priority' ? 'priority' : 'volume_split' + } + + return { + globals: {}, + default_selection: buildOutput( + defaultOutput.type, + defaultOutput.priorityGateways, + defaultOutput.volumeGateways + ), + rules: rules.map((r) => ({ + name: r.name, + routing_type: getRoutingType(r.outputType), + output: buildOutput(r.outputType, r.priorityGateways, r.volumeGateways), + statements: [ + { + condition: r.conditions.map((c) => ({ + lhs: c.lhs, + comparison: OPERATOR_TO_API[c.operator] || c.operator, + value: { + type: routingKeys[c.lhs]?.type === 'integer' ? 'number' : 'enum_variant', + value: routingKeys[c.lhs]?.type === 'integer' ? Number(c.value) : c.value, + }, + metadata: {}, + })), + }, + ], + })), + } +} + +// ---- Main Page ---- +export function EuclidRulesPage() { + const { merchantId } = useMerchantStore() + const { routingKeysConfig, isLoading: routingKeysLoading, error: routingKeysError } = useDynamicRoutingConfig() + const routingKeys = routingKeysConfig + const hasRoutingKeys = Object.keys(routingKeys).length > 0 + const routingKeysUnavailable = !routingKeysLoading && (!hasRoutingKeys || Boolean(routingKeysError)) + const [ruleName, setRuleName] = useState('') + const [ruleDesc, setRuleDesc] = useState('') + const [ruleBlocks, setRuleBlocks] = useState([]) + const [defaultOutput, setDefaultOutput] = useState({ + type: 'priority', + priorityGateways: [], + volumeGateways: [], + }) + const [showJson, setShowJson] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [submitError, setSubmitError] = useState(null) + const [createdId, setCreatedId] = useState(null) + const [activating, setActivating] = useState(false) + const [activateError, setActivateError] = useState(null) + const [activateSuccess, setActivateSuccess] = useState(false) + const [expandedRuleIds, setExpandedRuleIds] = useState>(new Set()) + + const { data: allAlgorithms, mutate } = useSWR( + merchantId ? `/routing/list/${merchantId}` : null, + () => apiPost(`/routing/list/${merchantId}`) + ) + + const { data: activeAlgorithms } = useSWR( + merchantId ? `/routing/list/active/${merchantId}` : null, + () => apiPost(`/routing/list/active/${merchantId}`) + ) + + const activeIds = new Set((activeAlgorithms || []).map((a) => a.id)) + + const algorithmData = buildAlgorithmData(ruleBlocks, defaultOutput, routingKeys) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!merchantId) { setSubmitError('Set a Merchant ID first.'); return } + if (routingKeysUnavailable) { + setSubmitError('Routing key config is unavailable. Ensure backend /config/routing-keys is reachable and valid.') + return + } + if (!ruleName.trim()) { setSubmitError('Rule name is required.'); return } + setSubmitting(true) + setSubmitError(null) + setCreatedId(null) + try { + const result = await apiPost('/routing/create', { + name: ruleName.trim(), + description: ruleDesc, + created_by: merchantId, + algorithm_for: 'payment', + algorithm: { type: 'advanced', data: algorithmData }, + }) + setCreatedId(result.id) + mutate() + } catch (err) { + setSubmitError(String(err)) + } finally { + setSubmitting(false) + } + } + + async function handleActivate(id: string) { + if (!merchantId) return + setActivating(true) + setActivateError(null) + setActivateSuccess(false) + try { + await apiPost('/routing/activate', { + created_by: merchantId, + routing_algorithm_id: id, + }) + setActivateSuccess(true) + mutate() + } catch (err) { + setActivateError(String(err)) + } finally { + setActivating(false) + } + } + + function toggleRuleExpand(id: string) { + setExpandedRuleIds(prev => { + const newSet = new Set(prev) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return newSet + }) + } + + function addRuleBlock() { + setRuleBlocks((prev) => [ + ...prev, + { + id: crypto.randomUUID(), + name: `Rule ${prev.length + 1}`, + conditions: [], + outputType: 'priority', + priorityGateways: [], + volumeGateways: [], + }, + ]) + } + + return ( +
+
+

Rule-Based Routing

+

Create declarative routing rules

+
+ +
+ {/* Rule list */} +
+ + +

Existing Rules

+
+ + {!merchantId ? ( +

Set merchant ID to load rules.

+ ) : !allAlgorithms ? ( +

Loading...

+ ) : allAlgorithms.length === 0 ? ( +

No rules yet.

+ ) : ( +
+ {allAlgorithms.map((algo) => { + const isActive = activeIds.has(algo.id) + const isExpanded = expandedRuleIds.has(algo.id) + const algorithm = algo.algorithm_data || algo.algorithm + + return ( +
+
+
+

{algo.name}

+

{algorithm?.type}

+
+ +
+ + {isActive ? 'Active' : 'Inactive'} + + + {!isActive && ( + + )} +
+
+ + {isExpanded && ( +
+
+

ID: {algo.id}

+

Description: {algo.description || 'N/A'}

+

Algorithm For: {algo.algorithm_for}

+ {algo.created_at && ( +

Created: {new Date(algo.created_at).toLocaleString()}

+ )} +
+ Configuration: +
+                                  {JSON.stringify(algorithm, null, 2)}
+                                
+
+
+
+ )} +
+ ) + })} +
+ )} +
+
+ {activateError && } + {activateSuccess && ( +
+ Rule activated successfully. +
+ )} +
+ + {/* Rule builder */} +
+
+ + +

Rule Builder

+
+ +
+
+ + setRuleName(e.target.value)} + placeholder="my-rule" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm w-full focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+
+ + setRuleDesc(e.target.value)} + placeholder="Optional description" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-2 py-1 text-sm w-full focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+
+ + {/* Rule blocks */} +
+

Rules

+ {routingKeysLoading && ( +

Loading routing keys from backend...

+ )} + {routingKeysUnavailable && ( + + )} + {ruleBlocks.map((block) => ( + + setRuleBlocks((prev) => + prev.map((b) => (b.id === block.id ? updated : b)) + ) + } + onRemove={() => + setRuleBlocks((prev) => prev.filter((b) => b.id !== block.id)) + } + /> + ))} + +
+ + {/* Default selection */} +
+

DEFAULT SELECTION (Fallback)

+
+ {(['priority', 'volume_split'] as const).map((t) => ( + + ))} +
+ {defaultOutput.type === 'priority' ? ( + + setDefaultOutput({ ...defaultOutput, priorityGateways: gws }) + } + /> + ) : ( + + setDefaultOutput({ ...defaultOutput, volumeGateways: gws }) + } + /> + )} +
+ + + {createdId && ( +
+ Rule created (ID: {createdId}) + +
+ )} +
+ + +
+
+
+
+ + {/* JSON preview */} + {showJson && ( + + +

JSON Preview

+
+ +
+                  {JSON.stringify(
+                    {
+                      name: ruleName,
+                      description: ruleDesc,
+                      created_by: merchantId,
+                      algorithm_for: 'payment',
+                      algorithm: { type: 'advanced', data: algorithmData },
+                    },
+                    null,
+                    2
+                  )}
+                
+
+
+ )} +
+
+
+ ) +} diff --git a/website/src/components/pages/OverviewPage.tsx b/website/src/components/pages/OverviewPage.tsx new file mode 100644 index 00000000..96c08bc7 --- /dev/null +++ b/website/src/components/pages/OverviewPage.tsx @@ -0,0 +1,602 @@ +import type { ElementType } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import useSWR from 'swr' +import { + Activity, + AlertCircle, + ArrowRight, + BarChart3, + CheckCircle2, + Clock3, + GitBranch, + ShieldCheck, + Sparkles, + XCircle, +} from 'lucide-react' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost, fetcher } from '../../lib/api' +import { + AnalyticsRange, + AnalyticsOverviewResponse, + AnalyticsRoutingStatsResponse, + RoutingAlgorithm, + RuleConfig, +} from '../../types/api' +import { Badge } from '../ui/Badge' +import { Card as GlassCard, SurfaceLabel } from '../ui/Card' + +const OVERVIEW_RANGE_OPTIONS: { + value: AnalyticsRange + label: string + detail: string + badge: string + summaryLabel: string +}[] = [ + { value: '15m', label: '15m', detail: 'Last 15 mins', badge: 'Live 15m', summaryLabel: 'Errors last 15 mins' }, + { value: '1h', label: '1h', detail: 'Last hour', badge: 'Live 1h', summaryLabel: 'Errors last hour' }, + { value: '24h', label: '1 day', detail: 'Last 1 day', badge: 'Live 1d', summaryLabel: 'Errors last 1 day' }, +] + +function useHealth() { + const [status, setStatus] = useState<'up' | 'down' | 'loading'>('loading') + + useEffect(() => { + fetch('/health') + .then((response) => setStatus(response.ok ? 'up' : 'down')) + .catch(() => setStatus('down')) + }, []) + + return status +} + +function formatCompactNumber(value: number | undefined) { + return new Intl.NumberFormat(undefined, { + notation: 'compact', + maximumFractionDigits: value && value < 100 ? 1 : 0, + }).format(value || 0) +} + +function formatPercent(value: number | undefined) { + if (value === undefined || value === null || Number.isNaN(value)) return '0%' + return `${value.toFixed(value >= 100 ? 0 : 1)}%` +} + +function formatUpdatedAt(timestampMs: number | undefined) { + if (!timestampMs) return 'No updates yet' + return new Intl.DateTimeFormat(undefined, { + day: 'numeric', + month: 'short', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(timestampMs)) +} + +function healthLabel(status: 'up' | 'down' | 'loading') { + if (status === 'up') return 'Healthy' + if (status === 'down') return 'Needs attention' + return 'Checking' +} + +function HeroStat({ + label, + value, + detail, +}: { + label: string + value: string + detail: string +}) { + return ( +
+ {label} +

+ {value} +

+

{detail}

+
+ ) +} + +function MetricCard({ + icon: Icon, + label, + value, + detail, +}: { + icon: ElementType + label: string + value: string + detail: string +}) { + return ( + +
+
+ {label} +

+ {value} +

+

{detail}

+
+
+ +
+
+
+ ) +} + +function EmptyWorkspace() { + return ( +
+ + Merchant required +

+ Set a merchant in the top bar to turn this into a live overview. +

+

+ Once selected, this page will show business-facing status: service health, active + routing, request count, and the gateway currently handling the most traffic. +

+
+ + +
+ {[ + { + icon: Activity, + title: 'System status', + text: 'Check whether the service is reachable.', + }, + { + icon: GitBranch, + title: 'Routing setup', + text: 'See whether a strategy is configured.', + }, + { + icon: BarChart3, + title: 'Gateway activity', + text: 'View recent request distribution by gateway.', + }, + ].map((item) => ( +
+
+ +
+
+

{item.title}

+

{item.text}

+
+
+ ))} +
+
+
+ ) +} + +export function OverviewPage() { + const navigate = useNavigate() + const { merchantId } = useMerchantStore() + const health = useHealth() + const [range, setRange] = useState('24h') + + const { data: activeAlgorithms } = useSWR( + merchantId ? `/routing/list/active/${merchantId}` : null, + () => apiPost(`/routing/list/active/${merchantId}`), + { shouldRetryOnError: false }, + ) + + const { data: srConfig } = useSWR( + merchantId ? ['/rule/get', 'successRate', merchantId] : null, + () => apiPost('/rule/get', { merchant_id: merchantId, algorithm: 'successRate' }), + { shouldRetryOnError: false }, + ) + + const analyticsOverviewUrl = merchantId + ? `/analytics/overview?scope=current&range=${range}&merchant_id=${encodeURIComponent(merchantId)}` + : null + const analyticsRoutingUrl = merchantId + ? `/analytics/routing-stats?scope=current&range=${range}&merchant_id=${encodeURIComponent(merchantId)}` + : null + + const analyticsOverview = useSWR(analyticsOverviewUrl, fetcher, { + refreshInterval: 15000, + revalidateOnFocus: true, + shouldRetryOnError: false, + }) + const analyticsRouting = useSWR(analyticsRoutingUrl, fetcher, { + refreshInterval: 15000, + revalidateOnFocus: true, + shouldRetryOnError: false, + }) + + const activeRouting = activeAlgorithms?.[0] || null + const hasRuleBasedRouting = (activeAlgorithms || []).some( + (algorithm) => (algorithm.algorithm_data || algorithm.algorithm)?.type === 'advanced', + ) + + const latestSync = + analyticsOverview.data?.generated_at_ms || analyticsRouting.data?.generated_at_ms || undefined + + const routeHits = analyticsOverview.data?.route_hits || [] + const decideHits = routeHits.find((item) => item.route === '/decide_gateway')?.count || 0 + const totalErrors = + analyticsOverview.data?.top_errors?.reduce((sum, item) => sum + item.count, 0) || 0 + + const gatewayUsage = useMemo(() => { + const totals = new Map() + + for (const point of analyticsRouting.data?.gateway_share || []) { + totals.set(point.gateway, (totals.get(point.gateway) || 0) + point.count) + } + + const totalTraffic = Array.from(totals.values()).reduce((sum, count) => sum + count, 0) + + return Array.from(totals.entries()) + .map(([gateway, count]) => ({ + gateway, + count, + share: totalTraffic ? (count / totalTraffic) * 100 : 0, + })) + .sort((left, right) => right.count - left.count) + }, [analyticsRouting.data]) + + const topGateway = gatewayUsage[0]?.gateway || analyticsOverview.data?.top_scores?.[0]?.gateway + const selectedWindow = + OVERVIEW_RANGE_OPTIONS.find((option) => option.value === range) || OVERVIEW_RANGE_OPTIONS[1] + const configuredBasics = [ + health === 'up', + Boolean(activeRouting), + Boolean(srConfig?.data), + hasRuleBasedRouting, + ].filter(Boolean).length + + const setupItems = [ + { + label: 'Service health', + description: health === 'up' ? 'Service is reachable.' : 'Please verify service health.', + state: health === 'up' ? 'Healthy' : health === 'down' ? 'Issue' : 'Checking', + icon: health === 'up' ? CheckCircle2 : health === 'down' ? XCircle : AlertCircle, + route: undefined, + }, + { + label: 'Routing strategy', + description: activeRouting ? activeRouting.name : 'No active routing configured.', + state: activeRouting ? 'Configured' : 'Not set', + icon: GitBranch, + route: '/routing', + }, + { + label: 'Auth-rate config', + description: srConfig?.data ? 'Configured and available.' : 'Not configured yet.', + state: srConfig?.data ? 'Configured' : 'Not set', + icon: ShieldCheck, + route: '/routing/sr', + }, + { + label: 'Rule-based routing', + description: hasRuleBasedRouting ? 'Enabled for this merchant.' : 'Not enabled.', + state: hasRuleBasedRouting ? 'Enabled' : 'Optional', + icon: Sparkles, + route: '/routing/rules', + }, + ] + + const workspaceBadge = !merchantId + ? { label: 'Merchant not selected', variant: 'orange' as const } + : health === 'up' + ? { label: 'System live', variant: 'green' as const } + : health === 'down' + ? { label: 'Attention needed', variant: 'red' as const } + : { label: 'Checking status', variant: 'gray' as const } + + return ( +
+
+
+
+
+ +
+
+ +
+
+ {workspaceBadge.label} + {merchantId ? {merchantId} : null} + {latestSync ? ( + + Last sync {formatUpdatedAt(latestSync)} + + ) : null} +
+
+

+ Overview +

+

+ Basic business-facing view of system status, setup, request volume, and gateway activity. +

+
+ {OVERVIEW_RANGE_OPTIONS.map((option) => { + const active = option.value === range + return ( + + ) + })} +
+
+
+ + {!merchantId ? ( + + ) : ( + <> +
+ +
+
+ Traffic leader +
+

+ {topGateway?.toUpperCase() || '--'} +

+
+

+ {gatewayUsage[0] ? formatPercent(gatewayUsage[0].share) : '0%'} +

+

+ Share in selected window +

+
+
+

+ {activeRouting + ? `${activeRouting.name} is the current routing strategy for this merchant.` + : 'No active routing strategy is configured for this merchant yet.'} +

+
+ +
+ + + +
+
+
+ +
+ + +
+ + +
+
+
+ +
+ +
+
+ Current setup +

+ The status cards you can explain in a demo without technical jargon. +

+
+ = 3 ? 'green' : 'orange'}> + {configuredBasics}/4 ready + +
+ +
+ {setupItems.map((item) => ( + navigate(item.route) : undefined} + > +
+
+
+ +
+ + {item.state} + +
+ +
+

{item.label}

+

+ {item.description} +

+
+
+
+ ))} +
+
+ + +
+
+ Gateway activity +

+ Request distribution by gateway for the selected window. +

+
+ {selectedWindow.badge} +
+ +
+ {gatewayUsage.length ? ( + gatewayUsage.slice(0, 4).map((item, index) => ( +
+
+
+ +
+

+ {item.gateway.toUpperCase()} +

+

+ {formatCompactNumber(item.count)} requests +

+
+
+

+ {formatPercent(item.share)} +

+
+ +
+
+
+
+ )) + ) : ( +
+

+ No gateway activity yet +

+

+ Once requests start flowing, this section will show traffic by gateway. +

+
+ )} +
+ +
+ +
+ + Quick summary +
+ {[ + { label: 'Selected merchant', value: merchantId }, + { label: 'Last sync', value: formatUpdatedAt(latestSync) }, + { label: selectedWindow.summaryLabel, value: formatCompactNumber(totalErrors) }, + { label: 'Top gateway', value: topGateway?.toUpperCase() || 'No activity' }, + ].map((item) => ( +
+ {item.label} + + {item.value} + +
+ ))} +
+
+ +
+ {[ + { + label: 'Routing Hub', + text: 'Configure routing strategies.', + icon: GitBranch, + route: '/routing', + }, + { + label: 'Analytics', + text: 'Inspect request and gateway trends.', + icon: BarChart3, + route: '/analytics', + }, + { + label: 'Audit Trail', + text: 'Review individual decision records.', + icon: Clock3, + route: '/audit', + }, + ].map((item) => ( + navigate(item.route)}> +
+
+ +
+
+

+ {item.label} +

+

+ {item.text} +

+
+ Open + +
+
+
+
+ ))} +
+
+ + )} +
+
+ ) +} diff --git a/website/src/components/pages/PaymentAuditPage.tsx b/website/src/components/pages/PaymentAuditPage.tsx new file mode 100644 index 00000000..9a994d89 --- /dev/null +++ b/website/src/components/pages/PaymentAuditPage.tsx @@ -0,0 +1,1062 @@ +import { useEffect, useMemo, useState } from 'react' +import useSWR from 'swr' +import { useSearchParams } from 'react-router-dom' +import { useMerchantStore } from '../../store/merchantStore' +import { fetcher } from '../../lib/api' +import { + AnalyticsRange, + PaymentAuditEvent, + PaymentAuditResponse, +} from '../../types/api' +import { Button } from '../ui/Button' +import { Badge } from '../ui/Badge' +import { Spinner } from '../ui/Spinner' +import { ErrorMessage } from '../ui/ErrorMessage' +import { Card as GlassCard, InsetPanel, SurfaceLabel } from '../ui/Card' + +const RANGE_OPTIONS: AnalyticsRange[] = ['15m', '1h', '24h'] +const STATUS_OPTIONS = [ + { value: '', label: 'Any status' }, + { value: 'success', label: 'Success' }, + { value: 'failure', label: 'Failure' }, +] +const ROUTE_OPTIONS = [ + { value: '', label: 'Any route' }, + { value: 'decide_gateway', label: 'Decide Gateway' }, + { value: 'update_gateway_score', label: 'Update Gateway' }, + { value: 'routing_evaluate', label: 'Rule Evaluate' }, +] +const INSPECTOR_TABS = ['summary', 'input', 'response', 'raw'] as const + +type AuditFilters = { + paymentId: string + requestId: string + gateway: string + route: string + status: string + eventType: string + errorCode: string +} + +type InspectorTab = (typeof INSPECTOR_TABS)[number] +type AuditMode = 'transactions' | 'rule_based' + +const EMPTY_FILTERS: AuditFilters = { + paymentId: '', + requestId: '', + gateway: '', + route: '', + status: '', + eventType: '', + errorCode: '', +} + +function normalizeAuditFilters(filters: AuditFilters): AuditFilters { + const paymentId = filters.paymentId.trim() + const requestId = paymentId ? '' : filters.requestId.trim() + return { + paymentId, + requestId, + gateway: filters.gateway.trim(), + route: filters.route, + status: filters.status, + eventType: filters.eventType, + errorCode: filters.errorCode.trim(), + } +} + +function queryString(params: Record) { + const search = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + search.set(key, String(value)) + } + }) + return search.toString() +} + +function buildAuditUrl( + path: '/analytics/payment-audit' | '/analytics/preview-trace', + range: AnalyticsRange, + merchantId: string, + page: number, + pageSize: number, + filters: AuditFilters, +) { + const normalizedFilters = normalizeAuditFilters(filters) + const params: Record = { + scope: 'current', + range, + page, + page_size: pageSize, + merchant_id: merchantId, + payment_id: normalizedFilters.paymentId || undefined, + request_id: normalizedFilters.requestId || undefined, + gateway: normalizedFilters.gateway || undefined, + route: normalizedFilters.route || undefined, + status: normalizedFilters.status || undefined, + event_type: normalizedFilters.eventType || undefined, + error_code: normalizedFilters.errorCode || undefined, + } + const qs = queryString(params) + return qs ? `${path}?${qs}` : path +} + +function parseRange(value: string | null): AnalyticsRange { + if (value === '15m' || value === '24h') return value + return '24h' +} + +function parseAuditMode(value: string | null): AuditMode { + return value === 'rule_based' ? 'rule_based' : 'transactions' +} + +function parseFilters(searchParams: URLSearchParams): AuditFilters { + return normalizeAuditFilters({ + paymentId: searchParams.get('payment_id') || '', + requestId: searchParams.get('request_id') || '', + gateway: searchParams.get('gateway') || '', + route: searchParams.get('route') || '', + status: searchParams.get('status') || '', + eventType: searchParams.get('event_type') || '', + errorCode: searchParams.get('error_code') || '', + }) +} + +function formatDateTime(ms: number) { + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(ms)) +} + +function formatRelative(ms: number) { + const diffMinutes = Math.max(0, Math.round((Date.now() - ms) / 60000)) + if (diffMinutes < 1) return 'just now' + if (diffMinutes < 60) return `${diffMinutes}m ago` + const diffHours = Math.round(diffMinutes / 60) + if (diffHours < 24) return `${diffHours}h ago` + const diffDays = Math.round(diffHours / 24) + return `${diffDays}d ago` +} + +function humanizeAuditValue(value?: string | null) { + if (!value) return '' + const normalized = value + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase() + + return normalized.replace(/\b\w/g, (char) => char.toUpperCase()) +} + +function compactMeta(parts: Array) { + return parts.filter(Boolean).join(' · ') +} + +function routeLabel(route?: string | null) { + if (!route) return 'Unknown route' + if (route === 'decision_gateway' || route === 'decide_gateway') return 'Decide Gateway' + if (route === 'update_gateway_score') return 'Update Gateway' + if (route === 'routing_evaluate') return 'Rule Evaluate' + return humanizeAuditValue(route) +} + +function stageLabel(event: PaymentAuditEvent) { + if (event.event_stage === 'gateway_decided') return 'Decide Gateway' + if (event.event_stage === 'score_updated') return 'Update Gateway' + if (event.event_stage === 'rule_applied') return 'Rule Evaluate' + if (event.event_stage === 'preview_evaluated' || event.event_type === 'rule_evaluation_preview') { + return 'Preview Result' + } + if (event.event_type === 'error') return 'Errors' + return humanizeAuditValue(event.event_stage || event.event_type) +} + +function eventPhase(event: PaymentAuditEvent) { + if (event.event_type === 'decision' || event.event_stage === 'gateway_decided') return 'Decide Gateway' + if (event.event_type === 'rule_hit' || event.event_stage === 'rule_applied') return 'Rule Evaluate' + if (event.event_type === 'rule_evaluation_preview' || event.event_stage === 'preview_evaluated') { + return 'Rule Preview' + } + if (event.event_type === 'gateway_update' || event.event_stage === 'score_updated') return 'Update Gateway' + return 'Errors' +} + +function badgeVariantForEvent(event: PaymentAuditEvent): 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' { + const normalizedStatus = (event.status || '').toUpperCase() + if ( + event.event_type === 'error' || + normalizedStatus === 'FAILURE' || + normalizedStatus.includes('FAILED') || + normalizedStatus.includes('DECLINED') + ) return 'red' + if (event.event_type === 'rule_evaluation_preview') return 'purple' + if (event.event_type === 'rule_hit') return 'purple' + if ( + normalizedStatus === 'CHARGED' || + normalizedStatus === 'AUTHORIZED' || + normalizedStatus === 'SUCCESS' + ) return 'green' + if (event.event_type === 'gateway_update') return 'green' + if (event.event_type === 'decision') return 'blue' + return 'orange' +} + +function summaryBadgeVariant(status?: string | null): 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' { + const normalizedStatus = (status || '').toUpperCase() + if ( + normalizedStatus === 'FAILURE' || + normalizedStatus.includes('FAILED') || + normalizedStatus.includes('DECLINED') + ) return 'red' + if ( + normalizedStatus === 'SUCCESS' || + normalizedStatus === 'CHARGED' || + normalizedStatus === 'AUTHORIZED' + ) return 'green' + if (normalizedStatus === 'HIT') return 'purple' + return 'gray' +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + +function cleanRecord(record: Record) { + return Object.fromEntries( + Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== ''), + ) +} + +function stringifyValue(value: unknown) { + if (typeof value === 'string') return value + return JSON.stringify(value, null, 2) +} + +function sectionButtonClass(active: boolean) { + return active + ? '!border-slate-200 !bg-white !text-slate-950 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.28)] dark:!border-[#2a303a] dark:!bg-[#161b24] dark:!text-white' + : '!border-transparent !bg-slate-100 !text-slate-600 hover:!bg-slate-200 hover:!text-slate-900 dark:!bg-[#161b24] dark:!text-[#a7b2c6] dark:hover:!bg-[#1c2330] dark:hover:!text-white' +} + +function controlClassName() { + return 'h-11 rounded-2xl border border-slate-200 bg-white/90 px-4 text-sm text-slate-700 shadow-[0_12px_30px_-24px_rgba(15,23,42,0.2)] outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:border-[#2a303a] dark:bg-[#161b24] dark:text-[#e5ecf7] dark:shadow-none' +} + +function KeyMetric({ label, value, helper }: { label: string; value: string; helper: string }) { + return ( + + {label} +

{value}

+

{helper}

+
+ ) +} + +function EmptyState({ title, body }: { title: string; body: string }) { + return ( + +

{title}

+

{body}

+
+ ) +} + +function InspectorKeyValueGrid({ rows }: { rows: Array<{ label: string; value: string }> }) { + if (!rows.length) return null + + return ( +
+ {rows.map((row) => ( + +

{row.label}

+

{row.value}

+
+ ))} +
+ ) +} + +function InspectorJsonPanel({ title, value, emptyMessage }: { title: string; value: unknown; emptyMessage: string }) { + return ( +
+
+

{title}

+
+ {value ? ( +
+          {stringifyValue(value)}
+        
+ ) : ( + + )} +
+ ) +} + +function buildInspectorModel(event: PaymentAuditEvent | null) { + if (!event) return null + + const details = isRecord(event.details_json) ? event.details_json : {} + const explicitResponse = + details.response ?? + details.response_payload ?? + details.result ?? + details.output ?? + null + const requestPayload = + details.request ?? + details.request_payload ?? + details.input ?? + details.payload ?? + cleanRecord({ + payment_id: event.payment_id, + request_id: event.request_id, + payment_method_type: event.payment_method_type, + payment_method: event.payment_method, + gateway: event.gateway, + }) + const responsePayload = + explicitResponse ?? + cleanRecord({ + event_type: event.event_type, + status: event.status, + error_code: event.error_code, + error_message: event.error_message, + score_value: event.score_value, + sigma_factor: event.sigma_factor, + average_latency: event.average_latency, + tp99_latency: event.tp99_latency, + transaction_count: event.transaction_count, + rule_name: event.rule_name, + routing_approach: event.routing_approach, + }) + const responseRecord = isRecord(explicitResponse) ? explicitResponse : null + const decidedGatewayRecord = isRecord(responseRecord?.['decided_gateway']) ? responseRecord['decided_gateway'] : null + const scoreContext = + details.score_context ?? + (decidedGatewayRecord ? decidedGatewayRecord['gateway_priority_map'] : null) ?? + (responseRecord ? responseRecord['gateway_priority_map'] : null) ?? + null + const selectionReason = details.selection_reason ?? null + + const summaryRows = [ + { label: 'Phase', value: eventPhase(event) }, + { label: 'Stage', value: stageLabel(event) }, + { label: 'Route', value: routeLabel(event.route) }, + { label: 'Timestamp', value: formatDateTime(event.created_at_ms) }, + { label: 'Merchant', value: event.merchant_id || 'unknown merchant' }, + ...(event.payment_id ? [{ label: 'Payment ID', value: event.payment_id }] : []), + ...(event.request_id ? [{ label: 'Request ID', value: event.request_id }] : []), + ...(event.gateway ? [{ label: 'Gateway', value: event.gateway }] : []), + ...(event.status ? [{ label: 'Status', value: event.status }] : []), + ] + + const signalRecord = cleanRecord( + Object.fromEntries( + Object.entries(details).filter(([key]) => ![ + 'request', + 'request_payload', + 'input', + 'payload', + 'response', + 'response_payload', + 'result', + 'output', + 'score_context', + 'selection_reason', + ].includes(key)), + ), + ) + + return { + summaryRows, + requestPayload: isRecord(requestPayload) && !Object.keys(requestPayload).length ? null : requestPayload, + responsePayload: isRecord(responsePayload) && !Object.keys(responsePayload).length ? null : responsePayload, + scoreContext, + selectionReason, + signalRecord: Object.keys(signalRecord).length ? signalRecord : null, + rawEvent: { + ...event, + details_json: event.details_json, + }, + } +} + +export function PaymentAuditPage() { + const { merchantId } = useMerchantStore() + const [searchParams, setSearchParams] = useSearchParams() + + const initialMode = parseAuditMode(searchParams.get('mode')) + const initialRange = parseRange(searchParams.get('range')) + const initialFilters = parseFilters(searchParams) + const initialPage = Math.max(1, Number(searchParams.get('page') || '1')) + const initialSelectedKey = searchParams.get('selected') || '' + + const [mode, setMode] = useState(initialMode) + const [range, setRange] = useState(initialRange) + const [filters, setFilters] = useState(initialFilters) + const [appliedFilters, setAppliedFilters] = useState(initialFilters) + const [page, setPage] = useState(initialPage) + const [selectedKey, setSelectedKey] = useState(initialSelectedKey) + const [selectedEventId, setSelectedEventId] = useState(null) + const [inspectorTab, setInspectorTab] = useState('summary') + const pageSize = 12 + + const canQueryCurrent = Boolean(merchantId) + const auditPath = mode === 'rule_based' ? '/analytics/preview-trace' : '/analytics/payment-audit' + + const searchUrl = canQueryCurrent && merchantId + ? buildAuditUrl(auditPath, range, merchantId, page, pageSize, appliedFilters) + : null + + const auditSearch = useSWR(searchUrl, fetcher, { + refreshInterval: 12000, + revalidateOnFocus: true, + }) + + const selectedSummary = useMemo(() => { + const rows = auditSearch.data?.results || [] + return rows.find((row) => row.lookup_key === selectedKey) || rows[0] || null + }, [auditSearch.data?.results, selectedKey]) + + useEffect(() => { + if (selectedSummary?.lookup_key) { + setSelectedKey(selectedSummary.lookup_key) + return + } + const first = auditSearch.data?.results?.[0] + if (first?.lookup_key) { + setSelectedKey(first.lookup_key) + } + }, [auditSearch.data?.results, selectedSummary?.lookup_key]) + + const detailFilters = useMemo(() => { + if (!selectedSummary) return null + const paymentId = selectedSummary.payment_id || '' + return { + paymentId, + requestId: paymentId ? '' : (selectedSummary.request_id || ''), + gateway: '', + route: '', + status: '', + eventType: '', + errorCode: '', + } + }, [selectedSummary]) + + const detailUrl = canQueryCurrent && merchantId && detailFilters + ? buildAuditUrl(auditPath, range, merchantId, 1, 50, detailFilters) + : null + + const auditDetail = useSWR(detailUrl, fetcher, { + refreshInterval: 12000, + revalidateOnFocus: true, + }) + + const selectedEvent = useMemo(() => { + const timeline = auditDetail.data?.timeline || [] + return timeline.find((event) => event.id === selectedEventId) || timeline[0] || null + }, [auditDetail.data?.timeline, selectedEventId]) + + useEffect(() => { + if (selectedEvent?.id) { + setSelectedEventId(selectedEvent.id) + return + } + const first = auditDetail.data?.timeline?.[0] + if (first?.id) { + setSelectedEventId(first.id) + } + }, [auditDetail.data?.timeline, selectedEvent?.id]) + + const groupedTimeline = useMemo(() => { + const groups: Array<{ phase: string; events: PaymentAuditEvent[] }> = [] + for (const event of auditDetail.data?.timeline || []) { + const phase = eventPhase(event) + const current = groups[groups.length - 1] + if (!current || current.phase !== phase) { + groups.push({ phase, events: [event] }) + } else { + current.events.push(event) + } + } + return groups + }, [auditDetail.data?.timeline]) + + const inspectorModel = useMemo(() => buildInspectorModel(selectedEvent), [selectedEvent]) + + const error = auditSearch.error?.message || auditDetail.error?.message || null + const loading = auditSearch.isLoading || auditDetail.isLoading + const totalEvents = auditDetail.data?.timeline?.length || 0 + const activeGateways = selectedSummary?.gateways?.length || 0 + const latestSeen = selectedSummary ? formatRelative(selectedSummary.last_seen_ms) : 'No activity' + const content = mode === 'rule_based' + ? { + title: 'Decision Audit', + description: 'Inspect preview-only rule activity from /routing/evaluate without mixing it into transaction outcomes.', + merchantPrompt: 'Use the merchant selector in the top bar to load the preview trace for a merchant.', + searchTitle: 'Search Rule Preview Trail', + searchDescription: 'Use preview payment IDs or request IDs when you have them. Gateway, status, and error code help narrow rule-preview activity quickly.', + matchingLabel: 'Matching previews', + matchingDescription: 'Scan the current result set and pick a preview to open its full trace.', + summaryLabel: 'Selected Preview Timeline', + summaryEmpty: 'Pick a preview from the left column to see the full rule evaluation trace.', + noMatchesTitle: 'No matching previews found', + noMatchesBody: 'Try widening the time range or searching by a preview payment ID, request ID, or gateway.', + } + : { + title: 'Decision Audit', + description: 'Search by payment or request, then inspect gateway decisions, gateway updates, rule evaluations, and errors with the exact payload captured at each step.', + merchantPrompt: 'Use the merchant selector in the top bar to load the decision trail for a merchant.', + searchTitle: 'Search Decision Trail', + searchDescription: 'Use payment or request IDs when you have them. Error code, gateway, route, and status narrow operational noise quickly.', + matchingLabel: 'Matching payments', + matchingDescription: 'Scan the current result set and pick a payment to open its full event trail.', + summaryLabel: 'Selected Payment Timeline', + summaryEmpty: 'Pick a payment from the left column to see the full transaction trail.', + noMatchesTitle: 'No matching payments found', + noMatchesBody: 'Try widening the time range or searching by a single payment ID, request ID, or error code.', + } + + function syncSearch( + nextMode: AuditMode, + nextRange: AnalyticsRange, + nextPage: number, + nextFilters: AuditFilters, + nextSelectedKey?: string, + ) { + const normalizedFilters = normalizeAuditFilters(nextFilters) + const nextQuery = queryString({ + mode: nextMode === 'rule_based' ? nextMode : undefined, + range: nextRange, + page: nextPage > 1 ? nextPage : undefined, + payment_id: normalizedFilters.paymentId || undefined, + request_id: normalizedFilters.requestId || undefined, + gateway: normalizedFilters.gateway || undefined, + route: normalizedFilters.route || undefined, + status: normalizedFilters.status || undefined, + event_type: normalizedFilters.eventType || undefined, + error_code: normalizedFilters.errorCode || undefined, + selected: nextSelectedKey || undefined, + }) + setSearchParams(nextQuery) + } + + function updateFilter(field: keyof AuditFilters, value: string) { + setFilters((current) => normalizeAuditFilters({ ...current, [field]: value })) + } + + function applyFilters() { + const nextPage = 1 + const normalizedFilters = normalizeAuditFilters({ + ...filters, + route: mode === 'rule_based' ? '' : filters.route, + }) + setPage(nextPage) + setFilters(normalizedFilters) + setAppliedFilters(normalizedFilters) + syncSearch(mode, range, nextPage, normalizedFilters) + } + + function clearFilters() { + const nextPage = 1 + const clearedFilters = { + ...EMPTY_FILTERS, + route: mode === 'rule_based' ? '' : EMPTY_FILTERS.route, + } + setPage(nextPage) + setFilters(clearedFilters) + setAppliedFilters(clearedFilters) + syncSearch(mode, range, nextPage, clearedFilters) + } + + function refreshAll() { + auditSearch.mutate() + auditDetail.mutate() + } + + function updateRange(nextRange: AnalyticsRange) { + const nextPage = 1 + setRange(nextRange) + setPage(nextPage) + syncSearch(mode, nextRange, nextPage, appliedFilters, selectedKey) + } + + function selectSummary(lookupKey: string) { + setSelectedKey(lookupKey) + syncSearch(mode, range, page, appliedFilters, lookupKey) + } + + function updateMode(nextMode: AuditMode) { + const nextPage = 1 + const nextFilters = normalizeAuditFilters({ + ...filters, + route: nextMode === 'rule_based' ? '' : filters.route, + }) + + setMode(nextMode) + setPage(nextPage) + setSelectedKey('') + setSelectedEventId(null) + setFilters(nextFilters) + setAppliedFilters(nextFilters) + syncSearch(nextMode, range, nextPage, nextFilters) + } + + async function copyValue(value: string | null | undefined) { + if (!value) return + try { + await navigator.clipboard.writeText(value) + } catch { + // Ignore clipboard failures in unsupported contexts. + } + } + + function openRelatedEvents() { + if (!selectedEvent) return + const paymentId = selectedEvent.payment_id || '' + const nextFilters: AuditFilters = { + paymentId, + requestId: paymentId ? '' : (selectedEvent.request_id || ''), + gateway: selectedEvent.gateway || '', + route: '', + status: '', + eventType: '', + errorCode: '', + } + setFilters(nextFilters) + setAppliedFilters(nextFilters) + setPage(1) + syncSearch(mode, range, 1, nextFilters, selectedKey) + } + + if (!canQueryCurrent) { + return ( +
+
+

{content.title}

+

+ {content.description} +

+
+ +
+ ) + } + + return ( +
+
+
+

{content.title}

+

+ {content.description} +

+
+
+ +
+
+ +
+
+ + + {merchantId || 'Current merchant'} +
+ +
+

+ Time window +

+ {RANGE_OPTIONS.map((value) => ( + + ))} +
+
+ + +
+ {content.searchTitle} +

+ {content.searchDescription} +

+
+
+
+ updateFilter('paymentId', event.target.value)} placeholder="Payment ID" /> + updateFilter('requestId', event.target.value)} placeholder="Request ID" /> + updateFilter('gateway', event.target.value)} placeholder="Gateway" /> + {mode === 'transactions' ? ( + + ) : null} + updateFilter('errorCode', event.target.value)} placeholder="Error code" /> + +
+
+ + +
+
+
+ + + + {loading && ( +
+ + Loading decision audit data… +
+ )} + +
+ + + + +
+ +
+ +
+
+ {content.matchingLabel} +

+ {content.matchingDescription} +

+
+
+
+ + +
+
+
+
+ {auditSearch.data?.results?.length ? auditSearch.data.results.map((row) => ( + + )) : ( + + )} +
+
+ +
+ +
+
+
+ {content.summaryLabel} +

+ {selectedSummary?.payment_id || selectedSummary?.request_id || (mode === 'rule_based' ? 'Choose a preview from the result list to inspect the timeline.' : 'Choose a payment from the result list to inspect the timeline.')} +

+
+
+

+ {compactMeta([ + selectedSummary?.latest_stage ? humanizeAuditValue(selectedSummary.latest_stage) : null, + selectedSummary?.latest_gateway ? `gateway ${selectedSummary.latest_gateway}` : null, + ])} +

+ {selectedSummary?.latest_status ? ( + + {humanizeAuditValue(selectedSummary.latest_status)} + + ) : null} +
+
+
+
+ {groupedTimeline.length ? ( +
+ {groupedTimeline.map((group) => ( +
+
+

{group.phase}

+

{group.events.length} event{group.events.length === 1 ? '' : 's'}

+
+ +
+ {group.events.map((event) => { + const selected = selectedEvent?.id === event.id + return ( + + ) + })} +
+
+ ))} +
+ ) : ( + + )} +
+
+ + +
+
+
+
+ Event Inspector +

+ {selectedEvent ? `${stageLabel(selectedEvent)} · ${formatDateTime(selectedEvent.created_at_ms)}` : 'Select a timeline event to inspect the captured payload.'} +

+
+ {selectedEvent ? ( +

{eventPhase(selectedEvent)}

+ ) : null} +
+ +
+ {INSPECTOR_TABS.map((tab) => ( + + ))} +
+
+
+
+ {selectedEvent && inspectorModel ? ( + <> +
+ + + + +
+ + {inspectorTab === 'summary' ? ( +
+ + + + +
+ ) : null} + + {inspectorTab === 'input' ? ( + + ) : null} + + {inspectorTab === 'response' ? ( + + ) : null} + + {inspectorTab === 'raw' ? ( + + ) : null} + + ) : ( + + )} +
+
+
+
+
+ ) +} diff --git a/website/src/components/pages/RoutingHubPage.tsx b/website/src/components/pages/RoutingHubPage.tsx new file mode 100644 index 00000000..e88f60a1 --- /dev/null +++ b/website/src/components/pages/RoutingHubPage.tsx @@ -0,0 +1,121 @@ +import { useNavigate } from 'react-router-dom' +import useSWR from 'swr' +import { Card, CardBody } from '../ui/Card' +import { Badge } from '../ui/Badge' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { RoutingAlgorithm, RuleConfig } from '../../types/api' +import { TrendingUp, Layers, PieChart, CreditCard } from 'lucide-react' + +interface RoutingCard { + id: string + title: string + description: string + icon: React.ElementType + route: string + algorithmType: string + checkConfigured: () => boolean +} + +export function RoutingHubPage() { + const navigate = useNavigate() + const { merchantId } = useMerchantStore() + + const { data: activeAlgorithms } = useSWR( + merchantId ? `/routing/list/active/${merchantId}` : null, + () => apiPost(`/routing/list/active/${merchantId}`) + ) + + const { data: srConfig } = useSWR( + merchantId ? ['/rule/get', 'successRate', merchantId] : null, + () => apiPost('/rule/get', { merchant_id: merchantId, algorithm: 'successRate' }) + ) + + const ROUTING_CARDS: RoutingCard[] = [ + { + id: 'sr', + title: 'Auth-Rate Based Routing', + description: 'Dynamically route to the best-performing gateway based on real-time authorization rates.', + icon: TrendingUp, + route: '/routing/sr', + algorithmType: 'successRate', + checkConfigured: () => !!(srConfig as any)?.config?.data, + }, + { + id: 'rules', + title: 'Rule-Based Routing', + description: 'Declarative routing rules to route payments based on conditions and attributes.', + icon: Layers, + route: '/routing/rules', + algorithmType: 'advanced', + checkConfigured: () => (activeAlgorithms || []).some(a => + (a.algorithm_data || a.algorithm)?.type === 'advanced' + ), + }, + { + id: 'volume', + title: 'Volume Split', + description: 'Distribute payment traffic across gateways by configurable percentage splits.', + icon: PieChart, + route: '/routing/volume', + algorithmType: 'volume_split', + checkConfigured: () => (activeAlgorithms || []).some(a => + (a.algorithm_data || a.algorithm)?.type === 'volume_split' + ), + }, + { + id: 'debit', + title: 'Network Routing', + description: 'Optimise debit network fees with acquirer-aware network-based routing.', + icon: CreditCard, + route: '/routing/debit', + algorithmType: 'debitRouting', + checkConfigured: () => false, // Not implemented yet + }, + ] + + return ( +
+
+

Routing Hub

+

+ Click on any routing strategy to configure +

+
+ +
+ {ROUTING_CARDS.map((card) => { + const Icon = card.icon + const isConfigured = card.checkConfigured() + return ( + navigate(card.route)} + > + +
+
+ +
+ + {isConfigured ? 'Configured' : 'Not Configured'} + +
+
+

{card.title}

+

{card.description}

+
+
+ + {isConfigured ? 'Manage →' : 'Setup →'} + +
+
+
+ ) + })} +
+
+ ) +} diff --git a/website/src/components/pages/SRRoutingPage.tsx b/website/src/components/pages/SRRoutingPage.tsx new file mode 100644 index 00000000..4482c372 --- /dev/null +++ b/website/src/components/pages/SRRoutingPage.tsx @@ -0,0 +1,503 @@ +import useSWR from 'swr' +import { useForm, useFieldArray } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { useEffect, useState } from 'react' +import { Card, CardBody, CardHeader } from '../ui/Card' +import { Badge } from '../ui/Badge' +import { Button } from '../ui/Button' +import { ErrorMessage } from '../ui/ErrorMessage' +import { Spinner } from '../ui/Spinner' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { PAYMENT_METHOD_TYPES, PAYMENT_METHODS } from '../../lib/constants' +import { Plus, Trash2, Eye } from 'lucide-react' + +// ---- Schema ---- +const subLevelSchema = z.object({ + paymentMethodType: z.string().min(1), + paymentMethod: z.string().min(1), + bucketSize: z.coerce.number().int().positive(), + hedgingPercent: z.preprocess( + (v) => (v === '' || v === null ? null : Number(v)), + z.number().nullable() + ), + latencyThreshold: z.preprocess( + (v) => (v === '' || v === null ? null : Number(v)), + z.number().nullable() + ), +}) + +const srFormSchema = z.object({ + defaultBucketSize: z.coerce.number().int().positive(), + defaultSuccessRate: z.preprocess( + (v) => (v === '' || v === null ? null : Number(v)), + z.number().min(0).max(1).nullable() + ), + defaultLatencyThreshold: z.preprocess( + (v) => (v === '' || v === null ? null : Number(v)), + z.number().nullable() + ), + defaultHedgingPercent: z.preprocess( + (v) => (v === '' || v === null ? null : Number(v)), + z.number().nullable() + ), + subLevelInputConfig: z.array(subLevelSchema), +}) + +type SRForm = z.infer + +interface SRConfigResponse { + merchant_id: string + modified_at?: string + config: { + type: string + data: { + defaultBucketSize: number + defaultSuccessRate: number | null + defaultLatencyThreshold: number | null + defaultHedgingPercent: number | null + subLevelInputConfig: { + paymentMethodType: string + paymentMethod: string + bucketSize: number + hedgingPercent: number | null + latencyThreshold: number | null + }[] | null + } + } +} + +export function SRRoutingPage() { + const { merchantId } = useMerchantStore() + const [saving, setSaving] = useState(false) + const [saveError, setSaveError] = useState(null) + const [saveSuccess, setSaveSuccess] = useState(false) + const [showCurrentConfig, setShowCurrentConfig] = useState(false) + const [deleting, setDeleting] = useState(false) + const [deleteError, setDeleteError] = useState(null) + + const { data: existing, isLoading, mutate } = useSWR( + merchantId ? ['rule-sr', merchantId] : null, + () => apiPost('/rule/get', { merchant_id: merchantId, algorithm: 'successRate' }), + { shouldRetryOnError: false } + ) + + const { + register, + control, + handleSubmit, + reset, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(srFormSchema), + defaultValues: { + defaultBucketSize: 200, + defaultSuccessRate: 0.5, + defaultLatencyThreshold: null, + defaultHedgingPercent: null, + subLevelInputConfig: [], + }, + }) + + // Pre-fill form from fetched config + useEffect(() => { + if (existing?.config?.data) { + const d = existing.config.data + reset({ + defaultBucketSize: d.defaultBucketSize ?? 200, + defaultSuccessRate: d.defaultSuccessRate ?? 0.5, + defaultLatencyThreshold: d.defaultLatencyThreshold ?? null, + defaultHedgingPercent: d.defaultHedgingPercent ?? null, + subLevelInputConfig: d.subLevelInputConfig ?? [], + }) + } + }, [existing, reset]) + + const { fields, append, remove } = useFieldArray({ control, name: 'subLevelInputConfig' }) + const watchedRows = watch('subLevelInputConfig') + + async function ensureMerchantExists() { + try { + await apiPost(`/merchant-account/create`, { + merchant_id: merchantId, + gateway_success_rate_based_decider_input: null, + }) + } catch { + // Ignore — merchant may already exist + } + } + + async function onSave(data: SRForm) { + if (!merchantId) { setSaveError('Set a Merchant ID first.'); return } + setSaving(true); setSaveError(null); setSaveSuccess(false) + try { + await ensureMerchantExists() + const endpoint = existing ? '/rule/update' : '/rule/create' + await apiPost(endpoint, { + merchant_id: merchantId, + config: { + type: 'successRate', + data: { + defaultBucketSize: data.defaultBucketSize, + defaultSuccessRate: data.defaultSuccessRate, + defaultLatencyThreshold: data.defaultLatencyThreshold, + defaultHedgingPercent: data.defaultHedgingPercent, + subLevelInputConfig: data.subLevelInputConfig.length > 0 + ? data.subLevelInputConfig + : null, + }, + }, + }) + setSaveSuccess(true) + mutate() + } catch (err: unknown) { + setSaveError(err instanceof Error ? err.message : String(err)) + } finally { + setSaving(false) + } + } + + async function handleDelete() { + if (!merchantId) return + setDeleting(true); setDeleteError(null) + try { + await apiPost('/rule/delete', { merchant_id: merchantId, algorithm: 'successRate' }) + mutate(undefined, { revalidate: false }) + } catch (err: unknown) { + setDeleteError(err instanceof Error ? err.message : String(err)) + } finally { + setDeleting(false) + } + } + + return ( +
+
+

Auth-Rate Based Routing

+

+ Configure success-rate based gateway routing +

+
+ + {!merchantId && ( +
+ Set a Merchant ID in the top bar to load and save configuration. +
+ )} + + {/* Status Card */} + {merchantId && !isLoading && ( + + +
+

Configuration Status

+

+ {existing?.config?.data + ? 'Success Rate routing is configured and active' + : 'No Success Rate configuration found'} +

+
+ + {existing?.config?.data ? 'Active' : 'Not Configured'} + +
+ {existing?.config?.data && ( + +
+
+ Last Modified: + + {existing.modified_at + ? new Date(existing.modified_at).toLocaleString() + : 'Unknown'} + +
+ +
+ {deleteError && ( +

{deleteError}

+ )} +
+ )} +
+ )} + + {isLoading ? ( +
+ ) : ( +
+ + +
+

Default Success Rate Config

+

+ Base settings used when there is no payment-method-specific override. +

+
+
+ + + + + + + + + +
+ + + +
+

Sub-Level Overrides

+

+ Optional overrides for specific payment method type and method combinations. +

+
+ +
+ + {fields.length ? ( + + + + + + + + + + + + {fields.map((field, idx) => { + const methodType = watchedRows?.[idx]?.paymentMethodType || '' + const methodOptions = PAYMENT_METHODS[methodType] || [] + return ( + + + + + + + + + ) + })} + +
Payment Method TypePayment MethodBucket SizeHedging %Latency Threshold (ms) +
+ + + + + + + + + + + +
+ ) : ( +
+ No sub-level overrides configured. The default row above is the only active configuration. +
+ )} +
+
+ + + {saveSuccess && ( +
+ Configuration saved successfully. +
+ )} + + {/* View Current Configuration Section */} + {existing?.config?.data && ( + + +

Current Active Configuration

+ +
+ {showCurrentConfig && ( + +
+ {/* Default Config */} +
+

Default Settings

+
+
+ Bucket Size: +

{existing.config.data.defaultBucketSize}

+
+
+ Success Rate: +

{existing.config.data.defaultSuccessRate ?? 'Not set'}

+
+
+ Hedging %: +

{existing.config.data.defaultHedgingPercent ?? 'Not set'}

+
+
+ Latency Threshold: +

{existing.config.data.defaultLatencyThreshold ?? 'Not set'} ms

+
+
+
+ + {/* Sub-level Configs */} + {existing.config.data.subLevelInputConfig && existing.config.data.subLevelInputConfig.length > 0 && ( +
+

Sub-Level Configurations

+
+ {existing.config.data.subLevelInputConfig.map((config, idx) => ( +
+
+
+ Payment Type: +

{config.paymentMethodType}

+
+
+ Payment Method: +

{config.paymentMethod}

+
+
+ Bucket Size: +

{config.bucketSize}

+
+
+ Hedging %: +

{config.hedgingPercent ?? 'Default'}

+
+
+ Latency Threshold: +

{config.latencyThreshold ?? 'Default'} ms

+
+
+
+ ))} +
+
+ )} + + {/* Raw JSON */} +
+

Raw Configuration (JSON)

+
+                        {JSON.stringify(existing.config, null, 2)}
+                      
+
+
+
+ )} +
+ )} + + + + )} +
+ ) +} diff --git a/website/src/components/pages/VolumeSplitPage.tsx b/website/src/components/pages/VolumeSplitPage.tsx new file mode 100644 index 00000000..4b1026ef --- /dev/null +++ b/website/src/components/pages/VolumeSplitPage.tsx @@ -0,0 +1,342 @@ +import { useState } from 'react' +import useSWR, { useSWRConfig } from 'swr' +import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts' +import { Card, CardBody, CardHeader } from '../ui/Card' +import { Button } from '../ui/Button' +import { Badge } from '../ui/Badge' +import { ErrorMessage } from '../ui/ErrorMessage' +import { Spinner } from '../ui/Spinner' +import { useMerchantStore } from '../../store/merchantStore' +import { apiPost } from '../../lib/api' +import { RoutingAlgorithm } from '../../types/api' +import { Plus, Trash2, Eye } from 'lucide-react' + +const COLORS = ['#0069ED', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'] + +interface GatewayEntry { id: string; name: string; split: number } + +function makeId() { return Math.random().toString(36).slice(2) } + +export function VolumeSplitPage() { + const { merchantId } = useMerchantStore() + const { mutate: mutateCache } = useSWRConfig() + + const { data: active, mutate: mutateActive } = useSWR( + merchantId ? ['active-routing', merchantId] : null, + () => apiPost(`/routing/list/active/${merchantId}`) + ) + + const activeVol = active?.find(r => (r.algorithm_data || r.algorithm)?.type === 'volume_split') + + const [gateways, setGateways] = useState([ + { id: makeId(), name: '', split: 50 }, + { id: makeId(), name: '', split: 50 }, + ]) + const [ruleName, setRuleName] = useState('') + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [showCurrentConfig, setShowCurrentConfig] = useState(false) + const [expandedRuleIds, setExpandedRuleIds] = useState>(new Set()) + + const total = gateways.reduce((s, g) => s + g.split, 0) + + function updateGateway(id: string, field: 'name' | 'split', val: string | number) { + setGateways(gs => gs.map(g => g.id === id ? { ...g, [field]: val } : g)) + } + + function addGateway() { + setGateways(gs => [...gs, { id: makeId(), name: '', split: 0 }]) + } + + function removeGateway(id: string) { + setGateways(gs => gs.filter(g => g.id !== id)) + } + + async function handleCreate() { + if (!merchantId) return setError('Set a merchant ID first') + if (!ruleName.trim()) return setError('Enter a rule name') + if (total !== 100) return setError(`Splits must sum to 100 (currently ${total})`) + if (gateways.some(g => !g.name.trim())) return setError('All gateways must have names') + + setSaving(true); setError(null); setSuccess(null) + try { + await apiPost('/routing/create', { + rule_id: null, + name: ruleName, + description: '', + created_by: merchantId, + algorithm_for: 'payment', + metadata: null, + algorithm: { + type: 'volume_split', + data: gateways.map(g => ({ + split: g.split, + output: { gateway_name: g.name.trim(), gateway_id: null }, + })), + }, + }) + await Promise.all([ + mutateActive(), + mutateCache(['routing-list', merchantId]), + ]) + setSuccess(`Rule "${ruleName}" created successfully. Find it in the list below to activate.`) + setRuleName('') + setGateways([ + { id: makeId(), name: '', split: 50 }, + { id: makeId(), name: '', split: 50 }, + ]) + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to create rule') + } finally { + setSaving(false) + } + } + + async function handleActivate(ruleId: string) { + if (!merchantId) return + try { + await apiPost('/routing/activate', { created_by: merchantId, routing_algorithm_id: ruleId }) + await Promise.all([ + mutateActive(), + mutateCache(['routing-list', merchantId]), + ]) + setSuccess('Rule activated.') + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to activate') + } + } + + function toggleRuleExpand(id: string) { + setExpandedRuleIds(prev => { + const newSet = new Set(prev) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return newSet + }) + } + + // Build pie data from active rule + const algo = activeVol ? (activeVol.algorithm_data || activeVol.algorithm) : null + const pieData = algo && 'data' in algo + ? (algo.data as { split: number; output: { gateway_name: string; gateway_id: string | null } }[]).map(item => ({ + name: item.output?.gateway_name ?? '?', + value: item.split, + })) + : [] + + return ( +
+
+

Volume Split Routing

+

Distribute payment traffic across gateways by percentage.

+
+ + {/* Active Configuration */} + {activeVol && ( + + +
+

Active Volume Split

+

{activeVol.name}

+
+
+ Active + +
+
+ {showCurrentConfig && ( + + + + `${name}: ${value}%`} labelLine={{ stroke: '#45454f' }}> + {pieData.map((_, i) => ( + + ))} + + `${v}%`} contentStyle={{ backgroundColor: '#0d0d12', border: '1px solid #1c1c24', borderRadius: '8px', color: '#e8e8f4' }} /> + + + +
+

Rule ID: {activeVol.id}

+

Created: {activeVol.created_at ? new Date(activeVol.created_at).toLocaleString() : 'Unknown'}

+
+
+ )} +
+ )} + + {/* Create Rule */} + + +

Create Volume Split Rule

+
+ +
+ + setRuleName(e.target.value)} + placeholder="e.g. ab-test-split" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm w-64 focus:outline-none focus:ring-1 focus:ring-brand-500" + /> +
+ +
+
+ Gateway Name + Split % + +
+ {gateways.map(g => ( +
+ updateGateway(g.id, 'name', e.target.value)} + placeholder="e.g. stripe" + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + updateGateway(g.id, 'split', Number(e.target.value))} + className="border border-slate-200 dark:border-[#222226] bg-transparent rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500" + /> + +
+ ))} +
+ + + Total: {total}%{total !== 100 && ' (must be 100)'} + +
+
+ + + {success &&

{success}

} + + +
+
+ + +
+ ) +} + +function ActiveRulesList({ + merchantId, + onActivate, + expandedRuleIds, + onToggleExpand +}: { + merchantId: string; + onActivate: (id: string) => void; + expandedRuleIds: Set; + onToggleExpand: (id: string) => void; +}) { + const { data: rules, isLoading } = useSWR( + merchantId ? ['routing-list', merchantId] : null, + () => apiPost(`/routing/list/${merchantId}`) + ) + + const volRules = rules?.filter(r => (r.algorithm_data || r.algorithm)?.type === 'volume_split') ?? [] + + if (!merchantId) return null + if (isLoading) return
+ if (!volRules.length) return null + + return ( + +

Saved Volume Split Rules

+ + + + + + + + + + {volRules.map(r => { + const algorithm = r.algorithm_data || r.algorithm + const items = algorithm?.data as { split: number; output: { gateway_name: string; gateway_id: string | null } }[] || [] + const isExpanded = expandedRuleIds.has(r.id) + return ( + <> + + + + + + {isExpanded && ( + + + + )} + + ) + })} + +
NameSplit +
{r.name} + {items.map(i => `${i.output?.gateway_name}:${i.split}%`).join(' | ')} + +
+ + +
+
+
+

ID: {r.id}

+

Description: {r.description || 'N/A'}

+ {r.created_at && ( +

Created: {new Date(r.created_at).toLocaleString()}

+ )} +
+ Configuration: +
+                              {JSON.stringify(algorithm, null, 2)}
+                            
+
+
+
+
+
+ ) +} diff --git a/website/src/components/ui/Badge.tsx b/website/src/components/ui/Badge.tsx new file mode 100644 index 00000000..50dbab8a --- /dev/null +++ b/website/src/components/ui/Badge.tsx @@ -0,0 +1,23 @@ +interface BadgeProps { + variant?: 'green' | 'gray' | 'blue' | 'red' | 'orange' | 'purple' + children: React.ReactNode +} + +const variantClasses: Record = { + green: 'bg-emerald-500/10 text-emerald-600 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-300', + gray: 'bg-slate-900/[0.04] text-slate-700 ring-1 ring-inset ring-slate-900/8 dark:bg-white/[0.05] dark:text-slate-300 dark:ring-white/8', + blue: 'bg-sky-500/12 text-sky-700 ring-1 ring-inset ring-sky-500/22 dark:bg-sky-400/14 dark:text-sky-200 dark:ring-sky-400/28', + red: 'bg-red-500/10 text-red-600 ring-1 ring-inset ring-red-500/20 dark:text-red-300', + orange: 'bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/20 dark:text-orange-300', + purple: 'bg-purple-500/10 text-purple-600 ring-1 ring-inset ring-purple-500/20 dark:text-purple-300', +} + +export function Badge({ variant = 'gray', children }: BadgeProps) { + return ( + + {children} + + ) +} diff --git a/website/src/components/ui/Button.tsx b/website/src/components/ui/Button.tsx new file mode 100644 index 00000000..1c104f06 --- /dev/null +++ b/website/src/components/ui/Button.tsx @@ -0,0 +1,38 @@ +import { ButtonHTMLAttributes } from 'react' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'danger' + size?: 'sm' | 'md' +} + +const variantClasses = { + primary: + 'bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 shadow-sm border border-transparent dark:bg-white dark:text-black dark:hover:bg-slate-200', + secondary: + 'bg-white text-slate-700 border border-slate-200 hover:bg-slate-50 hover:text-slate-900 disabled:opacity-40 shadow-sm dark:bg-[#121214] dark:text-[#a1a1aa] dark:border-[#27272a] dark:hover:bg-[#18181b] dark:hover:text-white', + ghost: + 'text-slate-500 hover:text-slate-900 hover:bg-slate-100 disabled:opacity-40 dark:text-[#a1a1aa] dark:hover:text-white dark:hover:bg-[#121214]', + danger: + 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200 disabled:opacity-40 dark:bg-[#2a0505] dark:text-red-500 dark:hover:bg-[#380808] dark:border-[#5c1c1c]', +} + +const sizeClasses = { + sm: 'px-4 py-1.5 text-xs font-semibold', + md: 'px-5 py-2.5 text-sm font-semibold', +} + +export function Button({ + variant = 'primary', + size = 'md', + className = '', + ...props +}: ButtonProps) { + return ( + + ) +} diff --git a/website/src/components/ui/Card.tsx b/website/src/components/ui/Card.tsx new file mode 100644 index 00000000..81c87717 --- /dev/null +++ b/website/src/components/ui/Card.tsx @@ -0,0 +1,62 @@ +import { ReactNode } from 'react' + +interface CardProps { + children: ReactNode + className?: string + onClick?: () => void +} + +export function Card({ children, className = '', onClick }: CardProps) { + const baseClassName = + `relative overflow-hidden rounded-[30px] border border-slate-200 bg-white shadow-[0_18px_60px_-42px_rgba(15,23,42,0.15)] dark:border-[#2a303a] dark:bg-[#11151d] dark:shadow-[0_18px_60px_-42px_rgba(0,0,0,0.7)] ${onClick ? 'cursor-pointer text-left transition duration-300 hover:-translate-y-0.5 hover:border-[#3b82f6]/35 hover:bg-slate-50 dark:hover:bg-[#141923]' : ''} ${className}` + + const inner = ( + <> +
+
+
{children}
+ + ) + + if (onClick) { + return ( + + ) + } + + return ( +
{inner}
+ ) +} + +export function CardHeader({ children, className = '' }: CardProps) { + return ( +
+ {children} +
+ ) +} + +export function CardBody({ children, className = '' }: CardProps) { + return
{children}
+} + +export function SurfaceLabel({ children, className = '' }: CardProps) { + return ( +

+ {children} +

+ ) +} + +export function InsetPanel({ children, className = '' }: CardProps) { + return ( +
+ {children} +
+ ) +} diff --git a/website/src/components/ui/ErrorMessage.tsx b/website/src/components/ui/ErrorMessage.tsx new file mode 100644 index 00000000..bacbe8f8 --- /dev/null +++ b/website/src/components/ui/ErrorMessage.tsx @@ -0,0 +1,12 @@ +interface ErrorMessageProps { + error: string | null +} + +export function ErrorMessage({ error }: ErrorMessageProps) { + if (!error) return null + return ( +
+ {error} +
+ ) +} diff --git a/website/src/components/ui/Spinner.tsx b/website/src/components/ui/Spinner.tsx new file mode 100644 index 00000000..cc4e8c38 --- /dev/null +++ b/website/src/components/ui/Spinner.tsx @@ -0,0 +1,25 @@ +export function Spinner({ size = 20 }: { size?: number }) { + return ( + + + + + ) +} diff --git a/website/src/hooks/useDynamicRoutingConfig.ts b/website/src/hooks/useDynamicRoutingConfig.ts new file mode 100644 index 00000000..c97b4c50 --- /dev/null +++ b/website/src/hooks/useDynamicRoutingConfig.ts @@ -0,0 +1,162 @@ +import useSWR from 'swr' +import { fetcher } from '../lib/api' + +export interface KeyConfig { + // backend may send either `type` (snake_case) or `data_type` (legacy) + type?: 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' + data_type?: 'Enum' | 'Integer' | 'Udf' | 'StrValue' | 'GlobalRef' | 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' + values?: string | string[] + min_value?: number + max_value?: number + min_length?: number + max_length?: number + exact_length?: number + regex?: string +} + +export interface RoutingConfig { + // preferred shape from backend: { keys: { payment_method: {...}, ... } } + keys?: unknown + // backward-compat shape + routing_config?: { + keys?: unknown + } +} + +export interface ParsedRoutingKey { + key: string + type: 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' + values?: string[] + min_value?: number + max_value?: number + min_length?: number + max_length?: number + exact_length?: number + regex?: string +} + +export type RoutingKeyType = 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' + +export interface RoutingKeyConfig { + type: RoutingKeyType + values: string[] +} + +function parseRoutingConfig(config: RoutingConfig | null): ParsedRoutingKey[] { + if (!config) { + return [] + } + + const resolveKeys = (source?: unknown): Record => { + if (!source || typeof source !== 'object') return {} + + const nested = (source as { keys?: unknown }).keys + if (nested && typeof nested === 'object') { + return nested as Record + } + + return source as Record + } + + const keysMap = { + ...resolveKeys(config.keys), + ...resolveKeys(config.routing_config?.keys), + } + + if (Object.keys(keysMap).length === 0) { + return [] + } + + return Object.entries(keysMap).map(([key, keyConfig]) => { + const normalizedType = (keyConfig.type || keyConfig.data_type || 'str_value') + .toString() + .toLowerCase() as ParsedRoutingKey['type'] + + const parsed: ParsedRoutingKey = { + key, + type: normalizedType, + } + + if (keyConfig.values) { + parsed.values = Array.isArray(keyConfig.values) + ? keyConfig.values.map(v => v.trim()) + : keyConfig.values.split(',').map(v => v.trim()) + } + + if (keyConfig.min_value !== undefined) { + parsed.min_value = keyConfig.min_value + } + + if (keyConfig.max_value !== undefined) { + parsed.max_value = keyConfig.max_value + } + + if (keyConfig.min_length !== undefined) { + parsed.min_length = keyConfig.min_length + } + + if (keyConfig.max_length !== undefined) { + parsed.max_length = keyConfig.max_length + } + + if (keyConfig.exact_length !== undefined) { + parsed.exact_length = keyConfig.exact_length + } + + if (keyConfig.regex) { + parsed.regex = keyConfig.regex + } + + return parsed + }) +} + +export function useDynamicRoutingConfig() { + const { data, error, isLoading } = useSWR( + '/config/routing-keys', + fetcher, + { + refreshInterval: 0, + revalidateOnFocus: false, + } + ) + + const parsedKeys = parseRoutingConfig(data || null) + + // Build a lookup map for easy access + const keysByName = parsedKeys.reduce((acc, key) => { + acc[key.key] = key + return acc + }, {} as Record) + + // Convert to the format expected by components (matching old ROUTING_KEYS structure) + const routingKeysConfig: Record = {} + + parsedKeys.forEach((key) => { + routingKeysConfig[key.key] = { + type: key.type, + values: key.values || [], + } + }) + + return { + config: data, + keys: parsedKeys, + keysByName, + routingKeysConfig, + isLoading, + error, + // Helper to get values for a specific key + getKeyValues: (keyName: string): string[] => { + return keysByName[keyName]?.values || [] + }, + // Check if key is integer type + isIntegerKey: (keyName: string): boolean => { + return keysByName[keyName]?.type === 'integer' + }, + // Check if key is enum type + isEnumKey: (keyName: string): boolean => { + return keysByName[keyName]?.type === 'enum' + }, + } +} diff --git a/website/src/index.css b/website/src/index.css new file mode 100644 index 00000000..1b96eced --- /dev/null +++ b/website/src/index.css @@ -0,0 +1,174 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + + html { + color-scheme: light; + } + + html.dark { + color-scheme: dark; + } + + html, + body { + font-family: 'Outfit', 'Inter', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + @apply bg-[#ffffff] text-slate-800 dark:bg-[#000000] dark:text-[#f4f4f5] transition-colors duration-300; + } + + /* Fallback text colors for robust contrast when not overridden */ + p, + span, + h1, + h2, + h3, + h4, + h5, + h6 { + @apply text-slate-800 dark:text-slate-200 transition-colors duration-300; + } + + p.text-slate-500, + span.text-slate-500, + div.text-slate-500 { + @apply dark:text-slate-400; + } + + p.text-slate-600, + span.text-slate-600, + div.text-slate-600 { + @apply dark:text-slate-300; + } + + /* Automatic inversion for tailwind colors in dark mode */ + .dark .text-slate-900, + .dark .text-slate-800 { + color: #f1f5f9 !important; + } + + .dark .text-slate-700 { + color: #e2e8f0 !important; + } + + .dark .text-slate-600 { + color: #cbd5e1 !important; + } + + .dark .text-slate-500 { + color: #94a3b8 !important; + } + + .dark .text-slate-400 { + color: #64748b !important; + } + + .dark .text-blue-800, + .dark .text-blue-700 { + color: #93c5fd !important; + } + + .dark .text-blue-600 { + color: #60a5fa !important; + } + + .dark .text-brand-500 { + color: #818cf8 !important; + } + + .dark .bg-slate-50 { + @apply bg-[#1a1a1d] !important; + } + + .dark .bg-slate-100 { + @apply bg-[#222226] !important; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 5px; + height: 5px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + @apply bg-slate-300 dark:bg-[#222222] rounded-full hover:bg-slate-400 dark:hover:bg-[#333333] transition-colors; + } + + /* Form elements — Dynamic override */ + :where(input:not([type='checkbox']):not([type='radio']):not([type='range'])), + :where(select), + :where(textarea) { + @apply bg-white dark:bg-[#0f0f11] border-slate-200 dark:border-transparent text-slate-800 dark:text-white rounded-full px-4 placeholder-slate-400 dark:placeholder-[#66666e] transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:focus:bg-[#151518] dark:focus:border-[#333338]; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + font-family: 'Outfit', sans-serif; + } + + .dark input:not([type='checkbox']):not([type='radio']):not([type='range']), + .dark select, + .dark textarea { + box-shadow: none !important; + } + + select option { + @apply bg-white dark:bg-[#0f0f11] text-slate-800 dark:text-white; + } + + /* Range input */ + input[type='range'] { + @apply accent-brand-500 dark:accent-white; + } + + /* Radio/checkbox */ + input[type='radio'], + input[type='checkbox'] { + @apply w-4 h-4 bg-white dark:bg-[#0f0f11] border-slate-300 dark:border-[#222226] accent-brand-500 dark:accent-white transition-all duration-200; + } + + input[type='radio'] { + border-radius: 50%; + } + + input[type='checkbox'] { + border-radius: 4px; + } + + input[type='radio']:checked, + input[type='checkbox']:checked { + @apply bg-brand-500 dark:bg-white border-brand-500 dark:border-white; + } +} + +/* Floating panel utility class */ +.glass-panel { + @apply bg-white dark:bg-[#0c0c0e] border border-slate-200 dark:border-[#1a1a1d] shadow-[0_18px_50px_-30px_rgba(15,23,42,0.12)] dark:shadow-2xl rounded-2xl relative overflow-hidden transition-all duration-300; +} + +.glass-panel-hover:hover { + @apply bg-white dark:bg-[#111114] border-slate-300 dark:border-[#222226]; +} + +/* Brand Gradient Text Utility */ +.text-gradient { + @apply bg-clip-text text-transparent bg-gradient-to-r from-brand-600 to-purple-500 dark:from-[#9b51e0] dark:via-[#4f46e5] dark:to-[#0ea5e9]; +} + +/* Aurora top edge */ +.aurora-top { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #9b51e0, #4f46e5, #0ea5e9, transparent); + opacity: 0.8; + z-index: 100; +} diff --git a/website/src/lib/api.ts b/website/src/lib/api.ts new file mode 100644 index 00000000..6ca5295e --- /dev/null +++ b/website/src/lib/api.ts @@ -0,0 +1,101 @@ +// All API calls use relative URLs so nginx/vite-proxy can handle routing + +const DEBUG_API = true +const DEFAULT_TENANT_ID = 'public' + +function logRequest(method: string, path: string, body?: unknown) { + if (!DEBUG_API) return + console.log('\n' + '='.repeat(80)) + console.log(`[API REQUEST] ${new Date().toISOString()}`) + console.log(`Method: ${method}`) + console.log(`Path: ${path}`) + if (body !== undefined) { + console.log('Body:', JSON.stringify(body, null, 2)) + } + console.log('='.repeat(80)) +} + +function logResponse(path: string, status: number, statusText: string, body: string) { + if (!DEBUG_API) return + console.log('\n' + '-'.repeat(80)) + console.log(`[API RESPONSE] ${new Date().toISOString()}`) + console.log(`Path: ${path}`) + console.log(`Status: ${status} ${statusText}`) + console.log('Response Body:', body) + console.log('-'.repeat(80) + '\n') +} + +function logError(path: string, error: unknown) { + if (!DEBUG_API) return + console.log('\n' + '!'.repeat(80)) + console.log(`[API ERROR] ${new Date().toISOString()}`) + console.log(`Path: ${path}`) + if (error instanceof Error) { + console.log('Error:', error.message) + console.log('Stack:', error.stack) + } else { + console.log('Error:', error) + } + console.log('!'.repeat(80) + '\n') +} + +export async function apiFetch( + path: string, + options?: RequestInit +): Promise { + const method = options?.method || 'GET' + const body = options?.body ? JSON.parse(options.body as string) : undefined + + logRequest(method, path, body) + + try { + const res = await fetch(path, { + headers: { + 'Content-Type': 'application/json', + 'x-tenant-id': DEFAULT_TENANT_ID, + ...options?.headers, + }, + ...options, + }) + + const responseText = await res.text() + let responseBody: string + + try { + const json = JSON.parse(responseText) + responseBody = JSON.stringify(json, null, 2) + } catch { + responseBody = responseText + } + + logResponse(path, res.status, res.statusText, responseBody) + + if (!res.ok) { + const error = new Error(`API error ${res.status}: ${responseText}`) + logError(path, error) + throw error + } + + // Handle empty response body + if (!responseText.trim()) { + return undefined as T + } + + return JSON.parse(responseText) as T + } catch (error) { + logError(path, error) + throw error + } +} + +export async function apiPost(path: string, body?: unknown): Promise { + return apiFetch(path, { + method: 'POST', + body: body !== undefined ? JSON.stringify(body) : undefined, + }) +} + +// Generic fetcher for SWR +export async function fetcher(url: string): Promise { + return apiFetch(url) +} diff --git a/website/src/lib/constants.ts b/website/src/lib/constants.ts new file mode 100644 index 00000000..343812cc --- /dev/null +++ b/website/src/lib/constants.ts @@ -0,0 +1,111 @@ +// Static routing keys configuration - fallback when API is unavailable +// These should match the values in the backend config +export type RoutingKeyType = 'enum' | 'integer' | 'udf' | 'str_value' | 'global_ref' + +export interface StaticRoutingKeyConfig { + type: RoutingKeyType + values: string[] +} + +export const STATIC_ROUTING_KEYS: Record = { + payment_method: { + type: 'enum', + values: [ + 'card', + 'card_redirect', + 'pay_later', + 'wallet', + 'bank_redirect', + 'bank_transfer', + 'crypto', + 'bank_debit', + 'reward', + 'real_time_payment', + 'upi', + 'voucher', + 'gift_card', + 'open_banking', + 'mobile_payment', + ], + }, + amount: { type: 'integer', values: [] }, + currency: { + type: 'enum', + values: ['USD', 'EUR', 'GBP', 'INR', 'JPY', 'CAD', 'AUD', 'SGD', 'AED'], + }, + payment_type: { + type: 'enum', + values: ['normal', 'new_mandate', 'setup_mandate', 'recurring_mandate', 'non_mandate'], + }, + card_network: { + type: 'enum', + values: ['visa', 'mastercard', 'amex', 'jcb', 'diners_club', 'discover', 'cartes_bancaires', 'union_pay', 'interac', 'rupay', 'maestro'], + }, + authentication_type: { + type: 'enum', + values: ['three_ds', 'no_three_ds'], + }, + card_type: { + type: 'enum', + values: ['debit', 'credit'], + }, + card: { + type: 'enum', + values: ['debit', 'credit'], + }, +} + +export type RoutingKey = keyof typeof STATIC_ROUTING_KEYS + +export const ROUTING_APPROACH_COLORS: Record = { + SR_SELECTION_V3_ROUTING: 'bg-blue-100 text-blue-800', + PRIORITY_LOGIC: 'bg-purple-100 text-purple-800', + NTW_BASED_ROUTING: 'bg-green-100 text-green-800', + SR_SELECTION_V3_ROUTING_WITH_HEDGING: 'bg-orange-100 text-orange-800', + HEDGING: 'bg-orange-100 text-orange-800', +} + +export const SR_DIMENSION_OPTIONS = [ + { key: 'currency', label: 'Currency' }, + { key: 'country', label: 'Country' }, + { key: 'auth_type', label: 'Auth Type' }, + { key: 'card_is_in', label: 'Card Is In' }, + { key: 'card_network', label: 'Card Network' }, +] + +// Full payment method types for reference (backend validates against config) +export const PAYMENT_METHOD_TYPES = [ + 'card', + 'card_redirect', + 'pay_later', + 'wallet', + 'bank_redirect', + 'bank_transfer', + 'crypto', + 'bank_debit', + 'reward', + 'real_time_payment', + 'upi', + 'voucher', + 'gift_card', + 'open_banking', + 'mobile_payment', +] + +export const PAYMENT_METHODS: Record = { + card: ['credit', 'debit'], + bank_debit: ['ach', 'sepa', 'bacs', 'becs'], + bank_transfer: ['ach', 'sepa', 'sepa_bank_transfer', 'bacs', 'multibanco', 'pix', 'pse', 'permata_bank_transfer', 'bca_bank_transfer', 'bni_va', 'bri_va', 'cimb_va', 'danamon_va', 'mandiri_va', 'local_bank_transfer', 'instant_bank_transfer'], + wallet: ['amazon_pay', 'apple_pay', 'google_pay', 'paypal', 'ali_pay', 'ali_pay_hk', 'dana', 'mb_way', 'mobile_pay', 'samsung_pay', 'twint', 'vipps', 'touch_n_go', 'swish', 'we_chat_pay', 'go_pay', 'gcash', 'momo', 'kakao_pay', 'cashapp', 'mifinity', 'paze'], + pay_later: ['affirm', 'alma', 'afterpay_clearpay', 'klarna', 'pay_bright', 'atome', 'walley'], + upi: ['upi_collect', 'upi_intent'], + voucher: ['boleto', 'efecty', 'pago_efectivo', 'red_compra', 'red_pagos', 'indomaret', 'alfamart', 'oxxo', 'seven_eleven', 'lawson', 'mini_stop', 'family_mart', 'seicomart', 'pay_easy'], + bank_redirect: ['giropay', 'ideal', 'sofort', 'eft', 'eps', 'bancontact_card', 'blik', 'local_bank_redirect', 'online_banking_thailand', 'online_banking_czech_republic', 'online_banking_finland', 'online_banking_fpx', 'online_banking_poland', 'online_banking_slovakia', 'przelewy24', 'trustly', 'bizum', 'interac', 'open_banking_uk', 'open_banking_pis'], + gift_card: ['givex', 'pay_safe_card'], + card_redirect: ['knet', 'benefit', 'momo_atm', 'card_redirect'], + real_time_payment: ['fps', 'duit_now', 'prompt_pay', 'viet_qr'], + crypto: ['crypto_currency'], + reward: ['evoucher', 'classic_reward'], + open_banking: ['open_banking_pis'], + mobile_payment: ['direct_carrier_billing'], +} diff --git a/website/src/main.tsx b/website/src/main.tsx new file mode 100644 index 00000000..faa66a4b --- /dev/null +++ b/website/src/main.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import { ErrorBoundary } from './ErrorBoundary' +import './index.css' + +const routerBaseName = import.meta.env.BASE_URL.endsWith('/') + ? import.meta.env.BASE_URL.slice(0, -1) + : import.meta.env.BASE_URL + +console.log('\n' + '='.repeat(80)) +console.log('[APP STARTUP] Dashboard initializing...') +console.log(`Timestamp: ${new Date().toISOString()}`) +console.log(`Environment: ${(import.meta as any).env?.MODE ?? 'production'}`) +console.log(`Base URL: ${import.meta.env.BASE_URL}`) +console.log('='.repeat(80) + '\n') + +window.onerror = (message, source, lineno, colno, error) => { + console.log('\n' + '!'.repeat(80)) + console.log('[WINDOW ERROR]') + console.log('Message:', message) + console.log('Source:', source) + console.log('Line:', lineno, 'Column:', colno) + if (error) { + console.log('Error:', error.message) + console.log('Stack:', error.stack) + } + console.log('!'.repeat(80) + '\n') +} + +window.onunhandledrejection = (event) => { + console.log('\n' + '!'.repeat(80)) + console.log('[UNHANDLED PROMISE REJECTION]') + console.log('Reason:', event.reason) + if (event.reason instanceof Error) { + console.log('Stack:', event.reason.stack) + } + console.log('!'.repeat(80) + '\n') +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +) diff --git a/website/src/store/merchantStore.ts b/website/src/store/merchantStore.ts new file mode 100644 index 00000000..31ff261a --- /dev/null +++ b/website/src/store/merchantStore.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface MerchantStore { + merchantId: string + setMerchantId: (id: string) => void +} + +const DEBUG_STORE = true + +export const useMerchantStore = create()( + persist( + (set) => ({ + merchantId: '', + setMerchantId: (id) => { + if (DEBUG_STORE) { + console.log(`\n[STORE] Merchant ID changed: "${id}"`) + } + set({ merchantId: id }) + }, + }), + { name: 'merchant-store' } + ) +) diff --git a/website/src/types/api.ts b/website/src/types/api.ts new file mode 100644 index 00000000..537e1dbb --- /dev/null +++ b/website/src/types/api.ts @@ -0,0 +1,369 @@ +// All types use snake_case to match Rust backend responses + +export interface DecideGatewayResponse { + decided_gateway: string + routing_approach: string + gateway_priority_map: Record + routing_dimension: string + routing_dimension_level: string + filter_wise_gateways: Record | null + reset_approach: string + is_scheduled_outage: boolean + latency: number +} + +export interface GatewayConnector { + gateway_name: string + gateway_id: string | null +} + +export interface VolumeSplitItem { + split: number + output: GatewayConnector +} + +export type RoutingAlgorithmData = + | GatewayConnector[] // priority + | VolumeSplitItem[] // volume_split + | GatewayConnector // single + | EuclidAlgorithmData // advanced + +export interface EuclidRule { + name: string + connectorSelection: EuclidOutput + statements: EuclidStatement[] +} + +export interface EuclidStatement { + condition: EuclidCondition[] +} + +export interface EuclidCondition { + lhs: string + comparison: string + value: { type: string; value: string | number } + metadata: Record +} + +export interface EuclidOutput { + type: 'priority' | 'volume_split' + data: GatewayConnector[] | VolumeSplitItem[] +} + +export interface EuclidAlgorithmData { + globals: Record + defaultSelection: EuclidOutput + rules: EuclidRule[] +} + +export interface RoutingAlgorithm { + id: string + name: string + description: string + created_by: string + algorithm_for: string + created_at?: string + modified_at?: string + // Backend returns algorithm_data, not algorithm + algorithm_data?: { + type: 'priority' | 'volume_split' | 'single' | 'advanced' + data: RoutingAlgorithmData + } + // For convenience, map algorithm_data to algorithm in the component + algorithm?: { + type: 'priority' | 'volume_split' | 'single' | 'advanced' + data: RoutingAlgorithmData + } +} + +export interface CreateRoutingRequest { + name: string + description: string + created_by: string + algorithm_for: string + algorithm: { + type: string + data: RoutingAlgorithmData + } +} + +export interface ActivateRoutingRequest { + created_by: string + routing_algorithm_id: string +} + +export interface SRConfigData { + defaultBucketSize: number + defaultLatencyThreshold: number | null + defaultHedgingPercent: number | null + defaultLowerResetFactor: number | null + defaultUpperResetFactor: number | null + defaultGatewayExtraScore: number | null + subLevelInputConfig: SubLevelConfig[] | null +} + +export interface SubLevelConfig { + paymentMethodType: string + paymentMethod: string + bucketSize: number + hedgingPercent: number | null + latencyThreshold: number | null +} + +export interface EliminationData { + threshold: number + txnLatency: { + gatewayLatency: number + } +} + +export interface RuleConfig { + type: 'successRate' | 'elimination' | 'debitRouting' + data?: SRConfigData | EliminationData | DebitRoutingData +} + +export interface CreateRuleRequest { + merchant_id: string + config: RuleConfig +} + +export interface DebitRoutingData { + merchant_category_code: string + acquirer_country: string +} + +export interface CreateMerchantRequest { + merchant_id: string + gateway_success_rate_based_decider_input: null +} + +export interface SRDimensionRequest { + merchant_id: string + paymentInfo: { + fields: string[] + } +} + +export type AnalyticsScope = 'current' | 'all' +export type AnalyticsRange = '15m' | '1h' | '24h' +export type AnalyticsRangeValue = AnalyticsRange | 'custom' + +export interface AnalyticsQuery { + merchant_id?: string + scope?: AnalyticsScope + range?: AnalyticsRange + start_ms?: number + end_ms?: number + page?: number + page_size?: number + payment_method_type?: string + payment_method?: string + card_network?: string + card_is_in?: string + currency?: string + country?: string + auth_type?: string + gateway?: string +} + +export interface AnalyticsKpi { + label: string + value: string + subtitle?: string | null +} + +export interface GatewayScoreSnapshot { + merchant_id: string + payment_method_type: string + payment_method: string + gateway: string + score_value: number + sigma_factor: number + average_latency: number + tp99_latency: number + transaction_count: number + last_updated_ms: number +} + +export interface GatewayScoreSeriesPoint { + bucket_ms: number + merchant_id: string + payment_method_type: string + payment_method: string + gateway: string + score_value: number +} + +export interface AnalyticsOverviewResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + kpis: AnalyticsKpi[] + route_hits: AnalyticsRouteHit[] + top_scores: GatewayScoreSnapshot[] + top_errors: AnalyticsErrorSummary[] + top_rules: AnalyticsRuleHit[] +} + +export interface AnalyticsRouteHit { + route: string + count: number +} + +export interface AnalyticsGatewayScoresResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + range: AnalyticsRangeValue + snapshots: GatewayScoreSnapshot[] + series: GatewayScoreSeriesPoint[] +} + +export interface AnalyticsDecisionPoint { + bucket_ms: number + routing_approach: string + count: number +} + +export interface AnalyticsDecisionResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + range: AnalyticsRangeValue + tiles: AnalyticsKpi[] + series: AnalyticsDecisionPoint[] + approaches: AnalyticsRuleHit[] +} + +export interface AnalyticsGatewaySharePoint { + bucket_ms: number + gateway: string + count: number +} + +export interface AnalyticsRoutingStatsResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + range: AnalyticsRangeValue + gateway_share: AnalyticsGatewaySharePoint[] + top_rules: AnalyticsRuleHit[] + sr_trend: GatewayScoreSeriesPoint[] + available_filters: RoutingFilterOptions +} + +export interface RoutingFilterOptions { + dimensions: RoutingFilterDimension[] + missing_dimensions: RoutingFilterDimensionHint[] + gateways: string[] +} + +export interface RoutingFilterDimension { + key: string + label: string + values: string[] +} + +export interface RoutingFilterDimensionHint { + key: string + label: string +} + +export interface AnalyticsErrorSummary { + route: string + error_code: string + error_message: string + count: number + last_seen_ms: number +} + +export interface AnalyticsLogSample { + route: string + merchant_id?: string | null + payment_id?: string | null + request_id?: string | null + gateway?: string | null + routing_approach?: string | null + status?: string | null + error_code?: string | null + error_message?: string | null + event_type?: string | null + created_at_ms: number +} + +export interface AnalyticsLogSummariesResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + range: AnalyticsRange + total_errors: number + errors: AnalyticsErrorSummary[] + samples: AnalyticsLogSample[] + page: number + page_size: number +} + +export interface AnalyticsRuleHit { + rule_name: string + count: number +} + +export interface PaymentAuditSummary { + lookup_key: string + payment_id?: string | null + request_id?: string | null + merchant_id?: string | null + first_seen_ms: number + last_seen_ms: number + event_count: number + latest_status?: string | null + latest_gateway?: string | null + latest_stage?: string | null + gateways: string[] + routes: string[] +} + +export interface PaymentAuditEvent { + id: number + event_type: string + event_stage?: string | null + route?: string | null + merchant_id?: string | null + payment_id?: string | null + request_id?: string | null + payment_method_type?: string | null + payment_method?: string | null + gateway?: string | null + routing_approach?: string | null + rule_name?: string | null + status?: string | null + error_code?: string | null + error_message?: string | null + score_value?: number | null + sigma_factor?: number | null + average_latency?: number | null + tp99_latency?: number | null + transaction_count?: number | null + details?: string | null + details_json?: Record | null + created_at_ms: number +} + +export interface PaymentAuditResponse { + generated_at_ms: number + scope: AnalyticsScope + merchant_id?: string | null + range: AnalyticsRangeValue + payment_id?: string | null + request_id?: string | null + gateway?: string | null + route?: string | null + status?: string | null + event_type?: string | null + error_code?: string | null + page: number + page_size: number + total_results: number + results: PaymentAuditSummary[] + timeline: PaymentAuditEvent[] +} diff --git a/website/src/vite-env.d.ts b/website/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/website/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/website/tailwind.config.ts b/website/tailwind.config.ts new file mode 100644 index 00000000..a298f58b --- /dev/null +++ b/website/tailwind.config.ts @@ -0,0 +1,32 @@ +import type { Config } from 'tailwindcss' + +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: 'class', + theme: { + extend: { + colors: { + brand: { + DEFAULT: '#4f46e5', + 50: '#eef2ff', + 100: '#e0e7ff', + 500: '#6366f1', + 600: '#4f46e5', + 700: '#4338ca', + }, + }, + fontFamily: { + sans: ['Outfit', 'Inter', 'system-ui', '-apple-system', 'sans-serif'], + mono: ['JetBrains Mono', 'Menlo', 'Monaco', 'monospace'], + }, + letterSpacing: { + tightest: '-0.03em', + }, + boxShadow: { + 'glass-light': '0 10px 40px -10px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.05)', + 'glass-dark': '0 20px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05), inset 0 0 0 1px rgba(255,255,255,0.02)', + }, + }, + }, + plugins: [], +} satisfies Config diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 00000000..3934b8f6 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/website/tsconfig.node.json b/website/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/website/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/website/vite.config.ts b/website/vite.config.ts new file mode 100644 index 00000000..10c0ddf5 --- /dev/null +++ b/website/vite.config.ts @@ -0,0 +1,177 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig(({ command }) => { + const isDevServer = command === 'serve' + const publicBaseUrl = isDevServer ? '/' : '/dashboard/' + + return { + plugins: [react()], + base: publicBaseUrl, + server: { + proxy: { + '/decide-gateway': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/routing': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/rule': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/merchant-account': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/config-sr-dimension': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/config': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/health': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/analytics': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + '/update-gateway-score': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, + }, + fs: { + strict: false, + }, + host: true, + port: 5173, + }, + build: { + outDir: 'dist', + }, + } +})