diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/conventional-commit-check.yml similarity index 88% rename from .github/workflows/pr-title-check.yml rename to .github/workflows/conventional-commit-check.yml index 8c15246f0e6c..5fd25e9332d1 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/conventional-commit-check.yml @@ -1,4 +1,4 @@ -name: PR Title Checks +name: Conventional Commit Message Check on: # This is a dangerous event trigger as it causes the workflow to run in the @@ -35,19 +35,6 @@ env: CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: - typos: - name: Spell check PR title - runs-on: ubuntu-latest - steps: - - name: Store PR title in a file - shell: bash - run: echo '${{ github.event.pull_request.title }}' > pr_title.txt - - - name: Spell check - uses: crate-ci/typos@master - with: - files: ./pr_title.txt - pr_title_check: name: Verify PR title follows conventional commit standards runs-on: ubuntu-latest @@ -66,8 +53,10 @@ jobs: id: pr_title_check if: ${{ github.event_name == 'pull_request_target' }} shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} continue-on-error: true - run: cog verify '${{ github.event.pull_request.title }}' + run: cog verify "$TITLE" - name: Verify commit message follows conventional commit standards id: commit_message_check diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index 6b0911d1b456..3291755b56cf 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -143,7 +143,7 @@ jobs: for i in $(echo "$CONNECTORS" | tr "," "\n"); do echo $i - if ! cargo run --bin test_utils -- --connector_name="$i" --base_url="$BASE_URL" --admin_api_key="$ADMIN_API_KEY"; then + if ! cargo run --bin test_utils -- --connector-name="$i" --base-url="$BASE_URL" --admin-api-key="$ADMIN_API_KEY"; then failed_connectors+=("$i") fi done diff --git a/.github/workflows/pr-title-spell-check.yml b/.github/workflows/pr-title-spell-check.yml new file mode 100644 index 000000000000..6ab6f184739d --- /dev/null +++ b/.github/workflows/pr-title-spell-check.yml @@ -0,0 +1,27 @@ +name: PR Title Spell Check + +on: + pull_request: + types: + - opened + - edited + - synchronize + +jobs: + typos: + name: Spell check PR title + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Store PR title in a file + shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} + run: echo $TITLE > pr_title.txt + + - name: Spell check + uses: crate-ci/typos@master + with: + files: ./pr_title.txt diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index f489a3f8de2a..872c207e8aa3 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -74,7 +74,11 @@ jobs: connector=$(basename ${connector_dir}) newman dir-import ${POSTMAN_DIR}/${connector} -o ${POSTMAN_JSON_DIR}/${connector}.postman_collection.json done - (git diff --quiet && git diff --staged --quiet) || (git commit -am 'test(postman): update postman collection files' && echo "Committed changes") || (echo "Unable to commit the following changes:" && git diff) + + if git add postman && ! git diff --staged --quiet postman; then + git commit --message 'test(postman): update postman collection files' + echo "Changes detected and commited." + fi - name: Obtain previous and new tag information shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf1cc629d8e..412b42afc2eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,159 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.75.0 (2023-11-09) + +### Features + +- **events:** Add extracted fields based on req/res types ([#2795](https://github.com/juspay/hyperswitch/pull/2795)) ([`8985794`](https://github.com/juspay/hyperswitch/commit/89857941b09c5fbe0f3e7d5b4f908bb144ae162d)) +- **router:** + - Added merchant custom name support for payment link ([#2685](https://github.com/juspay/hyperswitch/pull/2685)) ([`8b15189`](https://github.com/juspay/hyperswitch/commit/8b151898dc0d8eefe5ed2bbdafe59e8f58b4698c)) + - Add `gateway_status_map` CRUD APIs ([#2809](https://github.com/juspay/hyperswitch/pull/2809)) ([`5c9e235`](https://github.com/juspay/hyperswitch/commit/5c9e235bd30dd3e03d086a83613edfcc62b2ead2)) + +### Bug Fixes + +- **analytics:** Added hs latency to api event for paymentconfirm call ([#2787](https://github.com/juspay/hyperswitch/pull/2787)) ([`aab8f60`](https://github.com/juspay/hyperswitch/commit/aab8f6035c16ca19009f8f1e0db688c17bc0b2b6)) +- [mollie] locale validation irrespective of auth type ([#2814](https://github.com/juspay/hyperswitch/pull/2814)) ([`25a73c2`](https://github.com/juspay/hyperswitch/commit/25a73c29a4c4715a54862dd6a28c875fd3752f63)) + +**Full Changelog:** [`v1.74.0...v1.75.0`](https://github.com/juspay/hyperswitch/compare/v1.74.0...v1.75.0) + +- - - + + +## 1.74.0 (2023-11-08) + +### Features + +- **core:** Use redis as temp locker instead of basilisk ([#2789](https://github.com/juspay/hyperswitch/pull/2789)) ([`6678689`](https://github.com/juspay/hyperswitch/commit/6678689265ae9a4fbb7a43c1938237d349c5a68e)) +- **events:** Add request details to api events ([#2769](https://github.com/juspay/hyperswitch/pull/2769)) ([`164d1c6`](https://github.com/juspay/hyperswitch/commit/164d1c66fbcb84104db07412496114db2f8c5c0c)) +- **router:** Add `gateway_status_map` interface ([#2804](https://github.com/juspay/hyperswitch/pull/2804)) ([`a429b23`](https://github.com/juspay/hyperswitch/commit/a429b23c7f21c9d08a79895c0b770b35aab725f7)) +- **test_utils:** Add custom-headers and custom delay support to rustman ([#2636](https://github.com/juspay/hyperswitch/pull/2636)) ([`1effddd`](https://github.com/juspay/hyperswitch/commit/1effddd0a0d3985d6df03c4ae9be28712befc05e)) + +### Bug Fixes + +- **connector:** Add attempt_status in field in error_response ([#2794](https://github.com/juspay/hyperswitch/pull/2794)) ([`5642fef`](https://github.com/juspay/hyperswitch/commit/5642fef52a6d591d12c5745ed381f41a1593f183)) + +### Refactors + +- **config:** Update payment method filter of Klarna in Stripe ([#2807](https://github.com/juspay/hyperswitch/pull/2807)) ([`21ce807`](https://github.com/juspay/hyperswitch/commit/21ce8079f4cb11d70c5eaae78f83773141c67d0c)) +- **router:** Add parameter connectors to get_request_body function ([#2708](https://github.com/juspay/hyperswitch/pull/2708)) ([`7623ea9`](https://github.com/juspay/hyperswitch/commit/7623ea93bee61b0bb22b68e86f44de17f04f876b)) + +### Documentation + +- **README:** Update README ([#2800](https://github.com/juspay/hyperswitch/pull/2800)) ([`bef0a04`](https://github.com/juspay/hyperswitch/commit/bef0a04edc6323b3b7a2e0dd7eeb7954915ba7cf)) + +**Full Changelog:** [`v1.73.0...v1.74.0`](https://github.com/juspay/hyperswitch/compare/v1.73.0...v1.74.0) + +- - - + + +## 1.73.0 (2023-11-07) + +### Features + +- **connector:** + - [BANKOFAMERICA] Add Connector Template Code ([#2764](https://github.com/juspay/hyperswitch/pull/2764)) ([`4563935`](https://github.com/juspay/hyperswitch/commit/4563935372d2cdff3f746fa86a47f1166ffd32ac)) + - [Bitpay] Add order id as the reference id ([#2591](https://github.com/juspay/hyperswitch/pull/2591)) ([`d47d4ac`](https://github.com/juspay/hyperswitch/commit/d47d4ac682705d6ac692f9381149bbf08ad71264)) +- **router:** Make webhook events config disabled only and by default enable all the events ([#2770](https://github.com/juspay/hyperswitch/pull/2770)) ([`d335879`](https://github.com/juspay/hyperswitch/commit/d335879f9289b57a90a76c6587a58a0b3e12c9ad)) +- Make drainer logs queryable with request_id and global_id ([#2771](https://github.com/juspay/hyperswitch/pull/2771)) ([`ff73aba`](https://github.com/juspay/hyperswitch/commit/ff73aba8e72d8e072027881760335c0c818df665)) + +### Bug Fixes + +- **connector:** Fix amount conversion incase of minor unit ([#2793](https://github.com/juspay/hyperswitch/pull/2793)) ([`34f5226`](https://github.com/juspay/hyperswitch/commit/34f52260d3fa68b54e5b46207afaf2ad07a8d8ba)) + +### Refactors + +- **payment_methods:** Added support for account subtype in pmd ([#2651](https://github.com/juspay/hyperswitch/pull/2651)) ([`e7375d0`](https://github.com/juspay/hyperswitch/commit/e7375d0e26099a7e0e6efd1b83b8eb9c7b1c5411)) + +### Documentation + +- **README:** Add one-click deployment information using CDK ([#2798](https://github.com/juspay/hyperswitch/pull/2798)) ([`bb39cd4`](https://github.com/juspay/hyperswitch/commit/bb39cd4081fdcaf68b2b5de2234e93493dbd84b6)) + +**Full Changelog:** [`v1.72.0...v1.73.0`](https://github.com/juspay/hyperswitch/compare/v1.72.0...v1.73.0) + +- - - + + +## 1.72.0 (2023-11-05) + +### Features + +- **connector:** + - [ACI] Currency Unit Conversion ([#2750](https://github.com/juspay/hyperswitch/pull/2750)) ([`cdead78`](https://github.com/juspay/hyperswitch/commit/cdead78ea6a1f2dce92187f499f54498ba4bb173)) + - [Fiserv] Currency Unit Conversion ([#2715](https://github.com/juspay/hyperswitch/pull/2715)) ([`b6b9e4f`](https://github.com/juspay/hyperswitch/commit/b6b9e4f912e1c61cd31ab91be587ffb08c9f3a5b)) + - [Bitpay] Use `connector_request_reference_id` as reference to the connector ([#2697](https://github.com/juspay/hyperswitch/pull/2697)) ([`7141b89`](https://github.com/juspay/hyperswitch/commit/7141b89d231bae0c3b1c10095b88df16129b1665)) + - [NMI] Currency Unit Conversion ([#2707](https://github.com/juspay/hyperswitch/pull/2707)) ([`1b45a30`](https://github.com/juspay/hyperswitch/commit/1b45a302630ed8affc5abff0de1325fb5c6f870e)) + - [Payeezy] Currency Unit Conversion ([#2710](https://github.com/juspay/hyperswitch/pull/2710)) ([`25245b9`](https://github.com/juspay/hyperswitch/commit/25245b965371d93449f4584667adeb38ab7e0e59)) + +### Refactors + +- **connector:** [Stax] Currency Unit Conversion ([#2711](https://github.com/juspay/hyperswitch/pull/2711)) ([`2782923`](https://github.com/juspay/hyperswitch/commit/278292322c7c06f4239dd73861469e436bd941fa)) + +### Testing + +- **postman:** Update postman collection files ([`d11e7fd`](https://github.com/juspay/hyperswitch/commit/d11e7fd5642efe7da4b5021d87cf40f16d9eeded)) + +**Full Changelog:** [`v1.71.0...v1.72.0`](https://github.com/juspay/hyperswitch/compare/v1.71.0...v1.72.0) + +- - - + + +## 1.71.0 (2023-11-03) + +### Features + +- **merchant_connector_account:** Add cache for querying by `merchant_connector_id` ([#2738](https://github.com/juspay/hyperswitch/pull/2738)) ([`1ba6282`](https://github.com/juspay/hyperswitch/commit/1ba6282699b7dff5e6e95c9a14e51c0f8bf749cd)) +- **router:** Add Smart Routing to route payments efficiently ([#2665](https://github.com/juspay/hyperswitch/pull/2665)) ([`9b618d2`](https://github.com/juspay/hyperswitch/commit/9b618d24476967d364835d04010d9076a80aeb9c)) + +### Bug Fixes + +- **connector:** + - [Cryptopay]Remove default case handling for Cryptopay ([#2699](https://github.com/juspay/hyperswitch/pull/2699)) ([`255a4f8`](https://github.com/juspay/hyperswitch/commit/255a4f89a8e0124310d42bb63ad459bd8cde2cba)) + - [Bluesnap] fix psync status to failure when it is '403' ([#2772](https://github.com/juspay/hyperswitch/pull/2772)) ([`9314d14`](https://github.com/juspay/hyperswitch/commit/9314d1446326fd8a69f1f69657a976bbe7c27901)) +- Response spelling ([#2779](https://github.com/juspay/hyperswitch/pull/2779)) ([`5859372`](https://github.com/juspay/hyperswitch/commit/585937204d9071baa37d402f73159f8f650d0a07)) + +### Testing + +- **postman:** Update postman collection files ([`21e8a10`](https://github.com/juspay/hyperswitch/commit/21e8a105f9b47ded232b457a0420ad71ec2414ed)) + +**Full Changelog:** [`v1.70.1...v1.71.0`](https://github.com/juspay/hyperswitch/compare/v1.70.1...v1.71.0) + +- - - + + +## 1.70.1 (2023-11-03) + +### Revert + +- Fix(analytics): feat(analytics): analytics APIs ([#2777](https://github.com/juspay/hyperswitch/pull/2777)) ([`169d33b`](https://github.com/juspay/hyperswitch/commit/169d33bf8157b1a9910c841c8c55eddc4d2ad168)) + +**Full Changelog:** [`v1.70.0...v1.70.1`](https://github.com/juspay/hyperswitch/compare/v1.70.0...v1.70.1) + +- - - + + +## 1.70.0 (2023-11-03) + +### Features + +- **analytics:** Analytics APIs ([#2676](https://github.com/juspay/hyperswitch/pull/2676)) ([`c0a5e7b`](https://github.com/juspay/hyperswitch/commit/c0a5e7b7d945095053606e35c9bb23a06090c4e3)) +- **connector:** [Multisafepay] add error handling ([#2595](https://github.com/juspay/hyperswitch/pull/2595)) ([`b3c846d`](https://github.com/juspay/hyperswitch/commit/b3c846d637dd32a2d6d7044c118abbb2616642f0)) +- **events:** Add api auth type details to events ([#2760](https://github.com/juspay/hyperswitch/pull/2760)) ([`1094493`](https://github.com/juspay/hyperswitch/commit/10944937a02502e0727f16368d8d055e575dd518)) + +### Bug Fixes + +- **router:** Make customer_id optional when billing and shipping address is passed in payments create, update ([#2762](https://github.com/juspay/hyperswitch/pull/2762)) ([`e40a293`](https://github.com/juspay/hyperswitch/commit/e40a29351c7aa7b86a5684959a84f0236104cafd)) +- Null fields in payments response ([#2745](https://github.com/juspay/hyperswitch/pull/2745)) ([`42261a5`](https://github.com/juspay/hyperswitch/commit/42261a5306bb99d3e20eb3aa734a895e589b1d94)) + +### Testing + +- **postman:** Update postman collection files ([`772f03e`](https://github.com/juspay/hyperswitch/commit/772f03ee3836ce86de3874f6a5e7f636718e6034)) + +**Full Changelog:** [`v1.69.0...v1.70.0`](https://github.com/juspay/hyperswitch/compare/v1.69.0...v1.70.0) + +- - - + + ## 1.69.0 (2023-10-31) ### Features diff --git a/Cargo.lock b/Cargo.lock index 665703f3d505..ac7fde55d8e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstyle" version = "1.0.0" @@ -397,6 +403,7 @@ dependencies = [ "common_enums", "common_utils", "error-stack", + "euclid", "masking", "mime", "reqwest", @@ -1343,6 +1350,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.83" @@ -1413,6 +1426,33 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.3.4" @@ -1463,7 +1503,6 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" name = "common_enums" version = "0.1.0" dependencies = [ - "common_utils", "diesel", "router_derive", "serde", @@ -1479,6 +1518,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bytes", + "common_enums", "diesel", "error-stack", "fake", @@ -1497,6 +1537,7 @@ dependencies = [ "reqwest", "ring", "router_env", + "rustc-hash", "serde", "serde_json", "serde_urlencoded", @@ -1615,6 +1656,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -2022,6 +2099,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.4" @@ -2063,6 +2149,52 @@ dependencies = [ "serde", ] +[[package]] +name = "euclid" +version = "0.1.0" +dependencies = [ + "common_enums", + "criterion", + "erased-serde", + "euclid_macros", + "frunk", + "frunk_core", + "nom", + "once_cell", + "rustc-hash", + "serde", + "serde_json", + "strum 0.25.0", + "thiserror", +] + +[[package]] +name = "euclid_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rustc-hash", + "strum 0.24.1", + "syn 1.0.109", +] + +[[package]] +name = "euclid_wasm" +version = "0.1.0" +dependencies = [ + "api_models", + "euclid", + "getrandom 0.2.10", + "kgraph_utils", + "once_cell", + "ron-parser", + "serde", + "serde-wasm-bindgen", + "strum 0.25.0", + "wasm-bindgen", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2415,8 +2547,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2497,6 +2631,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -2811,6 +2951,17 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.17", + "windows-sys", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2905,6 +3056,19 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kgraph_utils" +version = "0.1.0" +dependencies = [ + "api_models", + "criterion", + "euclid", + "masking", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3365,6 +3529,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3729,6 +3899,34 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.16.8" @@ -4216,6 +4414,19 @@ dependencies = [ "serde", ] +[[package]] +name = "ron-parser" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7280c46017fafbe4275179689e446a9b0db3bd91ea61aaee22841ef618405a" +dependencies = [ + "nom", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "router" version = "0.2.0" @@ -4248,6 +4459,7 @@ dependencies = [ "dyn-clone", "encoding_rs", "error-stack", + "euclid", "external_services", "futures", "hex", @@ -4257,6 +4469,7 @@ dependencies = [ "infer 0.13.0", "josekit", "jsonwebtoken", + "kgraph_utils", "literally", "masking", "maud", @@ -4268,6 +4481,7 @@ dependencies = [ "openssl", "qrcode", "rand 0.8.5", + "rand_chacha 0.3.1", "redis_interface", "regex", "reqwest", @@ -4275,6 +4489,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rustc-hash", "scheduler", "serde", "serde_json", @@ -4651,6 +4866,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -5349,6 +5575,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/README.md b/README.md index cc19670a8fc3..129a0512d4a0 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,11 @@ The single API to access payment ecosystems across 130+ countries

-

🎉 Hacktoberfest is here! 🎉

- -New to Rust? Hyperswitch is the perfect place to start this hacktoberfest! 😁 - -> ⭐️ If you're new to Hacktoberfest, you can learn more and register to participate [here](https://hacktoberfest.com/participation/). Registration is from **September 26th - October 31st**. -
-Hyperswitch is an open source payments switch to make payments fast, reliable, and, affordable. -It lets you connect with multiple payment processors and route traffic effortlessly, all with a single API integration. - +Hyperswitch is a community-led, open payments switch to enable access to the best payments infrastructure for every digital business. Using Hyperswitch, you can: @@ -58,8 +50,6 @@ Using Hyperswitch, you can: - 🎨 **Customize payment flows** with full visibility and control - 🌐 **Increase business reach** with local/alternate payment methods -> Hyperswitch is **wire-compatible** with top processors like Stripe, making it easy to integrate. -
Hyperswitch-Product @@ -67,24 +57,23 @@ Using Hyperswitch, you can:

⚡️ Quick Start Guide

+

One-click deployment on AWS cloud

- +The fastest and easiest way to try hyperswitch is via our CDK scripts -Ways to get started with Hyperswitch: +1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. + No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -1. Try it in our Sandbox Environment: Fast and easy to - start. - No code or setup is required in your system, [learn more](/docs/try_sandbox.md) +   Click here if you have not bootstrapped your region before deploying +   - -2. A simple demo of integrating Hyperswitch with your React App, Try our React [Demo App](https://github.com/aashu331998/hyperswitch-react-demo-app/archive/refs/heads/main.zip). +2. Sign-in to your AWS console. +3. Follow the instructions provided on the console to successfully deploy Hyperswitch -3. Install in your local system: Configurations and - setup required in your system. - Suitable if you like to customise the core offering, [setup guide](/docs/try_local_system.md) +For an early access to the production-ready setup fill this Early Access Form

🔌 Fast Integration for Stripe Users

diff --git a/config/config.example.toml b/config/config.example.toml index 59083d6c71d3..ed9cf9698984 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -163,6 +163,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" diff --git a/config/development.toml b/config/development.toml index 5e74eafcb467..63c1f045d94f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -71,6 +71,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", @@ -136,6 +137,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -246,7 +248,7 @@ ideal = { country = "NL", currency = "EUR" } [pm_filters.stripe] google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" } -klarna = { country = "US", currency = "USD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } affirm = { country = "US", currency = "USD" } afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } giropay = { country = "DE", currency = "EUR" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 20ca175ceb84..282894b56d43 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -78,6 +78,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -145,6 +146,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 05f527d24662..7f21962109de 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -105,6 +105,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } @@ -156,7 +157,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -185,7 +186,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) .build(), )) } @@ -301,6 +302,7 @@ impl fn get_request_body( &self, _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -318,7 +320,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) .build(), )) } @@ -373,7 +375,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::RefundsRouterData) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -393,7 +395,7 @@ impl .url(&types::RefundExecuteType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundExecuteType::get_headers(self, req, connectors)?) - .body(types::RefundExecuteType::get_request_body(self, req)?) + .body(types::RefundExecuteType::get_request_body(self, req, connectors)?) .build(); Ok(Some(request)) } @@ -441,7 +443,7 @@ impl .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .body(types::RefundSyncType::get_request_body(self, req)?) + .body(types::RefundSyncType::get_request_body(self, req, connectors)?) .build(), )) } diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce61d30d36f5..d15fdeabf387 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -9,8 +9,12 @@ license.workspace = true [features] default = ["payouts"] +business_profile_routing = [] +connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] -dummy_connector = ["common_enums/dummy_connector"] +backwards_compatibility = ["connector_choice_bcompat"] +connector_choice_mca_id = ["euclid/connector_choice_mca_id"] +dummy_connector = ["common_enums/dummy_connector", "euclid/dummy_connector"] detailed_errors = [] payouts = [] @@ -32,5 +36,6 @@ thiserror = "1.0.40" cards = { version = "0.1.0", path = "../cards" } common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } +euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking" } router_derive = { version = "0.1.0", path = "../router_derive" } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index b1a258e6b26c..979214a071a9 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -443,72 +443,6 @@ pub mod payout_routing_algorithm { } } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum RoutingAlgorithm { - Single(RoutableConnectorChoice), -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum RoutableConnectorChoice { - ConnectorName(api_enums::RoutableConnectors), - ConnectorId { - merchant_connector_id: String, - connector: api_enums::RoutableConnectors, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde( - tag = "type", - content = "data", - rename_all = "snake_case", - from = "StraightThroughAlgorithmSerde", - into = "StraightThroughAlgorithmSerde" -)] -pub enum StraightThroughAlgorithm { - Single(RoutableConnectorChoice), -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum StraightThroughAlgorithmInner { - Single(RoutableConnectorChoice), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum StraightThroughAlgorithmSerde { - Direct(StraightThroughAlgorithmInner), - Nested { - algorithm: StraightThroughAlgorithmInner, - }, -} - -impl From for StraightThroughAlgorithm { - fn from(value: StraightThroughAlgorithmSerde) -> Self { - let inner = match value { - StraightThroughAlgorithmSerde::Direct(algorithm) => algorithm, - StraightThroughAlgorithmSerde::Nested { algorithm } => algorithm, - }; - - match inner { - StraightThroughAlgorithmInner::Single(conn) => Self::Single(conn), - } - } -} - -impl From for StraightThroughAlgorithmSerde { - fn from(value: StraightThroughAlgorithm) -> Self { - let inner = match value { - StraightThroughAlgorithm::Single(conn) => StraightThroughAlgorithmInner::Single(conn), - }; - - Self::Nested { algorithm: inner } - } -} - #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PrimaryBusinessDetails { @@ -529,9 +463,8 @@ pub struct PaymentLinkConfig { #[serde(deny_unknown_fields)] pub struct PaymentLinkColorSchema { - pub primary_color: Option, - pub primary_accent_color: Option, - pub secondary_color: Option, + pub background_primary_color: Option, + pub sdk_theme: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -959,6 +892,8 @@ pub struct ToggleKVResponse { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ToggleKVRequest { + #[serde(skip_deserializing)] + pub merchant_id: String, /// Status of KV for the specific merchant #[schema(example = true)] pub kv_enabled: bool, diff --git a/crates/api_models/src/api_keys.rs b/crates/api_models/src/api_keys.rs index f0ab403d9c65..805c5616c2a0 100644 --- a/crates/api_models/src/api_keys.rs +++ b/crates/api_models/src/api_keys.rs @@ -129,6 +129,12 @@ pub struct UpdateApiKeyRequest { /// rotating your keys once every 6 months. #[schema(example = "2022-09-10T10:11:12Z")] pub expiration: Option, + + #[serde(skip_deserializing)] + pub key_id: String, + + #[serde(skip_deserializing)] + pub merchant_id: String, } /// The response body for revoking an API Key. diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ee67c1187e6b..b27e71b9e8f5 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -75,8 +75,9 @@ pub enum Connector { Adyen, Airwallex, Authorizedotnet, - Bitpay, Bambora, + // Bankofamerica, Added as template code for future usage + Bitpay, Bluesnap, Boku, Braintree, @@ -195,6 +196,7 @@ pub enum RoutableConnectors { Adyen, Airwallex, Authorizedotnet, + // Bankofamerica, Added as template code for future usage Bitpay, Bambora, Bluesnap, diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs new file mode 100644 index 000000000000..23e7c9dc706a --- /dev/null +++ b/crates/api_models/src/events.rs @@ -0,0 +1,75 @@ +pub mod customer; +pub mod gsm; +pub mod payment; +#[cfg(feature = "payouts")] +pub mod payouts; +pub mod refund; +pub mod routing; + +use common_utils::{ + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, +}; + +use crate::{ + admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, + payments::*, verifications::*, +}; + +impl ApiEventMetric for TimeRange {} + +impl_misc_api_event_type!( + PaymentMethodId, + PaymentsSessionResponse, + PaymentMethodListResponse, + PaymentMethodCreate, + PaymentLinkInitiateRequest, + RetrievePaymentLinkResponse, + MandateListConstraints, + CreateFileResponse, + DisputeResponse, + SubmitEvidenceRequest, + MerchantConnectorResponse, + MerchantConnectorId, + MandateResponse, + MandateRevokedResponse, + RetrievePaymentLinkRequest, + MandateId, + DisputeListConstraints, + RetrieveApiKeyResponse, + BusinessProfileResponse, + BusinessProfileUpdate, + BusinessProfileCreate, + RevokeApiKeyResponse, + ToggleKVResponse, + ToggleKVRequest, + MerchantAccountDeleteResponse, + MerchantAccountUpdate, + CardInfoResponse, + CreateApiKeyResponse, + CreateApiKeyRequest, + MerchantConnectorDeleteResponse, + MerchantConnectorUpdate, + MerchantConnectorCreate, + MerchantId, + CardsInfoRequest, + MerchantAccountResponse, + MerchantAccountListRequest, + MerchantAccountCreate, + PaymentsSessionRequest, + ApplepayMerchantVerificationRequest, + ApplepayMerchantResponse, + ApplepayVerifiedDomainsResponse, + UpdateApiKeyRequest +); + +#[cfg(feature = "stripe")] +impl_misc_api_event_type!( + StripeSetupIntentResponse, + StripeRefundResponse, + StripePaymentIntentListResponse, + StripePaymentIntentResponse, + CustomerDeleteResponse, + CustomerPaymentMethodListResponse, + CreateCustomerResponse +); diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs new file mode 100644 index 000000000000..29f565042181 --- /dev/null +++ b/crates/api_models/src/events/customer.rs @@ -0,0 +1,35 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::customers::{CustomerDeleteResponse, CustomerId, CustomerRequest, CustomerResponse}; + +impl ApiEventMetric for CustomerDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} + +impl ApiEventMetric for CustomerId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone(), + }) + } +} diff --git a/crates/api_models/src/events/gsm.rs b/crates/api_models/src/events/gsm.rs new file mode 100644 index 000000000000..d984ae1ff698 --- /dev/null +++ b/crates/api_models/src/events/gsm.rs @@ -0,0 +1,33 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::gsm; + +impl ApiEventMetric for gsm::GsmCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmUpdateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs new file mode 100644 index 000000000000..2f3336fc2777 --- /dev/null +++ b/crates/api_models/src/events/payment.rs @@ -0,0 +1,151 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::{ + payment_methods::{ + CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, + PaymentMethodResponse, PaymentMethodUpdate, + }, + payments::{ + PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, + PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, + PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + }, +}; +impl ApiEventMetric for PaymentsRetrieveRequest { + fn get_api_event_type(&self) -> Option { + match self.resource_id { + PaymentIdType::PaymentIntentId(ref id) => Some(ApiEventsType::Payment { + payment_id: id.clone(), + }), + _ => None, + } + } +} + +impl ApiEventMetric for PaymentsStartRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsCaptureRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.to_owned(), + }) + } +} + +impl ApiEventMetric for PaymentsCancelRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsApproveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsRejectRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsRequest { + fn get_api_event_type(&self) -> Option { + match self.payment_id { + Some(PaymentIdType::PaymentIntentId(ref id)) => Some(ApiEventsType::Payment { + payment_id: id.clone(), + }), + _ => None, + } + } +} + +impl ApiEventMetric for PaymentsResponse { + fn get_api_event_type(&self) -> Option { + self.payment_id + .clone() + .map(|payment_id| ApiEventsType::Payment { payment_id }) + } +} + +impl ApiEventMetric for PaymentMethodResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: Some(self.payment_method), + payment_method_type: self.payment_method_type, + }) + } +} + +impl ApiEventMetric for PaymentMethodUpdate {} + +impl ApiEventMetric for PaymentMethodDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: None, + payment_method_type: None, + }) + } +} + +impl ApiEventMetric for CustomerPaymentMethodsListResponse {} + +impl ApiEventMetric for PaymentMethodListRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethodList { + payment_id: self + .client_secret + .as_ref() + .and_then(|cs| cs.rsplit_once("_secret_")) + .map(|(pid, _)| pid.to_string()), + }) + } +} + +impl ApiEventMetric for PaymentListFilterConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListFilters { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for PaymentListResponseV2 { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RedirectionResponse {} diff --git a/crates/api_models/src/events/payouts.rs b/crates/api_models/src/events/payouts.rs new file mode 100644 index 000000000000..303709acc476 --- /dev/null +++ b/crates/api_models/src/events/payouts.rs @@ -0,0 +1,29 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::payouts::{ + PayoutActionRequest, PayoutCreateRequest, PayoutCreateResponse, PayoutRetrieveRequest, +}; + +impl ApiEventMetric for PayoutRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutCreateResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} + +impl ApiEventMetric for PayoutActionRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payout) + } +} diff --git a/crates/api_models/src/events/refund.rs b/crates/api_models/src/events/refund.rs new file mode 100644 index 000000000000..424a3191db66 --- /dev/null +++ b/crates/api_models/src/events/refund.rs @@ -0,0 +1,63 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::refunds::{ + RefundListMetaData, RefundListRequest, RefundListResponse, RefundRequest, RefundResponse, + RefundUpdateRequest, RefundsRetrieveRequest, +}; + +impl ApiEventMetric for RefundRequest { + fn get_api_event_type(&self) -> Option { + let payment_id = self.payment_id.clone(); + self.refund_id + .clone() + .map(|refund_id| ApiEventsType::Refund { + payment_id: Some(payment_id), + refund_id, + }) + } +} + +impl ApiEventMetric for RefundResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: Some(self.payment_id.clone()), + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundsRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: None, + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundUpdateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Refund { + payment_id: None, + refund_id: self.refund_id.clone(), + }) + } +} + +impl ApiEventMetric for RefundListRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RefundListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +impl ApiEventMetric for RefundListMetaData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs new file mode 100644 index 000000000000..5eca01acc6fb --- /dev/null +++ b/crates/api_models/src/events/routing.rs @@ -0,0 +1,58 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::routing::{ + LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, RoutingAlgorithmId, + RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, +}; +#[cfg(feature = "business_profile_routing")] +use crate::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; + +impl ApiEventMetric for RoutingKind { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for MerchantRoutingAlgorithm { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingAlgorithmId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingDictionaryRecord { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for LinkedRoutingConfigRetrieveResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +#[cfg(feature = "business_profile_routing")] +impl ApiEventMetric for RoutingRetrieveQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for RoutingConfigRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +#[cfg(feature = "business_profile_routing")] +impl ApiEventMetric for RoutingRetrieveLinkQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs new file mode 100644 index 000000000000..6bd8fd99dd93 --- /dev/null +++ b/crates/api_models/src/gsm.rs @@ -0,0 +1,75 @@ +use crate::enums; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmCreateRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: GsmDecision, + pub step_up_possible: bool, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmRetrieveRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive( + Default, + Clone, + Copy, + Debug, + strum::Display, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::EnumString, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GsmDecision { + Retry, + Requeue, + #[default] + DoDefault, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmUpdateRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: Option, + pub router_error: Option, + pub decision: Option, + pub step_up_possible: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmDeleteRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct GsmDeleteResponse { + pub gsm_rule_delete: bool, + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index dab1b46adbad..5da916b14817 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -9,7 +9,9 @@ pub mod enums; pub mod ephemeral_key; #[cfg(feature = "errors")] pub mod errors; +pub mod events; pub mod files; +pub mod gsm; pub mod mandates; pub mod organization; pub mod payment_methods; @@ -17,5 +19,6 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod routing; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b30590bfd6f2..289f652981eb 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -12,7 +12,9 @@ use utoipa::ToSchema; #[cfg(feature = "payouts")] use crate::payouts; use crate::{ - admin, enums as api_enums, + admin, + customers::CustomerId, + enums as api_enums, payments::{self, BankCodeResponse}, }; @@ -167,6 +169,8 @@ pub struct CardDetailsPaymentMethod { pub struct PaymentMethodDataBankCreds { pub mask: String, pub hash: String, + pub account_type: Option, + pub account_name: Option, pub payment_method_type: api_enums::PaymentMethodType, pub connector_details: Vec, } @@ -474,6 +478,8 @@ pub struct RequestPaymentMethodTypes { #[derive(Debug, Clone, serde::Serialize, Default, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodListRequest { + #[serde(skip_deserializing)] + pub customer_id: Option, /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893ein2d")] pub client_secret: Option, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f9cb21dae5f2..22579ed6d6ea 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2232,7 +2232,9 @@ pub struct PaymentListFilters { pub authentication_type: Vec, } -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] +#[derive( + Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, ToSchema, +)] pub struct TimeRange { /// The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed #[serde(with = "common_utils::custom_serde::iso8601")] @@ -3098,6 +3100,8 @@ pub struct PaymentLinkObject { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, pub merchant_custom_domain_name: Option, + /// Custom merchant name for payment link + pub custom_merchant_name: Option, } #[derive(Default, Debug, serde::Deserialize, Clone, ToSchema, serde::Serialize)] @@ -3141,11 +3145,12 @@ pub struct PaymentLinkDetails { pub pub_key: String, pub client_secret: String, pub payment_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub expiry: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub expiry: Option, pub merchant_logo: String, pub return_url: String, - pub merchant_name: crypto::OptionalEncryptableName, - pub order_details: Vec, + pub merchant_name: String, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, + pub sdk_theme: Option, } diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 7b4eae4238ac..6fe8be8b5291 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use utoipa::ToSchema; +use super::payments::TimeRange; use crate::{admin, enums}; #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] @@ -75,6 +76,8 @@ pub struct RefundsRetrieveRequest { #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct RefundUpdateRequest { + #[serde(skip)] + pub refund_id: String, /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive #[schema(max_length = 255, example = "Customer returned the product")] pub reason: Option, @@ -152,16 +155,6 @@ pub struct RefundListRequest { pub refund_status: Option>, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, ToSchema)] -pub struct TimeRange { - /// The start time to filter refunds list or to get list of filters. To get list of filters start time is needed to be passed - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - /// The end time to filter refunds list or to get list of filters. If not passed the default time is now - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] pub struct RefundListResponse { /// The number of refunds included in the list diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs new file mode 100644 index 000000000000..425ca364191d --- /dev/null +++ b/crates/api_models/src/routing.rs @@ -0,0 +1,599 @@ +use std::fmt::Debug; + +use common_utils::errors::ParsingError; +use error_stack::IntoReport; +use euclid::{ + dssa::types::EuclidAnalysable, + enums as euclid_enums, + frontend::{ + ast, + dir::{DirKeyKind, EuclidDirFilter}, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::enums::{self, RoutableConnectors}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum ConnectorSelection { + Priority(Vec), + VolumeSplit(Vec), +} + +impl ConnectorSelection { + pub fn get_connector_list(&self) -> Vec { + match self { + Self::Priority(list) => list.clone(), + Self::VolumeSplit(splits) => { + splits.iter().map(|split| split.connector.clone()).collect() + } + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingConfigRequest { + pub name: Option, + pub description: Option, + pub algorithm: Option, + pub profile_id: Option, +} + +#[cfg(feature = "business_profile_routing")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct RoutingRetrieveQuery { + pub limit: Option, + pub offset: Option, + + pub profile_id: Option, +} + +#[cfg(feature = "business_profile_routing")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct RoutingRetrieveLinkQuery { + pub profile_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingRetrieveResponse { + pub algorithm: Option, +} + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum LinkedRoutingConfigRetrieveResponse { + MerchantAccountBased(RoutingRetrieveResponse), + ProfileBased(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MerchantRoutingAlgorithm { + pub id: String, + #[cfg(feature = "business_profile_routing")] + pub profile_id: String, + pub name: String, + pub description: String, + pub algorithm: RoutingAlgorithm, + pub created_at: i64, + pub modified_at: i64, +} + +impl EuclidDirFilter for ConnectorSelection { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardBin, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::PayLaterType, + DirKeyKind::WalletType, + DirKeyKind::UpiType, + DirKeyKind::BankRedirectType, + DirKeyKind::BankDebitType, + DirKeyKind::CryptoType, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::AuthenticationType, + DirKeyKind::MandateAcceptanceType, + DirKeyKind::MandateType, + DirKeyKind::PaymentType, + DirKeyKind::SetupFutureUsage, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + DirKeyKind::BusinessLabel, + DirKeyKind::MetaData, + DirKeyKind::RewardType, + DirKeyKind::VoucherType, + DirKeyKind::CardRedirectType, + DirKeyKind::BankTransferType, + ]; +} + +impl EuclidAnalysable for ConnectorSelection { + fn get_dir_value_for_analysis( + &self, + rule_name: String, + ) -> Vec<(euclid::frontend::dir::DirValue, euclid::types::Metadata)> { + self.get_connector_list() + .into_iter() + .map(|connector_choice| { + let connector_name = connector_choice.connector.to_string(); + #[cfg(not(feature = "connector_choice_mca_id"))] + let sub_label = connector_choice.sub_label.clone(); + #[cfg(feature = "connector_choice_mca_id")] + let mca_id = connector_choice.merchant_connector_id.clone(); + + ( + euclid::frontend::dir::DirValue::Connector(Box::new(connector_choice.into())), + std::collections::HashMap::from_iter([( + "CONNECTOR_SELECTION".to_string(), + #[cfg(feature = "connector_choice_mca_id")] + serde_json::json!({ + "rule_name": rule_name, + "connector_name": connector_name, + "mca_id": mca_id, + }), + #[cfg(not(feature = "connector_choice_mca_id"))] + serde_json ::json!({ + "rule_name": rule_name, + "connector_name": connector_name, + "sub_label": sub_label, + }), + )]), + ) + }) + .collect() + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConnectorVolumeSplit { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[cfg(feature = "connector_choice_bcompat")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum RoutableChoiceKind { + OnlyConnector, + FullStruct, +} + +#[cfg(feature = "connector_choice_bcompat")] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum RoutableChoiceSerde { + OnlyConnector(Box), + FullStruct { + connector: RoutableConnectors, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: Option, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr( + feature = "connector_choice_bcompat", + serde(from = "RoutableChoiceSerde"), + serde(into = "RoutableChoiceSerde") +)] +#[cfg_attr(not(feature = "connector_choice_bcompat"), derive(PartialEq, Eq))] +pub struct RoutableConnectorChoice { + #[cfg(feature = "connector_choice_bcompat")] + pub choice_kind: RoutableChoiceKind, + pub connector: RoutableConnectors, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +impl ToString for RoutableConnectorChoice { + fn to_string(&self) -> String { + #[cfg(feature = "connector_choice_mca_id")] + let base = self.connector.to_string(); + + #[cfg(not(feature = "connector_choice_mca_id"))] + let base = { + let mut sub_base = self.connector.to_string(); + if let Some(ref label) = self.sub_label { + sub_base.push('_'); + sub_base.push_str(label); + } + + sub_base + }; + + base + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl PartialEq for RoutableConnectorChoice { + fn eq(&self, other: &Self) -> bool { + #[cfg(not(feature = "connector_choice_mca_id"))] + { + self.connector.eq(&other.connector) && self.sub_label.eq(&other.sub_label) + } + + #[cfg(feature = "connector_choice_mca_id")] + { + self.connector.eq(&other.connector) + && self.merchant_connector_id.eq(&other.merchant_connector_id) + } + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl Eq for RoutableConnectorChoice {} + +#[cfg(feature = "connector_choice_bcompat")] +impl From for RoutableConnectorChoice { + fn from(value: RoutableChoiceSerde) -> Self { + match value { + RoutableChoiceSerde::OnlyConnector(connector) => Self { + choice_kind: RoutableChoiceKind::OnlyConnector, + connector: *connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + + RoutableChoiceSerde::FullStruct { + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label, + } => Self { + choice_kind: RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label, + }, + } + } +} + +#[cfg(feature = "connector_choice_bcompat")] +impl From for RoutableChoiceSerde { + fn from(value: RoutableConnectorChoice) -> Self { + match value.choice_kind { + RoutableChoiceKind::OnlyConnector => Self::OnlyConnector(Box::new(value.connector)), + RoutableChoiceKind::FullStruct => Self::FullStruct { + connector: value.connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: value.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: value.sub_label, + }, + } + } +} + +impl From for ast::ConnectorChoice { + fn from(value: RoutableConnectorChoice) -> Self { + Self { + connector: match value.connector { + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector1 => euclid_enums::Connector::DummyConnector1, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector2 => euclid_enums::Connector::DummyConnector2, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector3 => euclid_enums::Connector::DummyConnector3, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector4 => euclid_enums::Connector::DummyConnector4, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector5 => euclid_enums::Connector::DummyConnector5, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector6 => euclid_enums::Connector::DummyConnector6, + #[cfg(feature = "dummy_connector")] + RoutableConnectors::DummyConnector7 => euclid_enums::Connector::DummyConnector7, + RoutableConnectors::Aci => euclid_enums::Connector::Aci, + RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, + RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, + RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, + RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, + RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, + RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, + RoutableConnectors::Boku => euclid_enums::Connector::Boku, + RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, + RoutableConnectors::Cashtocode => euclid_enums::Connector::Cashtocode, + RoutableConnectors::Checkout => euclid_enums::Connector::Checkout, + RoutableConnectors::Coinbase => euclid_enums::Connector::Coinbase, + RoutableConnectors::Cryptopay => euclid_enums::Connector::Cryptopay, + RoutableConnectors::Cybersource => euclid_enums::Connector::Cybersource, + RoutableConnectors::Dlocal => euclid_enums::Connector::Dlocal, + RoutableConnectors::Fiserv => euclid_enums::Connector::Fiserv, + RoutableConnectors::Forte => euclid_enums::Connector::Forte, + RoutableConnectors::Globalpay => euclid_enums::Connector::Globalpay, + RoutableConnectors::Globepay => euclid_enums::Connector::Globepay, + RoutableConnectors::Gocardless => euclid_enums::Connector::Gocardless, + RoutableConnectors::Helcim => euclid_enums::Connector::Helcim, + RoutableConnectors::Iatapay => euclid_enums::Connector::Iatapay, + RoutableConnectors::Klarna => euclid_enums::Connector::Klarna, + RoutableConnectors::Mollie => euclid_enums::Connector::Mollie, + RoutableConnectors::Multisafepay => euclid_enums::Connector::Multisafepay, + RoutableConnectors::Nexinets => euclid_enums::Connector::Nexinets, + RoutableConnectors::Nmi => euclid_enums::Connector::Nmi, + RoutableConnectors::Noon => euclid_enums::Connector::Noon, + RoutableConnectors::Nuvei => euclid_enums::Connector::Nuvei, + RoutableConnectors::Opennode => euclid_enums::Connector::Opennode, + RoutableConnectors::Payme => euclid_enums::Connector::Payme, + RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, + RoutableConnectors::Payu => euclid_enums::Connector::Payu, + RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, + RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, + RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, + RoutableConnectors::Square => euclid_enums::Connector::Square, + RoutableConnectors::Stax => euclid_enums::Connector::Stax, + RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, + RoutableConnectors::Trustpay => euclid_enums::Connector::Trustpay, + RoutableConnectors::Tsys => euclid_enums::Connector::Tsys, + RoutableConnectors::Volt => euclid_enums::Connector::Volt, + RoutableConnectors::Wise => euclid_enums::Connector::Wise, + RoutableConnectors::Worldline => euclid_enums::Connector::Worldline, + RoutableConnectors::Worldpay => euclid_enums::Connector::Worldpay, + RoutableConnectors::Zen => euclid_enums::Connector::Zen, + }, + + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: value.sub_label, + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DetailedConnectorChoice { + pub connector: RoutableConnectors, + pub business_label: Option, + pub business_country: Option, + pub business_sub_label: Option, +} + +impl DetailedConnectorChoice { + pub fn get_connector_label(&self) -> Option { + self.business_country + .as_ref() + .zip(self.business_label.as_ref()) + .map(|(business_country, business_label)| { + let mut base_label = format!( + "{}_{:?}_{}", + self.connector, business_country, business_label + ); + + if let Some(ref sub_label) = self.business_sub_label { + base_label.push('_'); + base_label.push_str(sub_label); + } + + base_label + }) + } +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutingAlgorithmKind { + Single, + Priority, + VolumeSplit, + Advanced, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde( + tag = "type", + content = "data", + rename_all = "snake_case", + try_from = "RoutingAlgorithmSerde" +)] +pub enum RoutingAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(euclid::frontend::ast::Program), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum RoutingAlgorithmSerde { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(euclid::frontend::ast::Program), +} + +impl TryFrom for RoutingAlgorithm { + type Error = error_stack::Report; + + fn try_from(value: RoutingAlgorithmSerde) -> Result { + match &value { + RoutingAlgorithmSerde::Priority(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Priority Algorithm", + )) + .into_report()? + } + RoutingAlgorithmSerde::VolumeSplit(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Volume split Algorithm", + )) + .into_report()? + } + _ => {} + }; + Ok(match value { + RoutingAlgorithmSerde::Single(i) => Self::Single(i), + RoutingAlgorithmSerde::Priority(i) => Self::Priority(i), + RoutingAlgorithmSerde::VolumeSplit(i) => Self::VolumeSplit(i), + RoutingAlgorithmSerde::Advanced(i) => Self::Advanced(i), + }) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde( + tag = "type", + content = "data", + rename_all = "snake_case", + try_from = "StraightThroughAlgorithmSerde", + into = "StraightThroughAlgorithmSerde" +)] +pub enum StraightThroughAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum StraightThroughAlgorithmInner { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum StraightThroughAlgorithmSerde { + Direct(StraightThroughAlgorithmInner), + Nested { + algorithm: StraightThroughAlgorithmInner, + }, +} + +impl TryFrom for StraightThroughAlgorithm { + type Error = error_stack::Report; + + fn try_from(value: StraightThroughAlgorithmSerde) -> Result { + let inner = match value { + StraightThroughAlgorithmSerde::Direct(algorithm) => algorithm, + StraightThroughAlgorithmSerde::Nested { algorithm } => algorithm, + }; + + match &inner { + StraightThroughAlgorithmInner::Priority(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Priority Algorithm", + )) + .into_report()? + } + StraightThroughAlgorithmInner::VolumeSplit(i) if i.is_empty() => { + Err(ParsingError::StructParseFailure( + "Connectors list can't be empty for Volume split Algorithm", + )) + .into_report()? + } + _ => {} + }; + + Ok(match inner { + StraightThroughAlgorithmInner::Single(single) => Self::Single(single), + StraightThroughAlgorithmInner::Priority(plist) => Self::Priority(plist), + StraightThroughAlgorithmInner::VolumeSplit(vsplit) => Self::VolumeSplit(vsplit), + }) + } +} + +impl From for StraightThroughAlgorithmSerde { + fn from(value: StraightThroughAlgorithm) -> Self { + let inner = match value { + StraightThroughAlgorithm::Single(conn) => StraightThroughAlgorithmInner::Single(conn), + StraightThroughAlgorithm::Priority(plist) => { + StraightThroughAlgorithmInner::Priority(plist) + } + StraightThroughAlgorithm::VolumeSplit(vsplit) => { + StraightThroughAlgorithmInner::VolumeSplit(vsplit) + } + }; + + Self::Nested { algorithm: inner } + } +} + +impl From for RoutingAlgorithm { + fn from(value: StraightThroughAlgorithm) -> Self { + match value { + StraightThroughAlgorithm::Single(conn) => Self::Single(conn), + StraightThroughAlgorithm::Priority(conns) => Self::Priority(conns), + StraightThroughAlgorithm::VolumeSplit(splits) => Self::VolumeSplit(splits), + } + } +} + +impl RoutingAlgorithm { + pub fn get_kind(&self) -> RoutingAlgorithmKind { + match self { + Self::Single(_) => RoutingAlgorithmKind::Single, + Self::Priority(_) => RoutingAlgorithmKind::Priority, + Self::VolumeSplit(_) => RoutingAlgorithmKind::VolumeSplit, + Self::Advanced(_) => RoutingAlgorithmKind::Advanced, + } + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingAlgorithmRef { + pub algorithm_id: Option, + pub timestamp: i64, + pub config_algo_id: Option, + pub surcharge_config_algo_id: Option, +} + +impl RoutingAlgorithmRef { + pub fn update_algorithm_id(&mut self, new_id: String) { + self.algorithm_id = Some(new_id); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } + + pub fn update_conditional_config_id(&mut self, ids: String) { + self.config_algo_id = Some(ids); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } + + pub fn update_surcharge_config_id(&mut self, ids: String) { + self.surcharge_config_algo_id = Some(ids); + self.timestamp = common_utils::date_time::now_unix_timestamp(); + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct RoutingDictionaryRecord { + pub id: String, + #[cfg(feature = "business_profile_routing")] + pub profile_id: String, + pub name: String, + pub kind: RoutingAlgorithmKind, + pub description: String, + pub created_at: i64, + pub modified_at: i64, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RoutingDictionary { + pub merchant_id: String, + pub active_id: Option, + pub records: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(untagged)] +pub enum RoutingKind { + Config(RoutingDictionary), + RoutingAlgorithm(Vec), +} + +#[repr(transparent)] +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(transparent)] +pub struct RoutingAlgorithmId(pub String); diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 10b4fb509e88..db37d27ab0f1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -12,14 +12,13 @@ dummy_connector = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = [ "derive" ] } +serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates -common_utils = { version = "0.1.0", path = "../common_utils" } router_derive = { version = "0.1.0", path = "../router_derive" } [dev-dependencies] diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index e319cf86ccd0..62bd747da1b0 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -28,6 +28,7 @@ rand = "0.8.5" regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = { version = "0.16.20", features = ["std"] } +rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" serde_urlencoded = "0.7.1" @@ -41,6 +42,7 @@ phonenumber = "0.3.3" # First party crates masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true } +common_enums = { version = "0.1.0", path = "../common_enums" } [target.'cfg(not(target_os = "windows"))'.dependencies] signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 2f517295ae48..7bc248bf8d1b 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -29,3 +29,15 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; + +/// SDK Default Theme const +pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; + +/// Default Payment Link Background color +pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; + +/// Default product Img Link +pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; + +/// Default Merchant Logo Link +pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs new file mode 100644 index 000000000000..8c52f6c36d63 --- /dev/null +++ b/crates/common_utils/src/events.rs @@ -0,0 +1,92 @@ +use common_enums::{PaymentMethod, PaymentMethodType}; +use serde::Serialize; + +pub trait ApiEventMetric { + fn get_api_event_type(&self) -> Option { + None + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(tag = "flow_type")] +pub enum ApiEventsType { + Payout, + Payment { + payment_id: String, + }, + Refund { + payment_id: Option, + refund_id: String, + }, + PaymentMethod { + payment_method_id: String, + payment_method: Option, + payment_method_type: Option, + }, + Customer { + customer_id: String, + }, + User { + //specified merchant_id will overridden on global defined + merchant_id: String, + user_id: String, + }, + PaymentMethodList { + payment_id: Option, + }, + Webhooks { + connector: String, + payment_id: Option, + }, + Routing, + ResourceListAPI, + PaymentRedirectionResponse, + Gsm, + // TODO: This has to be removed once the corresponding apiEventTypes are created + Miscellaneous, +} + +impl ApiEventMetric for serde_json::Value {} +impl ApiEventMetric for () {} + +impl ApiEventMetric for Result { + fn get_api_event_type(&self) -> Option { + match self { + Ok(q) => q.get_api_event_type(), + Err(_) => None, + } + } +} + +// TODO: Ideally all these types should be replaced by newtype responses +impl ApiEventMetric for Vec { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} + +#[macro_export] +macro_rules! impl_misc_api_event_type { + ($($type:ty),+) => { + $( + impl ApiEventMetric for $type { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } + } + )+ + }; +} + +impl_misc_api_event_type!( + String, + (&String, &String), + (Option, Option, String), + bool +); + +impl ApiEventMetric for &T { + fn get_api_event_type(&self) -> Option { + T::get_api_event_type(self) + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index ca6bba480063..62428dccfb6a 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -6,6 +6,8 @@ pub mod consts; pub mod crypto; pub mod custom_serde; pub mod errors; +#[allow(missing_docs)] // Todo: add docs +pub mod events; pub mod ext_traits; pub mod fp_utils; pub mod pii; @@ -13,6 +15,8 @@ pub mod pii; pub mod request; #[cfg(feature = "signals")] pub mod signals; +#[allow(missing_docs)] // Todo: add docs +pub mod static_cache; pub mod types; pub mod validation; diff --git a/crates/common_utils/src/static_cache.rs b/crates/common_utils/src/static_cache.rs new file mode 100644 index 000000000000..ca608fa9a3b5 --- /dev/null +++ b/crates/common_utils/src/static_cache.rs @@ -0,0 +1,91 @@ +use std::sync::{Arc, RwLock}; + +use once_cell::sync::Lazy; +use rustc_hash::FxHashMap; + +#[derive(Debug)] +pub struct CacheEntry { + data: Arc, + timestamp: i64, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum CacheError { + #[error("Could not acquire the lock for cache entry")] + CouldNotAcquireLock, + #[error("Entry not found in cache")] + EntryNotFound, +} + +#[derive(Debug)] +pub struct StaticCache { + data: Lazy>>>, +} + +impl StaticCache +where + T: Send, +{ + pub const fn new() -> Self { + Self { + data: Lazy::new(|| RwLock::new(FxHashMap::default())), + } + } + + pub fn present(&self, key: &String) -> Result { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + Ok(the_map.get(key).is_some()) + } + + pub fn expired(&self, key: &String, timestamp: i64) -> Result { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + Ok(match the_map.get(key) { + None => false, + Some(entry) => timestamp > entry.timestamp, + }) + } + + pub fn retrieve(&self, key: &String) -> Result, CacheError> { + let the_map = self + .data + .read() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + let cache_entry = the_map.get(key).ok_or(CacheError::EntryNotFound)?; + + Ok(Arc::clone(&cache_entry.data)) + } + + pub fn save(&self, key: String, data: T, timestamp: i64) -> Result<(), CacheError> { + let mut the_map = self + .data + .write() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + let entry = CacheEntry { + data: Arc::new(data), + timestamp, + }; + + the_map.insert(key, entry); + Ok(()) + } + + pub fn clear(&self) -> Result<(), CacheError> { + let mut the_map = self + .data + .write() + .map_err(|_| CacheError::CouldNotAcquireLock)?; + + the_map.clear(); + Ok(()) + } +} diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 734de8fe4a55..cdd41ea9db2d 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -286,6 +286,8 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, amount_capturable: Option, updated_by: String, + authentication_data: Option, + encoded_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, diff --git a/crates/diesel_models/src/address.rs b/crates/diesel_models/src/address.rs index e67f37c90465..03dedfd60d8f 100644 --- a/crates/diesel_models/src/address.rs +++ b/crates/diesel_models/src/address.rs @@ -19,7 +19,7 @@ pub struct AddressNew { pub last_name: Option, pub phone_number: Option, pub country_code: Option, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub created_at: PrimitiveDateTime, @@ -45,7 +45,7 @@ pub struct Address { pub country_code: Option, pub created_at: PrimitiveDateTime, pub modified_at: PrimitiveDateTime, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub updated_by: String, diff --git a/crates/diesel_models/src/connector_response.rs b/crates/diesel_models/src/connector_response.rs deleted file mode 100644 index 863ce28ee0ae..000000000000 --- a/crates/diesel_models/src/connector_response.rs +++ /dev/null @@ -1,122 +0,0 @@ -use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; -use serde::{Deserialize, Serialize}; -use time::PrimitiveDateTime; - -use crate::schema::connector_response; - -#[derive(Clone, Debug, Deserialize, Serialize, Insertable, router_derive::DebugAsDisplay)] -#[diesel(table_name = connector_response)] -#[serde(deny_unknown_fields)] -pub struct ConnectorResponseNew { - pub payment_id: String, - pub merchant_id: String, - pub attempt_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub modified_at: PrimitiveDateTime, - pub connector_name: Option, - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub encoded_data: Option, - pub updated_by: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Identifiable, Queryable)] -#[diesel(table_name = connector_response)] -pub struct ConnectorResponse { - pub id: i32, - pub payment_id: String, - pub merchant_id: String, - pub attempt_id: String, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub modified_at: PrimitiveDateTime, - pub connector_name: Option, - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub encoded_data: Option, - pub updated_by: String, -} - -#[derive(Clone, Default, Debug, Deserialize, AsChangeset, Serialize)] -#[diesel(table_name = connector_response)] -pub struct ConnectorResponseUpdateInternal { - pub connector_transaction_id: Option, - pub authentication_data: Option, - pub modified_at: Option, - pub encoded_data: Option, - pub connector_name: Option, - pub updated_by: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ConnectorResponseUpdate { - ResponseUpdate { - connector_transaction_id: Option, - authentication_data: Option, - encoded_data: Option, - connector_name: Option, - updated_by: String, - }, - ErrorUpdate { - connector_name: Option, - updated_by: String, - }, -} - -impl ConnectorResponseUpdate { - pub fn apply_changeset(self, source: ConnectorResponse) -> ConnectorResponse { - let connector_response_update: ConnectorResponseUpdateInternal = self.into(); - ConnectorResponse { - modified_at: connector_response_update - .modified_at - .unwrap_or_else(common_utils::date_time::now), - connector_name: connector_response_update - .connector_name - .or(source.connector_name), - connector_transaction_id: source - .connector_transaction_id - .or(connector_response_update.connector_transaction_id), - authentication_data: connector_response_update - .authentication_data - .or(source.authentication_data), - encoded_data: connector_response_update - .encoded_data - .or(source.encoded_data), - updated_by: connector_response_update.updated_by, - ..source - } - } -} - -impl From for ConnectorResponseUpdateInternal { - fn from(connector_response_update: ConnectorResponseUpdate) -> Self { - match connector_response_update { - ConnectorResponseUpdate::ResponseUpdate { - connector_transaction_id, - authentication_data, - encoded_data, - connector_name, - updated_by, - } => Self { - connector_transaction_id, - authentication_data, - encoded_data, - modified_at: Some(common_utils::date_time::now()), - connector_name, - updated_by, - }, - ConnectorResponseUpdate::ErrorUpdate { - connector_name, - updated_by, - } => Self { - connector_name, - modified_at: Some(common_utils::date_time::now()), - updated_by, - ..Self::default() - }, - } - } -} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index b73eeefbb10b..ec021f0f51a5 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -14,6 +14,7 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRoutingAlgorithmKind as RoutingAlgorithmKind, }; } pub use common_enums::*; @@ -21,6 +22,27 @@ use common_utils::pii; use diesel::serialize::{Output, ToSql}; use time::PrimitiveDateTime; +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutingAlgorithmKind { + Single, + Priority, + VolumeSplit, + Advanced, +} + #[derive( Clone, Copy, @@ -379,3 +401,25 @@ pub enum FraudCheckLastStep { TransactionOrRecordRefund, Fulfillment, } + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum UserStatus { + Active, + #[default] + InvitationSent, +} diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index 96bd6e497c33..77b9c647e43b 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -14,3 +14,9 @@ pub struct EphemeralKey { pub expires: i64, pub secret: String, } + +impl common_utils::events::ApiEventMetric for EphemeralKey { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs new file mode 100644 index 000000000000..2e824758aa5a --- /dev/null +++ b/crates/diesel_models/src/gsm.rs @@ -0,0 +1,106 @@ +//! Gateway status mapping + +use common_utils::{ + custom_serde, + events::{ApiEventMetric, ApiEventsType}, +}; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::schema::gateway_status_map; + +#[derive( + Clone, + Debug, + Eq, + PartialEq, + router_derive::DebugAsDisplay, + Identifiable, + Queryable, + serde::Serialize, +)] +#[diesel(table_name = gateway_status_map, primary_key(connector, flow, sub_flow, code, message))] +pub struct GatewayStatusMap { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + #[serde(with = "custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "custom_serde::iso8601")] + pub last_modified: PrimitiveDateTime, + pub step_up_possible: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Insertable)] +#[diesel(table_name = gateway_status_map)] +pub struct GatewayStatusMappingNew { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: String, + pub step_up_possible: bool, +} + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + AsChangeset, + router_derive::DebugAsDisplay, + Default, + serde::Deserialize, +)] +#[diesel(table_name = gateway_status_map)] +pub struct GatewayStatusMapperUpdateInternal { + pub connector: Option, + pub flow: Option, + pub sub_flow: Option, + pub code: Option, + pub message: Option, + pub status: Option, + pub router_error: Option>, + pub decision: Option, + pub step_up_possible: Option, +} + +#[derive(Debug)] +pub struct GatewayStatusMappingUpdate { + pub status: Option, + pub router_error: Option>, + pub decision: Option, + pub step_up_possible: Option, +} + +impl From for GatewayStatusMapperUpdateInternal { + fn from(value: GatewayStatusMappingUpdate) -> Self { + let GatewayStatusMappingUpdate { + decision, + status, + router_error, + step_up_possible, + } = value; + Self { + status, + router_error, + decision, + step_up_possible, + ..Default::default() + } + } +} + +impl ApiEventMetric for GatewayStatusMap { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index f1145a4b6e1f..f56ef8304186 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; use crate::{ address::{Address, AddressNew, AddressUpdateInternal}, - connector_response::{ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate}, errors, payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, @@ -27,13 +26,21 @@ pub struct TypedSql { } impl TypedSql { - pub fn to_field_value_pairs(&self) -> crate::StorageResult> { - Ok(vec![( - "typed_sql", - serde_json::to_string(self) - .into_report() - .change_context(errors::DatabaseError::QueryGenerationFailed)?, - )]) + pub fn to_field_value_pairs( + &self, + request_id: String, + global_id: String, + ) -> crate::StorageResult> { + Ok(vec![ + ( + "typed_sql", + serde_json::to_string(self) + .into_report() + .change_context(errors::DatabaseError::QueryGenerationFailed)?, + ), + ("global_id", global_id), + ("request_id", request_id), + ]) } } @@ -43,7 +50,6 @@ pub enum Insertable { PaymentIntent(PaymentIntentNew), PaymentAttempt(PaymentAttemptNew), Refund(RefundNew), - ConnectorResponse(ConnectorResponseNew), Address(Box), ReverseLookUp(ReverseLookupNew), } @@ -54,16 +60,9 @@ pub enum Updateable { PaymentIntentUpdate(PaymentIntentUpdateMems), PaymentAttemptUpdate(PaymentAttemptUpdateMems), RefundUpdate(RefundUpdateMems), - ConnectorResponseUpdate(ConnectorResponseUpdateMems), AddressUpdate(Box), } -#[derive(Debug, Serialize, Deserialize)] -pub struct ConnectorResponseUpdateMems { - pub orig: ConnectorResponse, - pub update_data: ConnectorResponseUpdate, -} - #[derive(Debug, Serialize, Deserialize)] pub struct AddressUpdateMems { pub orig: Address, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 528446678015..781099662a50 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -4,7 +4,7 @@ pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; + pub mod customers; pub mod dispute; pub mod encryption; @@ -15,6 +15,7 @@ pub mod events; pub mod file; #[allow(unused)] pub mod fraud_check; +pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; pub mod locker_mock_up; @@ -34,18 +35,21 @@ pub mod process_tracker; pub mod query; pub mod refund; pub mod reverse_lookup; +pub mod routing_algorithm; #[allow(unused_qualifications)] pub mod schema; +pub mod user; +pub mod user_role; use diesel_impl::{DieselArray, OptionalDieselArray}; pub type StorageResult = error_stack::Result; pub type PgPooledConn = async_bb8_diesel::Connection; pub use self::{ - address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*, - dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, payment_attempt::*, payment_intent::*, - payment_method::*, process_tracker::*, refund::*, reverse_lookup::*, + address::*, api_keys::*, cards_info::*, configs::*, customers::*, dispute::*, ephemeral_key::*, + events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, + merchant_connector_account::*, payment_attempt::*, payment_intent::*, payment_method::*, + process_tracker::*, refund::*, reverse_lookup::*, }; /// The types and implementations provided by this module are required for the schema generated by diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 058086106111..ce388fea10eb 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -203,6 +203,8 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, amount_capturable: Option, updated_by: String, + authentication_data: Option, + encoded_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -478,6 +480,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => Self { status: Some(status), connector, @@ -494,6 +498,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 4b182a8155a5..50cc5e89cee9 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -20,8 +20,8 @@ pub struct PaymentLink { pub last_modified_at: PrimitiveDateTime, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, + pub custom_merchant_name: Option, } - #[derive( Clone, Debug, @@ -47,4 +47,5 @@ pub struct PaymentLinkNew { pub last_modified_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, + pub custom_merchant_name: Option, } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 6b705e29873e..cf5a993c2686 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -4,13 +4,14 @@ pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; + pub mod customers; pub mod dispute; pub mod events; pub mod file; pub mod fraud_check; pub mod generics; +pub mod gsm; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -26,3 +27,6 @@ pub mod payouts; pub mod process_tracker; pub mod refund; pub mod reverse_lookup; +pub mod routing_algorithm; +pub mod user; +pub mod user_role; diff --git a/crates/diesel_models/src/query/gsm.rs b/crates/diesel_models/src/query/gsm.rs new file mode 100644 index 000000000000..bd44ce4dc378 --- /dev/null +++ b/crates/diesel_models/src/query/gsm.rs @@ -0,0 +1,100 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use error_stack::report; + +use crate::{ + errors, gsm::*, query::generics, schema::gateway_status_map::dsl, PgPooledConn, StorageResult, +}; + +impl GatewayStatusMappingNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl GatewayStatusMap { + pub async fn find( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + ) + .await + } + + pub async fn retrieve_decision( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + Self::find(conn, connector, flow, sub_flow, code, message) + .await + .map(|item| item.decision) + } + + pub async fn update( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + gsm: GatewayStatusMappingUpdate, + ) -> StorageResult { + generics::generic_update_with_results::< + ::Table, + GatewayStatusMapperUpdateInternal, + _, + _, + >( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + gsm.into(), + ) + .await? + .first() + .cloned() + .ok_or_else(|| { + report!(errors::DatabaseError::NotFound) + .attach_printable("Error while updating gsm entry") + }) + } + + pub async fn delete( + conn: &PgPooledConn, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::connector + .eq(connector) + .and(dsl::flow.eq(flow)) + .and(dsl::sub_flow.eq(sub_flow)) + .and(dsl::code.eq(code)) + .and(dsl::message.eq(message)), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/routing_algorithm.rs b/crates/diesel_models/src/query/routing_algorithm.rs new file mode 100644 index 000000000000..533ac7194c41 --- /dev/null +++ b/crates/diesel_models/src/query/routing_algorithm.rs @@ -0,0 +1,200 @@ +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, QueryDsl}; +use error_stack::{IntoReport, ResultExt}; +use router_env::tracing::{self, instrument}; +use time::PrimitiveDateTime; + +use crate::{ + enums, + errors::DatabaseError, + query::generics, + routing_algorithm::{RoutingAlgorithm, RoutingAlgorithmMetadata, RoutingProfileMetadata}, + schema::routing_algorithm::dsl, + PgPooledConn, StorageResult, +}; + +impl RoutingAlgorithm { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } + + #[instrument(skip(conn))] + pub async fn find_by_algorithm_id_merchant_id( + conn: &PgPooledConn, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn find_by_algorithm_id_profile_id( + conn: &PgPooledConn, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::profile_id.eq(profile_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn find_metadata_by_algorithm_id_profile_id( + conn: &PgPooledConn, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + Self::table() + .select(( + dsl::profile_id, + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter( + dsl::algorithm_id + .eq(algorithm_id.to_owned()) + .and(dsl::profile_id.eq(profile_id.to_owned())), + ) + .limit(1) + .load_async::<( + String, + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .next() + .ok_or(DatabaseError::NotFound) + .into_report() + .map( + |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingProfileMetadata { + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + } + + #[instrument(skip(conn))] + pub async fn list_metadata_by_profile_id( + conn: &PgPooledConn, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + Ok(Self::table() + .select(( + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter(dsl::profile_id.eq(profile_id.to_owned())) + .limit(limit) + .offset(offset) + .load_async::<( + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .map( + |(algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingAlgorithmMetadata { + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + .collect()) + } + + #[instrument(skip(conn))] + pub async fn list_metadata_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + Ok(Self::table() + .select(( + dsl::profile_id, + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + )) + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .limit(limit) + .offset(offset) + .order(dsl::modified_at.desc()) + .load_async::<( + String, + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .map( + |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + RoutingProfileMetadata { + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + } + }, + ) + .collect()) + } +} diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs new file mode 100644 index 000000000000..5761d8af814d --- /dev/null +++ b/crates/diesel_models/src/query/user.rs @@ -0,0 +1,62 @@ +use diesel::{associations::HasTable, ExpressionMethods}; +use error_stack::report; +use router_env::tracing::{self, instrument}; + +use crate::{ + errors::{self}, + query::generics, + schema::users::dsl, + user::*, + PgPooledConn, StorageResult, +}; + +impl UserNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl User { + pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::email.eq(user_email.to_owned()), + ) + .await + } + + pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + ) + .await + } + + pub async fn update_by_user_id( + conn: &PgPooledConn, + user_id: &str, + user: UserUpdate, + ) -> StorageResult { + generics::generic_update_with_results::<::Table, _, _, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + UserUpdateInternal::from(user), + ) + .await? + .first() + .cloned() + .ok_or_else(|| { + report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") + }) + } + + pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id.eq(user_id.to_owned()), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs new file mode 100644 index 000000000000..d2f9564a5309 --- /dev/null +++ b/crates/diesel_models/src/query/user_role.rs @@ -0,0 +1,58 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{query::generics, schema::user_roles::dsl, user_role::*, PgPooledConn, StorageResult}; + +impl UserRoleNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl UserRole { + pub async fn find_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id.eq(user_id), + ) + .await + } + + pub async fn update_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + update: UserRoleUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + UserRoleUpdateInternal::from(update), + ) + .await + } + + pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { + generics::generic_delete::<::Table, _>(conn, dsl::user_id.eq(user_id)) + .await + } + + pub async fn list_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::user_id.eq(user_id), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 73ff34030f81..62aec3fb27d8 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -227,3 +227,12 @@ pub struct RefundCoreWorkflow { pub merchant_id: String, pub payment_id: String, } + +impl common_utils::events::ApiEventMetric for Refund { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Refund { + payment_id: Some(self.payment_id.clone()), + refund_id: self.refund_id.clone(), + }) + } +} diff --git a/crates/diesel_models/src/routing_algorithm.rs b/crates/diesel_models/src/routing_algorithm.rs new file mode 100644 index 000000000000..09f9baf7edb9 --- /dev/null +++ b/crates/diesel_models/src/routing_algorithm.rs @@ -0,0 +1,37 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::{enums, schema::routing_algorithm}; + +#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = routing_algorithm, primary_key(algorithm_id))] +pub struct RoutingAlgorithm { + pub algorithm_id: String, + pub profile_id: String, + pub merchant_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub algorithm_data: serde_json::Value, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} + +pub struct RoutingAlgorithmMetadata { + pub algorithm_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} + +pub struct RoutingProfileMetadata { + pub profile_id: String, + pub algorithm_id: String, + pub name: String, + pub description: Option, + pub kind: enums::RoutingAlgorithmKind, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 02abfb842b8d..72d5217038c1 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -24,7 +24,7 @@ diesel::table! { created_at -> Timestamp, modified_at -> Timestamp, #[max_length = 64] - customer_id -> Varchar, + customer_id -> Nullable, #[max_length = 64] merchant_id -> Varchar, #[max_length = 64] @@ -157,31 +157,6 @@ diesel::table! { } } -diesel::table! { - use diesel::sql_types::*; - use crate::enums::diesel_exports::*; - - connector_response (id) { - id -> Int4, - #[max_length = 64] - payment_id -> Varchar, - #[max_length = 64] - merchant_id -> Varchar, - #[max_length = 64] - attempt_id -> Varchar, - created_at -> Timestamp, - modified_at -> Timestamp, - #[max_length = 64] - connector_name -> Nullable, - #[max_length = 128] - connector_transaction_id -> Nullable, - authentication_data -> Nullable, - encoded_data -> Nullable, - #[max_length = 32] - updated_by -> Varchar, - } -} - diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -332,6 +307,33 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + gateway_status_map (connector, flow, sub_flow, code, message) { + #[max_length = 64] + connector -> Varchar, + #[max_length = 64] + flow -> Varchar, + #[max_length = 64] + sub_flow -> Varchar, + #[max_length = 255] + code -> Varchar, + #[max_length = 1024] + message -> Varchar, + #[max_length = 64] + status -> Varchar, + #[max_length = 64] + router_error -> Nullable, + #[max_length = 64] + decision -> Varchar, + created_at -> Timestamp, + last_modified -> Timestamp, + step_up_possible -> Bool, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -664,6 +666,8 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, fulfilment_time -> Nullable, + #[max_length = 64] + custom_merchant_name -> Nullable, } } @@ -874,6 +878,73 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + routing_algorithm (algorithm_id) { + #[max_length = 64] + algorithm_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + name -> Varchar, + #[max_length = 256] + description -> Nullable, + kind -> RoutingAlgorithmKind, + algorithm_data -> Jsonb, + created_at -> Timestamp, + modified_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + user_roles (id) { + id -> Int4, + #[max_length = 64] + user_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + role_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + status -> Varchar, + #[max_length = 64] + created_by -> Varchar, + #[max_length = 64] + last_modified_by -> Varchar, + created_at -> Timestamp, + last_modified_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + users (id) { + id -> Int4, + #[max_length = 64] + user_id -> Varchar, + #[max_length = 255] + email -> Varchar, + #[max_length = 255] + name -> Varchar, + #[max_length = 255] + password -> Varchar, + is_verified -> Bool, + created_at -> Timestamp, + last_modified_at -> Timestamp, + } +} + diesel::allow_tables_to_appear_in_same_query!( address, api_keys, @@ -881,12 +952,12 @@ diesel::allow_tables_to_appear_in_same_query!( captures, cards_info, configs, - connector_response, customers, dispute, events, file_metadata, fraud_check, + gateway_status_map, locker_mock_up, mandate, merchant_account, @@ -902,4 +973,7 @@ diesel::allow_tables_to_appear_in_same_query!( process_tracker, refund, reverse_lookup, + routing_algorithm, + user_roles, + users, ); diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs new file mode 100644 index 000000000000..6a2e864b291c --- /dev/null +++ b/crates/diesel_models/src/user.rs @@ -0,0 +1,76 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use masking::Secret; +use time::PrimitiveDateTime; + +use crate::schema::users; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = users)] +pub struct User { + pub id: i32, + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub password: Secret, + pub is_verified: bool, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Default, Insertable, router_derive::DebugAsDisplay, +)] +#[diesel(table_name = users)] +pub struct UserNew { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub password: Secret, + pub is_verified: bool, + pub created_at: Option, + pub last_modified_at: Option, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = users)] +pub struct UserUpdateInternal { + name: Option, + password: Option>, + is_verified: Option, + last_modified_at: PrimitiveDateTime, +} + +#[derive(Debug)] +pub enum UserUpdate { + VerifyUser, + AccountUpdate { + name: Option, + password: Option>, + is_verified: Option, + }, +} + +impl From for UserUpdateInternal { + fn from(user_update: UserUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match user_update { + UserUpdate::VerifyUser => Self { + name: None, + password: None, + is_verified: Some(true), + last_modified_at, + }, + UserUpdate::AccountUpdate { + name, + password, + is_verified, + } => Self { + name, + password, + is_verified, + last_modified_at, + }, + } + } +} diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs new file mode 100644 index 000000000000..467584ac59db --- /dev/null +++ b/crates/diesel_models/src/user_role.rs @@ -0,0 +1,79 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::user_roles}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = user_roles)] +pub struct UserRole { + pub id: i32, + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, + pub status: enums::UserStatus, + pub created_by: String, + pub last_modified_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] +#[diesel(table_name = user_roles)] +pub struct UserRoleNew { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, + pub status: enums::UserStatus, + pub created_by: String, + pub last_modified_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = user_roles)] +pub struct UserRoleUpdateInternal { + role_id: Option, + status: Option, + last_modified_by: Option, + last_modified_at: PrimitiveDateTime, +} + +pub enum UserRoleUpdate { + UpdateStatus { + status: enums::UserStatus, + modified_by: String, + }, + UpdateRole { + role_id: String, + modified_by: String, + }, +} + +impl From for UserRoleUpdateInternal { + fn from(value: UserRoleUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match value { + UserRoleUpdate::UpdateRole { + role_id, + modified_by, + } => Self { + role_id: Some(role_id), + last_modified_by: Some(modified_by), + status: None, + last_modified_at, + }, + UserRoleUpdate::UpdateStatus { + status, + modified_by, + } => Self { + status: Some(status), + last_modified_at, + last_modified_by: Some(modified_by), + role_id: None, + }, + } + } +} diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7dcbc2c518cf..7ccfd600d662 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -10,6 +10,7 @@ use std::sync::{atomic, Arc}; use common_utils::signals::get_allowed_signals; use diesel_models::kv; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use tokio::sync::{mpsc, oneshot}; use crate::{connection::pg_connection, services::Store}; @@ -122,6 +123,7 @@ async fn drainer_handler( active_tasks.fetch_add(1, atomic::Ordering::Release); let stream_name = utils::get_drainer_stream_name(store.clone(), stream_index); + let drainer_result = Box::pin(drainer(store.clone(), max_read_count, stream_name.as_str())).await; @@ -130,6 +132,7 @@ async fn drainer_handler( } let flag_stream_name = utils::get_stream_key_flag(store.clone(), stream_index); + //TODO: USE THE RESULT FOR LOGGING let output = utils::make_stream_available(flag_stream_name.as_str(), store.redis_conn.as_ref()).await; @@ -137,6 +140,7 @@ async fn drainer_handler( output } +#[instrument(skip_all, fields(global_id, request_id, session_id))] async fn drainer( store: Arc, max_read_count: u64, @@ -174,9 +178,21 @@ async fn drainer( }], ); + let session_id = common_utils::generate_id_with_default_len("drainer_session"); + // TODO: Handle errors when deserialization fails and when DB error occurs for entry in entries { let typed_sql = entry.1.get("typed_sql").map_or(String::new(), Clone::clone); + let request_id = entry + .1 + .get("request_id") + .map_or(String::new(), Clone::clone); + let global_id = entry.1.get("global_id").map_or(String::new(), Clone::clone); + + tracing::Span::current().record("request_id", request_id); + tracing::Span::current().record("global_id", global_id); + tracing::Span::current().record("session_id", &session_id); + let result = serde_json::from_str::(&typed_sql); let db_op = match result { Ok(f) => f, @@ -190,7 +206,6 @@ async fn drainer( let payment_attempt = "payment_attempt"; let refund = "refund"; let reverse_lookup = "reverse_lookup"; - let connector_response = "connector_response"; let address = "address"; match db_op { // TODO: Handle errors @@ -214,13 +229,6 @@ async fn drainer( kv::Insertable::Refund(a) => { macro_util::handle_resp!(a.insert(&conn).await, insert_op, refund) } - kv::Insertable::ConnectorResponse(a) => { - macro_util::handle_resp!( - a.insert(&conn).await, - insert_op, - connector_response - ) - } kv::Insertable::Address(addr) => { macro_util::handle_resp!(addr.insert(&conn).await, insert_op, address) } @@ -267,11 +275,6 @@ async fn drainer( refund ) } - kv::Updateable::ConnectorResponseUpdate(a) => macro_util::handle_resp!( - a.orig.update(&conn, a.update_data).await, - update_op, - connector_response - ), kv::Updateable::AddressUpdate(a) => macro_util::handle_resp!( a.orig.update(&conn, a.update_data).await, update_op, diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index 6edec31f26d7..73f66f27dbf5 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -7,6 +7,7 @@ pub struct Store { pub master_pool: PgPool, pub redis_conn: Arc, pub config: StoreConfig, + pub request_id: Option, } #[derive(Clone)] @@ -30,6 +31,7 @@ impl Store { drainer_stream_name: config.drainer.stream_name.clone(), drainer_num_partitions: config.drainer.num_partitions, }, + request_id: None, } } diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml new file mode 100644 index 000000000000..f0e24b1ff63c --- /dev/null +++ b/crates/euclid/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "euclid" +description = "DSL for static routing" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +frunk = "0.4.1" +frunk_core = "0.4.1" +nom = { version = "7.1.3", features = ["alloc"], optional = true } +once_cell = "1.18.0" +rustc-hash = "1.1.0" +serde = { version = "1.0.163", features = ["derive", "rc"] } +serde_json = "1.0.96" +erased-serde = "0.3.28" +strum = { version = "0.25", features = ["derive"] } +thiserror = "1.0.43" + +# First party dependencies +common_enums = { version = "0.1.0", path = "../common_enums" } +euclid_macros = { version = "0.1.0", path = "../euclid_macros" } + +[features] +ast_parser = ["dep:nom"] +valued_jit = [] +connector_choice_bcompat = [] +connector_choice_mca_id = [] +dummy_connector = [] +backwards_compatibility = ["connector_choice_bcompat"] + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "backends" +harness = false +required-features = ["ast_parser", "valued_jit"] diff --git a/crates/euclid/benches/backends.rs b/crates/euclid/benches/backends.rs new file mode 100644 index 000000000000..9d29c41d34c6 --- /dev/null +++ b/crates/euclid/benches/backends.rs @@ -0,0 +1,93 @@ +#![allow(unused, clippy::expect_used)] + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use euclid::{ + backend::{inputs, EuclidBackend, InterpreterBackend, VirInterpreterBackend}, + enums, + frontend::ast::{self, parser}, + types::DummyOutput, +}; + +fn get_program_data() -> (ast::Program, inputs::BackendInput) { + let code1 = r#" + default: ["stripe", "adyen", "checkout"] + + stripe_first: ["stripe", "aci"] + { + payment_method = card & amount = 40 { + payment_method = (card, bank_redirect) + amount = (40, 50) + } + } + + adyen_first: ["adyen", "checkout"] + { + payment_method = bank_redirect & amount > 60 { + payment_method = (card, bank_redirect) + amount = (40, 50) + } + } + + auth_first: ["authorizedotnet", "adyen"] + { + payment_method = wallet + } + "#; + + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Sofort), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let (_, program) = parser::program(code1).expect("Parser"); + + (program, inp) +} + +fn interpreter_vs_jit_vs_vir_interpreter(c: &mut Criterion) { + let (program, binputs) = get_program_data(); + + let interp_b = InterpreterBackend::with_program(program.clone()).expect("Interpreter backend"); + + let vir_interp_b = + VirInterpreterBackend::with_program(program).expect("Vir Interpreter Backend"); + + c.bench_function("Raw Interpreter Backend", |b| { + b.iter(|| { + interp_b + .execute(binputs.clone()) + .expect("Interpreter EXECUTION"); + }); + }); + + c.bench_function("Valued Interpreter Backend", |b| { + b.iter(|| { + vir_interp_b + .execute(binputs.clone()) + .expect("Vir Interpreter execution"); + }) + }); +} + +criterion_group!(benches, interpreter_vs_jit_vs_vir_interpreter); +criterion_main!(benches); diff --git a/crates/euclid/src/backend.rs b/crates/euclid/src/backend.rs new file mode 100644 index 000000000000..caf0a87b69cb --- /dev/null +++ b/crates/euclid/src/backend.rs @@ -0,0 +1,25 @@ +pub mod inputs; +pub mod interpreter; +#[cfg(feature = "valued_jit")] +pub mod vir_interpreter; + +pub use inputs::BackendInput; +pub use interpreter::InterpreterBackend; +#[cfg(feature = "valued_jit")] +pub use vir_interpreter::VirInterpreterBackend; + +use crate::frontend::ast; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct BackendOutput { + pub rule_name: Option, + pub connector_selection: O, +} + +pub trait EuclidBackend: Sized { + type Error: serde::Serialize; + + fn with_program(program: ast::Program) -> Result; + + fn execute(&self, input: BackendInput) -> Result, Self::Error>; +} diff --git a/crates/euclid/src/backend/inputs.rs b/crates/euclid/src/backend/inputs.rs new file mode 100644 index 000000000000..18298d4c358d --- /dev/null +++ b/crates/euclid/src/backend/inputs.rs @@ -0,0 +1,39 @@ +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; + +use crate::enums; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MandateData { + pub mandate_acceptance_type: Option, + pub mandate_type: Option, + pub payment_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentMethodInput { + pub payment_method: Option, + pub payment_method_type: Option, + pub card_network: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentInput { + pub amount: i64, + pub currency: enums::Currency, + pub authentication_type: Option, + pub card_bin: Option, + pub capture_method: Option, + pub business_country: Option, + pub billing_country: Option, + pub business_label: Option, + pub setup_future_usage: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackendInput { + pub metadata: Option>, + pub payment: PaymentInput, + pub payment_method: PaymentMethodInput, + pub mandate: MandateData, +} diff --git a/crates/euclid/src/backend/interpreter.rs b/crates/euclid/src/backend/interpreter.rs new file mode 100644 index 000000000000..bf0a561bf3f3 --- /dev/null +++ b/crates/euclid/src/backend/interpreter.rs @@ -0,0 +1,180 @@ +pub mod types; + +use crate::{ + backend::{self, inputs, EuclidBackend}, + frontend::ast, +}; + +pub struct InterpreterBackend { + program: ast::Program, +} + +impl InterpreterBackend +where + O: Clone, +{ + fn eval_number_comparison_array( + num: i64, + array: &[ast::NumberComparison], + ) -> Result { + for comparison in array { + let other = comparison.number; + let res = match comparison.comparison_type { + ast::ComparisonType::GreaterThan => num > other, + ast::ComparisonType::LessThan => num < other, + ast::ComparisonType::LessThanEqual => num <= other, + ast::ComparisonType::GreaterThanEqual => num >= other, + ast::ComparisonType::Equal => num == other, + ast::ComparisonType::NotEqual => num != other, + }; + + if res { + return Ok(true); + } + } + + Ok(false) + } + + fn eval_comparison( + comparison: &ast::Comparison, + ctx: &types::Context, + ) -> Result { + use ast::{ComparisonType::*, ValueType::*}; + + let value = ctx + .get(&comparison.lhs) + .ok_or_else(|| types::InterpreterError { + error_type: types::InterpreterErrorType::InvalidKey(comparison.lhs.clone()), + metadata: comparison.metadata.clone(), + })?; + + if let Some(val) = value { + match (val, &comparison.comparison, &comparison.value) { + (EnumVariant(e1), Equal, EnumVariant(e2)) => Ok(e1 == e2), + (EnumVariant(e1), NotEqual, EnumVariant(e2)) => Ok(e1 != e2), + (EnumVariant(e), Equal, EnumVariantArray(evec)) => Ok(evec.iter().any(|v| e == v)), + (EnumVariant(e), NotEqual, EnumVariantArray(evec)) => { + Ok(evec.iter().all(|v| e != v)) + } + (Number(n1), Equal, Number(n2)) => Ok(n1 == n2), + (Number(n1), NotEqual, Number(n2)) => Ok(n1 != n2), + (Number(n1), LessThanEqual, Number(n2)) => Ok(n1 <= n2), + (Number(n1), GreaterThanEqual, Number(n2)) => Ok(n1 >= n2), + (Number(n1), LessThan, Number(n2)) => Ok(n1 < n2), + (Number(n1), GreaterThan, Number(n2)) => Ok(n1 > n2), + (Number(n), Equal, NumberArray(nvec)) => Ok(nvec.iter().any(|v| v == n)), + (Number(n), NotEqual, NumberArray(nvec)) => Ok(nvec.iter().all(|v| v != n)), + (Number(n), Equal, NumberComparisonArray(ncvec)) => { + Self::eval_number_comparison_array(*n, ncvec) + } + _ => Err(types::InterpreterError { + error_type: types::InterpreterErrorType::InvalidComparison, + metadata: comparison.metadata.clone(), + }), + } + } else { + Ok(false) + } + } + + fn eval_if_condition( + condition: &ast::IfCondition, + ctx: &types::Context, + ) -> Result { + for comparison in condition { + let res = Self::eval_comparison(comparison, ctx)?; + + if !res { + return Ok(false); + } + } + + Ok(true) + } + + fn eval_if_statement( + stmt: &ast::IfStatement, + ctx: &types::Context, + ) -> Result { + let cond_res = Self::eval_if_condition(&stmt.condition, ctx)?; + + if !cond_res { + return Ok(false); + } + + if let Some(ref nested) = stmt.nested { + for nested_if in nested { + let res = Self::eval_if_statement(nested_if, ctx)?; + + if res { + return Ok(true); + } + } + + return Ok(false); + } + + Ok(true) + } + + fn eval_rule_statements( + statements: &[ast::IfStatement], + ctx: &types::Context, + ) -> Result { + for stmt in statements { + let res = Self::eval_if_statement(stmt, ctx)?; + + if res { + return Ok(true); + } + } + + Ok(false) + } + + #[inline] + fn eval_rule( + rule: &ast::Rule, + ctx: &types::Context, + ) -> Result { + Self::eval_rule_statements(&rule.statements, ctx) + } + + fn eval_program( + program: &ast::Program, + ctx: &types::Context, + ) -> Result, types::InterpreterError> { + for rule in &program.rules { + let res = Self::eval_rule(rule, ctx)?; + + if res { + return Ok(backend::BackendOutput { + connector_selection: rule.connector_selection.clone(), + rule_name: Some(rule.name.clone()), + }); + } + } + + Ok(backend::BackendOutput { + connector_selection: program.default_selection.clone(), + rule_name: None, + }) + } +} + +impl EuclidBackend for InterpreterBackend +where + O: Clone, +{ + type Error = types::InterpreterError; + + fn with_program(program: ast::Program) -> Result { + Ok(Self { program }) + } + + fn execute(&self, input: inputs::BackendInput) -> Result, Self::Error> { + let ctx: types::Context = input.into(); + Self::eval_program(&self.program, &ctx) + } +} diff --git a/crates/euclid/src/backend/interpreter/types.rs b/crates/euclid/src/backend/interpreter/types.rs new file mode 100644 index 000000000000..a6384dbdf3ce --- /dev/null +++ b/crates/euclid/src/backend/interpreter/types.rs @@ -0,0 +1,81 @@ +use std::{collections::HashMap, fmt, ops::Deref, string::ToString}; + +use serde::Serialize; + +use crate::{backend::inputs, frontend::ast::ValueType, types::EuclidKey}; + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum InterpreterErrorType { + #[error("Invalid key received '{0}'")] + InvalidKey(String), + #[error("Invalid Comparison")] + InvalidComparison, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +pub struct InterpreterError { + pub error_type: InterpreterErrorType, + pub metadata: HashMap, +} + +impl fmt::Display for InterpreterError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + InterpreterErrorType::fmt(&self.error_type, f) + } +} + +pub struct Context(HashMap>); + +impl Deref for Context { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Context { + fn from(input: inputs::BackendInput) -> Self { + let ctx = HashMap::>::from_iter([ + ( + EuclidKey::PaymentMethod.to_string(), + input + .payment_method + .payment_method + .map(|pm| ValueType::EnumVariant(pm.to_string())), + ), + ( + EuclidKey::PaymentMethodType.to_string(), + input + .payment_method + .payment_method_type + .map(|pt| ValueType::EnumVariant(pt.to_string())), + ), + ( + EuclidKey::AuthenticationType.to_string(), + input + .payment + .authentication_type + .map(|at| ValueType::EnumVariant(at.to_string())), + ), + ( + EuclidKey::CaptureMethod.to_string(), + input + .payment + .capture_method + .map(|cm| ValueType::EnumVariant(cm.to_string())), + ), + ( + EuclidKey::PaymentAmount.to_string(), + Some(ValueType::Number(input.payment.amount)), + ), + ( + EuclidKey::PaymentCurrency.to_string(), + Some(ValueType::EnumVariant(input.payment.currency.to_string())), + ), + ]); + + Self(ctx) + } +} diff --git a/crates/euclid/src/backend/vir_interpreter.rs b/crates/euclid/src/backend/vir_interpreter.rs new file mode 100644 index 000000000000..b7be62cf6740 --- /dev/null +++ b/crates/euclid/src/backend/vir_interpreter.rs @@ -0,0 +1,583 @@ +pub mod types; + +use crate::{ + backend::{self, inputs, EuclidBackend}, + frontend::{ + ast, + dir::{self, EuclidDirFilter}, + vir, + }, +}; + +pub struct VirInterpreterBackend { + program: vir::ValuedProgram, +} + +impl VirInterpreterBackend +where + O: Clone, +{ + #[inline] + fn eval_comparison(comp: &vir::ValuedComparison, ctx: &types::Context) -> bool { + match &comp.logic { + vir::ValuedComparisonLogic::PositiveDisjunction => { + comp.values.iter().any(|v| ctx.check_presence(v)) + } + vir::ValuedComparisonLogic::NegativeConjunction => { + comp.values.iter().all(|v| !ctx.check_presence(v)) + } + } + } + + #[inline] + fn eval_condition(cond: &vir::ValuedIfCondition, ctx: &types::Context) -> bool { + cond.iter().all(|comp| Self::eval_comparison(comp, ctx)) + } + + fn eval_statement(stmt: &vir::ValuedIfStatement, ctx: &types::Context) -> bool { + Self::eval_condition(&stmt.condition, ctx) + .then(|| { + stmt.nested.as_ref().map_or(true, |nested_stmts| { + nested_stmts.iter().any(|s| Self::eval_statement(s, ctx)) + }) + }) + .unwrap_or(false) + } + + fn eval_rule(rule: &vir::ValuedRule, ctx: &types::Context) -> bool { + rule.statements + .iter() + .any(|stmt| Self::eval_statement(stmt, ctx)) + } + + fn eval_program( + program: &vir::ValuedProgram, + ctx: &types::Context, + ) -> backend::BackendOutput { + program + .rules + .iter() + .find(|rule| Self::eval_rule(rule, ctx)) + .map_or_else( + || backend::BackendOutput { + connector_selection: program.default_selection.clone(), + rule_name: None, + }, + |rule| backend::BackendOutput { + connector_selection: rule.connector_selection.clone(), + rule_name: Some(rule.name.clone()), + }, + ) + } +} + +impl EuclidBackend for VirInterpreterBackend +where + O: Clone + EuclidDirFilter, +{ + type Error = types::VirInterpreterError; + + fn with_program(program: ast::Program) -> Result { + let dir_program = ast::lowering::lower_program(program) + .map_err(types::VirInterpreterError::LoweringError)?; + + let vir_program = dir::lowering::lower_program(dir_program) + .map_err(types::VirInterpreterError::LoweringError)?; + + Ok(Self { + program: vir_program, + }) + } + + fn execute( + &self, + input: inputs::BackendInput, + ) -> Result, Self::Error> { + let ctx = types::Context::from_input(input); + Ok(Self::eval_program(&self.program, &ctx)) + } +} +#[cfg(all(test, feature = "ast_parser"))] +mod test { + #![allow(clippy::expect_used)] + use rustc_hash::FxHashMap; + + use super::*; + use crate::{enums, types::DummyOutput}; + + #[test] + fn test_execution() { + let program_str = r#" + default: [ "stripe", "adyen"] + + rule_1: ["stripe"] + { + pay_later = klarna + } + + rule_2: ["adyen"] + { + pay_later = affirm + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_2"); + } + #[test] + fn test_payment_type() { + let program_str = r#" + default: ["stripe", "adyen"] + rule_1: ["stripe"] + { + payment_type = setup_mandate + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: Some(enums::PaymentType::SetupMandate), + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_mandate_type() { + let program_str = r#" + default: ["stripe", "adyen"] + rule_1: ["stripe"] + { + mandate_type = single_use + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: Some(enums::MandateType::SingleUse), + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_mandate_acceptance_type() { + let program_str = r#" + default: ["stripe","adyen"] + rule_1: ["stripe"] + { + mandate_acceptance_type = online + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: Some(enums::MandateAcceptanceType::Online), + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_card_bin() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + card_bin="123456" + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: Some("123456".to_string()), + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_payment_amount() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount = 32 + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_payment_method() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + payment_method = pay_later + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + #[test] + fn test_future_usage() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + setup_future_usage = off_session + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 32, + currency: enums::Currency::USD, + card_bin: None, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: Some(enums::SetupFutureUsage::OffSession), + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_metadata_execution() { + let program_str = r#" + default: ["stripe"," adyen"] + + rule_1: ["stripe"] + { + "metadata_key" = "arbitrary meta" + } + "#; + let mut meta_map = FxHashMap::default(); + meta_map.insert("metadata_key".to_string(), "arbitrary meta".to_string()); + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp = inputs::BackendInput { + metadata: Some(meta_map), + payment: inputs::PaymentInput { + amount: 32, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result = backend.execute(inp).expect("Execution"); + assert_eq!(result.rule_name.expect("Rule Name").as_str(), "rule_1"); + } + + #[test] + fn test_less_than_operator() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount>=123 + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp_greater = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 150, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + let mut inp_equal = inp_greater.clone(); + inp_equal.payment.amount = 123; + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result_greater = backend.execute(inp_greater).expect("Execution"); + let result_equal = backend.execute(inp_equal).expect("Execution"); + assert_eq!( + result_equal.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + assert_eq!( + result_greater.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + } + + #[test] + fn test_greater_than_operator() { + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + amount<=123 + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let inp_lower = inputs::BackendInput { + metadata: None, + payment: inputs::PaymentInput { + amount: 120, + card_bin: None, + currency: enums::Currency::USD, + authentication_type: Some(enums::AuthenticationType::NoThreeDs), + capture_method: Some(enums::CaptureMethod::Automatic), + business_country: Some(enums::Country::UnitedStatesOfAmerica), + billing_country: Some(enums::Country::France), + business_label: None, + setup_future_usage: None, + }, + payment_method: inputs::PaymentMethodInput { + payment_method: Some(enums::PaymentMethod::PayLater), + payment_method_type: Some(enums::PaymentMethodType::Affirm), + card_network: None, + }, + mandate: inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + let mut inp_equal = inp_lower.clone(); + inp_equal.payment.amount = 123; + let backend = VirInterpreterBackend::::with_program(program).expect("Program"); + let result_equal = backend.execute(inp_equal).expect("Execution"); + let result_lower = backend.execute(inp_lower).expect("Execution"); + assert_eq!( + result_equal.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + assert_eq!( + result_lower.rule_name.expect("Rule Name").as_str(), + "rule_1" + ); + } +} diff --git a/crates/euclid/src/backend/vir_interpreter/types.rs b/crates/euclid/src/backend/vir_interpreter/types.rs new file mode 100644 index 000000000000..a144cdaafd08 --- /dev/null +++ b/crates/euclid/src/backend/vir_interpreter/types.rs @@ -0,0 +1,126 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + backend::inputs::BackendInput, + dssa, + types::{self, EuclidKey, EuclidValue, MetadataValue, NumValueRefinement, StrValue}, +}; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum VirInterpreterError { + #[error("Error when lowering the program: {0:?}")] + LoweringError(dssa::types::AnalysisError), +} + +pub struct Context { + atomic_values: FxHashSet, + numeric_values: FxHashMap, +} + +impl Context { + pub fn check_presence(&self, value: &EuclidValue) -> bool { + let key = value.get_key(); + + match key.key_type() { + types::DataType::MetadataValue => self.atomic_values.contains(value), + types::DataType::StrValue => self.atomic_values.contains(value), + types::DataType::EnumVariant => self.atomic_values.contains(value), + types::DataType::Number => { + let ctx_num_value = self + .numeric_values + .get(&key) + .and_then(|value| value.get_num_value()); + + value.get_num_value().zip(ctx_num_value).map_or( + false, + |(program_value, ctx_value)| { + let program_num = program_value.number; + let ctx_num = ctx_value.number; + + match &program_value.refinement { + None => program_num == ctx_num, + Some(NumValueRefinement::NotEqual) => ctx_num != program_num, + Some(NumValueRefinement::GreaterThan) => ctx_num > program_num, + Some(NumValueRefinement::GreaterThanEqual) => ctx_num >= program_num, + Some(NumValueRefinement::LessThanEqual) => ctx_num <= program_num, + Some(NumValueRefinement::LessThan) => ctx_num < program_num, + } + }, + ) + } + } + } + + pub fn from_input(input: BackendInput) -> Self { + let payment = input.payment; + let payment_method = input.payment_method; + let meta_data = input.metadata; + let payment_mandate = input.mandate; + + let mut enum_values: FxHashSet = + FxHashSet::from_iter([EuclidValue::PaymentCurrency(payment.currency)]); + + if let Some(pm) = payment_method.payment_method { + enum_values.insert(EuclidValue::PaymentMethod(pm)); + } + + if let Some(pmt) = payment_method.payment_method_type { + enum_values.insert(EuclidValue::PaymentMethodType(pmt)); + } + + if let Some(met) = meta_data { + for (key, value) in met.into_iter() { + enum_values.insert(EuclidValue::Metadata(MetadataValue { key, value })); + } + } + + if let Some(at) = payment.authentication_type { + enum_values.insert(EuclidValue::AuthenticationType(at)); + } + + if let Some(capture_method) = payment.capture_method { + enum_values.insert(EuclidValue::CaptureMethod(capture_method)); + } + + if let Some(country) = payment.business_country { + enum_values.insert(EuclidValue::BusinessCountry(country)); + } + + if let Some(country) = payment.billing_country { + enum_values.insert(EuclidValue::BillingCountry(country)); + } + if let Some(card_bin) = payment.card_bin { + enum_values.insert(EuclidValue::CardBin(StrValue { value: card_bin })); + } + if let Some(business_label) = payment.business_label { + enum_values.insert(EuclidValue::BusinessLabel(StrValue { + value: business_label, + })); + } + if let Some(setup_future_usage) = payment.setup_future_usage { + enum_values.insert(EuclidValue::SetupFutureUsage(setup_future_usage)); + } + if let Some(payment_type) = payment_mandate.payment_type { + enum_values.insert(EuclidValue::PaymentType(payment_type)); + } + if let Some(mandate_type) = payment_mandate.mandate_type { + enum_values.insert(EuclidValue::MandateType(mandate_type)); + } + if let Some(mandate_acceptance_type) = payment_mandate.mandate_acceptance_type { + enum_values.insert(EuclidValue::MandateAcceptanceType(mandate_acceptance_type)); + } + + let numeric_values: FxHashMap = FxHashMap::from_iter([( + EuclidKey::PaymentAmount, + EuclidValue::PaymentAmount(types::NumValue { + number: payment.amount, + refinement: None, + }), + )]); + + Self { + atomic_values: enum_values, + numeric_values, + } + } +} diff --git a/crates/euclid/src/dssa.rs b/crates/euclid/src/dssa.rs new file mode 100644 index 000000000000..2f6f35dfb27c --- /dev/null +++ b/crates/euclid/src/dssa.rs @@ -0,0 +1,7 @@ +//! Domain Specific Static Analyzer +pub mod analyzer; +pub mod graph; +pub mod state_machine; +pub mod truth; +pub mod types; +pub mod utils; diff --git a/crates/euclid/src/dssa/analyzer.rs b/crates/euclid/src/dssa/analyzer.rs new file mode 100644 index 000000000000..149ed1fd79cd --- /dev/null +++ b/crates/euclid/src/dssa/analyzer.rs @@ -0,0 +1,447 @@ +//! Static Analysis for the Euclid Rule DSL +//! +//! Exposes certain functions that can be used to perform static analysis over programs +//! in the Euclid Rule DSL. These include standard control flow analyses like testing +//! conflicting assertions, to Domain Specific Analyses making use of the +//! [`Knowledge Graph Framework`](crate::dssa::graph). +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{graph::Memoization, types::EuclidAnalysable}; +use crate::{ + dssa::{graph, state_machine, truth, types}, + frontend::{ + ast, + dir::{self, EuclidDirFilter}, + vir, + }, + types::{DataType, Metadata}, +}; + +/// Analyses conflicting assertions on the same key in a conjunctive context. +/// +/// For example, +/// ```notrust +/// payment_method = card && ... && payment_method = bank_debit +/// ```notrust +/// This is a condition that will never evaluate to `true` given a single +/// payment method and needs to be caught in analysis. +pub fn analyze_conflicting_assertions( + keywise_assertions: &FxHashMap>, + assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>, +) -> Result<(), types::AnalysisError> { + for (key, value_set) in keywise_assertions { + if value_set.len() > 1 { + let err_type = types::AnalysisErrorType::ConflictingAssertions { + key: key.clone(), + values: value_set + .iter() + .map(|val| types::ValueData { + value: (*val).clone(), + metadata: assertion_metadata + .get(val) + .map(|meta| (*meta).clone()) + .unwrap_or_default(), + }) + .collect(), + }; + + Err(types::AnalysisError { + error_type: err_type, + metadata: Default::default(), + })?; + } + } + Ok(()) +} + +/// Analyses exhaustive negations on the same key in a conjunctive context. +/// +/// For example, +/// ```notrust +/// authentication_type /= three_ds && ... && authentication_type /= no_three_ds +/// ```notrust +/// This is a condition that will never evaluate to `true` given any authentication_type +/// since all the possible values authentication_type can take have been negated. +pub fn analyze_exhaustive_negations( + keywise_negations: &FxHashMap>, + keywise_negation_metadata: &FxHashMap>, +) -> Result<(), types::AnalysisError> { + for (key, negation_set) in keywise_negations { + let mut value_set = if let Some(set) = key.kind.get_value_set() { + set + } else { + continue; + }; + + value_set.retain(|val| !negation_set.contains(val)); + + if value_set.is_empty() { + let error_type = types::AnalysisErrorType::ExhaustiveNegation { + key: key.clone(), + metadata: keywise_negation_metadata + .get(key) + .cloned() + .unwrap_or_default() + .iter() + .cloned() + .cloned() + .collect(), + }; + + Err(types::AnalysisError { + error_type, + metadata: Default::default(), + })?; + } + } + Ok(()) +} + +fn analyze_negated_assertions( + keywise_assertions: &FxHashMap>, + assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>, + keywise_negations: &FxHashMap>, + negation_metadata: &FxHashMap<&dir::DirValue, &Metadata>, +) -> Result<(), types::AnalysisError> { + for (key, negation_set) in keywise_negations { + let assertion_set = if let Some(set) = keywise_assertions.get(key) { + set + } else { + continue; + }; + + let intersection = negation_set & assertion_set; + + intersection.iter().next().map_or(Ok(()), |val| { + let error_type = types::AnalysisErrorType::NegatedAssertion { + value: (*val).clone(), + assertion_metadata: assertion_metadata + .get(*val) + .cloned() + .cloned() + .unwrap_or_default(), + negation_metadata: negation_metadata + .get(*val) + .cloned() + .cloned() + .unwrap_or_default(), + }; + + Err(types::AnalysisError { + error_type, + metadata: Default::default(), + }) + })?; + } + Ok(()) +} + +fn perform_condition_analyses( + context: &types::ConjunctiveContext<'_>, +) -> Result<(), types::AnalysisError> { + let mut assertion_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default(); + let mut keywise_assertions: FxHashMap> = + FxHashMap::default(); + let mut negation_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default(); + let mut keywise_negation_metadata: FxHashMap> = + FxHashMap::default(); + let mut keywise_negations: FxHashMap> = + FxHashMap::default(); + + for ctx_val in context { + let key = if let Some(k) = ctx_val.value.get_key() { + k + } else { + continue; + }; + + if let dir::DirKeyKind::Connector = key.kind { + continue; + } + + if !matches!(key.kind.get_type(), DataType::EnumVariant) { + continue; + } + + match ctx_val.value { + types::CtxValueKind::Assertion(val) => { + keywise_assertions + .entry(key.clone()) + .or_default() + .insert(val); + + assertion_metadata.insert(val, ctx_val.metadata); + } + + types::CtxValueKind::Negation(vals) => { + let negation_set = keywise_negations.entry(key.clone()).or_default(); + + for val in vals { + negation_set.insert(val); + negation_metadata.insert(val, ctx_val.metadata); + } + + keywise_negation_metadata + .entry(key.clone()) + .or_default() + .push(ctx_val.metadata); + } + } + } + + analyze_conflicting_assertions(&keywise_assertions, &assertion_metadata)?; + analyze_exhaustive_negations(&keywise_negations, &keywise_negation_metadata)?; + analyze_negated_assertions( + &keywise_assertions, + &assertion_metadata, + &keywise_negations, + &negation_metadata, + )?; + + Ok(()) +} + +fn perform_context_analyses( + context: &types::ConjunctiveContext<'_>, + knowledge_graph: &graph::KnowledgeGraph<'_>, +) -> Result<(), types::AnalysisError> { + perform_condition_analyses(context)?; + let mut memo = Memoization::new(); + knowledge_graph + .perform_context_analysis(context, &mut memo) + .map_err(|err| types::AnalysisError { + error_type: types::AnalysisErrorType::GraphAnalysis(err, memo), + metadata: Default::default(), + })?; + Ok(()) +} + +pub fn analyze( + program: ast::Program, + knowledge_graph: Option<&graph::KnowledgeGraph<'_>>, +) -> Result, types::AnalysisError> { + let dir_program = ast::lowering::lower_program(program)?; + + let selection_data = state_machine::make_connector_selection_data(&dir_program); + let mut ctx_manager = state_machine::AnalysisContextManager::new(&dir_program, &selection_data); + while let Some(ctx) = ctx_manager.advance().map_err(|err| types::AnalysisError { + metadata: Default::default(), + error_type: types::AnalysisErrorType::StateMachine(err), + })? { + perform_context_analyses(ctx, knowledge_graph.unwrap_or(&truth::ANALYSIS_GRAPH))?; + } + + dir::lowering::lower_program(dir_program) +} + +#[cfg(all(test, feature = "ast_parser"))] +mod tests { + #![allow(clippy::panic, clippy::expect_used)] + + use std::{ops::Deref, sync::Weak}; + + use euclid_macros::knowledge; + + use super::*; + use crate::{dirval, types::DummyOutput}; + + #[test] + fn test_conflicting_assertion_detection() { + let program_str = r#" + default: ["stripe", "adyen"] + + stripe_first: ["stripe", "adyen"] + { + payment_method = wallet { + amount > 500 & capture_method = automatic + amount < 500 & payment_method = card + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::ConflictingAssertions { key, values }, + .. + }) = analysis_result + { + assert!( + matches!(key.kind, dir::DirKeyKind::PaymentMethod), + "Key should be payment_method" + ); + let values: Vec = values.into_iter().map(|v| v.value).collect(); + assert_eq!(values.len(), 2, "There should be 2 conflicting conditions"); + assert!( + values.contains(&dirval!(PaymentMethod = Wallet)), + "Condition should include payment_method = wallet" + ); + assert!( + values.contains(&dirval!(PaymentMethod = Card)), + "Condition should include payment_method = card" + ); + } else { + panic!("Did not receive conflicting assertions error"); + } + } + + #[test] + fn test_exhaustive_negation_detection() { + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + payment_method /= wallet { + capture_method = manual & payment_method /= card { + authentication_type = three_ds & payment_method /= pay_later { + amount > 1000 & payment_method /= bank_redirect { + payment_method /= crypto + & payment_method /= bank_debit + & payment_method /= bank_transfer + & payment_method /= upi + & payment_method /= reward + & payment_method /= voucher + & payment_method /= gift_card + + } + } + } + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::ExhaustiveNegation { key, .. }, + .. + }) = analysis_result + { + assert!( + matches!(key.kind, dir::DirKeyKind::PaymentMethod), + "Expected key to be payment_method" + ); + } else { + panic!("Expected exhaustive negation error"); + } + } + + #[test] + fn test_negated_assertions_detection() { + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + payment_method = wallet { + amount > 500 { + capture_method = automatic + } + + amount < 501 { + payment_method /= wallet + } + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let analysis_result = analyze(program, None); + + if let Err(types::AnalysisError { + error_type: types::AnalysisErrorType::NegatedAssertion { value, .. }, + .. + }) = analysis_result + { + assert_eq!( + value, + dirval!(PaymentMethod = Wallet), + "Expected to catch payment_method = wallet as conflict" + ); + } else { + panic!("Expected negated assertion error"); + } + } + + #[test] + fn test_negation_graph_analysis() { + let graph = knowledge! {crate + CaptureMethod(Automatic) ->> PaymentMethod(Card); + }; + + let program_str = r#" + default: ["stripe"] + + rule_1: ["adyen"] + { + amount > 500 { + payment_method = pay_later + } + + amount < 500 { + payment_method /= wallet & payment_method /= pay_later + } + } + "#; + + let (_, program) = ast::parser::program::(program_str).expect("Graph"); + let analysis_result = analyze(program, Some(&graph)); + + let error_type = match analysis_result { + Err(types::AnalysisError { error_type, .. }) => error_type, + _ => panic!("Error_type not found"), + }; + + let a_err = match error_type { + types::AnalysisErrorType::GraphAnalysis(trace, memo) => (trace, memo), + _ => panic!("Graph Analysis not found"), + }; + + let (trace, metadata) = match a_err.0 { + graph::AnalysisError::NegationTrace { trace, metadata } => (trace, metadata), + _ => panic!("Negation Trace not found"), + }; + + let predecessor = match Weak::upgrade(&trace) + .expect("Expected Arc not found") + .deref() + .clone() + { + graph::AnalysisTrace::Value { predecessors, .. } => { + let _value = graph::NodeValue::Value(dir::DirValue::PaymentMethod( + dir::enums::PaymentMethod::Card, + )); + let _relation = graph::Relation::Positive; + predecessors + } + _ => panic!("Expected Negation Trace for payment method = card"), + }; + + let pred = match predecessor { + Some(graph::ValueTracePredecessor::Mandatory(predecessor)) => predecessor, + _ => panic!("No predecessor found"), + }; + assert_eq!( + metadata.len(), + 2, + "Expected two metadats for wallet and pay_later" + ); + assert!(matches!( + *Weak::upgrade(&pred) + .expect("Expected Arc not found") + .deref(), + graph::AnalysisTrace::Value { + value: graph::NodeValue::Value(dir::DirValue::CaptureMethod( + dir::enums::CaptureMethod::Automatic + )), + relation: graph::Relation::Positive, + info: None, + metadata: None, + predecessors: None, + } + )); + } +} diff --git a/crates/euclid/src/dssa/graph.rs b/crates/euclid/src/dssa/graph.rs new file mode 100644 index 000000000000..bd23ae385226 --- /dev/null +++ b/crates/euclid/src/dssa/graph.rs @@ -0,0 +1,1478 @@ +use std::{ + fmt::Debug, + hash::Hash, + ops::{Deref, DerefMut}, + sync::{Arc, Weak}, +}; + +use erased_serde::{self, Serialize as ErasedSerialize}; +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::Serialize; + +use crate::{ + dssa::types, + frontend::dir, + types::{DataType, Metadata}, + utils, +}; + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash, strum::Display)] +pub enum Strength { + Weak, + Normal, + Strong, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Relation { + Positive, + Negative, +} + +impl From for bool { + fn from(value: Relation) -> Self { + matches!(value, Relation::Positive) + } +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)] +pub struct NodeId(usize); + +impl utils::EntityId for NodeId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DomainInfo<'a> { + pub domain_identifier: DomainIdentifier<'a>, + pub domain_description: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DomainIdentifier<'a>(&'a str); + +impl<'a> DomainIdentifier<'a> { + pub fn new(domain_identifier: &'a str) -> Self { + Self(domain_identifier) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DomainId(usize); + +impl utils::EntityId for DomainId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EdgeId(usize); + +impl utils::EntityId for EdgeId { + #[inline] + fn get_id(&self) -> usize { + self.0 + } + + #[inline] + fn with_id(id: usize) -> Self { + Self(id) + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Memoization(FxHashMap<(NodeId, Relation, Strength), Result<(), Arc>>); + +impl Memoization { + pub fn new() -> Self { + Self(FxHashMap::default()) + } +} + +impl Default for Memoization { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Deref for Memoization { + type Target = FxHashMap<(NodeId, Relation, Strength), Result<(), Arc>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Memoization { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +#[derive(Debug, Clone)] +pub struct Edge { + pub strength: Strength, + pub relation: Relation, + pub pred: NodeId, + pub succ: NodeId, +} + +#[derive(Debug)] +pub struct Node { + pub node_type: NodeType, + pub preds: Vec, + pub succs: Vec, + pub domain_ids: Vec, +} + +impl Node { + fn new(node_type: NodeType, domain_ids: Vec) -> Self { + Self { + node_type, + preds: Vec::new(), + succs: Vec::new(), + domain_ids, + } + } +} + +pub trait KgraphMetadata: ErasedSerialize + std::any::Any + Sync + Send + Debug {} +erased_serde::serialize_trait_object!(KgraphMetadata); + +impl KgraphMetadata for M where M: ErasedSerialize + std::any::Any + Sync + Send + Debug {} + +#[derive(Debug)] +pub struct KnowledgeGraph<'a> { + domain: utils::DenseMap>, + nodes: utils::DenseMap, + edges: utils::DenseMap, + value_map: FxHashMap, + node_info: utils::DenseMap>, + node_metadata: utils::DenseMap>>, +} + +pub struct KnowledgeGraphBuilder<'a> { + domain: utils::DenseMap>, + nodes: utils::DenseMap, + edges: utils::DenseMap, + domain_identifier_map: FxHashMap, DomainId>, + value_map: FxHashMap, + edges_map: FxHashMap<(NodeId, NodeId), EdgeId>, + node_info: utils::DenseMap>, + node_metadata: utils::DenseMap>>, +} + +impl<'a> Default for KnowledgeGraphBuilder<'a> { + #[inline] + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NodeType { + AllAggregator, + AnyAggregator, + InAggregator(FxHashSet), + Value(NodeValue), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum NodeValue { + Key(dir::DirKey), + Value(dir::DirValue), +} + +impl From for NodeValue { + fn from(value: dir::DirValue) -> Self { + Self::Value(value) + } +} + +impl From for NodeValue { + fn from(key: dir::DirKey) -> Self { + Self::Key(key) + } +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "predecessor", rename_all = "snake_case")] +pub enum ValueTracePredecessor { + Mandatory(Box>), + OneOf(Vec>), +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "trace", rename_all = "snake_case")] +pub enum AnalysisTrace { + Value { + value: NodeValue, + relation: Relation, + predecessors: Option, + info: Option<&'static str>, + metadata: Option>, + }, + + AllAggregation { + unsatisfied: Vec>, + info: Option<&'static str>, + metadata: Option>, + }, + + AnyAggregation { + unsatisfied: Vec>, + info: Option<&'static str>, + metadata: Option>, + }, + + InAggregation { + expected: Vec, + found: Option, + relation: Relation, + info: Option<&'static str>, + metadata: Option>, + }, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "details", rename_all = "snake_case")] +pub enum AnalysisError { + Graph(GraphError), + AssertionTrace { + trace: Weak, + metadata: Metadata, + }, + NegationTrace { + trace: Weak, + metadata: Vec, + }, +} + +impl AnalysisError { + fn assertion_from_graph_error(metadata: &Metadata, graph_error: GraphError) -> Self { + match graph_error { + GraphError::AnalysisError(trace) => Self::AssertionTrace { + trace, + metadata: metadata.clone(), + }, + + other => Self::Graph(other), + } + } + + fn negation_from_graph_error(metadata: Vec<&Metadata>, graph_error: GraphError) -> Self { + match graph_error { + GraphError::AnalysisError(trace) => Self::NegationTrace { + trace, + metadata: metadata.iter().map(|m| (*m).clone()).collect(), + }, + + other => Self::Graph(other), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum GraphError { + #[error("An edge was not found in the graph")] + EdgeNotFound, + #[error("Attempted to create a conflicting edge between two nodes")] + ConflictingEdgeCreated, + #[error("Cycle detected in graph")] + CycleDetected, + #[error("Domain wasn't found in the Graph")] + DomainNotFound, + #[error("Malformed Graph: {reason}")] + MalformedGraph { reason: String }, + #[error("A node was not found in the graph")] + NodeNotFound, + #[error("A value node was not found: {0:#?}")] + ValueNodeNotFound(dir::DirValue), + #[error("No values provided for an 'in' aggregator node")] + NoInAggregatorValues, + #[error("Error during analysis: {0:#?}")] + AnalysisError(Weak), +} + +impl GraphError { + fn get_analysis_trace(self) -> Result, Self> { + match self { + Self::AnalysisError(trace) => Ok(trace), + _ => Err(self), + } + } +} + +impl PartialEq for NodeValue { + fn eq(&self, other: &dir::DirValue) -> bool { + match self { + Self::Key(dir_key) => *dir_key == other.get_key(), + Self::Value(dir_value) if dir_value.get_key() == other.get_key() => { + if let (Some(left), Some(right)) = + (dir_value.get_num_value(), other.get_num_value()) + { + left.fits(&right) + } else { + dir::DirValue::check_equality(dir_value, other) + } + } + Self::Value(_) => false, + } + } +} + +pub struct AnalysisContext { + keywise_values: FxHashMap>, +} + +impl AnalysisContext { + pub fn from_dir_values(vals: impl IntoIterator) -> Self { + let mut keywise_values: FxHashMap> = + FxHashMap::default(); + + for dir_val in vals { + let key = dir_val.get_key(); + let set = keywise_values.entry(key).or_default(); + set.insert(dir_val); + } + + Self { keywise_values } + } + + fn check_presence(&self, value: &NodeValue, weak: bool) -> bool { + match value { + NodeValue::Key(k) => self.keywise_values.contains_key(k) || weak, + NodeValue::Value(val) => { + let key = val.get_key(); + let value_set = if let Some(set) = self.keywise_values.get(&key) { + set + } else { + return weak; + }; + + match key.kind.get_type() { + DataType::EnumVariant | DataType::StrValue | DataType::MetadataValue => { + value_set.contains(val) + } + DataType::Number => val.get_num_value().map_or(false, |num_val| { + value_set.iter().any(|ctx_val| { + ctx_val + .get_num_value() + .map_or(false, |ctx_num_val| num_val.fits(&ctx_num_val)) + }) + }), + } + } + } + } + + pub fn insert(&mut self, value: dir::DirValue) { + self.keywise_values + .entry(value.get_key()) + .or_default() + .insert(value); + } + + pub fn remove(&mut self, value: dir::DirValue) { + let set = self.keywise_values.entry(value.get_key()).or_default(); + + set.remove(&value); + + if set.is_empty() { + self.keywise_values.remove(&value.get_key()); + } + } +} + +impl<'a> KnowledgeGraphBuilder<'a> { + pub fn new() -> Self { + Self { + domain: utils::DenseMap::new(), + nodes: utils::DenseMap::new(), + edges: utils::DenseMap::new(), + domain_identifier_map: FxHashMap::default(), + value_map: FxHashMap::default(), + edges_map: FxHashMap::default(), + node_info: utils::DenseMap::new(), + node_metadata: utils::DenseMap::new(), + } + } + + pub fn build(self) -> KnowledgeGraph<'a> { + KnowledgeGraph { + domain: self.domain, + nodes: self.nodes, + edges: self.edges, + value_map: self.value_map, + node_info: self.node_info, + node_metadata: self.node_metadata, + } + } + + pub fn make_domain( + &mut self, + domain_identifier: DomainIdentifier<'a>, + domain_description: String, + ) -> Result { + Ok(self + .domain_identifier_map + .clone() + .get(&domain_identifier) + .map_or_else( + || { + let domain_id = self.domain.push(DomainInfo { + domain_identifier: domain_identifier.clone(), + domain_description, + }); + self.domain_identifier_map + .insert(domain_identifier.clone(), domain_id); + domain_id + }, + |domain_id| *domain_id, + )) + } + + pub fn make_value_node( + &mut self, + value: NodeValue, + info: Option<&'static str>, + domain_identifiers: Vec>, + metadata: Option, + ) -> Result { + match self.value_map.get(&value).copied() { + Some(node_id) => Ok(node_id), + None => { + let mut domain_ids: Vec = Vec::new(); + domain_identifiers + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let node_id = self + .nodes + .push(Node::new(NodeType::Value(value.clone()), domain_ids)); + let _node_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + self.value_map.insert(value, node_id); + Ok(node_id) + } + } + } + + pub fn make_edge( + &mut self, + pred_id: NodeId, + succ_id: NodeId, + strength: Strength, + relation: Relation, + ) -> Result { + self.ensure_node_exists(pred_id)?; + self.ensure_node_exists(succ_id)?; + self.edges_map + .get(&(pred_id, succ_id)) + .copied() + .and_then(|edge_id| self.edges.get(edge_id).cloned().map(|edge| (edge_id, edge))) + .map_or_else( + || { + let edge_id = self.edges.push(Edge { + strength, + relation, + pred: pred_id, + succ: succ_id, + }); + self.edges_map.insert((pred_id, succ_id), edge_id); + + let pred = self + .nodes + .get_mut(pred_id) + .ok_or(GraphError::NodeNotFound)?; + pred.succs.push(edge_id); + + let succ = self + .nodes + .get_mut(succ_id) + .ok_or(GraphError::NodeNotFound)?; + succ.preds.push(edge_id); + + Ok(edge_id) + }, + |(edge_id, edge)| { + if edge.strength == strength && edge.relation == relation { + Ok(edge_id) + } else { + Err(GraphError::ConflictingEdgeCreated) + } + }, + ) + } + + pub fn make_all_aggregator( + &mut self, + nodes: &[(NodeId, Relation, Strength)], + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + nodes + .iter() + .try_for_each(|(node_id, _, _)| self.ensure_node_exists(*node_id))?; + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let aggregator_id = self + .nodes + .push(Node::new(NodeType::AllAggregator, domain_ids)); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + for (node_id, relation, strength) in nodes { + self.make_edge(*node_id, aggregator_id, *strength, *relation)?; + } + + Ok(aggregator_id) + } + + pub fn make_any_aggregator( + &mut self, + nodes: &[(NodeId, Relation)], + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + nodes + .iter() + .try_for_each(|(node_id, _)| self.ensure_node_exists(*node_id))?; + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let aggregator_id = self + .nodes + .push(Node::new(NodeType::AnyAggregator, domain_ids)); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + for (node_id, relation) in nodes { + self.make_edge(*node_id, aggregator_id, Strength::Strong, *relation)?; + } + + Ok(aggregator_id) + } + + pub fn make_in_aggregator( + &mut self, + values: Vec, + info: Option<&'static str>, + metadata: Option, + domain: Vec>, + ) -> Result { + let key = values + .first() + .ok_or(GraphError::NoInAggregatorValues)? + .get_key(); + + for val in &values { + if val.get_key() != key { + Err(GraphError::MalformedGraph { + reason: "Values for 'In' aggregator not of same key".to_string(), + })?; + } + } + + let mut domain_ids: Vec = Vec::new(); + domain + .iter() + .try_for_each(|ident| { + self.domain_identifier_map + .get(ident) + .map(|id| domain_ids.push(*id)) + }) + .ok_or(GraphError::DomainNotFound)?; + + let node_id = self.nodes.push(Node::new( + NodeType::InAggregator(FxHashSet::from_iter(values)), + domain_ids, + )); + let _aggregator_info_id = self.node_info.push(info); + + let _node_metadata_id = self + .node_metadata + .push(metadata.map(|meta| -> Arc { Arc::new(meta) })); + + Ok(node_id) + } + + fn ensure_node_exists(&self, id: NodeId) -> Result<(), GraphError> { + if self.nodes.contains_key(id) { + Ok(()) + } else { + Err(GraphError::NodeNotFound) + } + } +} + +impl<'a> KnowledgeGraph<'a> { + fn check_node( + &self, + ctx: &AnalysisContext, + node_id: NodeId, + relation: Relation, + strength: Strength, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + let node = self.nodes.get(node_id).ok_or(GraphError::NodeNotFound)?; + if let Some(already_memo) = memo.get(&(node_id, relation, strength)) { + already_memo + .clone() + .map_err(|err| GraphError::AnalysisError(Arc::downgrade(&err))) + } else { + match &node.node_type { + NodeType::AllAggregator => { + let mut unsatisfied = Vec::>::new(); + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + + if let Err(e) = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo) + { + unsatisfied.push(e.get_analysis_trace()?); + } + } + + if !unsatisfied.is_empty() { + let err = Arc::new(AnalysisTrace::AllAggregation { + unsatisfied, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } else { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } + } + + NodeType::AnyAggregator => { + let mut unsatisfied = Vec::>::new(); + let mut matched_one = false; + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + + if let Err(e) = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo) + { + unsatisfied.push(e.get_analysis_trace()?); + } else { + matched_one = true; + } + } + + if matched_one || node.preds.is_empty() { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::AnyAggregation { + unsatisfied: unsatisfied.clone(), + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } + } + + NodeType::InAggregator(expected) => { + let the_key = expected + .iter() + .next() + .ok_or_else(|| GraphError::MalformedGraph { + reason: + "An OnlyIn aggregator node must have at least one expected value" + .to_string(), + })? + .get_key(); + + let ctx_vals = if let Some(vals) = ctx.keywise_values.get(&the_key) { + vals + } else { + return if let Strength::Weak = strength { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::InAggregation { + expected: expected.iter().cloned().collect(), + found: None, + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + }; + }; + + let relation_bool: bool = relation.into(); + for ctx_value in ctx_vals { + if expected.contains(ctx_value) != relation_bool { + let err = Arc::new(AnalysisTrace::InAggregation { + expected: expected.iter().cloned().collect(), + found: Some(ctx_value.clone()), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + } + + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } + + NodeType::Value(val) => { + let in_context = ctx.check_presence(val, matches!(strength, Strength::Weak)); + let relation_bool: bool = relation.into(); + + if in_context != relation_bool { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + predecessors: None, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + + if !relation_bool { + memo.insert((node_id, relation, strength), Ok(())); + return Ok(()); + } + + let mut errors = Vec::>::new(); + let mut matched_one = false; + + for edge_id in node.preds.iter().copied() { + let edge = self.edges.get(edge_id).ok_or(GraphError::EdgeNotFound)?; + let result = + self.check_node(ctx, edge.pred, edge.relation, edge.strength, memo); + + match (edge.strength, result) { + (Strength::Strong, Err(trace)) => { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + predecessors: Some(ValueTracePredecessor::Mandatory(Box::new( + trace.get_analysis_trace()?, + ))), + }); + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err)))?; + } + + (Strength::Strong, Ok(_)) => { + matched_one = true; + } + + (Strength::Normal | Strength::Weak, Err(trace)) => { + errors.push(trace.get_analysis_trace()?); + } + + (Strength::Normal | Strength::Weak, Ok(_)) => { + matched_one = true; + } + } + } + + if matched_one || node.preds.is_empty() { + memo.insert((node_id, relation, strength), Ok(())); + Ok(()) + } else { + let err = Arc::new(AnalysisTrace::Value { + value: val.clone(), + relation, + info: self.node_info.get(node_id).cloned().flatten(), + metadata: self.node_metadata.get(node_id).cloned().flatten(), + predecessors: Some(ValueTracePredecessor::OneOf(errors.clone())), + }); + + memo.insert((node_id, relation, strength), Err(Arc::clone(&err))); + Err(GraphError::AnalysisError(Arc::downgrade(&err))) + } + } + } + } + } + + fn key_analysis( + &self, + key: dir::DirKey, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.value_map + .get(&NodeValue::Key(key)) + .map_or(Ok(()), |node_id| { + self.check_node(ctx, *node_id, Relation::Positive, Strength::Strong, memo) + }) + } + + fn value_analysis( + &self, + val: dir::DirValue, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.value_map + .get(&NodeValue::Value(val)) + .map_or(Ok(()), |node_id| { + self.check_node(ctx, *node_id, Relation::Positive, Strength::Strong, memo) + }) + } + + pub fn check_value_validity( + &self, + val: dir::DirValue, + analysis_ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result { + let maybe_node_id = self.value_map.get(&NodeValue::Value(val)); + + let node_id = if let Some(nid) = maybe_node_id { + nid + } else { + return Ok(false); + }; + + let result = self.check_node( + analysis_ctx, + *node_id, + Relation::Positive, + Strength::Weak, + memo, + ); + + match result { + Ok(_) => Ok(true), + Err(e) => { + e.get_analysis_trace()?; + Ok(false) + } + } + } + + pub fn key_value_analysis( + &self, + val: dir::DirValue, + ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), GraphError> { + self.key_analysis(val.get_key(), ctx, memo) + .and_then(|_| self.value_analysis(val, ctx, memo)) + } + + fn assertion_analysis( + &self, + positive_ctx: &[(&dir::DirValue, &Metadata)], + analysis_ctx: &AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + positive_ctx.iter().try_for_each(|(value, metadata)| { + self.key_value_analysis((*value).clone(), analysis_ctx, memo) + .map_err(|e| AnalysisError::assertion_from_graph_error(metadata, e)) + }) + } + + fn negation_analysis( + &self, + negative_ctx: &[(&[dir::DirValue], &Metadata)], + analysis_ctx: &mut AnalysisContext, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + let mut keywise_metadata: FxHashMap> = FxHashMap::default(); + let mut keywise_negation: FxHashMap> = + FxHashMap::default(); + + for (values, metadata) in negative_ctx { + let mut metadata_added = false; + + for dir_value in *values { + if !metadata_added { + keywise_metadata + .entry(dir_value.get_key()) + .or_default() + .push(metadata); + + metadata_added = true; + } + + keywise_negation + .entry(dir_value.get_key()) + .or_default() + .insert(dir_value); + } + } + + for (key, negation_set) in keywise_negation { + let all_metadata = keywise_metadata.remove(&key).unwrap_or_default(); + let first_metadata = all_metadata.first().cloned().cloned().unwrap_or_default(); + + self.key_analysis(key.clone(), analysis_ctx, memo) + .map_err(|e| AnalysisError::assertion_from_graph_error(&first_metadata, e))?; + + let mut value_set = if let Some(set) = key.kind.get_value_set() { + set + } else { + continue; + }; + + value_set.retain(|v| !negation_set.contains(v)); + + for value in value_set { + analysis_ctx.insert(value.clone()); + self.value_analysis(value.clone(), analysis_ctx, memo) + .map_err(|e| { + AnalysisError::negation_from_graph_error(all_metadata.clone(), e) + })?; + analysis_ctx.remove(value); + } + } + + Ok(()) + } + + pub fn perform_context_analysis( + &self, + ctx: &types::ConjunctiveContext<'_>, + memo: &mut Memoization, + ) -> Result<(), AnalysisError> { + let mut analysis_ctx = AnalysisContext::from_dir_values( + ctx.iter() + .filter_map(|ctx_val| ctx_val.value.get_assertion().cloned()), + ); + + let positive_ctx = ctx + .iter() + .filter_map(|ctx_val| { + ctx_val + .value + .get_assertion() + .map(|val| (val, ctx_val.metadata)) + }) + .collect::>(); + self.assertion_analysis(&positive_ctx, &analysis_ctx, memo)?; + + let negative_ctx = ctx + .iter() + .filter_map(|ctx_val| { + ctx_val + .value + .get_negation() + .map(|vals| (vals, ctx_val.metadata)) + }) + .collect::>(); + self.negation_analysis(&negative_ctx, &mut analysis_ctx, memo)?; + + Ok(()) + } + + pub fn combine<'b>(g1: &'b Self, g2: &'b Self) -> Result { + let mut node_builder = KnowledgeGraphBuilder::new(); + let mut g1_old2new_id = utils::DenseMap::::new(); + let mut g2_old2new_id = utils::DenseMap::::new(); + let mut g1_old2new_domain_id = utils::DenseMap::::new(); + let mut g2_old2new_domain_id = utils::DenseMap::::new(); + + let add_domain = |node_builder: &mut KnowledgeGraphBuilder<'a>, + domain: DomainInfo<'a>| + -> Result { + node_builder.make_domain(domain.domain_identifier, domain.domain_description) + }; + + let add_node = |node_builder: &mut KnowledgeGraphBuilder<'a>, + node: &Node, + domains: Vec>| + -> Result { + match &node.node_type { + NodeType::Value(node_value) => { + node_builder.make_value_node(node_value.clone(), None, domains, None::<()>) + } + + NodeType::AllAggregator => { + Ok(node_builder.make_all_aggregator(&[], None, None::<()>, domains)?) + } + + NodeType::AnyAggregator => { + Ok(node_builder.make_any_aggregator(&[], None, None::<()>, Vec::new())?) + } + + NodeType::InAggregator(expected) => Ok(node_builder.make_in_aggregator( + expected.iter().cloned().collect(), + None, + None::<()>, + Vec::new(), + )?), + } + }; + + for (_old_domain_id, domain) in g1.domain.iter() { + let new_domain_id = add_domain(&mut node_builder, domain.clone())?; + g1_old2new_domain_id.push(new_domain_id); + } + + for (_old_domain_id, domain) in g2.domain.iter() { + let new_domain_id = add_domain(&mut node_builder, domain.clone())?; + g2_old2new_domain_id.push(new_domain_id); + } + + for (_old_node_id, node) in g1.nodes.iter() { + let mut domain_identifiers: Vec> = Vec::new(); + for domain_id in &node.domain_ids { + match g1.domain.get(*domain_id) { + Some(domain) => domain_identifiers.push(domain.domain_identifier.clone()), + None => return Err(GraphError::DomainNotFound), + } + } + let new_node_id = add_node(&mut node_builder, node, domain_identifiers.clone())?; + g1_old2new_id.push(new_node_id); + } + + for (_old_node_id, node) in g2.nodes.iter() { + let mut domain_identifiers: Vec> = Vec::new(); + for domain_id in &node.domain_ids { + match g2.domain.get(*domain_id) { + Some(domain) => domain_identifiers.push(domain.domain_identifier.clone()), + None => return Err(GraphError::DomainNotFound), + } + } + let new_node_id = add_node(&mut node_builder, node, domain_identifiers.clone())?; + g2_old2new_id.push(new_node_id); + } + + for edge in g1.edges.values() { + let new_pred_id = g1_old2new_id + .get(edge.pred) + .ok_or(GraphError::NodeNotFound)?; + let new_succ_id = g1_old2new_id + .get(edge.succ) + .ok_or(GraphError::NodeNotFound)?; + + node_builder.make_edge(*new_pred_id, *new_succ_id, edge.strength, edge.relation)?; + } + + for edge in g2.edges.values() { + let new_pred_id = g2_old2new_id + .get(edge.pred) + .ok_or(GraphError::NodeNotFound)?; + let new_succ_id = g2_old2new_id + .get(edge.succ) + .ok_or(GraphError::NodeNotFound)?; + + node_builder.make_edge(*new_pred_id, *new_succ_id, edge.strength, edge.relation)?; + } + + Ok(node_builder.build()) + } +} + +#[cfg(test)] +mod test { + #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + + use euclid_macros::knowledge; + + use super::*; + use crate::{dirval, frontend::dir::enums}; + + #[test] + fn test_strong_positive_relation_success() { + let graph = knowledge! {crate + PaymentMethod(Card) ->> CaptureMethod(Automatic); + PaymentMethod(not Wallet) + & PaymentMethod(not PayLater) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_strong_positive_relation_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) ->> CaptureMethod(Automatic); + PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([dirval!(CaptureMethod = Automatic)]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_strong_negative_relation_success() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_strong_negative_relation_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Wallet), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_normal_one_of_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) -> CaptureMethod(Automatic); + PaymentMethod(Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + assert!(matches!( + *Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected Arc"), + AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::OneOf(_)), + .. + } + )); + } + + #[test] + fn test_all_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_all_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_all_aggregator_mandatory_failure() { + let graph = knowledge! {crate + PaymentMethod(Card) & PaymentMethod(not Wallet) ->> CaptureMethod(Automatic); + }; + let mut memo = Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + ]), + &mut memo, + ); + + assert!(matches!( + *Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected Arc"), + AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::Mandatory(_)), + .. + } + )); + } + + #[test] + fn test_in_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_in_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) -> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_not_in_aggregator_success() { + let graph = knowledge! {crate + PaymentMethod(not in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + dirval!(PaymentMethod = BankRedirect), + ]), + memo, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_not_in_aggregator_failure() { + let graph = knowledge! {crate + PaymentMethod(not in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = PayLater), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentMethod = Card), + ]), + memo, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_in_aggregator_failure_trace() { + let graph = knowledge! {crate + PaymentMethod(in [Card, Wallet]) ->> CaptureMethod(Automatic); + }; + let memo = &mut Memoization::new(); + let result = graph.key_value_analysis( + dirval!(CaptureMethod = Automatic), + &AnalysisContext::from_dir_values([ + dirval!(CaptureMethod = Automatic), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + ]), + memo, + ); + + if let AnalysisTrace::Value { + predecessors: Some(ValueTracePredecessor::Mandatory(agg_error)), + .. + } = Weak::upgrade(&result.unwrap_err().get_analysis_trace().unwrap()) + .expect("Expected arc") + .deref() + { + assert!(matches!( + *Weak::upgrade(agg_error.deref()).expect("Expected Arc"), + AnalysisTrace::InAggregation { + found: Some(dir::DirValue::PaymentMethod(enums::PaymentMethod::PayLater)), + .. + } + )); + } else { + panic!("Failed unwrapping OnlyInAggregation trace from AnalysisTrace"); + } + } + + #[test] + fn _test_memoization_in_kgraph() { + let mut builder = KnowledgeGraphBuilder::new(); + let _node_1 = builder.make_value_node( + NodeValue::Value(dir::DirValue::PaymentMethod(enums::PaymentMethod::Wallet)), + None, + Vec::new(), + None::<()>, + ); + let _node_2 = builder.make_value_node( + NodeValue::Value(dir::DirValue::BillingCountry(enums::BillingCountry::India)), + None, + Vec::new(), + None::<()>, + ); + let _node_3 = builder.make_value_node( + NodeValue::Value(dir::DirValue::BusinessCountry( + enums::BusinessCountry::UnitedStatesOfAmerica, + )), + None, + Vec::new(), + None::<()>, + ); + let mut memo = Memoization::new(); + let _edge_1 = builder + .make_edge( + _node_1.expect("node1 constructtion failed"), + _node_2.clone().expect("node2 construction failed"), + Strength::Strong, + Relation::Positive, + ) + .expect("Failed to make an edge"); + let _edge_2 = builder + .make_edge( + _node_2.expect("node2 construction failed"), + _node_3.clone().expect("node3 construction failed"), + Strength::Strong, + Relation::Positive, + ) + .expect("Failed to an edge"); + let graph = builder.build(); + let _result = graph.key_value_analysis( + dirval!(BusinessCountry = UnitedStatesOfAmerica), + &AnalysisContext::from_dir_values([ + dirval!(PaymentMethod = Wallet), + dirval!(BillingCountry = India), + dirval!(BusinessCountry = UnitedStatesOfAmerica), + ]), + &mut memo, + ); + let _ans = memo + .0 + .get(&( + _node_3.expect("node3 construction failed"), + Relation::Positive, + Strength::Strong, + )) + .expect("Memoization not workng"); + matches!(_ans, Ok(())); + } +} diff --git a/crates/euclid/src/dssa/state_machine.rs b/crates/euclid/src/dssa/state_machine.rs new file mode 100644 index 000000000000..4cd53911dfe4 --- /dev/null +++ b/crates/euclid/src/dssa/state_machine.rs @@ -0,0 +1,714 @@ +use super::types::EuclidAnalysable; +use crate::{dssa::types, frontend::dir, types::Metadata}; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum StateMachineError { + #[error("Index out of bounds: {0}")] + IndexOutOfBounds(&'static str), +} + +#[derive(Debug)] +struct ComparisonStateMachine<'a> { + values: &'a [dir::DirValue], + logic: &'a dir::DirComparisonLogic, + metadata: &'a Metadata, + count: usize, + ctx_idx: usize, +} + +impl<'a> ComparisonStateMachine<'a> { + #[inline] + fn is_finished(&self) -> bool { + self.count + 1 >= self.values.len() + || matches!(self.logic, dir::DirComparisonLogic::NegativeConjunction) + } + + #[inline] + fn advance(&mut self) { + if let dir::DirComparisonLogic::PositiveDisjunction = self.logic { + self.count = (self.count + 1) % self.values.len(); + } + } + + #[inline] + fn reset(&mut self) { + self.count = 0; + } + + #[inline] + fn put(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + if let dir::DirComparisonLogic::PositiveDisjunction = self.logic { + *context + .get_mut(self.ctx_idx) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while indexing into context", + ))? = types::ContextValue::assertion( + self.values + .get(self.count) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while indexing into values", + ))?, + self.metadata, + ); + } + Ok(()) + } + + #[inline] + fn push(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + match self.logic { + dir::DirComparisonLogic::PositiveDisjunction => { + context.push(types::ContextValue::assertion( + self.values + .get(self.count) + .ok_or(StateMachineError::IndexOutOfBounds( + "in ComparisonStateMachine while pushing", + ))?, + self.metadata, + )); + } + + dir::DirComparisonLogic::NegativeConjunction => { + context.push(types::ContextValue::negation(self.values, self.metadata)); + } + } + Ok(()) + } +} + +#[derive(Debug)] +struct ConditionStateMachine<'a> { + state_machines: Vec>, + start_ctx_idx: usize, +} + +impl<'a> ConditionStateMachine<'a> { + fn new(condition: &'a [dir::DirComparison], start_idx: usize) -> Self { + let mut machines = Vec::>::with_capacity(condition.len()); + + let mut machine_idx = start_idx; + for cond in condition { + let machine = ComparisonStateMachine { + values: &cond.values, + logic: &cond.logic, + metadata: &cond.metadata, + count: 0, + ctx_idx: machine_idx, + }; + machines.push(machine); + machine_idx += 1; + } + + Self { + state_machines: machines, + start_ctx_idx: start_idx, + } + } + + fn init(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> { + for machine in &self.state_machines { + machine.push(context)?; + } + Ok(()) + } + + #[inline] + fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) { + context.truncate(self.start_ctx_idx); + } + + #[inline] + fn is_finished(&self) -> bool { + !self + .state_machines + .iter() + .any(|machine| !machine.is_finished()) + } + + #[inline] + fn get_next_ctx_idx(&self) -> usize { + self.start_ctx_idx + self.state_machines.len() + } + + fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + for machine in self.state_machines.iter_mut().rev() { + if machine.is_finished() { + machine.reset(); + machine.put(context)?; + } else { + machine.advance(); + machine.put(context)?; + break; + } + } + Ok(()) + } +} + +#[derive(Debug)] +struct IfStmtStateMachine<'a> { + condition_machine: ConditionStateMachine<'a>, + nested: Vec<&'a dir::DirIfStatement>, + nested_idx: usize, +} + +impl<'a> IfStmtStateMachine<'a> { + fn new(stmt: &'a dir::DirIfStatement, ctx_start_idx: usize) -> Self { + let condition_machine = ConditionStateMachine::new(&stmt.condition, ctx_start_idx); + let nested: Vec<&'a dir::DirIfStatement> = match &stmt.nested { + None => Vec::new(), + Some(nested_stmts) => nested_stmts.iter().collect(), + }; + + Self { + condition_machine, + nested, + nested_idx: 0, + } + } + + fn init( + &self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result, StateMachineError> { + self.condition_machine.init(context)?; + Ok(self + .nested + .first() + .map(|nested| Self::new(nested, self.condition_machine.get_next_ctx_idx()))) + } + + #[inline] + fn is_finished(&self) -> bool { + self.nested_idx + 1 >= self.nested.len() + } + + #[inline] + fn is_condition_machine_finished(&self) -> bool { + self.condition_machine.is_finished() + } + + #[inline] + fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) { + self.condition_machine.destroy(context); + } + + #[inline] + fn advance_condition_machine( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + self.condition_machine.advance(context)?; + Ok(()) + } + + fn advance(&mut self) -> Result, StateMachineError> { + if self.nested.is_empty() { + Ok(None) + } else { + self.nested_idx = (self.nested_idx + 1) % self.nested.len(); + Ok(Some(Self::new( + self.nested + .get(self.nested_idx) + .ok_or(StateMachineError::IndexOutOfBounds( + "in IfStmtStateMachine while advancing", + ))?, + self.condition_machine.get_next_ctx_idx(), + ))) + } + } +} + +#[derive(Debug)] +struct RuleStateMachine<'a> { + connector_selection_data: &'a [(dir::DirValue, Metadata)], + connectors_added: bool, + if_stmt_machines: Vec>, + running_stack: Vec>, +} + +impl<'a> RuleStateMachine<'a> { + fn new( + rule: &'a dir::DirRule, + connector_selection_data: &'a [(dir::DirValue, Metadata)], + ) -> Self { + let mut if_stmt_machines: Vec> = + Vec::with_capacity(rule.statements.len()); + + for stmt in rule.statements.iter().rev() { + if_stmt_machines.push(IfStmtStateMachine::new( + stmt, + connector_selection_data.len(), + )); + } + + Self { + connector_selection_data, + connectors_added: false, + if_stmt_machines, + running_stack: Vec::new(), + } + } + + fn is_finished(&self) -> bool { + self.if_stmt_machines.is_empty() && self.running_stack.is_empty() + } + + fn init_next( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if self.if_stmt_machines.is_empty() || !self.running_stack.is_empty() { + return Ok(()); + } + + if !self.connectors_added { + for (dir_val, metadata) in self.connector_selection_data { + context.push(types::ContextValue::assertion(dir_val, metadata)); + } + self.connectors_added = true; + } + + context.truncate(self.connector_selection_data.len()); + + if let Some(mut next_running) = self.if_stmt_machines.pop() { + while let Some(nested_running) = next_running.init(context)? { + self.running_stack.push(next_running); + next_running = nested_running; + } + + self.running_stack.push(next_running); + } + + Ok(()) + } + + fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + let mut condition_machines_finished = true; + + for stmt_machine in self.running_stack.iter_mut().rev() { + if !stmt_machine.is_condition_machine_finished() { + condition_machines_finished = false; + stmt_machine.advance_condition_machine(context)?; + break; + } else { + stmt_machine.advance_condition_machine(context)?; + } + } + + if !condition_machines_finished { + return Ok(()); + } + + let mut maybe_next_running: Option> = None; + + while let Some(last) = self.running_stack.last_mut() { + if !last.is_finished() { + maybe_next_running = last.advance()?; + break; + } else { + last.destroy(context); + self.running_stack.pop(); + } + } + + if let Some(mut next_running) = maybe_next_running { + while let Some(nested_running) = next_running.init(context)? { + self.running_stack.push(next_running); + next_running = nested_running; + } + + self.running_stack.push(next_running); + } else { + self.init_next(context)?; + } + + Ok(()) + } +} + +#[derive(Debug)] +pub struct RuleContextManager<'a> { + context: types::ConjunctiveContext<'a>, + machine: RuleStateMachine<'a>, + init: bool, +} + +impl<'a> RuleContextManager<'a> { + pub fn new( + rule: &'a dir::DirRule, + connector_selection_data: &'a [(dir::DirValue, Metadata)], + ) -> Self { + Self { + context: Vec::new(), + machine: RuleStateMachine::new(rule, connector_selection_data), + init: false, + } + } + + pub fn advance(&mut self) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init_next(&mut self.context)?; + Ok(Some(&self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&self.context)) + } + } + } + + pub fn advance_mut( + &mut self, + ) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init_next(&mut self.context)?; + Ok(Some(&mut self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&mut self.context)) + } + } + } +} + +#[derive(Debug)] +pub struct ProgramStateMachine<'a> { + rule_machines: Vec>, + current_rule_machine: Option>, + is_init: bool, +} + +impl<'a> ProgramStateMachine<'a> { + pub fn new( + program: &'a dir::DirProgram, + connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>], + ) -> Self { + let mut rule_machines: Vec> = program + .rules + .iter() + .zip(connector_selection_data.iter()) + .rev() + .map(|(rule, connector_selection_data)| { + RuleStateMachine::new(rule, connector_selection_data) + }) + .collect(); + + Self { + current_rule_machine: rule_machines.pop(), + rule_machines, + is_init: false, + } + } + + pub fn is_finished(&self) -> bool { + self.current_rule_machine + .as_ref() + .map_or(true, |rsm| rsm.is_finished()) + && self.rule_machines.is_empty() + } + + pub fn init( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if !self.is_init { + if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.init_next(context)?; + } + self.is_init = true; + } + + Ok(()) + } + + pub fn advance( + &mut self, + context: &mut types::ConjunctiveContext<'a>, + ) -> Result<(), StateMachineError> { + if self + .current_rule_machine + .as_ref() + .map_or(true, |rsm| rsm.is_finished()) + { + self.current_rule_machine = self.rule_machines.pop(); + context.clear(); + if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.init_next(context)?; + } + } else if let Some(rsm) = self.current_rule_machine.as_mut() { + rsm.advance(context)?; + } + + Ok(()) + } +} + +pub struct AnalysisContextManager<'a> { + context: types::ConjunctiveContext<'a>, + machine: ProgramStateMachine<'a>, + init: bool, +} + +impl<'a> AnalysisContextManager<'a> { + pub fn new( + program: &'a dir::DirProgram, + connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>], + ) -> Self { + let machine = ProgramStateMachine::new(program, connector_selection_data); + let context: types::ConjunctiveContext<'a> = Vec::new(); + + Self { + context, + machine, + init: false, + } + } + + pub fn advance(&mut self) -> Result>, StateMachineError> { + if !self.init { + self.init = true; + self.machine.init(&mut self.context)?; + Ok(Some(&self.context)) + } else if self.machine.is_finished() { + Ok(None) + } else { + self.machine.advance(&mut self.context)?; + + if self.machine.is_finished() { + Ok(None) + } else { + Ok(Some(&self.context)) + } + } + } +} + +pub fn make_connector_selection_data( + program: &dir::DirProgram, +) -> Vec> { + program + .rules + .iter() + .map(|rule| { + rule.connector_selection + .get_dir_value_for_analysis(rule.name.clone()) + }) + .collect() +} + +#[cfg(all(test, feature = "ast_parser"))] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::{dirval, frontend::ast, types::DummyOutput}; + + #[test] + fn test_correct_contexts() { + let program_str = r#" + default: ["stripe", "adyen"] + + stripe_first: ["stripe", "adyen"] + { + payment_method = wallet { + payment_method = (card, bank_redirect) { + currency = USD + currency = GBP + } + + payment_method = pay_later { + capture_method = automatic + capture_method = manual + } + } + + payment_method = card { + payment_method = (card, bank_redirect) & capture_method = (automatic, manual) { + currency = (USD, GBP) + } + } + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + let lowered = ast::lowering::lower_program(program).expect("Lowering"); + + let selection_data = make_connector_selection_data(&lowered); + let mut state_machine = ProgramStateMachine::new(&lowered, &selection_data); + let mut ctx: types::ConjunctiveContext<'_> = Vec::new(); + state_machine.init(&mut ctx).expect("State machine init"); + + let expected_contexts: Vec> = vec![ + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = Card), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = Card), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = BankRedirect), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + dirval!(CaptureMethod = Automatic), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Wallet), + dirval!(PaymentMethod = PayLater), + dirval!(CaptureMethod = Manual), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = Card), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Automatic), + dirval!(PaymentCurrency = GBP), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = USD), + ], + vec![ + dirval!("MetadataKey" = "stripe"), + dirval!("MetadataKey" = "adyen"), + dirval!(PaymentMethod = Card), + dirval!(PaymentMethod = BankRedirect), + dirval!(CaptureMethod = Manual), + dirval!(PaymentCurrency = GBP), + ], + ]; + + let mut expected_idx = 0usize; + while !state_machine.is_finished() { + let values = ctx + .iter() + .flat_map(|c| match c.value { + types::CtxValueKind::Assertion(val) => vec![val], + types::CtxValueKind::Negation(vals) => vals.iter().collect(), + }) + .collect::>(); + assert_eq!( + values, + expected_contexts[expected_idx] + .iter() + .collect::>() + ); + expected_idx += 1; + state_machine + .advance(&mut ctx) + .expect("State Machine advance"); + } + + assert_eq!(expected_idx, 14); + + let mut ctx_manager = AnalysisContextManager::new(&lowered, &selection_data); + expected_idx = 0; + while let Some(ctx) = ctx_manager.advance().expect("Context Manager Context") { + let values = ctx + .iter() + .flat_map(|c| match c.value { + types::CtxValueKind::Assertion(val) => vec![val], + types::CtxValueKind::Negation(vals) => vals.iter().collect(), + }) + .collect::>(); + assert_eq!( + values, + expected_contexts[expected_idx] + .iter() + .collect::>() + ); + expected_idx += 1; + } + + assert_eq!(expected_idx, 14); + } +} diff --git a/crates/euclid/src/dssa/truth.rs b/crates/euclid/src/dssa/truth.rs new file mode 100644 index 000000000000..17e6e728e68f --- /dev/null +++ b/crates/euclid/src/dssa/truth.rs @@ -0,0 +1,29 @@ +use euclid_macros::knowledge; +use once_cell::sync::Lazy; + +use crate::dssa::graph; + +pub static ANALYSIS_GRAPH: Lazy> = Lazy::new(|| { + knowledge! {crate + // Payment Method should be `Card` for a CardType to be present + PaymentMethod(Card) ->> CardType(any); + + // Payment Method should be `PayLater` for a PayLaterType to be present + PaymentMethod(PayLater) ->> PayLaterType(any); + + // Payment Method should be `Wallet` for a WalletType to be present + PaymentMethod(Wallet) ->> WalletType(any); + + // Payment Method should be `BankRedirect` for a BankRedirectType to + // be present + PaymentMethod(BankRedirect) ->> BankRedirectType(any); + + // Payment Method should be `BankTransfer` for a BankTransferType to + // be present + PaymentMethod(BankTransfer) ->> BankTransferType(any); + + // Payment Method should be `GiftCard` for a GiftCardType to + // be present + PaymentMethod(GiftCard) ->> GiftCardType(any); + } +}); diff --git a/crates/euclid/src/dssa/types.rs b/crates/euclid/src/dssa/types.rs new file mode 100644 index 000000000000..4070e0825ef7 --- /dev/null +++ b/crates/euclid/src/dssa/types.rs @@ -0,0 +1,158 @@ +use std::fmt; + +use serde::Serialize; + +use crate::{ + dssa::{self, graph}, + frontend::{ast, dir}, + types::{DataType, EuclidValue, Metadata}, +}; + +pub trait EuclidAnalysable: Sized { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(dir::DirValue, Metadata)>; +} + +#[derive(Debug, Clone)] +pub enum CtxValueKind<'a> { + Assertion(&'a dir::DirValue), + Negation(&'a [dir::DirValue]), +} + +impl<'a> CtxValueKind<'a> { + pub fn get_assertion(&self) -> Option<&dir::DirValue> { + if let Self::Assertion(val) = self { + Some(val) + } else { + None + } + } + + pub fn get_negation(&self) -> Option<&[dir::DirValue]> { + if let Self::Negation(vals) = self { + Some(vals) + } else { + None + } + } + + pub fn get_key(&self) -> Option { + match self { + Self::Assertion(val) => Some(val.get_key()), + Self::Negation(vals) => vals.first().map(|v| (*v).get_key()), + } + } +} + +#[derive(Debug, Clone)] +pub struct ContextValue<'a> { + pub value: CtxValueKind<'a>, + pub metadata: &'a Metadata, +} + +impl<'a> ContextValue<'a> { + #[inline] + pub fn assertion(value: &'a dir::DirValue, metadata: &'a Metadata) -> Self { + Self { + value: CtxValueKind::Assertion(value), + metadata, + } + } + + #[inline] + pub fn negation(values: &'a [dir::DirValue], metadata: &'a Metadata) -> Self { + Self { + value: CtxValueKind::Negation(values), + metadata, + } + } +} + +pub type ConjunctiveContext<'a> = Vec>; + +#[derive(Clone, Serialize)] +pub enum AnalyzeResult { + AllOk, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +pub struct AnalysisError { + #[serde(flatten)] + pub error_type: AnalysisErrorType, + pub metadata: Metadata, +} +impl fmt::Display for AnalysisError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.error_type.fmt(f) + } +} +#[derive(Debug, Clone, Serialize)] +pub struct ValueData { + pub value: dir::DirValue, + pub metadata: Metadata, +} + +#[derive(Debug, Clone, Serialize, thiserror::Error)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum AnalysisErrorType { + #[error("Invalid program key given: '{0}'")] + InvalidKey(String), + #[error("Invalid variant '{got}' received for key '{key}'")] + InvalidVariant { + key: String, + expected: Vec, + got: String, + }, + #[error( + "Invalid data type for value '{}' (expected {expected}, got {got})", + key + )] + InvalidType { + key: String, + expected: DataType, + got: DataType, + }, + #[error("Invalid comparison '{operator:?}' for value type {value_type}")] + InvalidComparison { + operator: ast::ComparisonType, + value_type: DataType, + }, + #[error("Invalid value received for length as '{value}: {:?}'", message)] + InvalidValue { + key: dir::DirKeyKind, + value: String, + message: Option, + }, + #[error("Conflicting assertions received for key '{}'", .key.kind)] + ConflictingAssertions { + key: dir::DirKey, + values: Vec, + }, + + #[error("Key '{}' exhaustively negated", .key.kind)] + ExhaustiveNegation { + key: dir::DirKey, + metadata: Vec, + }, + #[error("The condition '{value}' was asserted and negated in the same condition")] + NegatedAssertion { + value: dir::DirValue, + assertion_metadata: Metadata, + negation_metadata: Metadata, + }, + #[error("Graph analysis error: {0:#?}")] + GraphAnalysis(graph::AnalysisError, graph::Memoization), + #[error("State machine error")] + StateMachine(dssa::state_machine::StateMachineError), + #[error("Unsupported program key '{0}'")] + UnsupportedProgramKey(dir::DirKeyKind), + #[error("Ran into an unimplemented feature")] + NotImplemented, + #[error("The payment method type is not supported under the payment method")] + NotSupported, +} + +#[derive(Debug, Clone)] +pub enum ValueType { + EnumVariants(Vec), + Number, +} diff --git a/crates/euclid/src/dssa/utils.rs b/crates/euclid/src/dssa/utils.rs new file mode 100644 index 000000000000..df4ff82cbdb7 --- /dev/null +++ b/crates/euclid/src/dssa/utils.rs @@ -0,0 +1 @@ +pub struct Unpacker; diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs new file mode 100644 index 000000000000..4188860ab90f --- /dev/null +++ b/crates/euclid/src/enums.rs @@ -0,0 +1,191 @@ +pub use common_enums::{ + AuthenticationType, CaptureMethod, CardNetwork, Country, Currency, + FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, +}; +use serde::{Deserialize, Serialize}; +use strum::VariantNames; + +pub trait CollectVariants { + fn variants>() -> T; +} +macro_rules! collect_variants { + ($the_enum:ident) => { + impl $crate::enums::CollectVariants for $the_enum { + fn variants() -> T + where + T: FromIterator, + { + Self::VARIANTS.iter().map(|s| String::from(*s)).collect() + } + } + }; +} + +pub(crate) use collect_variants; + +collect_variants!(PaymentMethod); +collect_variants!(PaymentType); +collect_variants!(MandateType); +collect_variants!(MandateAcceptanceType); +collect_variants!(PaymentMethodType); +collect_variants!(CardNetwork); +collect_variants!(AuthenticationType); +collect_variants!(CaptureMethod); +collect_variants!(Currency); +collect_variants!(Country); +collect_variants!(Connector); +collect_variants!(SetupFutureUsage); + +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum Connector { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + DummyConnector1, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + DummyConnector2, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + DummyConnector3, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "stripe_test")] + #[strum(serialize = "stripe_test")] + DummyConnector4, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "adyen_test")] + #[strum(serialize = "adyen_test")] + DummyConnector5, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "checkout_test")] + #[strum(serialize = "checkout_test")] + DummyConnector6, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "paypal_test")] + #[strum(serialize = "paypal_test")] + DummyConnector7, + Aci, + Adyen, + Airwallex, + Authorizedotnet, + Bitpay, + Bambora, + Bluesnap, + Boku, + Braintree, + Cashtocode, + Checkout, + Coinbase, + Cryptopay, + Cybersource, + Dlocal, + Fiserv, + Forte, + Globalpay, + Globepay, + Gocardless, + Helcim, + Iatapay, + Klarna, + Mollie, + Multisafepay, + Nexinets, + Nmi, + Noon, + Nuvei, + Opennode, + Payme, + Paypal, + Payu, + Powertranz, + Rapyd, + Shift4, + Square, + Stax, + Stripe, + Trustpay, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum MandateAcceptanceType { + Online, + Offline, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PaymentType { + SetupMandate, + NonMandate, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum MandateType { + SingleUse, + MultiUse, +} diff --git a/crates/euclid/src/frontend.rs b/crates/euclid/src/frontend.rs new file mode 100644 index 000000000000..17fc8f3502e2 --- /dev/null +++ b/crates/euclid/src/frontend.rs @@ -0,0 +1,3 @@ +pub mod ast; +pub mod dir; +pub mod vir; diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs new file mode 100644 index 000000000000..3adb06ab1873 --- /dev/null +++ b/crates/euclid/src/frontend/ast.rs @@ -0,0 +1,156 @@ +pub mod lowering; +#[cfg(feature = "ast_parser")] +pub mod parser; + +use serde::{Deserialize, Serialize}; + +use crate::{ + enums::Connector, + types::{DataType, Metadata}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct ConnectorChoice { + pub connector: Connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MetadataValue { + pub key: String, + pub value: String, +} + +/// Represents a value in the DSL +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum ValueType { + /// Represents a number literal + Number(i64), + /// Represents an enum variant + EnumVariant(String), + /// Represents a Metadata variant + MetadataVariant(MetadataValue), + /// Represents a arbitrary String value + StrValue(String), + /// Represents an array of numbers. This is basically used for + /// "one of the given numbers" operations + /// eg: payment.method.amount = (1, 2, 3) + NumberArray(Vec), + /// Similar to NumberArray but for enum variants + /// eg: payment.method.cardtype = (debit, credit) + EnumVariantArray(Vec), + /// Like a number array but can include comparisons. Useful for + /// conditions like "500 < amount < 1000" + /// eg: payment.amount = (> 500, < 1000) + NumberComparisonArray(Vec), +} + +impl ValueType { + pub fn get_type(&self) -> DataType { + match self { + Self::Number(_) => DataType::Number, + Self::StrValue(_) => DataType::StrValue, + Self::MetadataVariant(_) => DataType::MetadataValue, + Self::EnumVariant(_) => DataType::EnumVariant, + Self::NumberComparisonArray(_) => DataType::Number, + Self::NumberArray(_) => DataType::Number, + Self::EnumVariantArray(_) => DataType::EnumVariant, + } + } +} + +/// Represents a number comparison for "NumberComparisonArrayValue" +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberComparison { + pub comparison_type: ComparisonType, + pub number: i64, +} + +/// Conditional comparison type +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ComparisonType { + Equal, + NotEqual, + LessThan, + LessThanEqual, + GreaterThan, + GreaterThanEqual, +} + +/// Represents a single comparison condition. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Comparison { + /// The left hand side which will always be a domain input identifier like "payment.method.cardtype" + pub lhs: String, + /// The comparison operator + pub comparison: ComparisonType, + /// The value to compare against + pub value: ValueType, + /// Additional metadata that the Static Analyzer and Backend does not touch. + /// This can be used to store useful information for the frontend and is required for communication + /// between the static analyzer and the frontend. + pub metadata: Metadata, +} + +/// Represents all the conditions of an IF statement +/// eg: +/// +/// ```text +/// payment.method = card & payment.method.cardtype = debit & payment.method.network = diners +/// ``` +pub type IfCondition = Vec; + +/// Represents an IF statement with conditions and optional nested IF statements +/// +/// ```text +/// payment.method = card { +/// payment.method.cardtype = (credit, debit) { +/// payment.method.network = (amex, rupay, diners) +/// } +/// } +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IfStatement { + pub condition: IfCondition, + pub nested: Option>, +} + +/// Represents a rule +/// +/// ```text +/// rule_name: [stripe, adyen, checkout] +/// { +/// payment.method = card { +/// payment.method.cardtype = (credit, debit) { +/// payment.method.network = (amex, rupay, diners) +/// } +/// +/// payment.method.cardtype = credit +/// } +/// } +/// ``` + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rule { + pub name: String, + #[serde(alias = "routingOutput")] + pub connector_selection: O, + pub statements: Vec, +} + +/// The program, having a default connector selection and +/// a bunch of rules. Also can hold arbitrary metadata. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Program { + pub default_selection: O, + pub rules: Vec>, + pub metadata: Metadata, +} diff --git a/crates/euclid/src/frontend/ast/lowering.rs b/crates/euclid/src/frontend/ast/lowering.rs new file mode 100644 index 000000000000..ffce88a35db6 --- /dev/null +++ b/crates/euclid/src/frontend/ast/lowering.rs @@ -0,0 +1,377 @@ +//! Analysis for the Lowering logic in ast +//! +//!Certain functions that can be used to perform the complete lowering of ast to dir. +//!This includes lowering of enums, numbers, strings as well as Comparison logics. + +use std::str::FromStr; + +use crate::{ + dssa::types::{AnalysisError, AnalysisErrorType}, + enums::CollectVariants, + frontend::{ + ast, + dir::{self, enums as dir_enums, EuclidDirFilter}, + }, + types::{self, DataType}, +}; + +/// lowers the provided key (enum variant) & value to the respective DirValue +/// +/// For example +/// ```notrust +/// CardType = Visa +/// ```notrust +/// +/// This serves for the purpose were we have the DirKey as an explicit Enum type and value as one +/// of the member of the same Enum. +/// So particularly it lowers a predefined Enum from DirKey to an Enum of DirValue. + +macro_rules! lower_enum { + ($key:ident, $value:ident) => { + match $value { + ast::ValueType::EnumVariant(ev) => Ok(vec![dir::DirValue::$key( + dir_enums::$key::from_str(&ev).map_err(|_| AnalysisErrorType::InvalidVariant { + key: dir::DirKeyKind::$key.to_string(), + got: ev, + expected: dir_enums::$key::variants(), + })?, + )]), + + ast::ValueType::EnumVariantArray(eva) => eva + .into_iter() + .map(|ev| { + Ok(dir::DirValue::$key( + dir_enums::$key::from_str(&ev).map_err(|_| { + AnalysisErrorType::InvalidVariant { + key: dir::DirKeyKind::$key.to_string(), + got: ev, + expected: dir_enums::$key::variants(), + } + })?, + )) + }) + .collect(), + + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::EnumVariant, + got: $value.get_type(), + }), + } + }; +} + +/// lowers the provided key for a numerical value +/// +/// For example +/// ```notrust +/// payment_amount = 17052001 +/// ```notrust +/// This is for the cases in which there are numerical values involved and they are lowered +/// accordingly on basis of the supplied key, currently payment_amount is the only key having this +/// use case + +macro_rules! lower_number { + ($key:ident, $value:ident, $comp:ident) => { + match $value { + ast::ValueType::Number(num) => Ok(vec![dir::DirValue::$key(types::NumValue { + number: num, + refinement: $comp.into(), + })]), + + ast::ValueType::NumberArray(na) => na + .into_iter() + .map(|num| { + Ok(dir::DirValue::$key(types::NumValue { + number: num, + refinement: $comp.clone().into(), + })) + }) + .collect(), + + ast::ValueType::NumberComparisonArray(nca) => nca + .into_iter() + .map(|nc| { + Ok(dir::DirValue::$key(types::NumValue { + number: nc.number, + refinement: nc.comparison_type.into(), + })) + }) + .collect(), + + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::Number, + got: $value.get_type(), + }), + } + }; +} + +/// lowers the provided key & value to the respective DirValue +/// +/// For example +/// ```notrust +/// card_bin = "123456" +/// ```notrust +/// +/// This serves for the purpose were we have the DirKey as Card_bin and value as an arbitrary string +/// So particularly it lowers an arbitrary value to a predefined key. + +macro_rules! lower_str { + ($key:ident, $value:ident $(, $validation_closure:expr)?) => { + match $value { + ast::ValueType::StrValue(st) => { + $($validation_closure(&st)?;)? + Ok(vec![dir::DirValue::$key(types::StrValue { value: st })]) + } + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::StrValue, + got: $value.get_type(), + }), + } + }; +} + +macro_rules! lower_metadata { + ($key:ident, $value:ident) => { + match $value { + ast::ValueType::MetadataVariant(md) => { + Ok(vec![dir::DirValue::$key(types::MetadataValue { + key: md.key, + value: md.value, + })]) + } + _ => Err(AnalysisErrorType::InvalidType { + key: dir::DirKeyKind::$key.to_string(), + expected: DataType::MetadataValue, + got: $value.get_type(), + }), + } + }; +} +/// lowers the comparison operators for different subtle value types present +/// by throwing required errors for comparisons that can't be performed for a certain value type +/// for example +/// can't have greater/less than operations on enum types + +fn lower_comparison_inner( + comp: ast::Comparison, +) -> Result, AnalysisErrorType> { + let key_enum = dir::DirKeyKind::from_str(comp.lhs.as_str()) + .map_err(|_| AnalysisErrorType::InvalidKey(comp.lhs.clone()))?; + + if !O::is_key_allowed(&key_enum) { + return Err(AnalysisErrorType::InvalidKey(key_enum.to_string())); + } + + match (&comp.comparison, &comp.value) { + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::EnumVariant(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::EnumVariant, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::NumberArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::Number, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::EnumVariantArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::EnumVariant, + })?; + } + + ( + ast::ComparisonType::LessThan + | ast::ComparisonType::GreaterThan + | ast::ComparisonType::GreaterThanEqual + | ast::ComparisonType::LessThanEqual, + ast::ValueType::NumberComparisonArray(_), + ) => { + Err(AnalysisErrorType::InvalidComparison { + operator: comp.comparison.clone(), + value_type: DataType::Number, + })?; + } + + _ => {} + } + + let value = comp.value; + let comparison = comp.comparison; + + match key_enum { + dir::DirKeyKind::PaymentMethod => lower_enum!(PaymentMethod, value), + + dir::DirKeyKind::CardType => lower_enum!(CardType, value), + + dir::DirKeyKind::CardNetwork => lower_enum!(CardNetwork, value), + + dir::DirKeyKind::PayLaterType => lower_enum!(PayLaterType, value), + + dir::DirKeyKind::WalletType => lower_enum!(WalletType, value), + + dir::DirKeyKind::BankDebitType => lower_enum!(BankDebitType, value), + + dir::DirKeyKind::BankRedirectType => lower_enum!(BankRedirectType, value), + + dir::DirKeyKind::CryptoType => lower_enum!(CryptoType, value), + + dir::DirKeyKind::PaymentType => lower_enum!(PaymentType, value), + + dir::DirKeyKind::MandateType => lower_enum!(MandateType, value), + + dir::DirKeyKind::MandateAcceptanceType => lower_enum!(MandateAcceptanceType, value), + + dir::DirKeyKind::RewardType => lower_enum!(RewardType, value), + + dir::DirKeyKind::PaymentCurrency => lower_enum!(PaymentCurrency, value), + + dir::DirKeyKind::AuthenticationType => lower_enum!(AuthenticationType, value), + + dir::DirKeyKind::CaptureMethod => lower_enum!(CaptureMethod, value), + + dir::DirKeyKind::BusinessCountry => lower_enum!(BusinessCountry, value), + + dir::DirKeyKind::BillingCountry => lower_enum!(BillingCountry, value), + + dir::DirKeyKind::SetupFutureUsage => lower_enum!(SetupFutureUsage, value), + + dir::DirKeyKind::UpiType => lower_enum!(UpiType, value), + + dir::DirKeyKind::VoucherType => lower_enum!(VoucherType, value), + + dir::DirKeyKind::GiftCardType => lower_enum!(GiftCardType, value), + + dir::DirKeyKind::BankTransferType => lower_enum!(BankTransferType, value), + + dir::DirKeyKind::CardRedirectType => lower_enum!(CardRedirectType, value), + + dir::DirKeyKind::CardBin => { + let validation_closure = |st: &String| -> Result<(), AnalysisErrorType> { + if st.len() == 6 && st.chars().all(|x| x.is_ascii_digit()) { + Ok(()) + } else { + Err(AnalysisErrorType::InvalidValue { + key: dir::DirKeyKind::CardBin, + value: st.clone(), + message: Some("Expected 6 digits".to_string()), + }) + } + }; + lower_str!(CardBin, value, validation_closure) + } + + dir::DirKeyKind::BusinessLabel => lower_str!(BusinessLabel, value), + + dir::DirKeyKind::MetaData => lower_metadata!(MetaData, value), + + dir::DirKeyKind::PaymentAmount => lower_number!(PaymentAmount, value, comparison), + + dir::DirKeyKind::Connector => Err(AnalysisErrorType::InvalidKey( + dir::DirKeyKind::Connector.to_string(), + )), + } +} + +/// returns all the comparison values by matching them appropriately to ComparisonTypes and in turn +/// calls the lower_comparison_inner function +fn lower_comparison( + comp: ast::Comparison, +) -> Result { + let metadata = comp.metadata.clone(); + let logic = match &comp.comparison { + ast::ComparisonType::Equal => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::NotEqual => dir::DirComparisonLogic::NegativeConjunction, + ast::ComparisonType::LessThan => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::LessThanEqual => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::GreaterThanEqual => dir::DirComparisonLogic::PositiveDisjunction, + ast::ComparisonType::GreaterThan => dir::DirComparisonLogic::PositiveDisjunction, + }; + let values = lower_comparison_inner::(comp).map_err(|etype| AnalysisError { + error_type: etype, + metadata: metadata.clone(), + })?; + + Ok(dir::DirComparison { + values, + logic, + metadata, + }) +} + +/// lowers the if statement accordingly with a condition and following nested if statements (if +/// present) +fn lower_if_statement( + stmt: ast::IfStatement, +) -> Result { + Ok(dir::DirIfStatement { + condition: stmt + .condition + .into_iter() + .map(lower_comparison::) + .collect::>()?, + nested: stmt + .nested + .map(|n| n.into_iter().map(lower_if_statement::).collect()) + .transpose()?, + }) +} + +/// lowers the rules supplied accordingly to DirRule struct by specifying the rule_name, +/// connector_selection and statements that are a bunch of if statements +pub fn lower_rule( + rule: ast::Rule, +) -> Result, AnalysisError> { + Ok(dir::DirRule { + name: rule.name, + connector_selection: rule.connector_selection, + statements: rule + .statements + .into_iter() + .map(lower_if_statement::) + .collect::>()?, + }) +} + +/// uses the above rules and lowers the whole ast Program into DirProgram by specifying +/// default_selection that is ast ConnectorSelection, a vector of DirRules and clones the metadata +/// whatever comes in the ast_program +pub fn lower_program( + program: ast::Program, +) -> Result, AnalysisError> { + Ok(dir::DirProgram { + default_selection: program.default_selection, + rules: program + .rules + .into_iter() + .map(lower_rule) + .collect::>()?, + metadata: program.metadata, + }) +} diff --git a/crates/euclid/src/frontend/ast/parser.rs b/crates/euclid/src/frontend/ast/parser.rs new file mode 100644 index 000000000000..8b2f717a8688 --- /dev/null +++ b/crates/euclid/src/frontend/ast/parser.rs @@ -0,0 +1,441 @@ +use nom::{ + branch, bytes::complete, character::complete as pchar, combinator, error, multi, sequence, +}; + +use crate::{frontend::ast, types::DummyOutput}; +pub type ParseResult = nom::IResult>; + +pub enum EuclidError { + InvalidPercentage(String), + InvalidConnector(String), + InvalidOperator(String), + InvalidNumber(String), +} + +pub trait EuclidParsable: Sized { + fn parse_output(input: &str) -> ParseResult<&str, Self>; +} + +impl EuclidParsable for DummyOutput { + fn parse_output(input: &str) -> ParseResult<&str, Self> { + let string_w = sequence::delimited( + skip_ws(complete::tag("\"")), + complete::take_while(|c| c != '"'), + skip_ws(complete::tag("\"")), + ); + let full_sequence = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + sequence::delimited( + skip_ws(complete::tag("\"")), + complete::take_while(|c| c != '"'), + skip_ws(complete::tag("\"")), + ), + )); + let sequence = sequence::pair(string_w, full_sequence); + error::context( + "dummy_strings", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("[")), + sequence, + skip_ws(complete::tag("]")), + ), + |out: (&str, Vec<&str>)| { + let mut first = out.1; + first.insert(0, out.0); + let v = first.iter().map(|s| s.to_string()).collect(); + Self { outputs: v } + }, + ), + )(input) + } +} +pub fn skip_ws<'a, F: 'a, O>(inner: F) -> impl FnMut(&'a str) -> ParseResult<&str, O> +where + F: FnMut(&'a str) -> ParseResult<&str, O>, +{ + sequence::preceded(pchar::multispace0, inner) +} + +pub fn num_i64(input: &str) -> ParseResult<&str, i64> { + error::context( + "num_i32", + combinator::map_res( + complete::take_while1(|c: char| c.is_ascii_digit()), + |o: &str| { + o.parse::() + .map_err(|_| EuclidError::InvalidNumber(o.to_string())) + }, + ), + )(input) +} + +pub fn string_str(input: &str) -> ParseResult<&str, String> { + error::context( + "String", + combinator::map( + sequence::delimited( + complete::tag("\""), + complete::take_while1(|c: char| c != '"'), + complete::tag("\""), + ), + |val: &str| val.to_string(), + ), + )(input) +} + +pub fn identifier(input: &str) -> ParseResult<&str, String> { + error::context( + "identifier", + combinator::map( + sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + ), + |out: (&str, &str)| out.0.to_string() + out.1, + ), + )(input) +} +pub fn percentage(input: &str) -> ParseResult<&str, u8> { + error::context( + "volume_split_percentage", + combinator::map_res( + sequence::terminated( + complete::take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), + complete::tag("%"), + ), + |o: &str| { + o.parse::() + .map_err(|_| EuclidError::InvalidPercentage(o.to_string())) + }, + ), + )(input) +} + +pub fn number_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "number_value", + combinator::map(num_i64, ast::ValueType::Number), + )(input) +} + +pub fn str_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "str_value", + combinator::map(string_str, ast::ValueType::StrValue), + )(input) +} +pub fn enum_value_string(input: &str) -> ParseResult<&str, String> { + combinator::map( + sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + ), + |out: (&str, &str)| out.0.to_string() + out.1, + )(input) +} + +pub fn enum_variant_value(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "enum_variant_value", + combinator::map(enum_value_string, ast::ValueType::EnumVariant), + )(input) +} + +pub fn number_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(num_i64), + )); + + let full_sequence = sequence::pair(skip_ws(num_i64), many_with_comma); + + error::context( + "number_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (i64, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::NumberArray(rest) + }, + ), + )(input) +} + +pub fn enum_variant_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(enum_value_string), + )); + + let full_sequence = sequence::pair(skip_ws(enum_value_string), many_with_comma); + + error::context( + "enum_variant_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (String, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::EnumVariantArray(rest) + }, + ), + )(input) +} + +pub fn number_comparison(input: &str) -> ParseResult<&str, ast::NumberComparison> { + let operator = combinator::map_res( + branch::alt(( + complete::tag(">="), + complete::tag("<="), + complete::tag(">"), + complete::tag("<"), + )), + |s: &str| match s { + ">=" => Ok(ast::ComparisonType::GreaterThanEqual), + "<=" => Ok(ast::ComparisonType::LessThanEqual), + ">" => Ok(ast::ComparisonType::GreaterThan), + "<" => Ok(ast::ComparisonType::LessThan), + _ => Err(EuclidError::InvalidOperator(s.to_string())), + }, + ); + + error::context( + "number_comparison", + combinator::map( + sequence::pair(operator, num_i64), + |tup: (ast::ComparisonType, i64)| ast::NumberComparison { + comparison_type: tup.0, + number: tup.1, + }, + ), + )(input) +} + +pub fn number_comparison_array_value(input: &str) -> ParseResult<&str, ast::ValueType> { + let many_with_comma = multi::many0(sequence::preceded( + skip_ws(complete::tag(",")), + skip_ws(number_comparison), + )); + + let full_sequence = sequence::pair(skip_ws(number_comparison), many_with_comma); + + error::context( + "number_comparison_array_value", + combinator::map( + sequence::delimited( + skip_ws(complete::tag("(")), + full_sequence, + skip_ws(complete::tag(")")), + ), + |tup: (ast::NumberComparison, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + ast::ValueType::NumberComparisonArray(rest) + }, + ), + )(input) +} + +pub fn value_type(input: &str) -> ParseResult<&str, ast::ValueType> { + error::context( + "value_type", + branch::alt(( + number_value, + enum_variant_value, + enum_variant_array_value, + number_array_value, + number_comparison_array_value, + str_value, + )), + )(input) +} + +pub fn comparison_type(input: &str) -> ParseResult<&str, ast::ComparisonType> { + error::context( + "comparison_operator", + combinator::map_res( + branch::alt(( + complete::tag("/="), + complete::tag(">="), + complete::tag("<="), + complete::tag("="), + complete::tag(">"), + complete::tag("<"), + )), + |s: &str| match s { + "/=" => Ok(ast::ComparisonType::NotEqual), + ">=" => Ok(ast::ComparisonType::GreaterThanEqual), + "<=" => Ok(ast::ComparisonType::LessThanEqual), + "=" => Ok(ast::ComparisonType::Equal), + ">" => Ok(ast::ComparisonType::GreaterThan), + "<" => Ok(ast::ComparisonType::LessThan), + _ => Err(EuclidError::InvalidOperator(s.to_string())), + }, + ), + )(input) +} + +pub fn comparison(input: &str) -> ParseResult<&str, ast::Comparison> { + error::context( + "condition", + combinator::map( + sequence::tuple(( + skip_ws(complete::take_while1(|c: char| { + c.is_ascii_alphabetic() || c == '.' || c == '_' + })), + skip_ws(comparison_type), + skip_ws(value_type), + )), + |tup: (&str, ast::ComparisonType, ast::ValueType)| ast::Comparison { + lhs: tup.0.to_string(), + comparison: tup.1, + value: tup.2, + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} + +pub fn arbitrary_comparison(input: &str) -> ParseResult<&str, ast::Comparison> { + error::context( + "condition", + combinator::map( + sequence::tuple(( + skip_ws(string_str), + skip_ws(comparison_type), + skip_ws(string_str), + )), + |tup: (String, ast::ComparisonType, String)| ast::Comparison { + lhs: "metadata".to_string(), + comparison: tup.1, + value: ast::ValueType::MetadataVariant(ast::MetadataValue { + key: tup.0, + value: tup.2, + }), + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} + +pub fn comparison_array(input: &str) -> ParseResult<&str, Vec> { + let many_with_ampersand = error::context( + "many_with_amp", + multi::many0(sequence::preceded(skip_ws(complete::tag("&")), comparison)), + ); + + let full_sequence = sequence::pair( + skip_ws(branch::alt((comparison, arbitrary_comparison))), + many_with_ampersand, + ); + + error::context( + "comparison_array", + combinator::map( + full_sequence, + |tup: (ast::Comparison, Vec)| { + let mut rest = tup.1; + rest.insert(0, tup.0); + rest + }, + ), + )(input) +} + +pub fn if_statement(input: &str) -> ParseResult<&str, ast::IfStatement> { + let nested_block = sequence::delimited( + skip_ws(complete::tag("{")), + multi::many0(if_statement), + skip_ws(complete::tag("}")), + ); + + error::context( + "if_statement", + combinator::map( + sequence::pair(comparison_array, combinator::opt(nested_block)), + |tup: (ast::IfCondition, Option>)| ast::IfStatement { + condition: tup.0, + nested: tup.1, + }, + ), + )(input) +} + +pub fn rule_conditions_array(input: &str) -> ParseResult<&str, Vec> { + error::context( + "rules_array", + sequence::delimited( + skip_ws(complete::tag("{")), + multi::many1(if_statement), + skip_ws(complete::tag("}")), + ), + )(input) +} + +pub fn rule(input: &str) -> ParseResult<&str, ast::Rule> { + let rule_name = error::context( + "rule_name", + combinator::map( + skip_ws(sequence::pair( + complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + )), + |out: (&str, &str)| out.0.to_string() + out.1, + ), + ); + + let connector_selection = error::context( + "parse_output", + sequence::preceded(skip_ws(complete::tag(":")), output), + ); + + error::context( + "rule", + combinator::map( + sequence::tuple((rule_name, connector_selection, rule_conditions_array)), + |tup: (String, O, Vec)| ast::Rule { + name: tup.0, + connector_selection: tup.1, + statements: tup.2, + }, + ), + )(input) +} + +pub fn output(input: &str) -> ParseResult<&str, O> { + O::parse_output(input) +} + +pub fn default_output(input: &str) -> ParseResult<&str, O> { + error::context( + "default_output", + sequence::preceded( + sequence::pair(skip_ws(complete::tag("default")), skip_ws(pchar::char(':'))), + skip_ws(output), + ), + )(input) +} + +pub fn program(input: &str) -> ParseResult<&str, ast::Program> { + error::context( + "program", + combinator::map( + sequence::pair(default_output, multi::many1(skip_ws(rule::))), + |tup: (O, Vec>)| ast::Program { + default_selection: tup.0, + rules: tup.1, + metadata: std::collections::HashMap::new(), + }, + ), + )(input) +} diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs new file mode 100644 index 000000000000..7f2fc252d232 --- /dev/null +++ b/crates/euclid/src/frontend/dir.rs @@ -0,0 +1,803 @@ +//! Domain Intermediate Representation +pub mod enums; +pub mod lowering; +pub mod transformers; + +use strum::IntoEnumIterator; + +use crate::{enums as euclid_enums, frontend::ast, types}; + +#[macro_export] +#[cfg(feature = "connector_choice_mca_id")] +macro_rules! dirval { + (Connector = $name:ident) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + }, + )) + }; + + ($key:ident = $val:ident) => {{ + pub use $crate::frontend::dir::enums::*; + + $crate::frontend::dir::DirValue::$key($key::$val) + }}; + + ($key:ident = $num:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::NumValue { + number: $num, + refinement: None, + }) + }}; + + ($key:ident s= $str:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::StrValue { + value: $str.to_string(), + }) + }}; + + ($key:literal = $str:literal) => {{ + $crate::frontend::dir::DirValue::MetaData($crate::types::MetadataValue { + key: $key.to_string(), + value: $str.to_string(), + }) + }}; +} + +#[macro_export] +#[cfg(not(feature = "connector_choice_mca_id"))] +macro_rules! dirval { + (Connector = $name:ident) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + sub_label: None, + }, + )) + }; + + (Connector = ($name:ident, $sub_label:literal)) => { + $crate::frontend::dir::DirValue::Connector(Box::new( + $crate::frontend::ast::ConnectorChoice { + connector: $crate::frontend::dir::enums::Connector::$name, + sub_label: Some($sub_label.to_string()), + }, + )) + }; + + ($key:ident = $val:ident) => {{ + pub use $crate::frontend::dir::enums::*; + + $crate::frontend::dir::DirValue::$key($key::$val) + }}; + + ($key:ident = $num:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::NumValue { + number: $num, + refinement: None, + }) + }}; + + ($key:ident s= $str:literal) => {{ + $crate::frontend::dir::DirValue::$key($crate::types::StrValue { + value: $str.to_string(), + }) + }}; + ($key:literal = $str:literal) => {{ + $crate::frontend::dir::DirValue::MetaData($crate::types::MetadataValue { + key: $key.to_string(), + value: $str.to_string(), + }) + }}; +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] +pub struct DirKey { + pub kind: DirKeyKind, + pub value: Option, +} + +impl DirKey { + pub fn new(kind: DirKeyKind, value: Option) -> Self { + Self { kind, value } + } +} + +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + serde::Serialize, + strum::Display, + strum::EnumIter, + strum::EnumVariantNames, + strum::EnumString, + strum::EnumMessage, + strum::EnumProperty, +)] +pub enum DirKeyKind { + #[strum( + serialize = "payment_method", + detailed_message = "Different modes of payment - eg. cards, wallets, banks", + props(Category = "Payment Methods") + )] + #[serde(rename = "payment_method")] + PaymentMethod, + #[strum( + serialize = "card_bin", + detailed_message = "First 4 to 6 digits of a payment card number", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_bin")] + CardBin, + #[strum( + serialize = "card_type", + detailed_message = "Type of the payment card - eg. credit, debit", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_type")] + CardType, + #[strum( + serialize = "card_network", + detailed_message = "Network that facilitates payment card transactions", + props(Category = "Payment Methods") + )] + #[serde(rename = "card_network")] + CardNetwork, + #[strum( + serialize = "pay_later", + detailed_message = "Supported types of Pay Later payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "pay_later")] + PayLaterType, + #[strum( + serialize = "gift_card", + detailed_message = "Supported types of Gift Card payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "gift_card")] + GiftCardType, + #[strum( + serialize = "mandate_acceptance_type", + detailed_message = "Mode of customer acceptance for mandates - online and offline", + props(Category = "Payments") + )] + #[serde(rename = "mandate_acceptance_type")] + MandateAcceptanceType, + #[strum( + serialize = "mandate_type", + detailed_message = "Type of mandate acceptance - single use and multi use", + props(Category = "Payments") + )] + #[serde(rename = "mandate_type")] + MandateType, + #[strum( + serialize = "payment_type", + detailed_message = "Indicates if a payment is mandate or non-mandate", + props(Category = "Payments") + )] + #[serde(rename = "payment_type")] + PaymentType, + #[strum( + serialize = "wallet", + detailed_message = "Supported types of Wallet payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "wallet")] + WalletType, + #[strum( + serialize = "upi", + detailed_message = "Supported types of UPI payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "upi")] + UpiType, + #[strum( + serialize = "voucher", + detailed_message = "Supported types of Voucher payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "voucher")] + VoucherType, + #[strum( + serialize = "bank_transfer", + detailed_message = "Supported types of Bank Transfer payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_transfer")] + BankTransferType, + #[strum( + serialize = "bank_redirect", + detailed_message = "Supported types of Bank Redirect payment methods", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_redirect")] + BankRedirectType, + #[strum( + serialize = "bank_debit", + detailed_message = "Supported types of Bank Debit payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "bank_debit")] + BankDebitType, + #[strum( + serialize = "crypto", + detailed_message = "Supported types of Crypto payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "crypto")] + CryptoType, + #[strum( + serialize = "metadata", + detailed_message = "Aribitrary Key and value pair", + props(Category = "Metadata") + )] + #[serde(rename = "metadata")] + MetaData, + #[strum( + serialize = "reward", + detailed_message = "Supported types of Reward payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "reward")] + RewardType, + #[strum( + serialize = "amount", + detailed_message = "Value of the transaction", + props(Category = "Payments") + )] + #[serde(rename = "amount")] + PaymentAmount, + #[strum( + serialize = "currency", + detailed_message = "Currency used for the payment", + props(Category = "Payments") + )] + #[serde(rename = "currency")] + PaymentCurrency, + #[strum( + serialize = "authentication_type", + detailed_message = "Type of authentication for the payment", + props(Category = "Payments") + )] + #[serde(rename = "authentication_type")] + AuthenticationType, + #[strum( + serialize = "capture_method", + detailed_message = "Modes of capturing a payment", + props(Category = "Payments") + )] + #[serde(rename = "capture_method")] + CaptureMethod, + #[strum( + serialize = "country", + serialize = "business_country", + detailed_message = "Country of the business unit", + props(Category = "Merchant") + )] + #[serde(rename = "business_country", alias = "country")] + BusinessCountry, + #[strum( + serialize = "billing_country", + detailed_message = "Country of the billing address of the customer", + props(Category = "Customer") + )] + #[serde(rename = "billing_country")] + BillingCountry, + #[serde(skip_deserializing, rename = "connector")] + #[strum(disabled)] + Connector, + #[strum( + serialize = "business_label", + detailed_message = "Identifier for business unit", + props(Category = "Merchant") + )] + #[serde(rename = "business_label")] + BusinessLabel, + #[strum( + serialize = "setup_future_usage", + detailed_message = "Identifier for recurring payments", + props(Category = "Payments") + )] + #[serde(rename = "setup_future_usage")] + SetupFutureUsage, + #[strum( + serialize = "card_redirect_type", + detailed_message = "Supported types of Card Redirect payment method", + props(Category = "Payment Method Types") + )] + #[serde(rename = "card_redirect")] + CardRedirectType, +} + +pub trait EuclidDirFilter: Sized +where + Self: 'static, +{ + const ALLOWED: &'static [DirKeyKind]; + fn get_allowed_keys() -> &'static [DirKeyKind] { + Self::ALLOWED + } + + fn is_key_allowed(key: &DirKeyKind) -> bool { + Self::ALLOWED.contains(key) + } +} + +impl DirKeyKind { + pub fn get_type(&self) -> types::DataType { + match self { + Self::PaymentMethod => types::DataType::EnumVariant, + Self::CardBin => types::DataType::StrValue, + Self::CardType => types::DataType::EnumVariant, + Self::CardNetwork => types::DataType::EnumVariant, + Self::MetaData => types::DataType::MetadataValue, + Self::MandateType => types::DataType::EnumVariant, + Self::PaymentType => types::DataType::EnumVariant, + Self::MandateAcceptanceType => types::DataType::EnumVariant, + Self::PayLaterType => types::DataType::EnumVariant, + Self::WalletType => types::DataType::EnumVariant, + Self::UpiType => types::DataType::EnumVariant, + Self::VoucherType => types::DataType::EnumVariant, + Self::BankTransferType => types::DataType::EnumVariant, + Self::GiftCardType => types::DataType::EnumVariant, + Self::BankRedirectType => types::DataType::EnumVariant, + Self::CryptoType => types::DataType::EnumVariant, + Self::RewardType => types::DataType::EnumVariant, + Self::PaymentAmount => types::DataType::Number, + Self::PaymentCurrency => types::DataType::EnumVariant, + Self::AuthenticationType => types::DataType::EnumVariant, + Self::CaptureMethod => types::DataType::EnumVariant, + Self::BusinessCountry => types::DataType::EnumVariant, + Self::BillingCountry => types::DataType::EnumVariant, + Self::Connector => types::DataType::EnumVariant, + Self::BankDebitType => types::DataType::EnumVariant, + Self::BusinessLabel => types::DataType::StrValue, + Self::SetupFutureUsage => types::DataType::EnumVariant, + Self::CardRedirectType => types::DataType::EnumVariant, + } + } + pub fn get_value_set(&self) -> Option> { + match self { + Self::PaymentMethod => Some( + enums::PaymentMethod::iter() + .map(DirValue::PaymentMethod) + .collect(), + ), + Self::CardBin => None, + Self::CardType => Some(enums::CardType::iter().map(DirValue::CardType).collect()), + Self::MandateAcceptanceType => Some( + euclid_enums::MandateAcceptanceType::iter() + .map(DirValue::MandateAcceptanceType) + .collect(), + ), + Self::PaymentType => Some( + euclid_enums::PaymentType::iter() + .map(DirValue::PaymentType) + .collect(), + ), + Self::MandateType => Some( + euclid_enums::MandateType::iter() + .map(DirValue::MandateType) + .collect(), + ), + Self::CardNetwork => Some( + enums::CardNetwork::iter() + .map(DirValue::CardNetwork) + .collect(), + ), + Self::PayLaterType => Some( + enums::PayLaterType::iter() + .map(DirValue::PayLaterType) + .collect(), + ), + Self::MetaData => None, + Self::WalletType => Some( + enums::WalletType::iter() + .map(DirValue::WalletType) + .collect(), + ), + Self::UpiType => Some(enums::UpiType::iter().map(DirValue::UpiType).collect()), + Self::VoucherType => Some( + enums::VoucherType::iter() + .map(DirValue::VoucherType) + .collect(), + ), + Self::BankTransferType => Some( + enums::BankTransferType::iter() + .map(DirValue::BankTransferType) + .collect(), + ), + Self::GiftCardType => Some( + enums::GiftCardType::iter() + .map(DirValue::GiftCardType) + .collect(), + ), + Self::BankRedirectType => Some( + enums::BankRedirectType::iter() + .map(DirValue::BankRedirectType) + .collect(), + ), + Self::CryptoType => Some( + enums::CryptoType::iter() + .map(DirValue::CryptoType) + .collect(), + ), + Self::RewardType => Some( + enums::RewardType::iter() + .map(DirValue::RewardType) + .collect(), + ), + Self::PaymentAmount => None, + Self::PaymentCurrency => Some( + enums::PaymentCurrency::iter() + .map(DirValue::PaymentCurrency) + .collect(), + ), + Self::AuthenticationType => Some( + enums::AuthenticationType::iter() + .map(DirValue::AuthenticationType) + .collect(), + ), + Self::CaptureMethod => Some( + enums::CaptureMethod::iter() + .map(DirValue::CaptureMethod) + .collect(), + ), + Self::BankDebitType => Some( + enums::BankDebitType::iter() + .map(DirValue::BankDebitType) + .collect(), + ), + Self::BusinessCountry => Some( + enums::Country::iter() + .map(DirValue::BusinessCountry) + .collect(), + ), + Self::BillingCountry => Some( + enums::Country::iter() + .map(DirValue::BillingCountry) + .collect(), + ), + Self::Connector => Some( + enums::Connector::iter() + .map(|connector| { + DirValue::Connector(Box::new(ast::ConnectorChoice { + connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + })) + }) + .collect(), + ), + Self::BusinessLabel => None, + Self::SetupFutureUsage => Some( + enums::SetupFutureUsage::iter() + .map(DirValue::SetupFutureUsage) + .collect(), + ), + Self::CardRedirectType => Some( + enums::CardRedirectType::iter() + .map(DirValue::CardRedirectType) + .collect(), + ), + } + } +} + +#[derive( + Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, strum::Display, strum::EnumVariantNames, +)] +#[serde(tag = "key", content = "value")] +pub enum DirValue { + #[serde(rename = "payment_method")] + PaymentMethod(enums::PaymentMethod), + #[serde(rename = "card_bin")] + CardBin(types::StrValue), + #[serde(rename = "card_type")] + CardType(enums::CardType), + #[serde(rename = "card_network")] + CardNetwork(enums::CardNetwork), + #[serde(rename = "metadata")] + MetaData(types::MetadataValue), + #[serde(rename = "pay_later")] + PayLaterType(enums::PayLaterType), + #[serde(rename = "wallet")] + WalletType(enums::WalletType), + #[serde(rename = "acceptance_type")] + MandateAcceptanceType(euclid_enums::MandateAcceptanceType), + #[serde(rename = "mandate_type")] + MandateType(euclid_enums::MandateType), + #[serde(rename = "payment_type")] + PaymentType(euclid_enums::PaymentType), + #[serde(rename = "upi")] + UpiType(enums::UpiType), + #[serde(rename = "voucher")] + VoucherType(enums::VoucherType), + #[serde(rename = "bank_transfer")] + BankTransferType(enums::BankTransferType), + #[serde(rename = "bank_redirect")] + BankRedirectType(enums::BankRedirectType), + #[serde(rename = "bank_debit")] + BankDebitType(enums::BankDebitType), + #[serde(rename = "crypto")] + CryptoType(enums::CryptoType), + #[serde(rename = "reward")] + RewardType(enums::RewardType), + #[serde(rename = "gift_card")] + GiftCardType(enums::GiftCardType), + #[serde(rename = "amount")] + PaymentAmount(types::NumValue), + #[serde(rename = "currency")] + PaymentCurrency(enums::PaymentCurrency), + #[serde(rename = "authentication_type")] + AuthenticationType(enums::AuthenticationType), + #[serde(rename = "capture_method")] + CaptureMethod(enums::CaptureMethod), + #[serde(rename = "business_country", alias = "country")] + BusinessCountry(enums::Country), + #[serde(rename = "billing_country")] + BillingCountry(enums::Country), + #[serde(skip_deserializing, rename = "connector")] + Connector(Box), + #[serde(rename = "business_label")] + BusinessLabel(types::StrValue), + #[serde(rename = "setup_future_usage")] + SetupFutureUsage(enums::SetupFutureUsage), + #[serde(rename = "card_redirect")] + CardRedirectType(enums::CardRedirectType), +} + +impl DirValue { + pub fn get_key(&self) -> DirKey { + let (kind, data) = match self { + Self::PaymentMethod(_) => (DirKeyKind::PaymentMethod, None), + Self::CardBin(_) => (DirKeyKind::CardBin, None), + Self::RewardType(_) => (DirKeyKind::RewardType, None), + Self::BusinessCountry(_) => (DirKeyKind::BusinessCountry, None), + Self::BillingCountry(_) => (DirKeyKind::CardBin, None), + Self::BankTransferType(_) => (DirKeyKind::BankTransferType, None), + Self::UpiType(_) => (DirKeyKind::UpiType, None), + Self::CardType(_) => (DirKeyKind::CardType, None), + Self::CardNetwork(_) => (DirKeyKind::CardNetwork, None), + Self::MetaData(met) => (DirKeyKind::MetaData, Some(met.key.clone())), + Self::PayLaterType(_) => (DirKeyKind::PayLaterType, None), + Self::WalletType(_) => (DirKeyKind::WalletType, None), + Self::BankRedirectType(_) => (DirKeyKind::BankRedirectType, None), + Self::CryptoType(_) => (DirKeyKind::CryptoType, None), + Self::AuthenticationType(_) => (DirKeyKind::AuthenticationType, None), + Self::CaptureMethod(_) => (DirKeyKind::CaptureMethod, None), + Self::PaymentAmount(_) => (DirKeyKind::PaymentAmount, None), + Self::PaymentCurrency(_) => (DirKeyKind::PaymentCurrency, None), + Self::Connector(_) => (DirKeyKind::Connector, None), + Self::BankDebitType(_) => (DirKeyKind::BankDebitType, None), + Self::MandateAcceptanceType(_) => (DirKeyKind::MandateAcceptanceType, None), + Self::MandateType(_) => (DirKeyKind::MandateType, None), + Self::PaymentType(_) => (DirKeyKind::PaymentType, None), + Self::BusinessLabel(_) => (DirKeyKind::BusinessLabel, None), + Self::SetupFutureUsage(_) => (DirKeyKind::SetupFutureUsage, None), + Self::CardRedirectType(_) => (DirKeyKind::CardRedirectType, None), + Self::VoucherType(_) => (DirKeyKind::VoucherType, None), + Self::GiftCardType(_) => (DirKeyKind::GiftCardType, None), + }; + + DirKey::new(kind, data) + } + pub fn get_metadata_val(&self) -> Option { + match self { + Self::MetaData(val) => Some(val.clone()), + Self::PaymentMethod(_) => None, + Self::CardBin(_) => None, + Self::CardType(_) => None, + Self::CardNetwork(_) => None, + Self::PayLaterType(_) => None, + Self::WalletType(_) => None, + Self::BankRedirectType(_) => None, + Self::CryptoType(_) => None, + Self::AuthenticationType(_) => None, + Self::CaptureMethod(_) => None, + Self::GiftCardType(_) => None, + Self::PaymentAmount(_) => None, + Self::PaymentCurrency(_) => None, + Self::BusinessCountry(_) => None, + Self::BillingCountry(_) => None, + Self::Connector(_) => None, + Self::BankTransferType(_) => None, + Self::UpiType(_) => None, + Self::BankDebitType(_) => None, + Self::RewardType(_) => None, + Self::VoucherType(_) => None, + Self::MandateAcceptanceType(_) => None, + Self::MandateType(_) => None, + Self::PaymentType(_) => None, + Self::BusinessLabel(_) => None, + Self::SetupFutureUsage(_) => None, + Self::CardRedirectType(_) => None, + } + } + + pub fn get_str_val(&self) -> Option { + match self { + Self::CardBin(val) => Some(val.clone()), + _ => None, + } + } + + pub fn get_num_value(&self) -> Option { + match self { + Self::PaymentAmount(val) => Some(val.clone()), + _ => None, + } + } + + pub fn check_equality(v1: &Self, v2: &Self) -> bool { + match (v1, v2) { + (Self::PaymentMethod(pm1), Self::PaymentMethod(pm2)) => pm1 == pm2, + (Self::CardType(ct1), Self::CardType(ct2)) => ct1 == ct2, + (Self::CardNetwork(cn1), Self::CardNetwork(cn2)) => cn1 == cn2, + (Self::MetaData(md1), Self::MetaData(md2)) => md1 == md2, + (Self::PayLaterType(plt1), Self::PayLaterType(plt2)) => plt1 == plt2, + (Self::WalletType(wt1), Self::WalletType(wt2)) => wt1 == wt2, + (Self::BankDebitType(bdt1), Self::BankDebitType(bdt2)) => bdt1 == bdt2, + (Self::BankRedirectType(brt1), Self::BankRedirectType(brt2)) => brt1 == brt2, + (Self::BankTransferType(btt1), Self::BankTransferType(btt2)) => btt1 == btt2, + (Self::GiftCardType(gct1), Self::GiftCardType(gct2)) => gct1 == gct2, + (Self::CryptoType(ct1), Self::CryptoType(ct2)) => ct1 == ct2, + (Self::AuthenticationType(at1), Self::AuthenticationType(at2)) => at1 == at2, + (Self::CaptureMethod(cm1), Self::CaptureMethod(cm2)) => cm1 == cm2, + (Self::PaymentCurrency(pc1), Self::PaymentCurrency(pc2)) => pc1 == pc2, + (Self::BusinessCountry(c1), Self::BusinessCountry(c2)) => c1 == c2, + (Self::BillingCountry(c1), Self::BillingCountry(c2)) => c1 == c2, + (Self::PaymentType(pt1), Self::PaymentType(pt2)) => pt1 == pt2, + (Self::MandateType(mt1), Self::MandateType(mt2)) => mt1 == mt2, + (Self::MandateAcceptanceType(mat1), Self::MandateAcceptanceType(mat2)) => mat1 == mat2, + (Self::RewardType(rt1), Self::RewardType(rt2)) => rt1 == rt2, + (Self::Connector(c1), Self::Connector(c2)) => c1 == c2, + (Self::BusinessLabel(bl1), Self::BusinessLabel(bl2)) => bl1 == bl2, + (Self::SetupFutureUsage(sfu1), Self::SetupFutureUsage(sfu2)) => sfu1 == sfu2, + (Self::UpiType(ut1), Self::UpiType(ut2)) => ut1 == ut2, + (Self::VoucherType(vt1), Self::VoucherType(vt2)) => vt1 == vt2, + (Self::CardRedirectType(crt1), Self::CardRedirectType(crt2)) => crt1 == crt2, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum DirComparisonLogic { + NegativeConjunction, + PositiveDisjunction, +} + +#[derive(Debug, Clone)] +pub struct DirComparison { + pub values: Vec, + pub logic: DirComparisonLogic, + pub metadata: types::Metadata, +} + +pub type DirIfCondition = Vec; + +#[derive(Debug, Clone)] +pub struct DirIfStatement { + pub condition: DirIfCondition, + pub nested: Option>, +} + +#[derive(Debug, Clone)] +pub struct DirRule { + pub name: String, + pub connector_selection: O, + pub statements: Vec, +} + +#[derive(Debug, Clone)] +pub struct DirProgram { + pub default_selection: O, + pub rules: Vec>, + pub metadata: types::Metadata, +} + +#[cfg(test)] +mod test { + #![allow(clippy::expect_used)] + use rustc_hash::FxHashMap; + use strum::IntoEnumIterator; + + use super::*; + + #[test] + fn test_consistent_dir_key_naming() { + let mut key_names: FxHashMap = FxHashMap::default(); + + for key in DirKeyKind::iter() { + let json_str = if let DirKeyKind::MetaData = key { + r#""metadata""#.to_string() + } else { + serde_json::to_string(&key).expect("JSON Serialization") + }; + let display_str = key.to_string(); + + assert_eq!(&json_str[1..json_str.len() - 1], display_str); + key_names.insert(key, display_str); + } + + let values = vec![ + dirval!(PaymentMethod = Card), + dirval!(CardBin s= "123456"), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PayLaterType = Klarna), + dirval!(WalletType = Paypal), + dirval!(BankRedirectType = Sofort), + dirval!(BankDebitType = Bacs), + dirval!(CryptoType = CryptoCurrency), + dirval!("" = "metadata"), + dirval!(PaymentAmount = 100), + dirval!(PaymentCurrency = USD), + dirval!(CardRedirectType = Benefit), + dirval!(AuthenticationType = ThreeDs), + dirval!(CaptureMethod = Manual), + dirval!(BillingCountry = UnitedStatesOfAmerica), + dirval!(BusinessCountry = France), + ]; + + for val in values { + let json_val = serde_json::to_value(&val).expect("JSON Value Serialization"); + + let json_key = json_val + .as_object() + .expect("Serialized Object") + .get("key") + .expect("Object Key"); + + let value_str = json_key.as_str().expect("Value string"); + let dir_key = val.get_key(); + + let key_name = key_names.get(&dir_key.kind).expect("Key name"); + + assert_eq!(key_name, value_str); + } + } + + #[cfg(feature = "ast_parser")] + #[test] + fn test_allowed_dir_keys() { + use crate::types::DummyOutput; + + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + payment_method = card + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + + let out = ast::lowering::lower_program::(program); + assert!(out.is_ok()) + } + #[cfg(feature = "ast_parser")] + #[test] + fn test_not_allowed_dir_keys() { + use crate::types::DummyOutput; + + let program_str = r#" + default: ["stripe", "adyen"] + + rule_1: ["stripe"] + { + bank_debit = ach + } + "#; + let (_, program) = ast::parser::program::(program_str).expect("Program"); + + let out = ast::lowering::lower_program::(program); + assert!(out.is_err()) + } +} diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs new file mode 100644 index 000000000000..17699940363f --- /dev/null +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -0,0 +1,321 @@ +use strum::VariantNames; + +use crate::enums::collect_variants; +pub use crate::enums::{ + AuthenticationType, CaptureMethod, CardNetwork, Connector, Country, Country as BusinessCountry, + Country as BillingCountry, Currency as PaymentCurrency, MandateAcceptanceType, MandateType, + PaymentMethod, PaymentType, SetupFutureUsage, +}; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CardType { + Credit, + Debit, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PayLaterType { + Affirm, + AfterpayClearpay, + Alma, + Klarna, + PayBright, + Walley, + Atome, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum WalletType { + GooglePay, + ApplePay, + Paypal, + AliPay, + AliPayHk, + MbWay, + MobilePay, + WeChatPay, + SamsungPay, + GoPay, + KakaoPay, + Twint, + Gcash, + Vipps, + Momo, + Dana, + TouchNGo, + Swish, + Cashapp, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum VoucherType { + Boleto, + Efecty, + PagoEfectivo, + RedCompra, + RedPagos, + Alfamart, + Indomaret, + SevenEleven, + Lawson, + MiniStop, + FamilyMart, + Seicomart, + PayEasy, + Oxxo, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankRedirectType { + Bizum, + Giropay, + Ideal, + Sofort, + Eps, + BancontactCard, + Blik, + Interac, + OnlineBankingCzechRepublic, + OnlineBankingFinland, + OnlineBankingPoland, + OnlineBankingSlovakia, + OnlineBankingFpx, + OnlineBankingThailand, + OpenBankingUk, + Przelewy24, + Trustly, +} +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankTransferType { + Multibanco, + Ach, + Sepa, + Bacs, + BcaBankTransfer, + BniVa, + BriVa, + CimbVa, + DanamonVa, + MandiriVa, + PermataBankTransfer, + Pix, + Pse, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GiftCardType { + PaySafeCard, + Givex, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CardRedirectType { + Benefit, + Knet, + MomoAtm, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CryptoType { + CryptoCurrency, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum UpiType { + UpiCollect, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BankDebitType { + Ach, + Sepa, + Bacs, + Becs, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RewardType { + ClassicReward, + Evoucher, +} + +collect_variants!(CardType); +collect_variants!(PayLaterType); +collect_variants!(WalletType); +collect_variants!(BankRedirectType); +collect_variants!(BankDebitType); +collect_variants!(CryptoType); +collect_variants!(RewardType); +collect_variants!(UpiType); +collect_variants!(VoucherType); +collect_variants!(GiftCardType); +collect_variants!(BankTransferType); +collect_variants!(CardRedirectType); diff --git a/crates/euclid/src/frontend/dir/lowering.rs b/crates/euclid/src/frontend/dir/lowering.rs new file mode 100644 index 000000000000..516e10e0389e --- /dev/null +++ b/crates/euclid/src/frontend/dir/lowering.rs @@ -0,0 +1,295 @@ +//! Analysis of the lowering logic for the DIR +//! +//! Consists of certain functions that supports the lowering logic from DIR to VIR. +//! These includes the lowering of the DIR program and vector of rules , and the lowering of ifstatements +//! ,and comparisonsLogic and also the lowering of the enums of value variants from DIR to VIR. +use super::enums; +use crate::{ + dssa::types::{AnalysisError, AnalysisErrorType}, + enums as global_enums, + frontend::{dir, vir}, + types::EuclidValue, +}; + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CardType) -> Self { + match value { + enums::CardType::Credit => Self::Credit, + enums::CardType::Debit => Self::Debit, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::PayLaterType) -> Self { + match value { + enums::PayLaterType::Affirm => Self::Affirm, + enums::PayLaterType::AfterpayClearpay => Self::AfterpayClearpay, + enums::PayLaterType::Alma => Self::Alma, + enums::PayLaterType::Klarna => Self::Klarna, + enums::PayLaterType::PayBright => Self::PayBright, + enums::PayLaterType::Walley => Self::Walley, + enums::PayLaterType::Atome => Self::Atome, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::WalletType) -> Self { + match value { + enums::WalletType::GooglePay => Self::GooglePay, + enums::WalletType::ApplePay => Self::ApplePay, + enums::WalletType::Paypal => Self::Paypal, + enums::WalletType::AliPay => Self::AliPay, + enums::WalletType::AliPayHk => Self::AliPayHk, + enums::WalletType::MbWay => Self::MbWay, + enums::WalletType::MobilePay => Self::MobilePay, + enums::WalletType::WeChatPay => Self::WeChatPay, + enums::WalletType::SamsungPay => Self::SamsungPay, + enums::WalletType::GoPay => Self::GoPay, + enums::WalletType::KakaoPay => Self::KakaoPay, + enums::WalletType::Twint => Self::Twint, + enums::WalletType::Gcash => Self::Gcash, + enums::WalletType::Vipps => Self::Vipps, + enums::WalletType::Momo => Self::Momo, + enums::WalletType::Dana => Self::Dana, + enums::WalletType::TouchNGo => Self::TouchNGo, + enums::WalletType::Swish => Self::Swish, + enums::WalletType::Cashapp => Self::Cashapp, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankDebitType) -> Self { + match value { + enums::BankDebitType::Ach => Self::Ach, + enums::BankDebitType::Sepa => Self::Sepa, + enums::BankDebitType::Bacs => Self::Bacs, + enums::BankDebitType::Becs => Self::Becs, + } + } +} +impl From for global_enums::PaymentMethodType { + fn from(value: enums::UpiType) -> Self { + match value { + enums::UpiType::UpiCollect => Self::UpiCollect, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::VoucherType) -> Self { + match value { + enums::VoucherType::Boleto => Self::Boleto, + enums::VoucherType::Efecty => Self::Efecty, + enums::VoucherType::PagoEfectivo => Self::PagoEfectivo, + enums::VoucherType::RedCompra => Self::RedCompra, + enums::VoucherType::RedPagos => Self::RedPagos, + enums::VoucherType::Alfamart => Self::Alfamart, + enums::VoucherType::Indomaret => Self::Indomaret, + enums::VoucherType::SevenEleven => Self::SevenEleven, + enums::VoucherType::Lawson => Self::Lawson, + enums::VoucherType::MiniStop => Self::MiniStop, + enums::VoucherType::FamilyMart => Self::FamilyMart, + enums::VoucherType::Seicomart => Self::Seicomart, + enums::VoucherType::PayEasy => Self::PayEasy, + enums::VoucherType::Oxxo => Self::Oxxo, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankTransferType) -> Self { + match value { + enums::BankTransferType::Multibanco => Self::Multibanco, + enums::BankTransferType::Pix => Self::Pix, + enums::BankTransferType::Pse => Self::Pse, + enums::BankTransferType::Ach => Self::Ach, + enums::BankTransferType::Sepa => Self::Sepa, + enums::BankTransferType::Bacs => Self::Bacs, + enums::BankTransferType::BcaBankTransfer => Self::BcaBankTransfer, + enums::BankTransferType::BniVa => Self::BniVa, + enums::BankTransferType::BriVa => Self::BriVa, + enums::BankTransferType::CimbVa => Self::CimbVa, + enums::BankTransferType::DanamonVa => Self::DanamonVa, + enums::BankTransferType::MandiriVa => Self::MandiriVa, + enums::BankTransferType::PermataBankTransfer => Self::PermataBankTransfer, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::GiftCardType) -> Self { + match value { + enums::GiftCardType::PaySafeCard => Self::PaySafeCard, + enums::GiftCardType::Givex => Self::Givex, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CardRedirectType) -> Self { + match value { + enums::CardRedirectType::Benefit => Self::Benefit, + enums::CardRedirectType::Knet => Self::Knet, + enums::CardRedirectType::MomoAtm => Self::MomoAtm, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::BankRedirectType) -> Self { + match value { + enums::BankRedirectType::Bizum => Self::Bizum, + enums::BankRedirectType::Giropay => Self::Giropay, + enums::BankRedirectType::Ideal => Self::Ideal, + enums::BankRedirectType::Sofort => Self::Sofort, + enums::BankRedirectType::Eps => Self::Eps, + enums::BankRedirectType::BancontactCard => Self::BancontactCard, + enums::BankRedirectType::Blik => Self::Blik, + enums::BankRedirectType::Interac => Self::Interac, + enums::BankRedirectType::OnlineBankingCzechRepublic => Self::OnlineBankingCzechRepublic, + enums::BankRedirectType::OnlineBankingFinland => Self::OnlineBankingFinland, + enums::BankRedirectType::OnlineBankingPoland => Self::OnlineBankingPoland, + enums::BankRedirectType::OnlineBankingSlovakia => Self::OnlineBankingSlovakia, + enums::BankRedirectType::OnlineBankingFpx => Self::OnlineBankingFpx, + enums::BankRedirectType::OnlineBankingThailand => Self::OnlineBankingThailand, + enums::BankRedirectType::OpenBankingUk => Self::OpenBankingUk, + enums::BankRedirectType::Przelewy24 => Self::Przelewy24, + enums::BankRedirectType::Trustly => Self::Trustly, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::CryptoType) -> Self { + match value { + enums::CryptoType::CryptoCurrency => Self::CryptoCurrency, + } + } +} + +impl From for global_enums::PaymentMethodType { + fn from(value: enums::RewardType) -> Self { + match value { + enums::RewardType::ClassicReward => Self::ClassicReward, + enums::RewardType::Evoucher => Self::Evoucher, + } + } +} + +/// Analyses of the lowering of the DirValues to EuclidValues . +/// +/// For example, +/// ```notrust +/// DirValue::PaymentMethod::Cards -> EuclidValue::PaymentMethod::Cards +/// ```notrust +/// This is a function that lowers the Values of the DIR variants into the Value of the VIR variants. +/// The function for each DirValue variant creates a corresponding EuclidValue variants and if there +/// lacks any direct mapping, it return an Error. +fn lower_value(dir_value: dir::DirValue) -> Result { + Ok(match dir_value { + dir::DirValue::PaymentMethod(pm) => EuclidValue::PaymentMethod(pm), + dir::DirValue::CardBin(ci) => EuclidValue::CardBin(ci), + dir::DirValue::CardType(ct) => EuclidValue::PaymentMethodType(ct.into()), + dir::DirValue::CardNetwork(cn) => EuclidValue::CardNetwork(cn), + dir::DirValue::MetaData(md) => EuclidValue::Metadata(md), + dir::DirValue::PayLaterType(plt) => EuclidValue::PaymentMethodType(plt.into()), + dir::DirValue::WalletType(wt) => EuclidValue::PaymentMethodType(wt.into()), + dir::DirValue::UpiType(ut) => EuclidValue::PaymentMethodType(ut.into()), + dir::DirValue::VoucherType(vt) => EuclidValue::PaymentMethodType(vt.into()), + dir::DirValue::BankTransferType(btt) => EuclidValue::PaymentMethodType(btt.into()), + dir::DirValue::GiftCardType(gct) => EuclidValue::PaymentMethodType(gct.into()), + dir::DirValue::CardRedirectType(crt) => EuclidValue::PaymentMethodType(crt.into()), + dir::DirValue::BankRedirectType(brt) => EuclidValue::PaymentMethodType(brt.into()), + dir::DirValue::CryptoType(ct) => EuclidValue::PaymentMethodType(ct.into()), + dir::DirValue::AuthenticationType(at) => EuclidValue::AuthenticationType(at), + dir::DirValue::CaptureMethod(cm) => EuclidValue::CaptureMethod(cm), + dir::DirValue::PaymentAmount(pa) => EuclidValue::PaymentAmount(pa), + dir::DirValue::PaymentCurrency(pc) => EuclidValue::PaymentCurrency(pc), + dir::DirValue::BusinessCountry(buc) => EuclidValue::BusinessCountry(buc), + dir::DirValue::BillingCountry(bic) => EuclidValue::BillingCountry(bic), + dir::DirValue::MandateAcceptanceType(mat) => EuclidValue::MandateAcceptanceType(mat), + dir::DirValue::MandateType(mt) => EuclidValue::MandateType(mt), + dir::DirValue::PaymentType(pt) => EuclidValue::PaymentType(pt), + dir::DirValue::Connector(_) => Err(AnalysisErrorType::UnsupportedProgramKey( + dir::DirKeyKind::Connector, + ))?, + dir::DirValue::BankDebitType(bdt) => EuclidValue::PaymentMethodType(bdt.into()), + dir::DirValue::RewardType(rt) => EuclidValue::PaymentMethodType(rt.into()), + dir::DirValue::BusinessLabel(bl) => EuclidValue::BusinessLabel(bl), + dir::DirValue::SetupFutureUsage(sfu) => EuclidValue::SetupFutureUsage(sfu), + }) +} + +fn lower_comparison( + dir_comparison: dir::DirComparison, +) -> Result { + Ok(vir::ValuedComparison { + values: dir_comparison + .values + .into_iter() + .map(lower_value) + .collect::>()?, + logic: match dir_comparison.logic { + dir::DirComparisonLogic::NegativeConjunction => { + vir::ValuedComparisonLogic::NegativeConjunction + } + dir::DirComparisonLogic::PositiveDisjunction => { + vir::ValuedComparisonLogic::PositiveDisjunction + } + }, + metadata: dir_comparison.metadata, + }) +} + +fn lower_if_statement( + dir_if_statement: dir::DirIfStatement, +) -> Result { + Ok(vir::ValuedIfStatement { + condition: dir_if_statement + .condition + .into_iter() + .map(lower_comparison) + .collect::>()?, + nested: dir_if_statement + .nested + .map(|v| { + v.into_iter() + .map(lower_if_statement) + .collect::>() + }) + .transpose()?, + }) +} + +fn lower_rule(dir_rule: dir::DirRule) -> Result, AnalysisErrorType> { + Ok(vir::ValuedRule { + name: dir_rule.name, + connector_selection: dir_rule.connector_selection, + statements: dir_rule + .statements + .into_iter() + .map(lower_if_statement) + .collect::>()?, + }) +} + +pub fn lower_program( + dir_program: dir::DirProgram, +) -> Result, AnalysisError> { + Ok(vir::ValuedProgram { + default_selection: dir_program.default_selection, + rules: dir_program + .rules + .into_iter() + .map(lower_rule) + .collect::>() + .map_err(|e| AnalysisError { + error_type: e, + metadata: Default::default(), + })?, + metadata: dir_program.metadata, + }) +} diff --git a/crates/euclid/src/frontend/dir/transformers.rs b/crates/euclid/src/frontend/dir/transformers.rs new file mode 100644 index 000000000000..da413d380c0f --- /dev/null +++ b/crates/euclid/src/frontend/dir/transformers.rs @@ -0,0 +1,166 @@ +use crate::{dirval, dssa::types::AnalysisErrorType, enums as global_enums, frontend::dir}; + +pub trait IntoDirValue { + fn into_dir_value(self) -> Result; +} +impl IntoDirValue for (global_enums::PaymentMethodType, global_enums::PaymentMethod) { + fn into_dir_value(self) -> Result { + match self.0 { + global_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), + global_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), + global_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), + global_enums::PaymentMethodType::Ideal => Ok(dirval!(BankRedirectType = Ideal)), + global_enums::PaymentMethodType::Sofort => Ok(dirval!(BankRedirectType = Sofort)), + global_enums::PaymentMethodType::Eps => Ok(dirval!(BankRedirectType = Eps)), + global_enums::PaymentMethodType::Klarna => Ok(dirval!(PayLaterType = Klarna)), + global_enums::PaymentMethodType::Affirm => Ok(dirval!(PayLaterType = Affirm)), + global_enums::PaymentMethodType::AfterpayClearpay => { + Ok(dirval!(PayLaterType = AfterpayClearpay)) + } + global_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)), + global_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)), + global_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)), + global_enums::PaymentMethodType::CryptoCurrency => { + Ok(dirval!(CryptoType = CryptoCurrency)) + } + global_enums::PaymentMethodType::Ach => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Ach)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Ach)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::Bacs => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Bacs)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Bacs)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::Becs => Ok(dirval!(BankDebitType = Becs)), + global_enums::PaymentMethodType::Sepa => match self.1 { + global_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Sepa)), + global_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Sepa)), + global_enums::PaymentMethod::PayLater + | global_enums::PaymentMethod::Card + | global_enums::PaymentMethod::CardRedirect + | global_enums::PaymentMethod::Wallet + | global_enums::PaymentMethod::BankRedirect + | global_enums::PaymentMethod::Crypto + | global_enums::PaymentMethod::Reward + | global_enums::PaymentMethod::Upi + | global_enums::PaymentMethod::Voucher + | global_enums::PaymentMethod::GiftCard => Err(AnalysisErrorType::NotSupported), + }, + global_enums::PaymentMethodType::AliPay => Ok(dirval!(WalletType = AliPay)), + global_enums::PaymentMethodType::AliPayHk => Ok(dirval!(WalletType = AliPayHk)), + global_enums::PaymentMethodType::BancontactCard => { + Ok(dirval!(BankRedirectType = BancontactCard)) + } + global_enums::PaymentMethodType::Blik => Ok(dirval!(BankRedirectType = Blik)), + global_enums::PaymentMethodType::MbWay => Ok(dirval!(WalletType = MbWay)), + global_enums::PaymentMethodType::MobilePay => Ok(dirval!(WalletType = MobilePay)), + global_enums::PaymentMethodType::Cashapp => Ok(dirval!(WalletType = Cashapp)), + global_enums::PaymentMethodType::Multibanco => { + Ok(dirval!(BankTransferType = Multibanco)) + } + global_enums::PaymentMethodType::Pix => Ok(dirval!(BankTransferType = Pix)), + global_enums::PaymentMethodType::Pse => Ok(dirval!(BankTransferType = Pse)), + global_enums::PaymentMethodType::Interac => Ok(dirval!(BankRedirectType = Interac)), + global_enums::PaymentMethodType::OnlineBankingCzechRepublic => { + Ok(dirval!(BankRedirectType = OnlineBankingCzechRepublic)) + } + global_enums::PaymentMethodType::OnlineBankingFinland => { + Ok(dirval!(BankRedirectType = OnlineBankingFinland)) + } + global_enums::PaymentMethodType::OnlineBankingPoland => { + Ok(dirval!(BankRedirectType = OnlineBankingPoland)) + } + global_enums::PaymentMethodType::OnlineBankingSlovakia => { + Ok(dirval!(BankRedirectType = OnlineBankingSlovakia)) + } + global_enums::PaymentMethodType::Swish => Ok(dirval!(WalletType = Swish)), + global_enums::PaymentMethodType::Trustly => Ok(dirval!(BankRedirectType = Trustly)), + global_enums::PaymentMethodType::Bizum => Ok(dirval!(BankRedirectType = Bizum)), + + global_enums::PaymentMethodType::PayBright => Ok(dirval!(PayLaterType = PayBright)), + global_enums::PaymentMethodType::Walley => Ok(dirval!(PayLaterType = Walley)), + global_enums::PaymentMethodType::Przelewy24 => { + Ok(dirval!(BankRedirectType = Przelewy24)) + } + global_enums::PaymentMethodType::WeChatPay => Ok(dirval!(WalletType = WeChatPay)), + + global_enums::PaymentMethodType::ClassicReward => { + Ok(dirval!(RewardType = ClassicReward)) + } + global_enums::PaymentMethodType::Evoucher => Ok(dirval!(RewardType = Evoucher)), + global_enums::PaymentMethodType::UpiCollect => Ok(dirval!(UpiType = UpiCollect)), + global_enums::PaymentMethodType::SamsungPay => Ok(dirval!(WalletType = SamsungPay)), + global_enums::PaymentMethodType::GoPay => Ok(dirval!(WalletType = GoPay)), + global_enums::PaymentMethodType::KakaoPay => Ok(dirval!(WalletType = KakaoPay)), + global_enums::PaymentMethodType::Twint => Ok(dirval!(WalletType = Twint)), + global_enums::PaymentMethodType::Gcash => Ok(dirval!(WalletType = Gcash)), + global_enums::PaymentMethodType::Vipps => Ok(dirval!(WalletType = Vipps)), + global_enums::PaymentMethodType::Momo => Ok(dirval!(WalletType = Momo)), + global_enums::PaymentMethodType::Alma => Ok(dirval!(PayLaterType = Alma)), + global_enums::PaymentMethodType::Dana => Ok(dirval!(WalletType = Dana)), + global_enums::PaymentMethodType::OnlineBankingFpx => { + Ok(dirval!(BankRedirectType = OnlineBankingFpx)) + } + global_enums::PaymentMethodType::OnlineBankingThailand => { + Ok(dirval!(BankRedirectType = OnlineBankingThailand)) + } + global_enums::PaymentMethodType::TouchNGo => Ok(dirval!(WalletType = TouchNGo)), + global_enums::PaymentMethodType::Atome => Ok(dirval!(PayLaterType = Atome)), + global_enums::PaymentMethodType::Boleto => Ok(dirval!(VoucherType = Boleto)), + global_enums::PaymentMethodType::Efecty => Ok(dirval!(VoucherType = Efecty)), + global_enums::PaymentMethodType::PagoEfectivo => { + Ok(dirval!(VoucherType = PagoEfectivo)) + } + global_enums::PaymentMethodType::RedCompra => Ok(dirval!(VoucherType = RedCompra)), + global_enums::PaymentMethodType::RedPagos => Ok(dirval!(VoucherType = RedPagos)), + global_enums::PaymentMethodType::Alfamart => Ok(dirval!(VoucherType = Alfamart)), + global_enums::PaymentMethodType::BcaBankTransfer => { + Ok(dirval!(BankTransferType = BcaBankTransfer)) + } + global_enums::PaymentMethodType::BniVa => Ok(dirval!(BankTransferType = BniVa)), + global_enums::PaymentMethodType::BriVa => Ok(dirval!(BankTransferType = BriVa)), + global_enums::PaymentMethodType::CimbVa => Ok(dirval!(BankTransferType = CimbVa)), + global_enums::PaymentMethodType::DanamonVa => Ok(dirval!(BankTransferType = DanamonVa)), + global_enums::PaymentMethodType::Indomaret => Ok(dirval!(VoucherType = Indomaret)), + global_enums::PaymentMethodType::MandiriVa => Ok(dirval!(BankTransferType = MandiriVa)), + global_enums::PaymentMethodType::PermataBankTransfer => { + Ok(dirval!(BankTransferType = PermataBankTransfer)) + } + global_enums::PaymentMethodType::PaySafeCard => Ok(dirval!(GiftCardType = PaySafeCard)), + global_enums::PaymentMethodType::SevenEleven => Ok(dirval!(VoucherType = SevenEleven)), + global_enums::PaymentMethodType::Lawson => Ok(dirval!(VoucherType = Lawson)), + global_enums::PaymentMethodType::MiniStop => Ok(dirval!(VoucherType = MiniStop)), + global_enums::PaymentMethodType::FamilyMart => Ok(dirval!(VoucherType = FamilyMart)), + global_enums::PaymentMethodType::Seicomart => Ok(dirval!(VoucherType = Seicomart)), + global_enums::PaymentMethodType::PayEasy => Ok(dirval!(VoucherType = PayEasy)), + global_enums::PaymentMethodType::Givex => Ok(dirval!(GiftCardType = Givex)), + global_enums::PaymentMethodType::Benefit => Ok(dirval!(CardRedirectType = Benefit)), + global_enums::PaymentMethodType::Knet => Ok(dirval!(CardRedirectType = Knet)), + global_enums::PaymentMethodType::OpenBankingUk => { + Ok(dirval!(BankRedirectType = OpenBankingUk)) + } + global_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), + global_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + } + } +} diff --git a/crates/euclid/src/frontend/vir.rs b/crates/euclid/src/frontend/vir.rs new file mode 100644 index 000000000000..750ff4e61ff8 --- /dev/null +++ b/crates/euclid/src/frontend/vir.rs @@ -0,0 +1,37 @@ +//! Valued Intermediate Representation +use crate::types::{EuclidValue, Metadata}; + +#[derive(Debug, Clone)] +pub enum ValuedComparisonLogic { + NegativeConjunction, + PositiveDisjunction, +} + +#[derive(Clone, Debug)] +pub struct ValuedComparison { + pub values: Vec, + pub logic: ValuedComparisonLogic, + pub metadata: Metadata, +} + +pub type ValuedIfCondition = Vec; + +#[derive(Clone, Debug)] +pub struct ValuedIfStatement { + pub condition: ValuedIfCondition, + pub nested: Option>, +} + +#[derive(Clone, Debug)] +pub struct ValuedRule { + pub name: String, + pub connector_selection: O, + pub statements: Vec, +} + +#[derive(Clone, Debug)] +pub struct ValuedProgram { + pub default_selection: O, + pub rules: Vec>, + pub metadata: Metadata, +} diff --git a/crates/euclid/src/lib.rs b/crates/euclid/src/lib.rs new file mode 100644 index 000000000000..d64297437aeb --- /dev/null +++ b/crates/euclid/src/lib.rs @@ -0,0 +1,7 @@ +#![allow(clippy::result_large_err)] +pub mod backend; +pub mod dssa; +pub mod enums; +pub mod frontend; +pub mod types; +pub mod utils; diff --git a/crates/euclid/src/types.rs b/crates/euclid/src/types.rs new file mode 100644 index 000000000000..59736ae65125 --- /dev/null +++ b/crates/euclid/src/types.rs @@ -0,0 +1,318 @@ +pub mod transformers; + +use euclid_macros::EnumNums; +use serde::Serialize; +use strum::VariantNames; + +use crate::{ + dssa::types::EuclidAnalysable, + enums, + frontend::{ + ast, + dir::{DirKeyKind, DirValue, EuclidDirFilter}, + }, +}; + +pub type Metadata = std::collections::HashMap; + +#[derive( + Debug, + Clone, + EnumNums, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumString, +)] +pub enum EuclidKey { + #[strum(serialize = "payment_method")] + PaymentMethod, + #[strum(serialize = "card_bin")] + CardBin, + #[strum(serialize = "metadata")] + Metadata, + #[strum(serialize = "mandate_type")] + MandateType, + #[strum(serialize = "mandate_acceptance_type")] + MandateAcceptanceType, + #[strum(serialize = "payment_type")] + PaymentType, + #[strum(serialize = "payment_method_type")] + PaymentMethodType, + #[strum(serialize = "card_network")] + CardNetwork, + #[strum(serialize = "authentication_type")] + AuthenticationType, + #[strum(serialize = "capture_method")] + CaptureMethod, + #[strum(serialize = "amount")] + PaymentAmount, + #[strum(serialize = "currency")] + PaymentCurrency, + #[strum(serialize = "country", to_string = "business_country")] + BusinessCountry, + #[strum(serialize = "billing_country")] + BillingCountry, + #[strum(serialize = "business_label")] + BusinessLabel, + #[strum(serialize = "setup_future_usage")] + SetupFutureUsage, +} +impl EuclidDirFilter for DummyOutput { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::AuthenticationType, + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::AuthenticationType, + DirKeyKind::CardBin, + DirKeyKind::PayLaterType, + DirKeyKind::PaymentAmount, + DirKeyKind::MetaData, + DirKeyKind::MandateAcceptanceType, + DirKeyKind::MandateType, + DirKeyKind::PaymentType, + DirKeyKind::SetupFutureUsage, + ]; +} +impl EuclidAnalysable for DummyOutput { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { + self.outputs + .iter() + .map(|dummyc| { + let metadata_key = "MetadataKey".to_string(); + let metadata_value = dummyc; + ( + DirValue::MetaData(MetadataValue { + key: metadata_key.clone(), + value: metadata_value.clone(), + }), + std::collections::HashMap::from_iter([( + "DUMMY_OUTPUT".to_string(), + serde_json::json!({ + "rule_name":rule_name, + "Metadata_Key" :metadata_key, + "Metadata_Value" : metadata_value, + }), + )]), + ) + }) + .collect() + } +} +#[derive(Debug, Clone, Serialize)] +pub struct DummyOutput { + pub outputs: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DataType { + Number, + EnumVariant, + MetadataValue, + StrValue, +} + +impl EuclidKey { + pub fn key_type(&self) -> DataType { + match self { + Self::PaymentMethod => DataType::EnumVariant, + Self::CardBin => DataType::StrValue, + Self::Metadata => DataType::MetadataValue, + Self::PaymentMethodType => DataType::EnumVariant, + Self::CardNetwork => DataType::EnumVariant, + Self::AuthenticationType => DataType::EnumVariant, + Self::CaptureMethod => DataType::EnumVariant, + Self::PaymentAmount => DataType::Number, + Self::PaymentCurrency => DataType::EnumVariant, + Self::BusinessCountry => DataType::EnumVariant, + Self::BillingCountry => DataType::EnumVariant, + Self::MandateType => DataType::EnumVariant, + Self::MandateAcceptanceType => DataType::EnumVariant, + Self::PaymentType => DataType::EnumVariant, + Self::BusinessLabel => DataType::StrValue, + Self::SetupFutureUsage => DataType::EnumVariant, + } + } +} + +enums::collect_variants!(EuclidKey); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NumValueRefinement { + NotEqual, + GreaterThan, + LessThan, + GreaterThanEqual, + LessThanEqual, +} + +impl From for Option { + fn from(comp_type: ast::ComparisonType) -> Self { + match comp_type { + ast::ComparisonType::Equal => None, + ast::ComparisonType::NotEqual => Some(NumValueRefinement::NotEqual), + ast::ComparisonType::GreaterThan => Some(NumValueRefinement::GreaterThan), + ast::ComparisonType::LessThan => Some(NumValueRefinement::LessThan), + ast::ComparisonType::LessThanEqual => Some(NumValueRefinement::LessThanEqual), + ast::ComparisonType::GreaterThanEqual => Some(NumValueRefinement::GreaterThanEqual), + } + } +} + +impl From for ast::ComparisonType { + fn from(value: NumValueRefinement) -> Self { + match value { + NumValueRefinement::NotEqual => Self::NotEqual, + NumValueRefinement::LessThan => Self::LessThan, + NumValueRefinement::GreaterThan => Self::GreaterThan, + NumValueRefinement::GreaterThanEqual => Self::GreaterThanEqual, + NumValueRefinement::LessThanEqual => Self::LessThanEqual, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct StrValue { + pub value: String, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct MetadataValue { + pub key: String, + pub value: String, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub struct NumValue { + pub number: i64, + pub refinement: Option, +} + +impl NumValue { + pub fn fits(&self, other: &Self) -> bool { + let this_num = self.number; + let other_num = other.number; + + match (&self.refinement, &other.refinement) { + (None, None) => this_num == other_num, + + (Some(NumValueRefinement::GreaterThan), None) => other_num > this_num, + + (Some(NumValueRefinement::LessThan), None) => other_num < this_num, + + (Some(NumValueRefinement::NotEqual), Some(NumValueRefinement::NotEqual)) => { + other_num == this_num + } + + (Some(NumValueRefinement::GreaterThan), Some(NumValueRefinement::GreaterThan)) => { + other_num > this_num + } + (Some(NumValueRefinement::LessThan), Some(NumValueRefinement::LessThan)) => { + other_num < this_num + } + + (Some(NumValueRefinement::GreaterThanEqual), None) => other_num >= this_num, + (Some(NumValueRefinement::LessThanEqual), None) => other_num <= this_num, + ( + Some(NumValueRefinement::GreaterThanEqual), + Some(NumValueRefinement::GreaterThanEqual), + ) => other_num >= this_num, + + (Some(NumValueRefinement::LessThanEqual), Some(NumValueRefinement::LessThanEqual)) => { + other_num <= this_num + } + + _ => false, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum EuclidValue { + PaymentMethod(enums::PaymentMethod), + CardBin(StrValue), + Metadata(MetadataValue), + PaymentMethodType(enums::PaymentMethodType), + CardNetwork(enums::CardNetwork), + AuthenticationType(enums::AuthenticationType), + CaptureMethod(enums::CaptureMethod), + PaymentType(enums::PaymentType), + MandateAcceptanceType(enums::MandateAcceptanceType), + MandateType(enums::MandateType), + PaymentAmount(NumValue), + PaymentCurrency(enums::Currency), + BusinessCountry(enums::Country), + BillingCountry(enums::Country), + BusinessLabel(StrValue), + SetupFutureUsage(enums::SetupFutureUsage), +} + +impl EuclidValue { + pub fn get_num_value(&self) -> Option { + match self { + Self::PaymentAmount(val) => Some(val.clone()), + _ => None, + } + } + + pub fn get_key(&self) -> EuclidKey { + match self { + Self::PaymentMethod(_) => EuclidKey::PaymentMethod, + Self::CardBin(_) => EuclidKey::CardBin, + Self::Metadata(_) => EuclidKey::Metadata, + Self::PaymentMethodType(_) => EuclidKey::PaymentMethodType, + Self::MandateType(_) => EuclidKey::MandateType, + Self::PaymentType(_) => EuclidKey::PaymentType, + Self::MandateAcceptanceType(_) => EuclidKey::MandateAcceptanceType, + Self::CardNetwork(_) => EuclidKey::CardNetwork, + Self::AuthenticationType(_) => EuclidKey::AuthenticationType, + Self::CaptureMethod(_) => EuclidKey::CaptureMethod, + Self::PaymentAmount(_) => EuclidKey::PaymentAmount, + Self::PaymentCurrency(_) => EuclidKey::PaymentCurrency, + Self::BusinessCountry(_) => EuclidKey::BusinessCountry, + Self::BillingCountry(_) => EuclidKey::BillingCountry, + Self::BusinessLabel(_) => EuclidKey::BusinessLabel, + Self::SetupFutureUsage(_) => EuclidKey::SetupFutureUsage, + } + } +} + +#[cfg(test)] +mod global_type_tests { + use super::*; + + #[test] + fn test_num_value_fits_greater_than() { + let val1 = NumValue { + number: 10, + refinement: Some(NumValueRefinement::GreaterThan), + }; + let val2 = NumValue { + number: 30, + refinement: Some(NumValueRefinement::GreaterThan), + }; + + assert!(val1.fits(&val2)) + } + + #[test] + fn test_num_value_fits_less_than() { + let val1 = NumValue { + number: 30, + refinement: Some(NumValueRefinement::LessThan), + }; + let val2 = NumValue { + number: 10, + refinement: Some(NumValueRefinement::LessThan), + }; + + assert!(val1.fits(&val2)); + } +} diff --git a/crates/euclid/src/types/transformers.rs b/crates/euclid/src/types/transformers.rs new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/crates/euclid/src/types/transformers.rs @@ -0,0 +1 @@ + diff --git a/crates/euclid/src/utils.rs b/crates/euclid/src/utils.rs new file mode 100644 index 000000000000..e8cb7901f0d7 --- /dev/null +++ b/crates/euclid/src/utils.rs @@ -0,0 +1,3 @@ +pub mod dense_map; + +pub use dense_map::{DenseMap, EntityId}; diff --git a/crates/euclid/src/utils/dense_map.rs b/crates/euclid/src/utils/dense_map.rs new file mode 100644 index 000000000000..8bd4487c77b9 --- /dev/null +++ b/crates/euclid/src/utils/dense_map.rs @@ -0,0 +1,224 @@ +use std::{fmt, iter, marker::PhantomData, ops, slice, vec}; + +pub trait EntityId { + fn get_id(&self) -> usize; + fn with_id(id: usize) -> Self; +} + +pub struct DenseMap { + data: Vec, + _marker: PhantomData, +} + +impl DenseMap { + pub fn new() -> Self { + Self { + data: Vec::new(), + _marker: PhantomData, + } + } +} + +impl Default for DenseMap { + fn default() -> Self { + Self::new() + } +} + +impl DenseMap +where + K: EntityId, +{ + pub fn push(&mut self, elem: V) -> K { + let curr_len = self.data.len(); + self.data.push(elem); + K::with_id(curr_len) + } + + #[inline] + pub fn get(&self, idx: K) -> Option<&V> { + self.data.get(idx.get_id()) + } + + #[inline] + pub fn get_mut(&mut self, idx: K) -> Option<&mut V> { + self.data.get_mut(idx.get_id()) + } + + #[inline] + pub fn contains_key(&self, key: K) -> bool { + key.get_id() < self.data.len() + } + + #[inline] + pub fn keys(&self) -> Keys { + Keys::new(0..self.data.len()) + } + + #[inline] + pub fn into_keys(self) -> Keys { + Keys::new(0..self.data.len()) + } + + #[inline] + pub fn values(&self) -> slice::Iter<'_, V> { + self.data.iter() + } + + #[inline] + pub fn values_mut(&mut self) -> slice::IterMut<'_, V> { + self.data.iter_mut() + } + + #[inline] + pub fn into_values(self) -> vec::IntoIter { + self.data.into_iter() + } + + #[inline] + pub fn iter(&self) -> Iter<'_, K, V> { + Iter::new(self.data.iter()) + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_, K, V> { + IterMut::new(self.data.iter_mut()) + } +} + +impl fmt::Debug for DenseMap +where + K: EntityId + fmt::Debug, + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +pub struct Keys { + inner: ops::Range, + _marker: PhantomData, +} + +impl Keys { + fn new(range: ops::Range) -> Self { + Self { + inner: range, + _marker: PhantomData, + } + } +} + +impl Iterator for Keys +where + K: EntityId, +{ + type Item = K; + + fn next(&mut self) -> Option { + self.inner.next().map(K::with_id) + } +} + +pub struct Iter<'a, K, V> { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl<'a, K, V> Iter<'a, K, V> { + fn new(iter: slice::Iter<'a, V>) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl<'a, K, V> Iterator for Iter<'a, K, V> +where + K: EntityId, +{ + type Item = (K, &'a V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +pub struct IterMut<'a, K, V> { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl<'a, K, V> IterMut<'a, K, V> { + fn new(iter: slice::IterMut<'a, V>) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl<'a, K, V> Iterator for IterMut<'a, K, V> +where + K: EntityId, +{ + type Item = (K, &'a mut V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +pub struct IntoIter { + inner: iter::Enumerate>, + _marker: PhantomData, +} + +impl IntoIter { + fn new(iter: vec::IntoIter) -> Self { + Self { + inner: iter.enumerate(), + _marker: PhantomData, + } + } +} + +impl Iterator for IntoIter +where + K: EntityId, +{ + type Item = (K, V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(id, val)| (K::with_id(id), val)) + } +} + +impl IntoIterator for DenseMap +where + K: EntityId, +{ + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self.data.into_iter()) + } +} + +impl FromIterator for DenseMap +where + K: EntityId, +{ + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Self { + data: Vec::from_iter(iter), + _marker: PhantomData, + } + } +} diff --git a/crates/euclid_macros/Cargo.toml b/crates/euclid_macros/Cargo.toml new file mode 100644 index 000000000000..2524887a8a0f --- /dev/null +++ b/crates/euclid_macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "euclid_macros" +description = "Macros for Euclid DSL" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.51" +quote = "1.0.23" +rustc-hash = "1.1.0" +strum = { version = "0.24", features = ["derive"] } +syn = "1.0.109" diff --git a/crates/euclid_macros/src/inner.rs b/crates/euclid_macros/src/inner.rs new file mode 100644 index 000000000000..979527560dd6 --- /dev/null +++ b/crates/euclid_macros/src/inner.rs @@ -0,0 +1,5 @@ +mod enum_nums; +mod knowledge; + +pub(crate) use enum_nums::enum_nums_inner; +pub(crate) use knowledge::knowledge_inner; diff --git a/crates/euclid_macros/src/inner/enum_nums.rs b/crates/euclid_macros/src/inner/enum_nums.rs new file mode 100644 index 000000000000..61f6765fce0e --- /dev/null +++ b/crates/euclid_macros/src/inner/enum_nums.rs @@ -0,0 +1,47 @@ +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; + +fn error() -> TokenStream2 { + syn::Error::new( + Span::call_site(), + "'EnumNums' can only be derived on enums with unit variants".to_string(), + ) + .to_compile_error() +} + +pub(crate) fn enum_nums_inner(ts: TokenStream) -> TokenStream { + let derive_input = syn::parse_macro_input!(ts as syn::DeriveInput); + + let enum_obj = match derive_input.data { + syn::Data::Enum(e) => e, + _ => return error().into(), + }; + + let enum_name = derive_input.ident; + + let mut match_arms = Vec::::with_capacity(enum_obj.variants.len()); + + for (i, variant) in enum_obj.variants.iter().enumerate() { + match variant.fields { + syn::Fields::Unit => {} + _ => return error().into(), + } + + let var_ident = &variant.ident; + + match_arms.push(quote! { Self::#var_ident => #i }); + } + + let impl_block = quote! { + impl #enum_name { + pub fn to_num(&self) -> usize { + match self { + #(#match_arms),* + } + } + } + }; + + impl_block.into() +} diff --git a/crates/euclid_macros/src/inner/knowledge.rs b/crates/euclid_macros/src/inner/knowledge.rs new file mode 100644 index 000000000000..73b94919c903 --- /dev/null +++ b/crates/euclid_macros/src/inner/knowledge.rs @@ -0,0 +1,680 @@ +use std::{hash::Hash, rc::Rc}; + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use rustc_hash::{FxHashMap, FxHashSet}; +use syn::{parse::Parse, Token}; + +mod strength { + syn::custom_punctuation!(Normal, ->); + syn::custom_punctuation!(Strong, ->>); +} + +mod kw { + syn::custom_keyword!(any); + syn::custom_keyword!(not); +} + +#[derive(Clone, PartialEq, Eq, Hash)] +enum Comparison { + LessThan, + Equal, + GreaterThan, + GreaterThanEqual, + LessThanEqual, +} + +impl ToString for Comparison { + fn to_string(&self) -> String { + match self { + Self::LessThan => "< ".to_string(), + Self::Equal => String::new(), + Self::GreaterThanEqual => ">= ".to_string(), + Self::LessThanEqual => "<= ".to_string(), + Self::GreaterThan => "> ".to_string(), + } + } +} + +impl Parse for Comparison { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + if input.peek(Token![>]) { + input.parse::]>()?; + Ok(Self::GreaterThan) + } else if input.peek(Token![<]) { + input.parse::()?; + Ok(Self::LessThan) + } else if input.peek(Token!(<=)) { + input.parse::()?; + Ok(Self::LessThanEqual) + } else if input.peek(Token!(>=)) { + input.parse::=]>()?; + Ok(Self::GreaterThanEqual) + } else { + Ok(Self::Equal) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +enum ValueType { + Any, + EnumVariant(String), + Number { number: i64, comparison: Comparison }, +} + +impl ValueType { + fn to_string(&self, key: &str) -> String { + match self { + Self::Any => format!("{key}(any)"), + Self::EnumVariant(s) => format!("{key}({s})"), + Self::Number { number, comparison } => { + format!("{}({}{})", key, comparison.to_string(), number) + } + } + } +} + +impl Parse for ValueType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Ident) { + let ident: syn::Ident = input.parse()?; + Ok(Self::EnumVariant(ident.to_string())) + } else if lookahead.peek(Token![>]) + || lookahead.peek(Token![<]) + || lookahead.peek(syn::LitInt) + { + let comparison: Comparison = input.parse()?; + let number: syn::LitInt = input.parse()?; + let num_val = number.base10_parse::()?; + Ok(Self::Number { + number: num_val, + comparison, + }) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct Atom { + key: String, + value: ValueType, +} + +impl ToString for Atom { + fn to_string(&self) -> String { + self.value.to_string(&self.key) + } +} + +impl Parse for Atom { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let maybe_any: syn::Ident = input.parse()?; + if maybe_any == "any" { + let actual_key: syn::Ident = input.parse()?; + Ok(Self { + key: actual_key.to_string(), + value: ValueType::Any, + }) + } else { + let content; + syn::parenthesized!(content in input); + let value: ValueType = content.parse()?; + Ok(Self { + key: maybe_any.to_string(), + value, + }) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash, strum::Display)] +enum Strength { + Normal, + Strong, +} + +impl Parse for Strength { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(strength::Strong) { + input.parse::()?; + Ok(Self::Strong) + } else if lookahead.peek(strength::Normal) { + input.parse::()?; + Ok(Self::Normal) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash, strum::Display)] +enum Relation { + Positive, + Negative, +} + +enum AtomType { + Value { + relation: Relation, + atom: Rc, + }, + + InAggregator { + key: String, + values: Vec, + relation: Relation, + }, +} + +fn parse_atom_type_inner( + input: syn::parse::ParseStream<'_>, + key: syn::Ident, + relation: Relation, +) -> syn::Result { + let result = if input.peek(Token![in]) { + input.parse::()?; + + let bracketed; + syn::bracketed!(bracketed in input); + + let mut values = Vec::::new(); + let first: syn::Ident = bracketed.parse()?; + values.push(first.to_string()); + while !bracketed.is_empty() { + bracketed.parse::()?; + let next: syn::Ident = bracketed.parse()?; + values.push(next.to_string()); + } + + AtomType::InAggregator { + key: key.to_string(), + values, + relation, + } + } else if input.peek(kw::any) { + input.parse::()?; + AtomType::Value { + relation, + atom: Rc::new(Atom { + key: key.to_string(), + value: ValueType::Any, + }), + } + } else { + let value: ValueType = input.parse()?; + AtomType::Value { + relation, + atom: Rc::new(Atom { + key: key.to_string(), + value, + }), + } + }; + + Ok(result) +} + +impl Parse for AtomType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let key: syn::Ident = input.parse()?; + let content; + syn::parenthesized!(content in input); + + let relation = if content.peek(kw::not) { + content.parse::()?; + Relation::Negative + } else { + Relation::Positive + }; + + let result = parse_atom_type_inner(&content, key, relation)?; + + if !content.is_empty() { + Err(content.error("Unexpected input received after atom value")) + } else { + Ok(result) + } + } +} + +fn parse_rhs_atom(input: syn::parse::ParseStream<'_>) -> syn::Result { + let key: syn::Ident = input.parse()?; + let content; + syn::parenthesized!(content in input); + + let lookahead = content.lookahead1(); + + let value_type = if lookahead.peek(kw::any) { + content.parse::()?; + ValueType::Any + } else if lookahead.peek(syn::Ident) { + let variant = content.parse::()?; + ValueType::EnumVariant(variant.to_string()) + } else { + return Err(lookahead.error()); + }; + + if !content.is_empty() { + Err(content.error("Unexpected input received after atom value")) + } else { + Ok(Atom { + key: key.to_string(), + value: value_type, + }) + } +} + +struct Rule { + lhs: Vec, + strength: Strength, + rhs: Rc, +} + +impl Parse for Rule { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let first_atom: AtomType = input.parse()?; + let mut lhs: Vec = vec![first_atom]; + + while input.peek(Token![&]) { + input.parse::()?; + let and_atom: AtomType = input.parse()?; + lhs.push(and_atom); + } + + let strength: Strength = input.parse()?; + + let rhs: Rc = Rc::new(parse_rhs_atom(input)?); + + input.parse::()?; + + Ok(Self { lhs, strength, rhs }) + } +} + +#[derive(Clone)] +enum Scope { + Crate, + Extern, +} + +impl Parse for Scope { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Token![crate]) { + input.parse::()?; + Ok(Self::Crate) + } else if lookahead.peek(Token![extern]) { + input.parse::()?; + Ok(Self::Extern) + } else { + Err(lookahead.error()) + } + } +} + +impl ToString for Scope { + fn to_string(&self) -> String { + match self { + Self::Crate => "crate".to_string(), + Self::Extern => "euclid".to_string(), + } + } +} + +#[derive(Clone)] +struct Program { + rules: Vec>, + scope: Scope, +} + +impl Parse for Program { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let scope: Scope = input.parse()?; + let mut rules: Vec> = Vec::new(); + + while !input.is_empty() { + rules.push(Rc::new(input.parse::()?)); + } + + Ok(Self { rules, scope }) + } +} + +struct GenContext { + next_idx: usize, + next_node_idx: usize, + idx2atom: FxHashMap>, + atom2idx: FxHashMap, usize>, + edges: FxHashMap>, + compiled_atoms: FxHashMap, proc_macro2::Ident>, +} + +impl GenContext { + fn new() -> Self { + Self { + next_idx: 1, + next_node_idx: 1, + idx2atom: FxHashMap::default(), + atom2idx: FxHashMap::default(), + edges: FxHashMap::default(), + compiled_atoms: FxHashMap::default(), + } + } + + fn register_node(&mut self, atom: Rc) -> usize { + if let Some(idx) = self.atom2idx.get(&atom) { + *idx + } else { + let this_idx = self.next_idx; + self.next_idx += 1; + + self.idx2atom.insert(this_idx, Rc::clone(&atom)); + self.atom2idx.insert(atom, this_idx); + + this_idx + } + } + + fn register_edge(&mut self, from: usize, to: usize) -> Result<(), String> { + let node_children = self.edges.entry(from).or_default(); + if node_children.contains(&to) { + Err("Duplicate edge detected".to_string()) + } else { + node_children.insert(to); + self.edges.entry(to).or_default(); + Ok(()) + } + } + + fn register_rule(&mut self, rule: &Rule) -> Result<(), String> { + let to_idx = self.register_node(Rc::clone(&rule.rhs)); + + for atom_type in &rule.lhs { + if let AtomType::Value { atom, .. } = atom_type { + let from_idx = self.register_node(Rc::clone(atom)); + self.register_edge(from_idx, to_idx)?; + } + } + + Ok(()) + } + + fn cycle_dfs( + &self, + node_id: usize, + explored: &mut FxHashSet, + visited: &mut FxHashSet, + order: &mut Vec, + ) -> Result>, String> { + if explored.contains(&node_id) { + let position = order + .iter() + .position(|v| *v == node_id) + .ok_or_else(|| "Error deciding cycle order".to_string())?; + + let cycle_order = order[position..].to_vec(); + Ok(Some(cycle_order)) + } else if visited.contains(&node_id) { + Ok(None) + } else { + visited.insert(node_id); + explored.insert(node_id); + order.push(node_id); + let dests = self + .edges + .get(&node_id) + .ok_or_else(|| "Error getting edges of node".to_string())?; + + for dest in dests.iter().copied() { + if let Some(cycle) = self.cycle_dfs(dest, explored, visited, order)? { + return Ok(Some(cycle)); + } + } + + order.pop(); + + Ok(None) + } + } + + fn detect_graph_cycles(&self) -> Result<(), String> { + let start_nodes = self.edges.keys().copied().collect::>(); + + let mut total_visited = FxHashSet::::default(); + + for node_id in start_nodes.iter().copied() { + let mut explored = FxHashSet::::default(); + let mut order = Vec::::new(); + + match self.cycle_dfs(node_id, &mut explored, &mut total_visited, &mut order)? { + None => {} + Some(order) => { + let mut display_strings = Vec::::with_capacity(order.len() + 1); + + for cycle_node_id in order { + let node = self.idx2atom.get(&cycle_node_id).ok_or_else(|| { + "Failed to find node during cycle display creation".to_string() + })?; + + display_strings.push(node.to_string()); + } + + let first = display_strings + .first() + .cloned() + .ok_or("Unable to fill cycle display array")?; + + display_strings.push(first); + + return Err(format!("Found cycle: {}", display_strings.join(" -> "))); + } + } + } + + Ok(()) + } + + fn next_node_ident(&mut self) -> (proc_macro2::Ident, usize) { + let this_idx = self.next_node_idx; + self.next_node_idx += 1; + (format_ident!("_node_{this_idx}"), this_idx) + } + + fn compile_atom( + &mut self, + atom: &Rc, + tokens: &mut TokenStream, + ) -> Result { + let maybe_ident = self.compiled_atoms.get(atom); + + if let Some(ident) = maybe_ident { + Ok(ident.clone()) + } else { + let (identifier, _) = self.next_node_ident(); + let key = format_ident!("{}", &atom.key); + let the_value = match &atom.value { + ValueType::Any => quote! { + NodeValue::Key(DirKey::new(DirKeyKind::#key,None)) + }, + ValueType::EnumVariant(variant) => { + let variant = format_ident!("{}", variant); + quote! { + NodeValue::Value(DirValue::#key(#key::#variant)) + } + } + ValueType::Number { number, comparison } => { + let comp_type = match comparison { + Comparison::Equal => quote! { + None + }, + Comparison::LessThan => quote! { + Some(NumValueRefinement::LessThan) + }, + Comparison::GreaterThan => quote! { + Some(NumValueRefinement::GreaterThan) + }, + Comparison::GreaterThanEqual => quote! { + Some(NumValueRefinement::GreaterThanEqual) + }, + Comparison::LessThanEqual => quote! { + Some(NumValueRefinement::LessThanEqual) + }, + }; + + quote! { + NodeValue::Value(DirValue::#key(NumValue { + number: #number, + refinement: #comp_type, + })) + } + } + }; + + let compiled = quote! { + let #identifier = graph.make_value_node(#the_value, None, Vec::new(), None::<()>).expect("NodeId derivation failed"); + }; + + tokens.extend(compiled); + self.compiled_atoms + .insert(Rc::clone(atom), identifier.clone()); + + Ok(identifier) + } + } + + fn compile_atom_type( + &mut self, + atom_type: &AtomType, + tokens: &mut TokenStream, + ) -> Result<(proc_macro2::Ident, Relation), String> { + match atom_type { + AtomType::Value { relation, atom } => { + let node_ident = self.compile_atom(atom, tokens)?; + + Ok((node_ident, relation.clone())) + } + + AtomType::InAggregator { + key, + values, + relation, + } => { + let key_ident = format_ident!("{key}"); + let mut values_tokens: Vec = Vec::new(); + + for value in values { + let value_ident = format_ident!("{value}"); + values_tokens.push(quote! { DirValue::#key_ident(#key_ident::#value_ident) }); + } + + let (node_ident, _) = self.next_node_ident(); + let node_code = quote! { + let #node_ident = graph.make_in_aggregator( + Vec::from_iter([#(#values_tokens),*]), + None, + None::<()>, + Vec::new(), + ).expect("Failed to make In aggregator"); + }; + + tokens.extend(node_code); + + Ok((node_ident, relation.clone())) + } + } + } + + fn compile_rule(&mut self, rule: &Rule, tokens: &mut TokenStream) -> Result<(), String> { + let rhs_ident = self.compile_atom(&rule.rhs, tokens)?; + let mut node_details: Vec<(proc_macro2::Ident, Relation)> = + Vec::with_capacity(rule.lhs.len()); + for lhs_atom_type in &rule.lhs { + let details = self.compile_atom_type(lhs_atom_type, tokens)?; + node_details.push(details); + } + + if node_details.len() <= 1 { + let strength = format_ident!("{}", rule.strength.to_string()); + for (from_node, relation) in &node_details { + let relation = format_ident!("{}", relation.to_string()); + tokens.extend(quote! { + graph.make_edge(#from_node, #rhs_ident, Strength::#strength, Relation::#relation) + .expect("Failed to make edge"); + }); + } + } else { + let mut all_agg_nodes: Vec = Vec::with_capacity(node_details.len()); + for (from_node, relation) in &node_details { + let relation = format_ident!("{}", relation.to_string()); + all_agg_nodes.push(quote! { (#from_node, Relation::#relation, Strength::Strong) }); + } + + let strength = format_ident!("{}", rule.strength.to_string()); + let (agg_node_ident, _) = self.next_node_ident(); + tokens.extend(quote! { + let #agg_node_ident = graph.make_all_aggregator(&[#(#all_agg_nodes),*], None, None::<()>, Vec::new()) + .expect("Failed to make all aggregator node"); + + graph.make_edge(#agg_node_ident, #rhs_ident, Strength::#strength, Relation::Positive) + .expect("Failed to create all aggregator edge"); + + }); + } + + Ok(()) + } + + fn compile(&mut self, program: Program) -> Result { + let mut tokens = TokenStream::new(); + for rule in &program.rules { + self.compile_rule(rule, &mut tokens)?; + } + + let scope = match &program.scope { + Scope::Crate => quote! { crate }, + Scope::Extern => quote! { euclid }, + }; + + let compiled = quote! {{ + use #scope::{ + dssa::graph::*, + types::*, + frontend::dir::{*, enums::*}, + }; + + use rustc_hash::{FxHashMap, FxHashSet}; + + let mut graph = KnowledgeGraphBuilder::new(); + + #tokens + + graph.build() + }}; + + Ok(compiled) + } +} + +pub(crate) fn knowledge_inner(ts: TokenStream) -> syn::Result { + let program = syn::parse::(ts.into())?; + let mut gen_context = GenContext::new(); + + for rule in &program.rules { + gen_context + .register_rule(rule) + .map_err(|msg| syn::Error::new(Span::call_site(), msg))?; + } + + gen_context + .detect_graph_cycles() + .map_err(|msg| syn::Error::new(Span::call_site(), msg))?; + + gen_context + .compile(program) + .map_err(|msg| syn::Error::new(Span::call_site(), msg)) +} diff --git a/crates/euclid_macros/src/lib.rs b/crates/euclid_macros/src/lib.rs new file mode 100644 index 000000000000..97b42aaa64c1 --- /dev/null +++ b/crates/euclid_macros/src/lib.rs @@ -0,0 +1,16 @@ +mod inner; + +use proc_macro::TokenStream; + +#[proc_macro_derive(EnumNums)] +pub fn enum_nums(ts: TokenStream) -> TokenStream { + inner::enum_nums_inner(ts) +} + +#[proc_macro] +pub fn knowledge(ts: TokenStream) -> TokenStream { + match inner::knowledge_inner(ts.into()) { + Ok(ts) => ts.into(), + Err(e) => e.into_compile_error().into(), + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml new file mode 100644 index 000000000000..90489eb78bf6 --- /dev/null +++ b/crates/euclid_wasm/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "euclid_wasm" +description = "WASM bindings for Euclid DSL" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib"] + +[features] +default = ["connector_choice_bcompat", "payouts"] +connector_choice_bcompat = [ + "euclid/connector_choice_bcompat", + "api_models/connector_choice_bcompat", + "kgraph_utils/backwards_compatibility" +] +connector_choice_mca_id = [ + "api_models/connector_choice_mca_id", + "euclid/connector_choice_mca_id", + "kgraph_utils/connector_choice_mca_id" +] +dummy_connector = ["kgraph_utils/dummy_connector"] +payouts = [] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +euclid = { path = "../euclid", features = [] } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +getrandom = { version = "0.2.10", features = ["js"] } +once_cell = "1.18.0" +serde = { version = "1.0", features = [] } +serde-wasm-bindgen = "0.5" +strum = { version = "0.25", features = ["derive"] } +wasm-bindgen = { version = "0.2.86" } +ron-parser = "0.1.4" diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs new file mode 100644 index 000000000000..e85a002544ff --- /dev/null +++ b/crates/euclid_wasm/src/lib.rs @@ -0,0 +1,227 @@ +#![allow(non_upper_case_globals)] +mod types; +mod utils; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; + +use api_models::{admin as admin_api, routing::ConnectorSelection}; +use euclid::{ + backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, + dssa::{ + self, analyzer, + graph::{self, Memoization}, + state_machine, truth, + }, + enums, + frontend::{ + ast, + dir::{self, enums as dir_enums}, + }, +}; +use once_cell::sync::OnceCell; +use strum::{EnumMessage, EnumProperty, VariantNames}; +use wasm_bindgen::prelude::*; + +use crate::utils::JsResultExt; +type JsResult = Result; + +struct SeedData<'a> { + kgraph: graph::KnowledgeGraph<'a>, + connectors: Vec, +} + +static SEED_DATA: OnceCell> = OnceCell::new(); + +/// This function can be used by the frontend to provide the WASM with information about +/// all the merchant's connector accounts. The input argument is a vector of all the merchant's +/// connector accounts from the API. +#[wasm_bindgen(js_name = seedKnowledgeGraph)] +pub fn seed_knowledge_graph(mcas: JsValue) -> JsResult { + let mcas: Vec = serde_wasm_bindgen::from_value(mcas)?; + let connectors: Vec = mcas + .iter() + .map(|mca| { + Ok::<_, strum::ParseError>(ast::ConnectorChoice { + connector: dir_enums::Connector::from_str(&mca.connector_name)?, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: mca.business_sub_label.clone(), + }) + }) + .collect::>() + .map_err(|_| "invalid connector name received") + .err_to_js()?; + + let mca_graph = kgraph_utils::mca::make_mca_graph(mcas).err_to_js()?; + let analysis_graph = + graph::KnowledgeGraph::combine(&mca_graph, &truth::ANALYSIS_GRAPH).err_to_js()?; + + SEED_DATA + .set(SeedData { + kgraph: analysis_graph, + connectors, + }) + .map_err(|_| "Knowledge Graph has been already seeded".to_string()) + .err_to_js()?; + + Ok(JsValue::NULL) +} + +/// This function allows the frontend to get all the merchant's configured +/// connectors that are valid for a rule based on the conditions specified in +/// the rule +#[wasm_bindgen(js_name = getValidConnectorsForRule)] +pub fn get_valid_connectors_for_rule(rule: JsValue) -> JsResult { + let seed_data = SEED_DATA.get().ok_or("Data not seeded").err_to_js()?; + + let rule: ast::Rule = serde_wasm_bindgen::from_value(rule)?; + let dir_rule = ast::lowering::lower_rule(rule).err_to_js()?; + let mut valid_connectors: Vec<(ast::ConnectorChoice, dir::DirValue)> = seed_data + .connectors + .iter() + .cloned() + .map(|choice| (choice.clone(), dir::DirValue::Connector(Box::new(choice)))) + .collect(); + let mut invalid_connectors: HashSet = HashSet::new(); + + let mut ctx_manager = state_machine::RuleContextManager::new(&dir_rule, &[]); + + let dummy_meta = HashMap::new(); + + // For every conjunctive context in the Rule, verify validity of all still-valid connectors + // using the knowledge graph + while let Some(ctx) = ctx_manager.advance_mut().err_to_js()? { + // Standalone conjunctive context analysis to ensure the context itself is valid before + // checking it against merchant's connectors + seed_data + .kgraph + .perform_context_analysis(ctx, &mut Memoization::new()) + .err_to_js()?; + + // Update conjunctive context and run analysis on all of merchant's connectors. + for (conn, choice) in &valid_connectors { + if invalid_connectors.contains(conn) { + continue; + } + + let ctx_val = dssa::types::ContextValue::assertion(choice, &dummy_meta); + ctx.push(ctx_val); + let analysis_result = seed_data + .kgraph + .perform_context_analysis(ctx, &mut Memoization::new()); + if analysis_result.is_err() { + invalid_connectors.insert(conn.clone()); + } + ctx.pop(); + } + } + + valid_connectors.retain(|(k, _)| !invalid_connectors.contains(k)); + + let valid_connectors: Vec = + valid_connectors.into_iter().map(|c| c.0).collect(); + + Ok(serde_wasm_bindgen::to_value(&valid_connectors)?) +} + +#[wasm_bindgen(js_name = analyzeProgram)] +pub fn analyze_program(js_program: JsValue) -> JsResult { + let program: ast::Program = serde_wasm_bindgen::from_value(js_program)?; + analyzer::analyze(program, SEED_DATA.get().map(|sd| &sd.kgraph)).err_to_js()?; + Ok(JsValue::NULL) +} + +#[wasm_bindgen(js_name = runProgram)] +pub fn run_program(program: JsValue, input: JsValue) -> JsResult { + let program: ast::Program = serde_wasm_bindgen::from_value(program)?; + let input: inputs::BackendInput = serde_wasm_bindgen::from_value(input)?; + + let backend = InterpreterBackend::with_program(program).err_to_js()?; + + let res: euclid::backend::BackendOutput = + backend.execute(input).err_to_js()?; + + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[wasm_bindgen(js_name = getAllConnectors)] +pub fn get_all_connectors() -> JsResult { + Ok(serde_wasm_bindgen::to_value(enums::Connector::VARIANTS)?) +} + +#[wasm_bindgen(js_name = getAllKeys)] +pub fn get_all_keys() -> JsResult { + let keys: Vec<&'static str> = dir::DirKeyKind::VARIANTS + .iter() + .copied() + .filter(|s| s != &"Connector") + .collect(); + Ok(serde_wasm_bindgen::to_value(&keys)?) +} + +#[wasm_bindgen(js_name = getKeyType)] +pub fn get_key_type(key: &str) -> Result { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + let key_str = key.get_type().to_string(); + Ok(key_str) +} + +#[wasm_bindgen(js_name=parseToString)] +pub fn parser(val: String) -> String { + ron_parser::my_parse(val) +} + +#[wasm_bindgen(js_name = getVariantValues)] +pub fn get_variant_values(key: &str) -> Result { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + + let variants: &[&str] = match key { + dir::DirKeyKind::PaymentMethod => dir_enums::PaymentMethod::VARIANTS, + dir::DirKeyKind::CardType => dir_enums::CardType::VARIANTS, + dir::DirKeyKind::CardNetwork => dir_enums::CardNetwork::VARIANTS, + dir::DirKeyKind::PayLaterType => dir_enums::PayLaterType::VARIANTS, + dir::DirKeyKind::WalletType => dir_enums::WalletType::VARIANTS, + dir::DirKeyKind::BankRedirectType => dir_enums::BankRedirectType::VARIANTS, + dir::DirKeyKind::CryptoType => dir_enums::CryptoType::VARIANTS, + dir::DirKeyKind::RewardType => dir_enums::RewardType::VARIANTS, + dir::DirKeyKind::AuthenticationType => dir_enums::AuthenticationType::VARIANTS, + dir::DirKeyKind::CaptureMethod => dir_enums::CaptureMethod::VARIANTS, + dir::DirKeyKind::PaymentCurrency => dir_enums::PaymentCurrency::VARIANTS, + dir::DirKeyKind::BusinessCountry => dir_enums::Country::VARIANTS, + dir::DirKeyKind::BillingCountry => dir_enums::Country::VARIANTS, + dir::DirKeyKind::BankTransferType => dir_enums::BankTransferType::VARIANTS, + dir::DirKeyKind::UpiType => dir_enums::UpiType::VARIANTS, + dir::DirKeyKind::SetupFutureUsage => dir_enums::SetupFutureUsage::VARIANTS, + dir::DirKeyKind::PaymentType => dir_enums::PaymentType::VARIANTS, + dir::DirKeyKind::MandateType => dir_enums::MandateType::VARIANTS, + dir::DirKeyKind::MandateAcceptanceType => dir_enums::MandateAcceptanceType::VARIANTS, + dir::DirKeyKind::CardRedirectType => dir_enums::CardRedirectType::VARIANTS, + dir::DirKeyKind::GiftCardType => dir_enums::GiftCardType::VARIANTS, + dir::DirKeyKind::VoucherType => dir_enums::VoucherType::VARIANTS, + dir::DirKeyKind::PaymentAmount + | dir::DirKeyKind::Connector + | dir::DirKeyKind::CardBin + | dir::DirKeyKind::BusinessLabel + | dir::DirKeyKind::MetaData => Err("Key does not have variants".to_string())?, + dir::DirKeyKind::BankDebitType => dir_enums::BankDebitType::VARIANTS, + }; + + Ok(serde_wasm_bindgen::to_value(variants)?) +} + +#[wasm_bindgen(js_name = addTwo)] +pub fn add_two(n1: i64, n2: i64) -> i64 { + n1 + n2 +} + +#[wasm_bindgen(js_name = getDescriptionCategory)] +pub fn get_description_category(key: &str) -> JsResult { + let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + + let result = types::Details { + description: key.get_detailed_message(), + category: key.get_str("Category"), + }; + Ok(serde_wasm_bindgen::to_value(&result)?) +} diff --git a/crates/euclid_wasm/src/types.rs b/crates/euclid_wasm/src/types.rs new file mode 100644 index 000000000000..ea40449971bc --- /dev/null +++ b/crates/euclid_wasm/src/types.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +#[derive(Serialize, Clone)] +pub struct Details<'a> { + pub description: Option<&'a str>, + pub category: Option<&'a str>, +} diff --git a/crates/euclid_wasm/src/utils.rs b/crates/euclid_wasm/src/utils.rs new file mode 100644 index 000000000000..c531dabd7e2a --- /dev/null +++ b/crates/euclid_wasm/src/utils.rs @@ -0,0 +1,17 @@ +use wasm_bindgen::prelude::*; + +pub trait JsResultExt { + fn err_to_js(self) -> Result; +} + +impl JsResultExt for Result +where + E: serde::Serialize, +{ + fn err_to_js(self) -> Result { + match self { + Ok(t) => Ok(t), + Err(e) => Err(serde_wasm_bindgen::to_value(&e)?), + } + } +} diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml new file mode 100644 index 000000000000..fa90b3974c20 --- /dev/null +++ b/crates/kgraph_utils/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "kgraph_utils" +description = "Utilities for constructing and working with Knowledge Graphs" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[features] +dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector"] +backwards_compatibility = ["euclid/backwards_compatibility", "euclid/backwards_compatibility"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id"] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +euclid = { version = "0.1.0", path = "../euclid" } +masking = { version = "0.1.0", path = "../masking/"} + +serde = "1.0.163" +serde_json = "1.0.96" +thiserror = "1.0.43" + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "evaluation" +harness = false diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs new file mode 100644 index 000000000000..ecea12203f8a --- /dev/null +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -0,0 +1,113 @@ +#![allow(unused, clippy::expect_used)] + +use std::str::FromStr; + +use api_models::{ + admin as admin_api, enums as api_enums, payment_methods::RequestPaymentMethodTypes, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use euclid::{ + dirval, + dssa::graph::{self, Memoization}, + frontend::dir, + types::{NumValue, NumValueRefinement}, +}; +use kgraph_utils::{error::KgraphError, transformers::IntoDirValue}; + +fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::KnowledgeGraph<'a> { + use api_models::{admin::*, payment_methods::*}; + + let mut pms_enabled: Vec = Vec::new(); + + for _ in (0..total_enabled) { + let mut pm_types: Vec = Vec::new(); + for _ in (0..total_pm_types) { + pm_types.push(RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Credit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Visa, + api_enums::CardNetwork::Mastercard, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::USD, + api_enums::Currency::INR, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }); + } + + pms_enabled.push(PaymentMethodsEnabled { + payment_method: api_enums::PaymentMethod::Card, + payment_method_types: Some(pm_types), + }); + } + + let stripe_account = MerchantConnectorResponse { + connector_type: api_enums::ConnectorType::FizOperations, + connector_name: "stripe".to_string(), + merchant_connector_id: "something".to_string(), + connector_account_details: masking::Secret::new(serde_json::json!({})), + test_mode: None, + disabled: None, + metadata: None, + payment_methods_enabled: Some(pms_enabled), + business_country: Some(api_enums::CountryAlpha2::US), + business_label: Some("hello".to_string()), + connector_label: Some("something".to_string()), + business_sub_label: Some("something".to_string()), + frm_configs: None, + connector_webhook_details: None, + profile_id: None, + applepay_verified_domains: None, + pm_auth_config: None, + }; + + kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") +} + +fn evaluation(c: &mut Criterion) { + let small_graph = build_test_data(3, 8); + let big_graph = build_test_data(20, 20); + + c.bench_function("MCA Small Graph Evaluation", |b| { + b.iter(|| { + small_graph.key_value_analysis( + dirval!(Connector = Stripe), + &graph::AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = BWP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + }); + }); + + c.bench_function("MCA Big Graph Evaluation", |b| { + b.iter(|| { + big_graph.key_value_analysis( + dirval!(Connector = Stripe), + &graph::AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = BWP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + }); + }); +} + +criterion_group!(benches, evaluation); +criterion_main!(benches); diff --git a/crates/kgraph_utils/src/error.rs b/crates/kgraph_utils/src/error.rs new file mode 100644 index 000000000000..5a16c6375b06 --- /dev/null +++ b/crates/kgraph_utils/src/error.rs @@ -0,0 +1,14 @@ +use euclid::dssa::{graph::GraphError, types::AnalysisErrorType}; + +#[derive(Debug, thiserror::Error, serde::Serialize)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum KgraphError { + #[error("Invalid connector name encountered: '{0}'")] + InvalidConnectorName(String), + #[error("There was an error constructing the graph: {0}")] + GraphConstructionError(GraphError), + #[error("There was an error constructing the context")] + ContextConstructionError(AnalysisErrorType), + #[error("there was an unprecedented indexing error")] + IndexingError, +} diff --git a/crates/kgraph_utils/src/lib.rs b/crates/kgraph_utils/src/lib.rs new file mode 100644 index 000000000000..eb8eef6dedb5 --- /dev/null +++ b/crates/kgraph_utils/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod mca; +pub mod transformers; diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs new file mode 100644 index 000000000000..34babd7a02bd --- /dev/null +++ b/crates/kgraph_utils/src/mca.rs @@ -0,0 +1,739 @@ +use std::str::FromStr; + +use api_models::{ + admin as admin_api, enums as api_enums, payment_methods::RequestPaymentMethodTypes, +}; +use euclid::{ + dssa::graph::{self, DomainIdentifier}, + frontend::{ + ast, + dir::{self, enums as dir_enums}, + }, + types::{NumValue, NumValueRefinement}, +}; + +use crate::{error::KgraphError, transformers::IntoDirValue}; + +pub const DOMAIN_IDENTIFIER: &str = "payment_methods_enabled_for_merchantconnectoraccount"; + +fn compile_request_pm_types( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + pm_types: RequestPaymentMethodTypes, + pm: api_enums::PaymentMethod, +) -> Result { + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation, graph::Strength)> = Vec::new(); + + let pmt_info = "PaymentMethodType"; + let pmt_id = builder + .make_value_node( + (pm_types.payment_method_type, pm) + .into_dir_value() + .map(Into::into)?, + Some(pmt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + agg_nodes.push(( + pmt_id, + graph::Relation::Positive, + match pm_types.payment_method_type { + api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit => { + graph::Strength::Weak + } + + _ => graph::Strength::Strong, + }, + )); + + if let Some(card_networks) = pm_types.card_networks { + if !card_networks.is_empty() { + let dir_vals: Vec = card_networks + .into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>()?; + + let card_network_info = "Card Networks"; + let card_network_id = builder + .make_in_aggregator(dir_vals, Some(card_network_info), None::<()>, Vec::new()) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push(( + card_network_id, + graph::Relation::Positive, + graph::Strength::Weak, + )); + } + } + + let currencies_data = pm_types + .accepted_currencies + .and_then(|accepted_currencies| match accepted_currencies { + admin_api::AcceptedCurrencies::EnableOnly(curr) if !curr.is_empty() => Some(( + curr.into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>() + .ok()?, + graph::Relation::Positive, + )), + + admin_api::AcceptedCurrencies::DisableOnly(curr) if !curr.is_empty() => Some(( + curr.into_iter() + .map(IntoDirValue::into_dir_value) + .collect::>() + .ok()?, + graph::Relation::Negative, + )), + + _ => None, + }); + + if let Some((currencies, relation)) = currencies_data { + let accepted_currencies_info = "Accepted Currencies"; + let accepted_currencies_id = builder + .make_in_aggregator( + currencies, + Some(accepted_currencies_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push((accepted_currencies_id, relation, graph::Strength::Strong)); + } + + let mut amount_nodes = Vec::with_capacity(2); + + if let Some(min_amt) = pm_types.minimum_amount { + let num_val = NumValue { + number: min_amt.into(), + refinement: Some(NumValueRefinement::GreaterThanEqual), + }; + + let min_amt_info = "Minimum Amount"; + let min_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(num_val).into(), + Some(min_amt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + amount_nodes.push(min_amt_id); + } + + if let Some(max_amt) = pm_types.maximum_amount { + let num_val = NumValue { + number: max_amt.into(), + refinement: Some(NumValueRefinement::LessThanEqual), + }; + + let max_amt_info = "Maximum Amount"; + let max_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(num_val).into(), + Some(max_amt_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + amount_nodes.push(max_amt_id); + } + + if !amount_nodes.is_empty() { + let zero_num_val = NumValue { + number: 0, + refinement: None, + }; + + let zero_amt_id = builder + .make_value_node( + dir::DirValue::PaymentAmount(zero_num_val).into(), + Some("zero_amount"), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + let or_node_neighbor_id = if amount_nodes.len() == 1 { + amount_nodes + .get(0) + .copied() + .ok_or(KgraphError::IndexingError)? + } else { + let nodes = amount_nodes + .iter() + .copied() + .map(|node_id| (node_id, graph::Relation::Positive, graph::Strength::Strong)) + .collect::>(); + + builder + .make_all_aggregator( + &nodes, + Some("amount_constraint_aggregator"), + None::<()>, + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + ) + .map_err(KgraphError::GraphConstructionError)? + }; + + let any_aggregator = builder + .make_any_aggregator( + &[ + (zero_amt_id, graph::Relation::Positive), + (or_node_neighbor_id, graph::Relation::Positive), + ], + Some("zero_plus_limits_amount_aggregator"), + None::<()>, + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + ) + .map_err(KgraphError::GraphConstructionError)?; + + agg_nodes.push(( + any_aggregator, + graph::Relation::Positive, + graph::Strength::Strong, + )); + } + + let pmt_all_aggregator_info = "All Aggregator for PaymentMethodType"; + builder + .make_all_aggregator( + &agg_nodes, + Some(pmt_all_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError) +} + +fn compile_payment_method_enabled( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + enabled: admin_api::PaymentMethodsEnabled, +) -> Result, KgraphError> { + let agg_id = if !enabled + .payment_method_types + .as_ref() + .map(|v| v.is_empty()) + .unwrap_or(true) + { + let pm_info = "PaymentMethod"; + let pm_id = builder + .make_value_node( + enabled.payment_method.into_dir_value().map(Into::into)?, + Some(pm_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); + + if let Some(pm_types) = enabled.payment_method_types { + for pm_type in pm_types { + let node_id = compile_request_pm_types(builder, pm_type, enabled.payment_method)?; + agg_nodes.push((node_id, graph::Relation::Positive)); + } + } + + let any_aggregator_info = "Any aggregation for PaymentMethodsType"; + let pm_type_agg_id = builder + .make_any_aggregator( + &agg_nodes, + Some(any_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + let all_aggregator_info = "All aggregation for PaymentMethod"; + let enabled_pm_agg_id = builder + .make_all_aggregator( + &[ + (pm_id, graph::Relation::Positive, graph::Strength::Strong), + ( + pm_type_agg_id, + graph::Relation::Positive, + graph::Strength::Strong, + ), + ], + Some(all_aggregator_info), + None::<()>, + Vec::new(), + ) + .map_err(KgraphError::GraphConstructionError)?; + + Some(enabled_pm_agg_id) + } else { + None + }; + + Ok(agg_id) +} + +fn compile_merchant_connector_graph( + builder: &mut graph::KnowledgeGraphBuilder<'_>, + mca: admin_api::MerchantConnectorResponse, +) -> Result<(), KgraphError> { + let connector = dir_enums::Connector::from_str(&mca.connector_name) + .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?; + + let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); + + if let Some(pms_enabled) = mca.payment_methods_enabled { + for pm_enabled in pms_enabled { + let maybe_pm_enabled_id = compile_payment_method_enabled(builder, pm_enabled)?; + if let Some(pm_enabled_id) = maybe_pm_enabled_id { + agg_nodes.push((pm_enabled_id, graph::Relation::Positive)); + } + } + } + + let aggregator_info = "Available Payment methods for connector"; + let pms_enabled_agg_id = builder + .make_any_aggregator(&agg_nodes, Some(aggregator_info), None::<()>, Vec::new()) + .map_err(KgraphError::GraphConstructionError)?; + + let connector_dir_val = dir::DirValue::Connector(Box::new(ast::ConnectorChoice { + connector, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: mca.business_sub_label, + })); + + let connector_info = "Connector"; + let connector_node_id = builder + .make_value_node( + connector_dir_val.into(), + Some(connector_info), + vec![DomainIdentifier::new(DOMAIN_IDENTIFIER)], + None::<()>, + ) + .map_err(KgraphError::GraphConstructionError)?; + + builder + .make_edge( + pms_enabled_agg_id, + connector_node_id, + graph::Strength::Normal, + graph::Relation::Positive, + ) + .map_err(KgraphError::GraphConstructionError)?; + + Ok(()) +} + +pub fn make_mca_graph<'a>( + accts: Vec, +) -> Result, KgraphError> { + let mut builder = graph::KnowledgeGraphBuilder::new(); + let _domain = builder.make_domain( + DomainIdentifier::new(DOMAIN_IDENTIFIER), + "Payment methods enabled for MerchantConnectorAccount".to_string(), + ); + for acct in accts { + compile_merchant_connector_graph(&mut builder, acct)?; + } + + Ok(builder.build()) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use api_models::enums as api_enums; + use euclid::{ + dirval, + dssa::graph::{AnalysisContext, Memoization}, + }; + + use super::*; + + fn build_test_data<'a>() -> graph::KnowledgeGraph<'a> { + use api_models::{admin::*, payment_methods::*}; + + let stripe_account = MerchantConnectorResponse { + connector_type: api_enums::ConnectorType::FizOperations, + connector_name: "stripe".to_string(), + merchant_connector_id: "something".to_string(), + business_country: Some(api_enums::CountryAlpha2::US), + connector_label: Some("something".to_string()), + business_label: Some("food".to_string()), + business_sub_label: None, + connector_account_details: masking::Secret::new(serde_json::json!({})), + test_mode: None, + disabled: None, + metadata: None, + payment_methods_enabled: Some(vec![PaymentMethodsEnabled { + payment_method: api_enums::PaymentMethod::Card, + payment_method_types: Some(vec![ + RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Credit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Visa, + api_enums::CardNetwork::Mastercard, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::USD, + api_enums::Currency::INR, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }, + RequestPaymentMethodTypes { + payment_method_type: api_enums::PaymentMethodType::Debit, + payment_experience: None, + card_networks: Some(vec![ + api_enums::CardNetwork::Maestro, + api_enums::CardNetwork::JCB, + ]), + accepted_currencies: Some(AcceptedCurrencies::EnableOnly(vec![ + api_enums::Currency::GBP, + api_enums::Currency::PHP, + ])), + accepted_countries: None, + minimum_amount: Some(10), + maximum_amount: Some(1000), + recurring_enabled: true, + installment_payment_enabled: true, + }, + ]), + }]), + frm_configs: None, + connector_webhook_details: None, + profile_id: None, + applepay_verified_domains: None, + pm_auth_config: None, + }; + + make_mca_graph(vec![stripe_account]).expect("Failed graph construction") + } + + #[test] + fn test_credit_card_success_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Credit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = USD), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_debit_card_success_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = Maestro), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_single_mismatch_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = DinersClub), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_err()); + } + + #[test] + fn test_amount_mismatch_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 7), + ]), + &mut Memoization::new(), + ); + + assert!(result.is_err()); + } + + #[test] + fn test_incomplete_data_failure_case() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentMethod = Card), + dirval!(CardType = Debit), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 7), + ]), + &mut Memoization::new(), + ); + + //println!("{:#?}", result); + //println!("{}", serde_json::to_string_pretty(&result).expect("Hello")); + + assert!(result.is_err()); + } + + #[test] + fn test_incomplete_data_failure_case2() { + let graph = build_test_data(); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(CardType = Debit), + dirval!(CardNetwork = Visa), + dirval!(PaymentCurrency = GBP), + dirval!(PaymentAmount = 100), + ]), + &mut Memoization::new(), + ); + + //println!("{:#?}", result); + //println!("{}", serde_json::to_string_pretty(&result).expect("Hello")); + + assert!(result.is_err()); + } + + #[test] + fn test_sandbox_applepay_bug_usecase() { + let value = serde_json::json!([ + { + "connector_type": "payment_processor", + "connector_name": "bluesnap", + "merchant_connector_id": "REDACTED", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "REDACTED", + "key1": "REDACTED" + }, + "test_mode": true, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "Interac", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": {}, + "business_country": "US", + "business_label": "default", + "business_sub_label": null, + "frm_configs": null + }, + { + "connector_type": "payment_processor", + "connector_name": "stripe", + "merchant_connector_id": "REDACTED", + "connector_account_details": { + "auth_type": "HeaderKey", + "api_key": "REDACTED" + }, + "test_mode": true, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "payment_experience": null, + "card_networks": [ + "Mastercard", + "Visa", + "Interac", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay" + ], + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [] + } + ], + "metadata": {}, + "business_country": "US", + "business_label": "default", + "business_sub_label": null, + "frm_configs": null + } + ]); + + let data: Vec = + serde_json::from_value(value).expect("data"); + + let graph = make_mca_graph(data).expect("graph"); + let context = AnalysisContext::from_dir_values([ + dirval!(Connector = Stripe), + dirval!(PaymentAmount = 212), + dirval!(PaymentCurrency = ILS), + dirval!(PaymentMethod = Wallet), + dirval!(WalletType = ApplePay), + ]); + + let result = graph.key_value_analysis( + dirval!(Connector = Stripe), + &context, + &mut Memoization::new(), + ); + + assert!(result.is_ok(), "stripe validation failed"); + + let result = graph.key_value_analysis( + dirval!(Connector = Bluesnap), + &context, + &mut Memoization::new(), + ); + assert!(result.is_err(), "bluesnap validation failed"); + } +} diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs new file mode 100644 index 000000000000..3d32cce38bd8 --- /dev/null +++ b/crates/kgraph_utils/src/transformers.rs @@ -0,0 +1,724 @@ +use api_models::enums as api_enums; +use euclid::{ + backend::BackendInput, + dirval, + dssa::types::AnalysisErrorType, + frontend::{ast, dir}, + types::{NumValue, StrValue}, +}; + +use crate::error::KgraphError; + +pub trait IntoContext { + fn into_context(self) -> Result, KgraphError>; +} + +impl IntoContext for BackendInput { + fn into_context(self) -> Result, KgraphError> { + let mut ctx: Vec = Vec::new(); + + ctx.push(dir::DirValue::PaymentAmount(NumValue { + number: self.payment.amount, + refinement: None, + })); + + ctx.push(dir::DirValue::PaymentCurrency(self.payment.currency)); + + if let Some(auth_type) = self.payment.authentication_type { + ctx.push(dir::DirValue::AuthenticationType(auth_type)); + } + + if let Some(capture_method) = self.payment.capture_method { + ctx.push(dir::DirValue::CaptureMethod(capture_method)); + } + + if let Some(business_country) = self.payment.business_country { + ctx.push(dir::DirValue::BusinessCountry(business_country)); + } + if let Some(business_label) = self.payment.business_label { + ctx.push(dir::DirValue::BusinessLabel(StrValue { + value: business_label, + })); + } + if let Some(billing_country) = self.payment.billing_country { + ctx.push(dir::DirValue::BillingCountry(billing_country)); + } + + if let Some(payment_method) = self.payment_method.payment_method { + ctx.push(dir::DirValue::PaymentMethod(payment_method)); + } + + if let (Some(pm_type), Some(payment_method)) = ( + self.payment_method.payment_method_type, + self.payment_method.payment_method, + ) { + ctx.push((pm_type, payment_method).into_dir_value()?) + } + + if let Some(card_network) = self.payment_method.card_network { + ctx.push(dir::DirValue::CardNetwork(card_network)); + } + if let Some(setup_future_usage) = self.payment.setup_future_usage { + ctx.push(dir::DirValue::SetupFutureUsage(setup_future_usage)); + } + if let Some(mandate_acceptance_type) = self.mandate.mandate_acceptance_type { + ctx.push(dir::DirValue::MandateAcceptanceType( + mandate_acceptance_type, + )); + } + if let Some(mandate_type) = self.mandate.mandate_type { + ctx.push(dir::DirValue::MandateType(mandate_type)); + } + if let Some(payment_type) = self.mandate.payment_type { + ctx.push(dir::DirValue::PaymentType(payment_type)); + } + + Ok(ctx) + } +} + +pub trait IntoDirValue { + fn into_dir_value(self) -> Result; +} + +impl IntoDirValue for ast::ConnectorChoice { + fn into_dir_value(self) -> Result { + Ok(dir::DirValue::Connector(Box::new(self))) + } +} + +impl IntoDirValue for api_enums::PaymentMethod { + fn into_dir_value(self) -> Result { + match self { + Self::Card => Ok(dirval!(PaymentMethod = Card)), + Self::Wallet => Ok(dirval!(PaymentMethod = Wallet)), + Self::PayLater => Ok(dirval!(PaymentMethod = PayLater)), + Self::BankRedirect => Ok(dirval!(PaymentMethod = BankRedirect)), + Self::Crypto => Ok(dirval!(PaymentMethod = Crypto)), + Self::BankDebit => Ok(dirval!(PaymentMethod = BankDebit)), + Self::BankTransfer => Ok(dirval!(PaymentMethod = BankTransfer)), + Self::Reward => Ok(dirval!(PaymentMethod = Reward)), + Self::Upi => Ok(dirval!(PaymentMethod = Upi)), + Self::Voucher => Ok(dirval!(PaymentMethod = Voucher)), + Self::GiftCard => Ok(dirval!(PaymentMethod = GiftCard)), + Self::CardRedirect => Ok(dirval!(PaymentMethod = CardRedirect)), + } + } +} + +impl IntoDirValue for api_enums::AuthenticationType { + fn into_dir_value(self) -> Result { + match self { + Self::ThreeDs => Ok(dirval!(AuthenticationType = ThreeDs)), + Self::NoThreeDs => Ok(dirval!(AuthenticationType = NoThreeDs)), + } + } +} + +impl IntoDirValue for api_enums::FutureUsage { + fn into_dir_value(self) -> Result { + match self { + Self::OnSession => Ok(dirval!(SetupFutureUsage = OnSession)), + Self::OffSession => Ok(dirval!(SetupFutureUsage = OffSession)), + } + } +} + +impl IntoDirValue for (api_enums::PaymentMethodType, api_enums::PaymentMethod) { + fn into_dir_value(self) -> Result { + match self.0 { + api_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), + api_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), + api_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), + api_enums::PaymentMethodType::Ideal => Ok(dirval!(BankRedirectType = Ideal)), + api_enums::PaymentMethodType::Sofort => Ok(dirval!(BankRedirectType = Sofort)), + api_enums::PaymentMethodType::Eps => Ok(dirval!(BankRedirectType = Eps)), + api_enums::PaymentMethodType::Klarna => Ok(dirval!(PayLaterType = Klarna)), + api_enums::PaymentMethodType::Affirm => Ok(dirval!(PayLaterType = Affirm)), + api_enums::PaymentMethodType::AfterpayClearpay => { + Ok(dirval!(PayLaterType = AfterpayClearpay)) + } + api_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)), + api_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)), + api_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)), + api_enums::PaymentMethodType::CryptoCurrency => { + Ok(dirval!(CryptoType = CryptoCurrency)) + } + api_enums::PaymentMethodType::Ach => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Ach)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Ach)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::Bacs => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Bacs)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Bacs)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::Becs => Ok(dirval!(BankDebitType = Becs)), + api_enums::PaymentMethodType::Sepa => match self.1 { + api_enums::PaymentMethod::BankDebit => Ok(dirval!(BankDebitType = Sepa)), + api_enums::PaymentMethod::BankTransfer => Ok(dirval!(BankTransferType = Sepa)), + api_enums::PaymentMethod::BankRedirect + | api_enums::PaymentMethod::Card + | api_enums::PaymentMethod::CardRedirect + | api_enums::PaymentMethod::PayLater + | api_enums::PaymentMethod::Wallet + | api_enums::PaymentMethod::Crypto + | api_enums::PaymentMethod::Reward + | api_enums::PaymentMethod::Upi + | api_enums::PaymentMethod::Voucher + | api_enums::PaymentMethod::GiftCard => Err(KgraphError::ContextConstructionError( + AnalysisErrorType::NotSupported, + )), + }, + api_enums::PaymentMethodType::AliPay => Ok(dirval!(WalletType = AliPay)), + api_enums::PaymentMethodType::AliPayHk => Ok(dirval!(WalletType = AliPayHk)), + api_enums::PaymentMethodType::BancontactCard => { + Ok(dirval!(BankRedirectType = BancontactCard)) + } + api_enums::PaymentMethodType::Blik => Ok(dirval!(BankRedirectType = Blik)), + api_enums::PaymentMethodType::MbWay => Ok(dirval!(WalletType = MbWay)), + api_enums::PaymentMethodType::MobilePay => Ok(dirval!(WalletType = MobilePay)), + api_enums::PaymentMethodType::Cashapp => Ok(dirval!(WalletType = Cashapp)), + api_enums::PaymentMethodType::Multibanco => Ok(dirval!(BankTransferType = Multibanco)), + api_enums::PaymentMethodType::Pix => Ok(dirval!(BankTransferType = Pix)), + api_enums::PaymentMethodType::Pse => Ok(dirval!(BankTransferType = Pse)), + api_enums::PaymentMethodType::Interac => Ok(dirval!(BankRedirectType = Interac)), + api_enums::PaymentMethodType::OnlineBankingCzechRepublic => { + Ok(dirval!(BankRedirectType = OnlineBankingCzechRepublic)) + } + api_enums::PaymentMethodType::OnlineBankingFinland => { + Ok(dirval!(BankRedirectType = OnlineBankingFinland)) + } + api_enums::PaymentMethodType::OnlineBankingPoland => { + Ok(dirval!(BankRedirectType = OnlineBankingPoland)) + } + api_enums::PaymentMethodType::OnlineBankingSlovakia => { + Ok(dirval!(BankRedirectType = OnlineBankingSlovakia)) + } + api_enums::PaymentMethodType::Swish => Ok(dirval!(WalletType = Swish)), + api_enums::PaymentMethodType::Trustly => Ok(dirval!(BankRedirectType = Trustly)), + api_enums::PaymentMethodType::Bizum => Ok(dirval!(BankRedirectType = Bizum)), + + api_enums::PaymentMethodType::PayBright => Ok(dirval!(PayLaterType = PayBright)), + api_enums::PaymentMethodType::Walley => Ok(dirval!(PayLaterType = Walley)), + api_enums::PaymentMethodType::Przelewy24 => Ok(dirval!(BankRedirectType = Przelewy24)), + api_enums::PaymentMethodType::WeChatPay => Ok(dirval!(WalletType = WeChatPay)), + + api_enums::PaymentMethodType::ClassicReward => Ok(dirval!(RewardType = ClassicReward)), + api_enums::PaymentMethodType::Evoucher => Ok(dirval!(RewardType = Evoucher)), + api_enums::PaymentMethodType::UpiCollect => Ok(dirval!(UpiType = UpiCollect)), + api_enums::PaymentMethodType::SamsungPay => Ok(dirval!(WalletType = SamsungPay)), + api_enums::PaymentMethodType::GoPay => Ok(dirval!(WalletType = GoPay)), + api_enums::PaymentMethodType::KakaoPay => Ok(dirval!(WalletType = KakaoPay)), + api_enums::PaymentMethodType::Twint => Ok(dirval!(WalletType = Twint)), + api_enums::PaymentMethodType::Gcash => Ok(dirval!(WalletType = Gcash)), + api_enums::PaymentMethodType::Vipps => Ok(dirval!(WalletType = Vipps)), + api_enums::PaymentMethodType::Momo => Ok(dirval!(WalletType = Momo)), + api_enums::PaymentMethodType::Alma => Ok(dirval!(PayLaterType = Alma)), + api_enums::PaymentMethodType::Dana => Ok(dirval!(WalletType = Dana)), + api_enums::PaymentMethodType::OnlineBankingFpx => { + Ok(dirval!(BankRedirectType = OnlineBankingFpx)) + } + api_enums::PaymentMethodType::OnlineBankingThailand => { + Ok(dirval!(BankRedirectType = OnlineBankingThailand)) + } + api_enums::PaymentMethodType::TouchNGo => Ok(dirval!(WalletType = TouchNGo)), + api_enums::PaymentMethodType::Atome => Ok(dirval!(PayLaterType = Atome)), + api_enums::PaymentMethodType::Boleto => Ok(dirval!(VoucherType = Boleto)), + api_enums::PaymentMethodType::Efecty => Ok(dirval!(VoucherType = Efecty)), + api_enums::PaymentMethodType::PagoEfectivo => Ok(dirval!(VoucherType = PagoEfectivo)), + api_enums::PaymentMethodType::RedCompra => Ok(dirval!(VoucherType = RedCompra)), + api_enums::PaymentMethodType::RedPagos => Ok(dirval!(VoucherType = RedPagos)), + api_enums::PaymentMethodType::Alfamart => Ok(dirval!(VoucherType = Alfamart)), + api_enums::PaymentMethodType::BcaBankTransfer => { + Ok(dirval!(BankTransferType = BcaBankTransfer)) + } + api_enums::PaymentMethodType::BniVa => Ok(dirval!(BankTransferType = BniVa)), + api_enums::PaymentMethodType::BriVa => Ok(dirval!(BankTransferType = BriVa)), + api_enums::PaymentMethodType::CimbVa => Ok(dirval!(BankTransferType = CimbVa)), + api_enums::PaymentMethodType::DanamonVa => Ok(dirval!(BankTransferType = DanamonVa)), + api_enums::PaymentMethodType::Indomaret => Ok(dirval!(VoucherType = Indomaret)), + api_enums::PaymentMethodType::MandiriVa => Ok(dirval!(BankTransferType = MandiriVa)), + api_enums::PaymentMethodType::PermataBankTransfer => { + Ok(dirval!(BankTransferType = PermataBankTransfer)) + } + api_enums::PaymentMethodType::PaySafeCard => Ok(dirval!(GiftCardType = PaySafeCard)), + api_enums::PaymentMethodType::SevenEleven => Ok(dirval!(VoucherType = SevenEleven)), + api_enums::PaymentMethodType::Lawson => Ok(dirval!(VoucherType = Lawson)), + api_enums::PaymentMethodType::MiniStop => Ok(dirval!(VoucherType = MiniStop)), + api_enums::PaymentMethodType::FamilyMart => Ok(dirval!(VoucherType = FamilyMart)), + api_enums::PaymentMethodType::Seicomart => Ok(dirval!(VoucherType = Seicomart)), + api_enums::PaymentMethodType::PayEasy => Ok(dirval!(VoucherType = PayEasy)), + api_enums::PaymentMethodType::Givex => Ok(dirval!(GiftCardType = Givex)), + api_enums::PaymentMethodType::Benefit => Ok(dirval!(CardRedirectType = Benefit)), + api_enums::PaymentMethodType::Knet => Ok(dirval!(CardRedirectType = Knet)), + api_enums::PaymentMethodType::OpenBankingUk => { + Ok(dirval!(BankRedirectType = OpenBankingUk)) + } + api_enums::PaymentMethodType::MomoAtm => Ok(dirval!(CardRedirectType = MomoAtm)), + api_enums::PaymentMethodType::Oxxo => Ok(dirval!(VoucherType = Oxxo)), + } + } +} + +impl IntoDirValue for api_enums::CardNetwork { + fn into_dir_value(self) -> Result { + match self { + Self::Visa => Ok(dirval!(CardNetwork = Visa)), + Self::Mastercard => Ok(dirval!(CardNetwork = Mastercard)), + Self::AmericanExpress => Ok(dirval!(CardNetwork = AmericanExpress)), + Self::JCB => Ok(dirval!(CardNetwork = JCB)), + Self::DinersClub => Ok(dirval!(CardNetwork = DinersClub)), + Self::Discover => Ok(dirval!(CardNetwork = Discover)), + Self::CartesBancaires => Ok(dirval!(CardNetwork = CartesBancaires)), + Self::UnionPay => Ok(dirval!(CardNetwork = UnionPay)), + Self::Interac => Ok(dirval!(CardNetwork = Interac)), + Self::RuPay => Ok(dirval!(CardNetwork = RuPay)), + Self::Maestro => Ok(dirval!(CardNetwork = Maestro)), + } + } +} + +impl IntoDirValue for api_enums::Currency { + fn into_dir_value(self) -> Result { + match self { + Self::AED => Ok(dirval!(PaymentCurrency = AED)), + Self::ALL => Ok(dirval!(PaymentCurrency = ALL)), + Self::AMD => Ok(dirval!(PaymentCurrency = AMD)), + Self::ANG => Ok(dirval!(PaymentCurrency = ANG)), + Self::ARS => Ok(dirval!(PaymentCurrency = ARS)), + Self::AUD => Ok(dirval!(PaymentCurrency = AUD)), + Self::AWG => Ok(dirval!(PaymentCurrency = AWG)), + Self::AZN => Ok(dirval!(PaymentCurrency = AZN)), + Self::BBD => Ok(dirval!(PaymentCurrency = BBD)), + Self::BDT => Ok(dirval!(PaymentCurrency = BDT)), + Self::BHD => Ok(dirval!(PaymentCurrency = BHD)), + Self::BIF => Ok(dirval!(PaymentCurrency = BIF)), + Self::BMD => Ok(dirval!(PaymentCurrency = BMD)), + Self::BND => Ok(dirval!(PaymentCurrency = BND)), + Self::BOB => Ok(dirval!(PaymentCurrency = BOB)), + Self::BRL => Ok(dirval!(PaymentCurrency = BRL)), + Self::BSD => Ok(dirval!(PaymentCurrency = BSD)), + Self::BWP => Ok(dirval!(PaymentCurrency = BWP)), + Self::BZD => Ok(dirval!(PaymentCurrency = BZD)), + Self::CAD => Ok(dirval!(PaymentCurrency = CAD)), + Self::CHF => Ok(dirval!(PaymentCurrency = CHF)), + Self::CLP => Ok(dirval!(PaymentCurrency = CLP)), + Self::CNY => Ok(dirval!(PaymentCurrency = CNY)), + Self::COP => Ok(dirval!(PaymentCurrency = COP)), + Self::CRC => Ok(dirval!(PaymentCurrency = CRC)), + Self::CUP => Ok(dirval!(PaymentCurrency = CUP)), + Self::CZK => Ok(dirval!(PaymentCurrency = CZK)), + Self::DJF => Ok(dirval!(PaymentCurrency = DJF)), + Self::DKK => Ok(dirval!(PaymentCurrency = DKK)), + Self::DOP => Ok(dirval!(PaymentCurrency = DOP)), + Self::DZD => Ok(dirval!(PaymentCurrency = DZD)), + Self::EGP => Ok(dirval!(PaymentCurrency = EGP)), + Self::ETB => Ok(dirval!(PaymentCurrency = ETB)), + Self::EUR => Ok(dirval!(PaymentCurrency = EUR)), + Self::FJD => Ok(dirval!(PaymentCurrency = FJD)), + Self::GBP => Ok(dirval!(PaymentCurrency = GBP)), + Self::GHS => Ok(dirval!(PaymentCurrency = GHS)), + Self::GIP => Ok(dirval!(PaymentCurrency = GIP)), + Self::GMD => Ok(dirval!(PaymentCurrency = GMD)), + Self::GNF => Ok(dirval!(PaymentCurrency = GNF)), + Self::GTQ => Ok(dirval!(PaymentCurrency = GTQ)), + Self::GYD => Ok(dirval!(PaymentCurrency = GYD)), + Self::HKD => Ok(dirval!(PaymentCurrency = HKD)), + Self::HNL => Ok(dirval!(PaymentCurrency = HNL)), + Self::HRK => Ok(dirval!(PaymentCurrency = HRK)), + Self::HTG => Ok(dirval!(PaymentCurrency = HTG)), + Self::HUF => Ok(dirval!(PaymentCurrency = HUF)), + Self::IDR => Ok(dirval!(PaymentCurrency = IDR)), + Self::ILS => Ok(dirval!(PaymentCurrency = ILS)), + Self::INR => Ok(dirval!(PaymentCurrency = INR)), + Self::JMD => Ok(dirval!(PaymentCurrency = JMD)), + Self::JOD => Ok(dirval!(PaymentCurrency = JOD)), + Self::JPY => Ok(dirval!(PaymentCurrency = JPY)), + Self::KES => Ok(dirval!(PaymentCurrency = KES)), + Self::KGS => Ok(dirval!(PaymentCurrency = KGS)), + Self::KHR => Ok(dirval!(PaymentCurrency = KHR)), + Self::KMF => Ok(dirval!(PaymentCurrency = KMF)), + Self::KRW => Ok(dirval!(PaymentCurrency = KRW)), + Self::KWD => Ok(dirval!(PaymentCurrency = KWD)), + Self::KYD => Ok(dirval!(PaymentCurrency = KYD)), + Self::KZT => Ok(dirval!(PaymentCurrency = KZT)), + Self::LAK => Ok(dirval!(PaymentCurrency = LAK)), + Self::LBP => Ok(dirval!(PaymentCurrency = LBP)), + Self::LKR => Ok(dirval!(PaymentCurrency = LKR)), + Self::LRD => Ok(dirval!(PaymentCurrency = LRD)), + Self::LSL => Ok(dirval!(PaymentCurrency = LSL)), + Self::MAD => Ok(dirval!(PaymentCurrency = MAD)), + Self::MDL => Ok(dirval!(PaymentCurrency = MDL)), + Self::MGA => Ok(dirval!(PaymentCurrency = MGA)), + Self::MKD => Ok(dirval!(PaymentCurrency = MKD)), + Self::MMK => Ok(dirval!(PaymentCurrency = MMK)), + Self::MNT => Ok(dirval!(PaymentCurrency = MNT)), + Self::MOP => Ok(dirval!(PaymentCurrency = MOP)), + Self::MUR => Ok(dirval!(PaymentCurrency = MUR)), + Self::MVR => Ok(dirval!(PaymentCurrency = MVR)), + Self::MWK => Ok(dirval!(PaymentCurrency = MWK)), + Self::MXN => Ok(dirval!(PaymentCurrency = MXN)), + Self::MYR => Ok(dirval!(PaymentCurrency = MYR)), + Self::NAD => Ok(dirval!(PaymentCurrency = NAD)), + Self::NGN => Ok(dirval!(PaymentCurrency = NGN)), + Self::NIO => Ok(dirval!(PaymentCurrency = NIO)), + Self::NOK => Ok(dirval!(PaymentCurrency = NOK)), + Self::NPR => Ok(dirval!(PaymentCurrency = NPR)), + Self::NZD => Ok(dirval!(PaymentCurrency = NZD)), + Self::OMR => Ok(dirval!(PaymentCurrency = OMR)), + Self::PEN => Ok(dirval!(PaymentCurrency = PEN)), + Self::PGK => Ok(dirval!(PaymentCurrency = PGK)), + Self::PHP => Ok(dirval!(PaymentCurrency = PHP)), + Self::PKR => Ok(dirval!(PaymentCurrency = PKR)), + Self::PLN => Ok(dirval!(PaymentCurrency = PLN)), + Self::PYG => Ok(dirval!(PaymentCurrency = PYG)), + Self::QAR => Ok(dirval!(PaymentCurrency = QAR)), + Self::RON => Ok(dirval!(PaymentCurrency = RON)), + Self::RUB => Ok(dirval!(PaymentCurrency = RUB)), + Self::RWF => Ok(dirval!(PaymentCurrency = RWF)), + Self::SAR => Ok(dirval!(PaymentCurrency = SAR)), + Self::SCR => Ok(dirval!(PaymentCurrency = SCR)), + Self::SEK => Ok(dirval!(PaymentCurrency = SEK)), + Self::SGD => Ok(dirval!(PaymentCurrency = SGD)), + Self::SLL => Ok(dirval!(PaymentCurrency = SLL)), + Self::SOS => Ok(dirval!(PaymentCurrency = SOS)), + Self::SSP => Ok(dirval!(PaymentCurrency = SSP)), + Self::SVC => Ok(dirval!(PaymentCurrency = SVC)), + Self::SZL => Ok(dirval!(PaymentCurrency = SZL)), + Self::THB => Ok(dirval!(PaymentCurrency = THB)), + Self::TRY => Ok(dirval!(PaymentCurrency = TRY)), + Self::TTD => Ok(dirval!(PaymentCurrency = TTD)), + Self::TWD => Ok(dirval!(PaymentCurrency = TWD)), + Self::TZS => Ok(dirval!(PaymentCurrency = TZS)), + Self::UGX => Ok(dirval!(PaymentCurrency = UGX)), + Self::USD => Ok(dirval!(PaymentCurrency = USD)), + Self::UYU => Ok(dirval!(PaymentCurrency = UYU)), + Self::UZS => Ok(dirval!(PaymentCurrency = UZS)), + Self::VND => Ok(dirval!(PaymentCurrency = VND)), + Self::VUV => Ok(dirval!(PaymentCurrency = VUV)), + Self::XAF => Ok(dirval!(PaymentCurrency = XAF)), + Self::XOF => Ok(dirval!(PaymentCurrency = XOF)), + Self::XPF => Ok(dirval!(PaymentCurrency = XPF)), + Self::YER => Ok(dirval!(PaymentCurrency = YER)), + Self::ZAR => Ok(dirval!(PaymentCurrency = ZAR)), + } + } +} + +pub fn get_dir_country_dir_value(c: api_enums::Country) -> dir::enums::Country { + match c { + api_enums::Country::Afghanistan => dir::enums::Country::Afghanistan, + api_enums::Country::AlandIslands => dir::enums::Country::AlandIslands, + api_enums::Country::Albania => dir::enums::Country::Albania, + api_enums::Country::Algeria => dir::enums::Country::Algeria, + api_enums::Country::AmericanSamoa => dir::enums::Country::AmericanSamoa, + api_enums::Country::Andorra => dir::enums::Country::Andorra, + api_enums::Country::Angola => dir::enums::Country::Angola, + api_enums::Country::Anguilla => dir::enums::Country::Anguilla, + api_enums::Country::Antarctica => dir::enums::Country::Antarctica, + api_enums::Country::AntiguaAndBarbuda => dir::enums::Country::AntiguaAndBarbuda, + api_enums::Country::Argentina => dir::enums::Country::Argentina, + api_enums::Country::Armenia => dir::enums::Country::Armenia, + api_enums::Country::Aruba => dir::enums::Country::Aruba, + api_enums::Country::Australia => dir::enums::Country::Australia, + api_enums::Country::Austria => dir::enums::Country::Austria, + api_enums::Country::Azerbaijan => dir::enums::Country::Azerbaijan, + api_enums::Country::Bahamas => dir::enums::Country::Bahamas, + api_enums::Country::Bahrain => dir::enums::Country::Bahrain, + api_enums::Country::Bangladesh => dir::enums::Country::Bangladesh, + api_enums::Country::Barbados => dir::enums::Country::Barbados, + api_enums::Country::Belarus => dir::enums::Country::Belarus, + api_enums::Country::Belgium => dir::enums::Country::Belgium, + api_enums::Country::Belize => dir::enums::Country::Belize, + api_enums::Country::Benin => dir::enums::Country::Benin, + api_enums::Country::Bermuda => dir::enums::Country::Bermuda, + api_enums::Country::Bhutan => dir::enums::Country::Bhutan, + api_enums::Country::BoliviaPlurinationalState => { + dir::enums::Country::BoliviaPlurinationalState + } + api_enums::Country::BonaireSintEustatiusAndSaba => { + dir::enums::Country::BonaireSintEustatiusAndSaba + } + api_enums::Country::BosniaAndHerzegovina => dir::enums::Country::BosniaAndHerzegovina, + api_enums::Country::Botswana => dir::enums::Country::Botswana, + api_enums::Country::BouvetIsland => dir::enums::Country::BouvetIsland, + api_enums::Country::Brazil => dir::enums::Country::Brazil, + api_enums::Country::BritishIndianOceanTerritory => { + dir::enums::Country::BritishIndianOceanTerritory + } + api_enums::Country::BruneiDarussalam => dir::enums::Country::BruneiDarussalam, + api_enums::Country::Bulgaria => dir::enums::Country::Bulgaria, + api_enums::Country::BurkinaFaso => dir::enums::Country::BurkinaFaso, + api_enums::Country::Burundi => dir::enums::Country::Burundi, + api_enums::Country::CaboVerde => dir::enums::Country::CaboVerde, + api_enums::Country::Cambodia => dir::enums::Country::Cambodia, + api_enums::Country::Cameroon => dir::enums::Country::Cameroon, + api_enums::Country::Canada => dir::enums::Country::Canada, + api_enums::Country::CaymanIslands => dir::enums::Country::CaymanIslands, + api_enums::Country::CentralAfricanRepublic => dir::enums::Country::CentralAfricanRepublic, + api_enums::Country::Chad => dir::enums::Country::Chad, + api_enums::Country::Chile => dir::enums::Country::Chile, + api_enums::Country::China => dir::enums::Country::China, + api_enums::Country::ChristmasIsland => dir::enums::Country::ChristmasIsland, + api_enums::Country::CocosKeelingIslands => dir::enums::Country::CocosKeelingIslands, + api_enums::Country::Colombia => dir::enums::Country::Colombia, + api_enums::Country::Comoros => dir::enums::Country::Comoros, + api_enums::Country::Congo => dir::enums::Country::Congo, + api_enums::Country::CongoDemocraticRepublic => dir::enums::Country::CongoDemocraticRepublic, + api_enums::Country::CookIslands => dir::enums::Country::CookIslands, + api_enums::Country::CostaRica => dir::enums::Country::CostaRica, + api_enums::Country::CotedIvoire => dir::enums::Country::CotedIvoire, + api_enums::Country::Croatia => dir::enums::Country::Croatia, + api_enums::Country::Cuba => dir::enums::Country::Cuba, + api_enums::Country::Curacao => dir::enums::Country::Curacao, + api_enums::Country::Cyprus => dir::enums::Country::Cyprus, + api_enums::Country::Czechia => dir::enums::Country::Czechia, + api_enums::Country::Denmark => dir::enums::Country::Denmark, + api_enums::Country::Djibouti => dir::enums::Country::Djibouti, + api_enums::Country::Dominica => dir::enums::Country::Dominica, + api_enums::Country::DominicanRepublic => dir::enums::Country::DominicanRepublic, + api_enums::Country::Ecuador => dir::enums::Country::Ecuador, + api_enums::Country::Egypt => dir::enums::Country::Egypt, + api_enums::Country::ElSalvador => dir::enums::Country::ElSalvador, + api_enums::Country::EquatorialGuinea => dir::enums::Country::EquatorialGuinea, + api_enums::Country::Eritrea => dir::enums::Country::Eritrea, + api_enums::Country::Estonia => dir::enums::Country::Estonia, + api_enums::Country::Ethiopia => dir::enums::Country::Ethiopia, + api_enums::Country::FalklandIslandsMalvinas => dir::enums::Country::FalklandIslandsMalvinas, + api_enums::Country::FaroeIslands => dir::enums::Country::FaroeIslands, + api_enums::Country::Fiji => dir::enums::Country::Fiji, + api_enums::Country::Finland => dir::enums::Country::Finland, + api_enums::Country::France => dir::enums::Country::France, + api_enums::Country::FrenchGuiana => dir::enums::Country::FrenchGuiana, + api_enums::Country::FrenchPolynesia => dir::enums::Country::FrenchPolynesia, + api_enums::Country::FrenchSouthernTerritories => { + dir::enums::Country::FrenchSouthernTerritories + } + api_enums::Country::Gabon => dir::enums::Country::Gabon, + api_enums::Country::Gambia => dir::enums::Country::Gambia, + api_enums::Country::Georgia => dir::enums::Country::Georgia, + api_enums::Country::Germany => dir::enums::Country::Germany, + api_enums::Country::Ghana => dir::enums::Country::Ghana, + api_enums::Country::Gibraltar => dir::enums::Country::Gibraltar, + api_enums::Country::Greece => dir::enums::Country::Greece, + api_enums::Country::Greenland => dir::enums::Country::Greenland, + api_enums::Country::Grenada => dir::enums::Country::Grenada, + api_enums::Country::Guadeloupe => dir::enums::Country::Guadeloupe, + api_enums::Country::Guam => dir::enums::Country::Guam, + api_enums::Country::Guatemala => dir::enums::Country::Guatemala, + api_enums::Country::Guernsey => dir::enums::Country::Guernsey, + api_enums::Country::Guinea => dir::enums::Country::Guinea, + api_enums::Country::GuineaBissau => dir::enums::Country::GuineaBissau, + api_enums::Country::Guyana => dir::enums::Country::Guyana, + api_enums::Country::Haiti => dir::enums::Country::Haiti, + api_enums::Country::HeardIslandAndMcDonaldIslands => { + dir::enums::Country::HeardIslandAndMcDonaldIslands + } + api_enums::Country::HolySee => dir::enums::Country::HolySee, + api_enums::Country::Honduras => dir::enums::Country::Honduras, + api_enums::Country::HongKong => dir::enums::Country::HongKong, + api_enums::Country::Hungary => dir::enums::Country::Hungary, + api_enums::Country::Iceland => dir::enums::Country::Iceland, + api_enums::Country::India => dir::enums::Country::India, + api_enums::Country::Indonesia => dir::enums::Country::Indonesia, + api_enums::Country::IranIslamicRepublic => dir::enums::Country::IranIslamicRepublic, + api_enums::Country::Iraq => dir::enums::Country::Iraq, + api_enums::Country::Ireland => dir::enums::Country::Ireland, + api_enums::Country::IsleOfMan => dir::enums::Country::IsleOfMan, + api_enums::Country::Israel => dir::enums::Country::Israel, + api_enums::Country::Italy => dir::enums::Country::Italy, + api_enums::Country::Jamaica => dir::enums::Country::Jamaica, + api_enums::Country::Japan => dir::enums::Country::Japan, + api_enums::Country::Jersey => dir::enums::Country::Jersey, + api_enums::Country::Jordan => dir::enums::Country::Jordan, + api_enums::Country::Kazakhstan => dir::enums::Country::Kazakhstan, + api_enums::Country::Kenya => dir::enums::Country::Kenya, + api_enums::Country::Kiribati => dir::enums::Country::Kiribati, + api_enums::Country::KoreaDemocraticPeoplesRepublic => { + dir::enums::Country::KoreaDemocraticPeoplesRepublic + } + api_enums::Country::KoreaRepublic => dir::enums::Country::KoreaRepublic, + api_enums::Country::Kuwait => dir::enums::Country::Kuwait, + api_enums::Country::Kyrgyzstan => dir::enums::Country::Kyrgyzstan, + api_enums::Country::LaoPeoplesDemocraticRepublic => { + dir::enums::Country::LaoPeoplesDemocraticRepublic + } + api_enums::Country::Latvia => dir::enums::Country::Latvia, + api_enums::Country::Lebanon => dir::enums::Country::Lebanon, + api_enums::Country::Lesotho => dir::enums::Country::Lesotho, + api_enums::Country::Liberia => dir::enums::Country::Liberia, + api_enums::Country::Libya => dir::enums::Country::Libya, + api_enums::Country::Liechtenstein => dir::enums::Country::Liechtenstein, + api_enums::Country::Lithuania => dir::enums::Country::Lithuania, + api_enums::Country::Luxembourg => dir::enums::Country::Luxembourg, + api_enums::Country::Macao => dir::enums::Country::Macao, + api_enums::Country::MacedoniaTheFormerYugoslavRepublic => { + dir::enums::Country::MacedoniaTheFormerYugoslavRepublic + } + api_enums::Country::Madagascar => dir::enums::Country::Madagascar, + api_enums::Country::Malawi => dir::enums::Country::Malawi, + api_enums::Country::Malaysia => dir::enums::Country::Malaysia, + api_enums::Country::Maldives => dir::enums::Country::Maldives, + api_enums::Country::Mali => dir::enums::Country::Mali, + api_enums::Country::Malta => dir::enums::Country::Malta, + api_enums::Country::MarshallIslands => dir::enums::Country::MarshallIslands, + api_enums::Country::Martinique => dir::enums::Country::Martinique, + api_enums::Country::Mauritania => dir::enums::Country::Mauritania, + api_enums::Country::Mauritius => dir::enums::Country::Mauritius, + api_enums::Country::Mayotte => dir::enums::Country::Mayotte, + api_enums::Country::Mexico => dir::enums::Country::Mexico, + api_enums::Country::MicronesiaFederatedStates => { + dir::enums::Country::MicronesiaFederatedStates + } + api_enums::Country::MoldovaRepublic => dir::enums::Country::MoldovaRepublic, + api_enums::Country::Monaco => dir::enums::Country::Monaco, + api_enums::Country::Mongolia => dir::enums::Country::Mongolia, + api_enums::Country::Montenegro => dir::enums::Country::Montenegro, + api_enums::Country::Montserrat => dir::enums::Country::Montserrat, + api_enums::Country::Morocco => dir::enums::Country::Morocco, + api_enums::Country::Mozambique => dir::enums::Country::Mozambique, + api_enums::Country::Myanmar => dir::enums::Country::Myanmar, + api_enums::Country::Namibia => dir::enums::Country::Namibia, + api_enums::Country::Nauru => dir::enums::Country::Nauru, + api_enums::Country::Nepal => dir::enums::Country::Nepal, + api_enums::Country::Netherlands => dir::enums::Country::Netherlands, + api_enums::Country::NewCaledonia => dir::enums::Country::NewCaledonia, + api_enums::Country::NewZealand => dir::enums::Country::NewZealand, + api_enums::Country::Nicaragua => dir::enums::Country::Nicaragua, + api_enums::Country::Niger => dir::enums::Country::Niger, + api_enums::Country::Nigeria => dir::enums::Country::Nigeria, + api_enums::Country::Niue => dir::enums::Country::Niue, + api_enums::Country::NorfolkIsland => dir::enums::Country::NorfolkIsland, + api_enums::Country::NorthernMarianaIslands => dir::enums::Country::NorthernMarianaIslands, + api_enums::Country::Norway => dir::enums::Country::Norway, + api_enums::Country::Oman => dir::enums::Country::Oman, + api_enums::Country::Pakistan => dir::enums::Country::Pakistan, + api_enums::Country::Palau => dir::enums::Country::Palau, + api_enums::Country::PalestineState => dir::enums::Country::PalestineState, + api_enums::Country::Panama => dir::enums::Country::Panama, + api_enums::Country::PapuaNewGuinea => dir::enums::Country::PapuaNewGuinea, + api_enums::Country::Paraguay => dir::enums::Country::Paraguay, + api_enums::Country::Peru => dir::enums::Country::Peru, + api_enums::Country::Philippines => dir::enums::Country::Philippines, + api_enums::Country::Pitcairn => dir::enums::Country::Pitcairn, + + api_enums::Country::Poland => dir::enums::Country::Poland, + api_enums::Country::Portugal => dir::enums::Country::Portugal, + api_enums::Country::PuertoRico => dir::enums::Country::PuertoRico, + + api_enums::Country::Qatar => dir::enums::Country::Qatar, + api_enums::Country::Reunion => dir::enums::Country::Reunion, + api_enums::Country::Romania => dir::enums::Country::Romania, + api_enums::Country::RussianFederation => dir::enums::Country::RussianFederation, + api_enums::Country::Rwanda => dir::enums::Country::Rwanda, + api_enums::Country::SaintBarthelemy => dir::enums::Country::SaintBarthelemy, + api_enums::Country::SaintHelenaAscensionAndTristandaCunha => { + dir::enums::Country::SaintHelenaAscensionAndTristandaCunha + } + api_enums::Country::SaintKittsAndNevis => dir::enums::Country::SaintKittsAndNevis, + api_enums::Country::SaintLucia => dir::enums::Country::SaintLucia, + api_enums::Country::SaintMartinFrenchpart => dir::enums::Country::SaintMartinFrenchpart, + api_enums::Country::SaintPierreAndMiquelon => dir::enums::Country::SaintPierreAndMiquelon, + api_enums::Country::SaintVincentAndTheGrenadines => { + dir::enums::Country::SaintVincentAndTheGrenadines + } + api_enums::Country::Samoa => dir::enums::Country::Samoa, + api_enums::Country::SanMarino => dir::enums::Country::SanMarino, + api_enums::Country::SaoTomeAndPrincipe => dir::enums::Country::SaoTomeAndPrincipe, + api_enums::Country::SaudiArabia => dir::enums::Country::SaudiArabia, + api_enums::Country::Senegal => dir::enums::Country::Senegal, + api_enums::Country::Serbia => dir::enums::Country::Serbia, + api_enums::Country::Seychelles => dir::enums::Country::Seychelles, + api_enums::Country::SierraLeone => dir::enums::Country::SierraLeone, + api_enums::Country::Singapore => dir::enums::Country::Singapore, + api_enums::Country::SintMaartenDutchpart => dir::enums::Country::SintMaartenDutchpart, + api_enums::Country::Slovakia => dir::enums::Country::Slovakia, + api_enums::Country::Slovenia => dir::enums::Country::Slovenia, + api_enums::Country::SolomonIslands => dir::enums::Country::SolomonIslands, + api_enums::Country::Somalia => dir::enums::Country::Somalia, + api_enums::Country::SouthAfrica => dir::enums::Country::SouthAfrica, + api_enums::Country::SouthGeorgiaAndTheSouthSandwichIslands => { + dir::enums::Country::SouthGeorgiaAndTheSouthSandwichIslands + } + api_enums::Country::SouthSudan => dir::enums::Country::SouthSudan, + api_enums::Country::Spain => dir::enums::Country::Spain, + api_enums::Country::SriLanka => dir::enums::Country::SriLanka, + api_enums::Country::Sudan => dir::enums::Country::Sudan, + api_enums::Country::Suriname => dir::enums::Country::Suriname, + api_enums::Country::SvalbardAndJanMayen => dir::enums::Country::SvalbardAndJanMayen, + api_enums::Country::Swaziland => dir::enums::Country::Swaziland, + api_enums::Country::Sweden => dir::enums::Country::Sweden, + api_enums::Country::Switzerland => dir::enums::Country::Switzerland, + api_enums::Country::SyrianArabRepublic => dir::enums::Country::SyrianArabRepublic, + api_enums::Country::TaiwanProvinceOfChina => dir::enums::Country::TaiwanProvinceOfChina, + api_enums::Country::Tajikistan => dir::enums::Country::Tajikistan, + api_enums::Country::TanzaniaUnitedRepublic => dir::enums::Country::TanzaniaUnitedRepublic, + api_enums::Country::Thailand => dir::enums::Country::Thailand, + api_enums::Country::TimorLeste => dir::enums::Country::TimorLeste, + api_enums::Country::Togo => dir::enums::Country::Togo, + api_enums::Country::Tokelau => dir::enums::Country::Tokelau, + api_enums::Country::Tonga => dir::enums::Country::Tonga, + api_enums::Country::TrinidadAndTobago => dir::enums::Country::TrinidadAndTobago, + api_enums::Country::Tunisia => dir::enums::Country::Tunisia, + api_enums::Country::Turkey => dir::enums::Country::Turkey, + api_enums::Country::Turkmenistan => dir::enums::Country::Turkmenistan, + api_enums::Country::TurksAndCaicosIslands => dir::enums::Country::TurksAndCaicosIslands, + api_enums::Country::Tuvalu => dir::enums::Country::Tuvalu, + api_enums::Country::Uganda => dir::enums::Country::Uganda, + api_enums::Country::Ukraine => dir::enums::Country::Ukraine, + api_enums::Country::UnitedArabEmirates => dir::enums::Country::UnitedArabEmirates, + api_enums::Country::UnitedKingdomOfGreatBritainAndNorthernIreland => { + dir::enums::Country::UnitedKingdomOfGreatBritainAndNorthernIreland + } + api_enums::Country::UnitedStatesOfAmerica => dir::enums::Country::UnitedStatesOfAmerica, + api_enums::Country::UnitedStatesMinorOutlyingIslands => { + dir::enums::Country::UnitedStatesMinorOutlyingIslands + } + api_enums::Country::Uruguay => dir::enums::Country::Uruguay, + api_enums::Country::Uzbekistan => dir::enums::Country::Uzbekistan, + api_enums::Country::Vanuatu => dir::enums::Country::Vanuatu, + api_enums::Country::VenezuelaBolivarianRepublic => { + dir::enums::Country::VenezuelaBolivarianRepublic + } + api_enums::Country::Vietnam => dir::enums::Country::Vietnam, + api_enums::Country::VirginIslandsBritish => dir::enums::Country::VirginIslandsBritish, + api_enums::Country::VirginIslandsUS => dir::enums::Country::VirginIslandsUS, + api_enums::Country::WallisAndFutuna => dir::enums::Country::WallisAndFutuna, + api_enums::Country::WesternSahara => dir::enums::Country::WesternSahara, + api_enums::Country::Yemen => dir::enums::Country::Yemen, + api_enums::Country::Zambia => dir::enums::Country::Zambia, + api_enums::Country::Zimbabwe => dir::enums::Country::Zimbabwe, + } +} + +pub fn business_country_to_dir_value(c: api_enums::Country) -> dir::DirValue { + dir::DirValue::BusinessCountry(get_dir_country_dir_value(c)) +} + +pub fn billing_country_to_dir_value(c: api_enums::Country) -> dir::DirValue { + dir::DirValue::BillingCountry(get_dir_country_dir_value(c)) +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 81b23314ffb8..9ab955813336 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,20 +9,23 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache", "dummy_connector", "payouts"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe","basilisk","s3", "email","accounts_cache","kv_store"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] oltp = ["data_models/oltp", "storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] openapi = ["olap", "oltp", "payouts"] vergen = ["router_env/vergen"] -dummy_connector = ["api_models/dummy_connector"] +backwards_compatibility = ["api_models/backwards_compatibility", "euclid/backwards_compatibility", "kgraph_utils/backwards_compatibility"] +business_profile_routing=["api_models/business_profile_routing"] +dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgraph_utils/dummy_connector"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] @@ -66,10 +69,12 @@ num_cpus = "1.15.0" once_cell = "1.18.0" qrcode = "0.12.0" rand = "0.8.5" +rand_chacha = "0.3.1" regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = "0.16.20" roxmltree = "0.18.0" +rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" serde_path_to_error = "0.1.11" @@ -96,6 +101,7 @@ api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] cards = { version = "0.1.0", path = "../cards" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } external_services = { version = "0.1.0", path = "../external_services" } +euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_derive = { version = "0.1.0", path = "../router_derive" } @@ -103,6 +109,7 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false} data_models = { version = "0.1.0", path = "../data_models", default-features = false } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } [target.'cfg(not(target_os = "windows"))'.dependencies] diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index 1076dfe410fc..c237f21dde66 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -9,7 +9,7 @@ use crate::{ core::{api_locking::GetLockingInput, payment_methods::Oss, payments}, routes, services::{api, authentication as auth}, - types::api::{self as api_types}, + types::api as api_types, }; #[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate))] @@ -50,6 +50,7 @@ pub async fn payment_intents_create( &req, create_payment_req, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -58,6 +59,7 @@ pub async fn payment_intents_create( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -117,6 +119,7 @@ pub async fn payment_intents_retrieve( payload, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -180,6 +183,7 @@ pub async fn payment_intents_retrieve_with_gateway_creds( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -236,6 +240,7 @@ pub async fn payment_intents_update( &req, payload, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -244,6 +249,7 @@ pub async fn payment_intents_update( req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -302,6 +308,7 @@ pub async fn payment_intents_confirm( &req, payload, |state, auth, req| { + let eligible_connectors = req.connector.clone(); payments::payments_core::( state, auth.merchant_account, @@ -310,6 +317,7 @@ pub async fn payment_intents_confirm( req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, api_types::HeaderPayload::default(), ) }, @@ -366,6 +374,7 @@ pub async fn payment_intents_capture( payload, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -426,6 +435,7 @@ pub async fn payment_intents_cancel( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 4d9632f8885e..c713011b80c8 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -282,9 +282,17 @@ impl TryFrom for payments::PaymentsRequest { let routing = routable_connector .map(|connector| { - crate::types::api::RoutingAlgorithm::Single( - api_models::admin::RoutableConnectorChoice::ConnectorName(connector), - ) + api_models::routing::RoutingAlgorithm::Single(Box::new( + api_models::routing::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: api_models::routing::RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + )) }) .map(|r| { serde_json::to_value(r) diff --git a/crates/router/src/compatibility/stripe/refunds.rs b/crates/router/src/compatibility/stripe/refunds.rs index dc147443828c..ad4accf6ca74 100644 --- a/crates/router/src/compatibility/stripe/refunds.rs +++ b/crates/router/src/compatibility/stripe/refunds.rs @@ -149,8 +149,8 @@ pub async fn refund_update( path: web::Path, form_payload: web::Form, ) -> HttpResponse { - let refund_id = path.into_inner(); - let payload = form_payload.into_inner(); + let mut payload = form_payload.into_inner(); + payload.refund_id = path.into_inner(); let create_refund_update_req: refund_types::RefundUpdateRequest = payload.into(); let flow = Flow::RefundsUpdate; @@ -169,9 +169,7 @@ pub async fn refund_update( state.into_inner(), &req, create_refund_update_req, - |state, auth, req| { - refunds::refund_update_core(state, auth.merchant_account, &refund_id, req) - }, + |state, auth, req| refunds::refund_update_core(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/compatibility/stripe/refunds/types.rs b/crates/router/src/compatibility/stripe/refunds/types.rs index e1486186491a..8d65a09187d3 100644 --- a/crates/router/src/compatibility/stripe/refunds/types.rs +++ b/crates/router/src/compatibility/stripe/refunds/types.rs @@ -17,6 +17,8 @@ pub struct StripeCreateRefundRequest { #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct StripeUpdateRefundRequest { + #[serde(skip)] + pub refund_id: String, pub metadata: Option, } @@ -58,6 +60,7 @@ impl From for refunds::RefundRequest { impl From for refunds::RefundUpdateRequest { fn from(req: StripeUpdateRefundRequest) -> Self { Self { + refund_id: req.refund_id, metadata: req.metadata, reason: None, } diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index 311498e1af58..515e41ec91fa 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -69,6 +69,7 @@ pub async fn setup_intents_create( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -128,6 +129,7 @@ pub async fn setup_intents_retrieve( payload, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -200,6 +202,7 @@ pub async fn setup_intents_update( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, @@ -273,6 +276,7 @@ pub async fn setup_intents_confirm( req, auth_flow, payments::CallConnectorAction::Trigger, + None, api_types::HeaderPayload::default(), ) }, diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 661a08e090e0..dde378e55925 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -185,9 +185,17 @@ impl TryFrom for payments::PaymentsRequest { let routing = routable_connector .map(|connector| { - crate::types::api::RoutingAlgorithm::Single( - api_models::admin::RoutableConnectorChoice::ConnectorName(connector), - ) + api_models::routing::RoutingAlgorithm::Single(Box::new( + api_models::routing::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: api_models::routing::RoutableChoiceKind::FullStruct, + connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: None, + }, + )) }) .map(|r| { serde_json::to_value(r) diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index 75cb07de02ba..1ab156d32ad4 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -7,6 +7,7 @@ use serde::Serialize; use crate::{ core::{api_locking, errors}, + events::api_logs::ApiEventMetric, routes::{app::AppStateInfo, metrics}, services::{self, api, authentication as auth, logger}, }; @@ -25,12 +26,12 @@ where F: Fn(A, U, T) -> Fut, Fut: Future, E2>>, E2: ErrorSwitch + std::error::Error + Send + Sync + 'static, - Q: Serialize + std::fmt::Debug + 'a, + Q: Serialize + std::fmt::Debug + 'a + ApiEventMetric, S: TryFrom + Serialize, E: Serialize + error_stack::Context + actix_web::ResponseError + Clone, error_stack::Report: services::EmbedError, errors::ApiErrorResponse: ErrorSwitch, - T: std::fmt::Debug + Serialize, + T: std::fmt::Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, { let request_method = request.method().as_str(); diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 204060b37aa0..df87c8a460ac 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -531,6 +531,7 @@ pub struct Connectors { pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, pub bambora: ConnectorParams, + pub bankofamerica: ConnectorParams, pub bitpay: ConnectorParams, pub bluesnap: ConnectorParamsWithSecondaryBaseUrl, pub boku: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 7849fd98a4d1..3a83fea0d910 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -3,6 +3,7 @@ pub mod adyen; pub mod airwallex; pub mod authorizedotnet; pub mod bambora; +pub mod bankofamerica; pub mod bitpay; pub mod bluesnap; pub mod boku; @@ -55,13 +56,14 @@ pub mod zen; pub use self::dummyconnector::DummyConnector; pub use self::{ aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet, - bambora::Bambora, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, braintree::Braintree, - cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay, - cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, forte::Forte, globalpay::Globalpay, - globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, - mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, - nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, - payu::Payu, powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, - square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, - wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + bambora::Bambora, bankofamerica::Bankofamerica, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, + braintree::Braintree, cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, + cryptopay::Cryptopay, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, forte::Forte, + globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, helcim::Helcim, + iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, + nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, + payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, + prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, + stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, + worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 0a6e0d8a6099..f6389c802f9e 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -30,7 +30,9 @@ impl ConnectorCommon for Aci { fn id(&self) -> &'static str { "aci" } - + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } fn common_get_content_type(&self) -> &'static str { "application/x-www-form-urlencoded" } @@ -76,6 +78,7 @@ impl ConnectorCommon for Aci { .collect::>() .join("; ") }), + attempt_status: None, }) } } @@ -198,7 +201,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -277,9 +282,16 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // encode only for for urlencoded things. - let connector_req = aci::AciPaymentsRequest::try_from(req)?; + let connector_router_data = aci::AciRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = aci::AciPaymentsRequest::try_from(&connector_router_data)?; let aci_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -308,7 +320,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -377,6 +391,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = aci::AciCancelRequest::try_from(req)?; let aci_req = types::RequestBody::log_and_get_request_body( @@ -397,7 +412,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -470,8 +487,15 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_req = aci::AciRefundRequest::try_from(req)?; + let connector_router_data = aci::AciRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_req = aci::AciRefundRequest::try_from(&connector_router_data)?; let body = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -493,7 +517,9 @@ impl services::ConnectorIntegration; +#[derive(Debug, Serialize)] +pub struct AciRouterData { + amount: String, + router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for AciRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} + pub struct AciAuthType { pub api_key: Secret, pub entity_id: Secret, @@ -101,14 +133,14 @@ impl TryFrom<&api_models::payments::WalletData> for PaymentDetails { impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, )> for PaymentDetails { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, ), ) -> Result { @@ -202,9 +234,9 @@ impl bank_account_bic: None, bank_account_iban: None, billing_country: Some(country.to_owned()), - merchant_customer_id: Some(Secret::new(item.get_customer_id()?)), + merchant_customer_id: Some(Secret::new(item.router_data.get_customer_id()?)), merchant_transaction_id: Some(Secret::new( - item.connector_request_reference_id.clone(), + item.router_data.connector_request_reference_id.clone(), )), customer_email: None, })) @@ -348,10 +380,12 @@ pub enum AciPaymentType { Refund, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { +impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.request.payment_method_data.clone() { + fn try_from( + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ref card_data) => Self::try_from((item, card_data)), api::PaymentMethodData::Wallet(ref wallet_data) => Self::try_from((item, wallet_data)), api::PaymentMethodData::PayLater(ref pay_later_data) => { @@ -361,7 +395,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { Self::try_from((item, bank_redirect_data)) } api::PaymentMethodData::MandatePayment => { - let mandate_id = item.request.mandate_id.clone().ok_or( + let mandate_id = item.router_data.request.mandate_id.clone().ok_or( errors::ConnectorError::MissingRequiredField { field_name: "mandate_id", }, @@ -376,7 +410,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.payment_method), + message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, } @@ -385,14 +419,14 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::WalletData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::WalletData, ), ) -> Result { @@ -404,21 +438,21 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::BankRedirectData, ), ) -> Result { @@ -430,21 +464,21 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::PayLaterData, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, &api_models::payments::PayLaterData, ), ) -> Result { @@ -456,15 +490,23 @@ impl txn_details, payment_method, instruction: None, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } -impl TryFrom<(&types::PaymentsAuthorizeRouterData, &api::Card)> for AciPaymentsRequest { +impl + TryFrom<( + &AciRouterData<&types::PaymentsAuthorizeRouterData>, + &api::Card, + )> for AciPaymentsRequest +{ type Error = Error; fn try_from( - value: (&types::PaymentsAuthorizeRouterData, &api::Card), + value: ( + &AciRouterData<&types::PaymentsAuthorizeRouterData>, + &api::Card, + ), ) -> Result { let (item, card_data) = value; let txn_details = get_transaction_details(item)?; @@ -482,14 +524,14 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, &api::Card)> for AciPaymentsR impl TryFrom<( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, api_models::payments::MandateIds, )> for AciPaymentsRequest { type Error = Error; fn try_from( value: ( - &types::PaymentsAuthorizeRouterData, + &AciRouterData<&types::PaymentsAuthorizeRouterData>, api_models::payments::MandateIds, ), ) -> Result { @@ -501,32 +543,34 @@ impl txn_details, payment_method: PaymentDetails::Mandate, instruction, - shopper_result_url: item.request.router_return_url.clone(), + shopper_result_url: item.router_data.request.router_return_url.clone(), }) } } fn get_transaction_details( - item: &types::PaymentsAuthorizeRouterData, + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - let auth = AciAuthType::try_from(&item.connector_auth_type)?; + let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(TransactionDetails { entity_id: auth.entity_id, - amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, - currency: item.request.currency.to_string(), + amount: item.amount.to_owned(), + currency: item.router_data.request.currency.to_string(), payment_type: AciPaymentType::Debit, }) } -fn get_instruction_details(item: &types::PaymentsAuthorizeRouterData) -> Option { - if item.request.setup_mandate_details.is_some() { +fn get_instruction_details( + item: &AciRouterData<&types::PaymentsAuthorizeRouterData>, +) -> Option { + if item.router_data.request.setup_mandate_details.is_some() { return Some(Instruction { mode: InstructionMode::Initial, transaction_type: InstructionType::Unscheduled, source: InstructionSource::CardholderInitiatedTransaction, create_registration: Some(true), }); - } else if item.request.mandate_id.is_some() { + } else if item.router_data.request.mandate_id.is_some() { return Some(Instruction { mode: InstructionMode::Repeated, transaction_type: InstructionType::Unscheduled, @@ -703,14 +747,13 @@ pub struct AciRefundRequest { pub entity_id: Secret, } -impl TryFrom<&types::RefundsRouterData> for AciRefundRequest { +impl TryFrom<&AciRouterData<&types::RefundsRouterData>> for AciRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let amount = - utils::to_currency_base_unit(item.request.refund_amount, item.request.currency)?; - let currency = item.request.currency; + fn try_from(item: &AciRouterData<&types::RefundsRouterData>) -> Result { + let amount = item.amount.to_owned(); + let currency = item.router_data.request.currency; let payment_type = AciPaymentType::Refund; - let auth = AciAuthType::try_from(&item.connector_auth_type)?; + let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { amount, diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 18a575b509cb..ef10fbb692fd 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -73,6 +73,7 @@ impl ConnectorCommon for Adyen { code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -171,6 +172,7 @@ impl fn get_request_body( &self, req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let authorize_req = types::PaymentsAuthorizeRouterData::from(( req, @@ -202,7 +204,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -251,6 +255,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -304,6 +309,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -332,7 +338,9 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -366,6 +374,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -395,6 +404,7 @@ impl fn get_request_body( &self, req: &types::RouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, @@ -479,7 +489,7 @@ impl req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let request_body = self.get_request_body(req)?; + let request_body = self.get_request_body(req, connectors)?; match request_body { Some(_) => Ok(Some( services::RequestBuilder::new() @@ -487,7 +497,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )), None => Ok(None), @@ -533,6 +545,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } @@ -628,6 +641,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -660,7 +674,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -699,6 +715,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -747,6 +764,7 @@ impl fn get_request_body( &self, req: &types::PaymentsBalanceRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -771,7 +789,9 @@ impl .headers(types::PaymentsBalanceType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body(self, req)?) + .body(types::PaymentsBalanceType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -840,6 +860,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenCancelRequest::try_from(req)?; @@ -861,7 +882,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -896,6 +919,7 @@ impl code: response.error_code, message: response.message, reason: None, + attempt_status: None, }) } } @@ -949,6 +973,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenPayoutCancelRequest::try_from(req)?; let adyen_req = types::RequestBody::log_and_get_request_body( @@ -969,7 +994,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1060,7 +1088,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1156,7 +1187,9 @@ impl .headers(types::PayoutEligibilityType::get_headers( self, req, connectors, )?) - .body(types::PayoutEligibilityType::get_request_body(self, req)?) + .body(types::PayoutEligibilityType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) @@ -1235,6 +1268,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1263,7 +1297,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), @@ -1363,7 +1400,9 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = airwallex::AirwallexIntentRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -273,7 +277,9 @@ impl .url(&types::PaymentsInitType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body(self, req)?) + .body(types::PaymentsInitType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -372,6 +378,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), @@ -403,7 +410,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?; @@ -571,7 +581,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -634,6 +644,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?; @@ -659,7 +670,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?; @@ -772,7 +786,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), @@ -847,7 +864,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -176,7 +177,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = authorizedotnet::AuthorizedotnetCreateSyncRequest::try_from(req)?; let sync_request = types::RequestBody::log_and_get_request_body( @@ -261,7 +265,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -362,7 +369,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; @@ -445,7 +455,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -542,7 +555,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -634,7 +650,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), @@ -735,7 +754,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -893,6 +912,7 @@ fn get_error_response( message: error.error_text.to_owned(), reason: Some(error.error_text), status_code, + attempt_status: None, }) }) .unwrap_or_else(|| types::ErrorResponse { @@ -900,6 +920,7 @@ fn get_error_response( message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, status_code, + attempt_status: None, })), Some(authorizedotnet::TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { @@ -909,6 +930,7 @@ fn get_error_response( message: message.to_string(), reason: Some(message.to_string()), status_code, + attempt_status: None, }) } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 561723be46cf..884504154e8f 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -573,6 +573,7 @@ impl message: error.error_text.clone(), reason: None, status_code: item.http_code, + attempt_status: None, }) }); let metadata = transaction_response @@ -647,6 +648,7 @@ impl message: error.error_text.clone(), reason: None, status_code: item.http_code, + attempt_status: None, }) }); let metadata = transaction_response @@ -789,6 +791,7 @@ impl TryFrom types::Error message: message.message[0].text.clone(), reason: None, status_code, + attempt_status: None, } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index d5e8119b66c8..802be26408df 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -95,6 +95,7 @@ impl ConnectorCommon for Bambora { code: response.code.to_string(), message: response.message, reason: Some(serde_json::to_string(&response.details).unwrap_or_default()), + attempt_status: None, }) } } @@ -172,6 +173,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraPaymentsRequest::try_from(req)?; @@ -194,7 +196,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bambora::BamboraPaymentsCaptureRequest::try_from(req)?; let bambora_req = types::RequestBody::log_and_get_request_body( @@ -367,7 +370,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraPaymentsRequest::try_from(req)?; @@ -463,7 +467,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = bambora::BamboraRefundRequest::try_from(req)?; let bambora_req = types::RequestBody::log_and_get_request_body( @@ -556,7 +563,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; @@ -765,7 +777,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(); Ok(Some(request)) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs new file mode 100644 index 000000000000..84870f7407fb --- /dev/null +++ b/crates/router/src/connector/bankofamerica.rs @@ -0,0 +1,548 @@ +pub mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use transformers as bankofamerica; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Bankofamerica; + +impl api::Payment for Bankofamerica {} +impl api::PaymentSession for Bankofamerica {} +impl api::ConnectorAccessToken for Bankofamerica {} +impl api::MandateSetup for Bankofamerica {} +impl api::PaymentAuthorize for Bankofamerica {} +impl api::PaymentSync for Bankofamerica {} +impl api::PaymentCapture for Bankofamerica {} +impl api::PaymentVoid for Bankofamerica {} +impl api::Refund for Bankofamerica {} +impl api::RefundExecute for Bankofamerica {} +impl api::RefundSync for Bankofamerica {} +impl api::PaymentToken for Bankofamerica {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Bankofamerica +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Bankofamerica +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Bankofamerica { + fn id(&self) -> &'static str { + "bankofamerica" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.bankofamerica.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = bankofamerica::BankofamericaAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaErrorResponse = res + .response + .parse_struct("BankofamericaErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + }) + } +} + +impl ConnectorValidation for Bankofamerica { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration + for Bankofamerica +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Bankofamerica +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Bankofamerica +{ +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = + bankofamerica::BankofamericaPaymentsRequest::try_from(&connector_router_data)?; + let bankofamerica_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("Bankofamerica PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("bankofamerica PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankofamericaPaymentsResponse = res + .response + .parse_struct("Bankofamerica PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = bankofamerica::BankofamericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = bankofamerica::BankofamericaRefundRequest::try_from(&connector_router_data)?; + let bankofamerica_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bankofamerica_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: bankofamerica::RefundResponse = res + .response + .parse_struct("bankofamerica RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Bankofamerica +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::RefundResponse = res + .response + .parse_struct("bankofamerica RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Bankofamerica { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs new file mode 100644 index 000000000000..a396c47a4ced --- /dev/null +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -0,0 +1,250 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +//TODO: Fill the struct with respective fields +pub struct BankofamericaRouterData { + pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for BankofamericaRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Ok(Self { + amount, + router_data: item, + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct BankofamericaPaymentsRequest { + amount: i64, + card: BankofamericaCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct BankofamericaCard { + name: Secret, + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankofamericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankofamericaRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = BankofamericaCard { + name: req_card.card_holder_name, + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.router_data.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.amount.to_owned(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct BankofamericaAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for BankofamericaAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +//TODO: Append the remaining status flags +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum BankofamericaPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: BankofamericaPaymentStatus) -> Self { + match item { + BankofamericaPaymentStatus::Succeeded => Self::Charged, + BankofamericaPaymentStatus::Failed => Self::Failure, + BankofamericaPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BankofamericaPaymentsResponse { + status: BankofamericaPaymentStatus, + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankofamericaPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct BankofamericaRefundRequest { + pub amount: i64, +} + +impl TryFrom<&BankofamericaRouterData<&types::RefundsRouterData>> + for BankofamericaRefundRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankofamericaRouterData<&types::RefundsRouterData>, + ) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct BankofamericaErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index e8826e933905..dc4571b75746 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -120,6 +120,7 @@ impl ConnectorCommon for Bitpay { .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), message: response.error, reason: response.message, + attempt_status: None, }) } } @@ -172,6 +173,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bitpay::BitpayRouterData::try_from(( &self.get_currency_unit(), @@ -204,7 +206,9 @@ impl ConnectorIntegration, + order_id: String, } impl TryFrom<&BitpayRouterData<&types::PaymentsAuthorizeRouterData>> for BitpayPaymentsRequest { @@ -133,6 +134,7 @@ pub struct BitpayPaymentResponseData { pub expiration_time: Option, pub current_time: Option, pub id: String, + pub order_id: Option, pub low_fee_detected: Option, pub display_amount_paid: Option, pub exception_status: ExceptionStatus, @@ -161,7 +163,7 @@ impl .data .url .map(|x| services::RedirectForm::from((x, services::Method::Get))); - let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id); + let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id.clone()); let attempt_status = item.response.data.status; Ok(Self { status: enums::AttemptStatus::from(attempt_status), @@ -171,7 +173,11 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item + .response + .data + .order_id + .or(Some(item.response.data.id)), }), ..item.data }) @@ -279,6 +285,7 @@ fn get_crypto_specific_payment_data( ConnectorAuthType::HeaderKey { api_key } => api_key, _ => String::default().into(), }; + let order_id = item.router_data.connector_request_reference_id.clone(); Ok(BitpayPaymentsRequest { price, @@ -287,6 +294,7 @@ fn get_crypto_specific_payment_data( notification_url, transaction_speed, token, + order_id, }) } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 24d5787aa8d8..7bd2ce052538 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -37,6 +37,8 @@ use crate::{ utils::{self, BytesExt}, }; +pub const BLUESNAP_TRANSACTION_NOT_FOUND: &str = "is not authorized to view merchant-transaction:"; + #[derive(Debug, Clone)] pub struct Bluesnap; @@ -124,6 +126,7 @@ impl ConnectorCommon for Bluesnap { .map(|error_code_message| error_code_message.error_message) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), + attempt_status: None, } } bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { @@ -131,13 +134,30 @@ impl ConnectorCommon for Bluesnap { code: error_res.error_code.clone(), message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), + attempt_status: None, }, - bluesnap::BluesnapErrors::General(error_response) => ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: error_response.clone(), - reason: Some(error_response), - }, + bluesnap::BluesnapErrors::General(error_response) => { + let (error_res, attempt_status) = if res.status_code == 403 + && error_response.contains(BLUESNAP_TRANSACTION_NOT_FOUND) + { + ( + format!( + "{} in bluesnap dashboard", + consts::REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + ), + Some(enums::AttemptStatus::Failure), // when bluesnap throws 403 for payment not found, we update the payment status to failure. + ) + } else { + (error_response.clone(), None) + }; + ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: error_response, + reason: Some(error_res), + attempt_status, + } + } }; Ok(response_error_message) } @@ -243,6 +263,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bluesnap::BluesnapVoidRequest::try_from(req)?; let bluesnap_req = types::RequestBody::log_and_get_request_body( @@ -263,7 +284,9 @@ impl ConnectorIntegration CustomResult { - let meta_data: CustomResult = - connector_utils::to_connector_meta_from_secret(req.connector_meta_data.clone()); - - match meta_data { - // if merchant_id is present, psync can be made using merchant_transaction_id - Ok(data) => get_url_with_merchant_transaction_id( - self.base_url(connectors).to_string(), - data.merchant_id, - req.attempt_id.to_owned(), - ), - // otherwise psync is made using connector_transaction_id - Err(_) => get_psync_url_with_connector_transaction_id( - &req.request.connector_transaction_id, - self.base_url(connectors).to_string(), - ), + let connector_transaction_id = req.request.connector_transaction_id.clone(); + match connector_transaction_id { + // if connector_transaction_id is present, we always sync with connector_transaction_id + types::ResponseId::ConnectorTransactionId(trans_id) => { + get_psync_url_with_connector_transaction_id( + trans_id, + self.base_url(connectors).to_string(), + ) + } + _ => { + // if connector_transaction_id is not present, we sync with merchant_transaction_id + let meta_data: bluesnap::BluesnapConnectorMetaData = + connector_utils::to_connector_meta_from_secret(req.connector_meta_data.clone()) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + get_url_with_merchant_transaction_id( + self.base_url(connectors).to_string(), + meta_data.merchant_id, + req.attempt_id.to_owned(), + ) + } } } @@ -412,6 +440,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -440,7 +469,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; let bluesnap_req = types::RequestBody::log_and_get_request_body( @@ -527,7 +559,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -643,7 +678,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -764,7 +802,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -830,6 +868,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), @@ -858,7 +897,9 @@ impl ConnectorIntegration CustomResult { - let connector_transaction_id = connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(format!( "{}{}{}", base_url, "services/2/transactions/", connector_transaction_id diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 826d218cd56e..7c2c1af0986b 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -130,6 +130,7 @@ impl ConnectorCommon for Boku { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }), Err(_) => get_xml_deserialized(res), } @@ -206,6 +207,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuPaymentsRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -231,7 +233,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuPsyncRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -317,7 +322,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -394,7 +402,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = boku::BokuRefundRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -484,7 +495,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = boku::BokuRsyncRequest::try_from(req)?; let boku_req = types::RequestBody::log_and_get_request_body( @@ -563,7 +577,9 @@ impl ConnectorIntegration CustomResult Ok(ErrorResponse { @@ -139,6 +140,7 @@ impl ConnectorCommon for Braintree { code: consts::NO_ERROR_CODE.to_string(), message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.errors), + attempt_status: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); @@ -240,7 +242,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = braintree::BraintreeSessionRequest::try_from(req)?; let braintree_session_request = types::RequestBody::log_and_get_request_body( @@ -322,6 +327,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; @@ -347,7 +353,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )), false => Ok(None), @@ -437,6 +445,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version.clone(); let connector_router_data = @@ -482,7 +491,9 @@ impl ConnectorIntegration Err(errors::ConnectorError::NotImplemented( @@ -587,6 +598,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -618,7 +630,9 @@ impl ConnectorIntegration Ok(Some( @@ -627,7 +641,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; let connector_router_data = @@ -918,7 +937,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -1055,6 +1077,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; let connector_router_data = @@ -1101,7 +1124,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { @@ -1219,7 +1245,9 @@ impl ConnectorIntegration Ok(None), @@ -1584,6 +1612,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -1629,7 +1658,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )), diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index b622e041915d..bf51973237c5 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -316,6 +316,7 @@ fn get_error_response( message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: error_reason, status_code: http_code, + attempt_status: None, }) } diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index ed994dca31fc..12a52e485396 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -119,6 +119,7 @@ impl ConnectorCommon for Cashtocode { code: response.error.to_string(), message: response.error_description, reason: None, + attempt_status: None, }) } } @@ -202,6 +203,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?; let cashtocode_req = types::RequestBody::log_and_get_request_body( @@ -227,7 +229,9 @@ impl ConnectorIntegration status_code: item.http_code, message: error_data.error_description, reason: None, + attempt_status: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f4cc4ac9640e..f24c08233ed7 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Checkout { .error_codes .map(|errors| errors.join(" & ")) .or(response.error_type), + attempt_status: None, }) } } @@ -207,6 +208,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = checkout::TokenRequest::try_from(req)?; let checkout_req = types::RequestBody::log_and_get_request_body( @@ -228,7 +230,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -312,6 +316,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -341,7 +346,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -531,7 +541,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = checkout::PaymentVoidRequest::try_from(req)?; let checkout_req = types::RequestBody::log_and_get_request_body( @@ -608,7 +621,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), @@ -703,7 +719,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let checkout_req = checkout::Evidence::try_from(req)?; let checkout_req_string = types::RequestBody::log_and_get_request_body( @@ -1064,7 +1085,9 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body(self, req)?) + .body(types::SubmitEvidenceType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 53182e65ed5b..6ad040da2842 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -576,6 +576,7 @@ impl TryFrom> .clone() .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, + attempt_status: None, }) } else { None @@ -623,6 +624,7 @@ impl TryFrom> .clone() .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, + attempt_status: None, }) } else { None diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index d50e490cfc30..5704ea15b005 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -108,6 +108,7 @@ impl ConnectorCommon for Coinbase { code: response.error.error_type, message: response.error.message, reason: response.error.code, + attempt_status: None, }) } } @@ -183,6 +184,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = coinbase::CoinbasePaymentsRequest::try_from(req)?; let coinbase_payment_request = types::RequestBody::log_and_get_request_body( @@ -207,7 +209,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { let api_method; - let payload = match self.get_request_body(req)? { + let payload = match self.get_request_body(req, connectors)? { Some(val) => { let body = types::RequestBody::get_inner_value(val).peek().to_owned(); api_method = "POST".to_string(); @@ -167,6 +167,7 @@ impl ConnectorCommon for Cryptopay { code: response.error.code, message: response.error.message, reason: response.error.reason, + attempt_status: None, }) } } @@ -216,6 +217,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = cryptopay::CryptopayRouterData::try_from(( &self.get_currency_unit(), @@ -248,7 +250,9 @@ impl ConnectorIntegration for CryptopayAuthType { } } // PaymentsResponse -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CryptopayPaymentStatus { - #[default] New, Completed, Unresolved, @@ -128,13 +127,14 @@ impl From for enums::AttemptStatus { CryptopayPaymentStatus::New => Self::AuthenticationPending, CryptopayPaymentStatus::Completed => Self::Charged, CryptopayPaymentStatus::Cancelled => Self::Failure, - CryptopayPaymentStatus::Unresolved => Self::Unresolved, - _ => Self::Voided, + CryptopayPaymentStatus::Unresolved | CryptopayPaymentStatus::Refunded => { + Self::Unresolved + } //mapped refunded to Unresolved because refund api is not available, also merchant has done the action on the connector dashboard. } } } -#[derive(Default, Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct CryptopayPaymentsResponse { data: CryptopayPaymentResponseData, } @@ -190,7 +190,7 @@ pub struct CryptopayErrorResponse { pub error: CryptopayErrorData, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct CryptopayPaymentResponseData { pub id: String, pub custom_id: Option, diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 0a13aa0cf141..ee6e93aebbd0 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Cybersource { code, message, reason: Some(connector_reason), + attempt_status: None, }) } } @@ -166,7 +167,7 @@ where ) -> CustomResult)>, errors::ConnectorError> { let date = OffsetDateTime::now_utc(); - let cybersource_req = self.get_request_body(req)?; + let cybersource_req = self.get_request_body(req, connectors)?; let auth = cybersource::CybersourceAuthType::try_from(&req.connector_auth_type)?; let merchant_account = auth.merchant_account.clone(); let base_url = connectors.cybersource.base_url.as_str(); @@ -297,6 +298,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; let cybersource_payments_request = types::RequestBody::log_and_get_request_body( @@ -319,7 +321,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) @@ -471,6 +476,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), @@ -502,7 +508,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) @@ -587,7 +594,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; let cybersource_refund_request = types::RequestBody::log_and_get_request_body( @@ -678,7 +686,7 @@ impl ConnectorIntegration message: error.message, reason: Some(error.reason), status_code: item.http_code, + attempt_status: None, }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index b706d694a3d5..64d3e6f1c12f 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -53,10 +53,10 @@ where fn build_headers( &self, req: &types::RouterData, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let dlocal_req = match self.get_request_body(req)? { + let dlocal_req = match self.get_request_body(req, connectors)? { Some(val) => val, None => types::RequestBody::log_and_get_request_body("".to_string(), Ok) .change_context(errors::ConnectorError::RequestEncodingFailed)?, @@ -135,6 +135,7 @@ impl ConnectorCommon for Dlocal { code: response.code.to_string(), message: response.message, reason: response.param, + attempt_status: None, }) } } @@ -210,6 +211,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), @@ -241,7 +243,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; let dlocal_payments_capture_request = types::RequestBody::log_and_get_request_body( @@ -391,7 +396,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), @@ -545,7 +553,9 @@ impl ConnectorIntegration ConnectorCommon for DummyConnector { code: response.error.code, message: response.error.message, reason: response.error.reason, + attempt_status: None, }) } } @@ -187,6 +188,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; let dummmy_payments_request = types::RequestBody::log_and_get_request_body( @@ -212,7 +214,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -347,6 +351,7 @@ impl fn get_request_body( &self, _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -429,6 +434,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = transformers::DummyConnectorRefundRequest::try_from(req)?; let dummmy_refund_request = types::RequestBody::log_and_get_request_body( @@ -451,7 +457,9 @@ impl ConnectorIntegration ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; let auth: fiserv::FiservAuthType = @@ -70,7 +70,7 @@ where let mut auth_header = self.get_auth_header(&req.connector_auth_type)?; let fiserv_req = self - .get_request_body(req)? + .get_request_body(req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let client_request_id = Uuid::new_v4().to_string(); @@ -104,6 +104,10 @@ impl ConnectorCommon for Fiserv { "fiserv" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -147,6 +151,7 @@ impl ConnectorCommon for Fiserv { message: first_error.message.to_owned(), reason: first_error.field.to_owned(), status_code: res.status_code, + attempt_status: None, }) }) .unwrap_or(types::ErrorResponse { @@ -154,6 +159,7 @@ impl ConnectorCommon for Fiserv { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, status_code: res.status_code, + attempt_status: None, })) } } @@ -240,6 +246,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservCancelRequest::try_from(req)?; let fiserv_payments_cancel_request = types::RequestBody::log_and_get_request_body( @@ -261,7 +268,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservSyncRequest::try_from(req)?; let fiserv_payments_sync_request = types::RequestBody::log_and_get_request_body( @@ -347,7 +357,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservCaptureRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = fiserv::FiservCaptureRequest::try_from(&router_obj)?; let fiserv_payments_capture_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -422,7 +441,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservPaymentsRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_request = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; let fiserv_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -529,7 +557,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservRefundRequest::try_from(req)?; + let router_obj = fiserv::FiservRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = fiserv::FiservRefundRequest::try_from(&router_obj)?; let fiserv_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -612,7 +649,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = fiserv::FiservSyncRequest::try_from(req)?; let fiserv_sync_request = types::RequestBody::log_and_get_request_body( @@ -692,7 +732,9 @@ impl ConnectorIntegration { + pub amount: String, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for FiservRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data, + }) + } +} + #[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { @@ -99,23 +131,25 @@ pub enum TransactionInteractionPosConditionCode { CardNotPresentEcom, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest { +impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let amount = Amount { - total: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }; let transaction_details = TransactionDetails { capture_flag: Some(matches!( - item.request.capture_method, + item.router_data.request.capture_method, Some(enums::CaptureMethod::Automatic) | None )), reversal_reason_code: None, - merchant_transaction_id: item.connector_request_reference_id.clone(), + merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; - let metadata = item.get_connector_meta()?; + let metadata = item.router_data.get_connector_meta()?; let session: SessionObject = metadata .parse_value("SessionObject") .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -133,7 +167,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest { //card not present in online transaction pos_condition_code: TransactionInteractionPosConditionCode::CardNotPresentEcom, }; - let source = match item.request.payment_method_data.clone() { + let source = match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ref ccard) => { let card = CardData { card_data: ccard.card_number.clone(), @@ -389,35 +423,40 @@ pub struct SessionObject { pub terminal_id: String, } -impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest { +impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let metadata = item + .router_data .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let session: SessionObject = metadata .parse_value("SessionObject") .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let amount = - utils::to_currency_base_unit(item.request.amount_to_capture, item.request.currency)?; Ok(Self { amount: Amount { - total: amount, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, transaction_details: TransactionDetails { capture_flag: Some(true), reversal_reason_code: None, - merchant_transaction_id: item.connector_request_reference_id.clone(), + merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }, merchant_details: MerchantDetails { merchant_id: auth.merchant_account, terminal_id: Some(session.terminal_id), }, reference_transaction_details: ReferenceTransactionDetails { - reference_transaction_id: item.request.connector_transaction_id.to_string(), + reference_transaction_id: item + .router_data + .request + .connector_transaction_id + .to_string(), }, }) } @@ -477,11 +516,14 @@ pub struct FiservRefundRequest { reference_transaction_details: ReferenceTransactionDetails, } -impl TryFrom<&types::RefundsRouterData> for FiservRefundRequest { +impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &FiservRouterData<&types::RefundsRouterData>, + ) -> Result { + let auth: FiservAuthType = FiservAuthType::try_from(&item.router_data.connector_auth_type)?; let metadata = item + .router_data .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; @@ -490,18 +532,19 @@ impl TryFrom<&types::RefundsRouterData> for FiservRefundRequest { .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { amount: Amount { - total: utils::to_currency_base_unit( - item.request.refund_amount, - item.request.currency, - )?, - currency: item.request.currency.to_string(), + total: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, merchant_details: MerchantDetails { merchant_id: auth.merchant_account, terminal_id: Some(session.terminal_id), }, reference_transaction_details: ReferenceTransactionDetails { - reference_transaction_id: item.request.connector_transaction_id.to_string(), + reference_transaction_id: item + .router_data + .request + .connector_transaction_id + .to_string(), }, }) } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 6f20e93e8c8d..40448c01fabf 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -130,6 +130,7 @@ impl ConnectorCommon for Forte { code, message, reason: None, + attempt_status: None, }) } } @@ -200,6 +201,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::FortePaymentsRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -225,7 +227,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::ForteCaptureRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -379,7 +384,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = forte::ForteCancelRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -460,7 +468,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = forte::ForteRefundRequest::try_from(req)?; let forte_req = types::RequestBody::log_and_get_request_body( @@ -541,7 +552,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let globalpay_req = types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -186,7 +188,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -262,7 +264,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = GlobalpayRefreshTokenRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -313,6 +318,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = requests::GlobalpayCancelRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -539,6 +548,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = requests::GlobalpayCaptureRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -562,7 +572,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = GlobalpayPaymentsRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -653,7 +666,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = requests::GlobalpayRefundRequest::try_from(req)?; let globalpay_req = types::RequestBody::log_and_get_request_body( @@ -740,7 +756,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = globepay::GlobepayPaymentsRequest::try_from(req)?; let globepay_req = types::RequestBody::log_and_get_request_body( @@ -212,7 +214,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = globepay::GlobepayRefundRequest::try_from(req)?; let globepay_req = types::RequestBody::log_and_get_request_body( @@ -386,7 +391,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessCustomerRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -177,7 +179,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -247,6 +251,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessBankAccountRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -268,7 +273,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -365,6 +372,7 @@ impl fn get_request_body( &self, req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = gocardless::GocardlessMandateRequest::try_from(req)?; let gocardless_req = types::RequestBody::log_and_get_request_body( @@ -388,7 +396,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } else { @@ -446,6 +456,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), @@ -477,7 +488,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), @@ -639,7 +653,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = helcim::HelcimVerifyRequest::try_from(req)?; @@ -213,7 +215,9 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -269,6 +273,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -300,7 +305,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -469,7 +477,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = helcim::HelcimVoidRequest::try_from(req)?; let helcim_req = types::RequestBody::log_and_get_request_body( @@ -545,7 +556,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), @@ -627,7 +641,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = iatapay::IatapayAuthUpdateRequest::try_from(req)?; let iatapay_req = types::RequestBody::log_and_get_request_body( @@ -198,7 +200,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), @@ -305,7 +311,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), @@ -488,7 +497,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = klarna::KlarnaSessionRequest::try_from(req)?; // encode only for for urlencoded things. @@ -177,7 +179,9 @@ impl .headers(types::PaymentsSessionType::get_headers( self, req, connectors, )?) - .body(types::PaymentsSessionType::get_request_body(self, req)?) + .body(types::PaymentsSessionType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -409,6 +413,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = klarna::KlarnaRouterData::try_from(( &self.get_currency_unit(), @@ -440,7 +445,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index 337e1b78c9a9..ef3eb6a3e7b3 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -98,6 +98,7 @@ impl ConnectorCommon for Mollie { .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), message: response.detail, reason: response.field, + attempt_status: None, }) } } @@ -149,6 +150,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = mollie::MollieCardTokenRequest::try_from(req)?; let mollie_req = types::RequestBody::log_and_get_request_body( @@ -169,7 +171,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -232,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), @@ -263,7 +268,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), @@ -454,7 +462,9 @@ impl ConnectorIntegration, #[serde(flatten)] payment_method_data: PaymentMethodData, - metadata: Option, + metadata: Option, sequence_type: SequenceType, mandate_id: Option, } @@ -148,8 +148,10 @@ pub struct Address { pub country: api_models::enums::CountryAlpha2, } -pub struct MollieBrowserInfo { - language: String, +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MollieMetadata { + pub order_id: String, } impl TryFrom<&MollieRouterData<&types::PaymentsAuthorizeRouterData>> for MolliePaymentsRequest { @@ -216,7 +218,9 @@ impl TryFrom<&MollieRouterData<&types::PaymentsAuthorizeRouterData>> for MollieP webhook_url: "".to_string(), locale: None, payment_method_data, - metadata: None, + metadata: Some(MollieMetadata { + order_id: item.router_data.connector_request_reference_id.clone(), + }), sequence_type: SequenceType::Oneoff, mandate_id: None, }) @@ -287,12 +291,7 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { let card_expiry_date = ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); let card_cvv = ccard.card_cvc; - let browser_info = get_browser_info(item)?; - let locale = browser_info - .ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "browser_info.language", - })? - .language; + let locale = item.request.get_browser_info()?.get_language()?; let testmode = item.test_mode .ok_or(errors::ConnectorError::MissingRequiredField { @@ -386,24 +385,6 @@ fn get_address_details( Ok(address_details) } -fn get_browser_info( - item: &types::TokenizationRouterData, -) -> Result, error_stack::Report> { - if matches!(item.auth_type, enums::AuthenticationType::ThreeDs) { - item.request - .browser_info - .as_ref() - .map(|info| { - Ok(MollieBrowserInfo { - language: info.get_language()?, - }) - }) - .transpose() - } else { - Ok(None) - } -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MolliePaymentsResponse { @@ -411,7 +392,7 @@ pub struct MolliePaymentsResponse { pub id: String, pub amount: Amount, pub description: Option, - pub metadata: Option, + pub metadata: Option, pub status: MolliePaymentStatus, pub is_cancelable: Option, pub sequence_type: SequenceType, @@ -544,12 +525,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: url, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.id), }), ..item.data }) @@ -561,6 +542,7 @@ impl pub struct MollieRefundRequest { amount: Amount, description: Option, + metadata: Option, } impl TryFrom<&MollieRouterData<&types::RefundsRouterData>> for MollieRefundRequest { @@ -575,6 +557,9 @@ impl TryFrom<&MollieRouterData<&types::RefundsRouterData>> for MollieRefun Ok(Self { amount, description: item.router_data.request.reason.to_owned(), + metadata: Some(MollieMetadata { + order_id: item.router_data.request.refund_id.clone(), + }), }) } } @@ -589,7 +574,7 @@ pub struct RefundResponse { settlement_amount: Option, status: MollieRefundStatus, description: Option, - metadata: serde_json::Value, + metadata: Option, payment_id: String, #[serde(rename = "_links")] links: Links, @@ -642,6 +627,4 @@ pub struct ErrorResponse { pub title: Option, pub detail: String, pub field: Option, - #[serde(rename = "_links")] - pub links: Option, } diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index e09f242abc8a..9dc54e7b72e3 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -83,6 +83,7 @@ impl ConnectorCommon for Multisafepay { code: response.error_code.to_string(), message: response.error_info, reason: None, + attempt_status: None, }) } } @@ -261,6 +262,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), @@ -292,7 +294,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), @@ -391,7 +396,9 @@ impl ConnectorIntegration message: error_response.error_info.clone(), reason: Some(error_response.error_info), status_code: item.http_code, + attempt_status: None, }), ..item.data }), @@ -808,6 +809,7 @@ impl TryFrom CustomResult, errors::ConnectorError> { let req_obj = nexinets::NexinetsPaymentsRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -224,7 +226,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -384,7 +389,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -465,7 +473,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = nexinets::NexinetsRefundRequest::try_from(req)?; let nexinets_req = types::RequestBody::log_and_get_request_body( @@ -550,7 +561,9 @@ impl ConnectorIntegration api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.nmi.base_url.as_ref() } @@ -139,6 +143,7 @@ impl fn get_request_body( &self, req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -159,7 +164,9 @@ impl .method(services::Method::Post) .url(&types::SetupMandateType::get_url(self, req, connectors)?) .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body(self, req)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -209,8 +216,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = nmi::NmiPaymentsRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -233,7 +247,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiSyncRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -303,7 +320,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiCaptureRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = nmi::NmiCaptureRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -372,7 +398,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiCancelRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -442,7 +471,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_req = nmi::NmiRefundRequest::try_from(req)?; + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_req = nmi::NmiRefundRequest::try_from(&connector_router_data)?; let nmi_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::url_encode, @@ -512,7 +550,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = nmi::NmiSyncRequest::try_from(req)?; let nmi_req = types::RequestBody::log_and_get_request_body( @@ -580,7 +621,9 @@ impl ConnectorIntegration for NmiAuthType { } } +#[derive(Debug, Serialize)] +pub struct NmiRouterData { + pub amount: f64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for NmiRouterData +{ + type Error = Report; + + fn try_from( + (_currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount: utils::to_currency_base_unit_asf64(amount, currency)?, + router_data, + }) + } +} + #[derive(Debug, Serialize)] pub struct NmiPaymentsRequest { #[serde(rename = "type")] @@ -77,25 +108,27 @@ pub struct ApplePayData { applepay_payment_data: Secret, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for NmiPaymentsRequest { +impl TryFrom<&NmiRouterData<&types::PaymentsAuthorizeRouterData>> for NmiPaymentsRequest { type Error = Error; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let transaction_type = match item.request.is_auto_capture()? { + fn try_from( + item: &NmiRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let transaction_type = match item.router_data.request.is_auto_capture()? { true => TransactionType::Sale, false => TransactionType::Auth, }; - let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; - let amount = - utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; - let payment_method = PaymentMethod::try_from(&item.request.payment_method_data)?; + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; + let amount = item.amount; + let payment_method = + PaymentMethod::try_from(&item.router_data.request.payment_method_data)?; Ok(Self { transaction_type, security_key: auth_type.api_key, amount, - currency: item.request.currency, + currency: item.router_data.request.currency, payment_method, - orderid: item.connector_request_reference_id.clone(), + orderid: item.router_data.connector_request_reference_id.clone(), }) } } @@ -243,18 +276,17 @@ pub struct NmiCaptureRequest { pub amount: Option, } -impl TryFrom<&types::PaymentsCaptureRouterData> for NmiCaptureRequest { +impl TryFrom<&NmiRouterData<&types::PaymentsCaptureRouterData>> for NmiCaptureRequest { type Error = Error; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let auth = NmiAuthType::try_from(&item.connector_auth_type)?; + fn try_from( + item: &NmiRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let auth = NmiAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { transaction_type: TransactionType::Capture, security_key: auth.api_key, - transactionid: item.request.connector_transaction_id.clone(), - amount: Some(utils::to_currency_base_unit_asf64( - item.request.amount_to_capture, - item.request.currency, - )?), + transactionid: item.router_data.request.connector_transaction_id.clone(), + amount: Some(item.amount), }) } } @@ -408,6 +440,7 @@ impl ForeignFrom<(StandardResponse, u16)> for types::ErrorResponse { message: response.responsetext, reason: None, status_code: http_code, + attempt_status: None, } } } @@ -577,18 +610,15 @@ pub struct NmiRefundRequest { amount: f64, } -impl TryFrom<&types::RefundsRouterData> for NmiRefundRequest { +impl TryFrom<&NmiRouterData<&types::RefundsRouterData>> for NmiRefundRequest { type Error = Error; - fn try_from(item: &types::RefundsRouterData) -> Result { - let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; + fn try_from(item: &NmiRouterData<&types::RefundsRouterData>) -> Result { + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; Ok(Self { transaction_type: TransactionType::Refund, security_key: auth_type.api_key, - transactionid: item.request.connector_transaction_id.clone(), - amount: utils::to_currency_base_unit_asf64( - item.request.refund_amount, - item.request.currency, - )?, + transactionid: item.router_data.request.connector_transaction_id.clone(), + amount: item.amount, }) } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 156e10928d3e..866f8f4c58fa 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Noon { code: response.result_code.to_string(), message: response.class_description, reason: Some(response.message), + attempt_status: None, }) } } @@ -201,6 +202,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -226,7 +228,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -370,7 +375,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = noon::NoonPaymentsCancelRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -445,7 +453,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; let noon_req = types::RequestBody::log_and_get_request_body( @@ -520,7 +531,9 @@ impl ConnectorIntegration message: error_message.clone(), reason: Some(error_message), status_code: item.http_code, + attempt_status: None, }), _ => { let connector_response_reference_id = diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 754ba5ff7443..15702829d378 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -149,6 +149,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; @@ -176,7 +177,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -235,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -255,7 +257,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -339,7 +344,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -421,7 +429,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; let req = types::RequestBody::log_and_get_request_body( @@ -581,7 +592,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -670,7 +684,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -727,6 +741,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; let req = types::RequestBody::log_and_get_request_body( @@ -749,7 +764,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -828,7 +846,9 @@ impl ConnectorIntegration( .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: None, status_code: http_code, + attempt_status: None, }) } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index 89e16416d27f..cc517ca1f3b8 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -107,6 +107,7 @@ impl ConnectorCommon for Opayo { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } @@ -172,6 +173,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = opayo::OpayoPaymentsRequest::try_from(req)?; let opayo_req = types::RequestBody::log_and_get_request_body( @@ -197,7 +199,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -331,7 +336,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = opayo::OpayoRefundRequest::try_from(req)?; @@ -412,7 +420,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = opennode::OpennodeRouterData::try_from(( &self.get_currency_unit(), @@ -202,7 +204,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { let auth = payeezy::PayeezyAuthType::try_from(&req.connector_auth_type)?; - let option_request_payload = self.get_request_body(req)?; + let option_request_payload = self.get_request_body(req, connectors)?; let request_payload = option_request_payload.map_or("{}".to_string(), |payload| { types::RequestBody::get_inner_value(payload).expose() }); @@ -90,6 +90,10 @@ impl ConnectorCommon for Payeezy { "payeezy" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -119,6 +123,7 @@ impl ConnectorCommon for Payeezy { code: response.transaction_status, message: error_messages.join(", "), reason: None, + attempt_status: None, }) } } @@ -195,6 +200,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; let payeezy_req = types::RequestBody::log_and_get_request_body( @@ -215,7 +221,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let req_obj = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, + &req_obj, utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_req)) } @@ -313,7 +329,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyPaymentsRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; + let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, + &req_obj, utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; @@ -403,7 +429,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_req = payeezy::PayeezyRefundRequest::try_from(req)?; + let router_obj = payeezy::PayeezyRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, + &req_obj, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(payeezy_req)) @@ -489,7 +524,9 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { + // Parse the response into a payeezy::RefundResponse let response: payeezy::RefundResponse = res .response .parse_struct("payeezy RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RefundsRouterData::try_from(types::ResponseRouterData { + + // Create a new instance of types::RefundsRouterData based on the response, input data, and HTTP code + let response_data = types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + }; + let router_data = types::RefundsRouterData::try_from(response_data) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + + Ok(router_data) } fn get_error_response( diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index efcd1b36d5bb..3a859b325300 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -9,6 +9,37 @@ use crate::{ core::errors, types::{self, api, storage::enums, transformers::ForeignFrom}, }; +#[derive(Debug, Serialize)] +pub struct PayeezyRouterData { + pub amount: String, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PayeezyRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (currency_unit, currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data, + }) + } +} #[derive(Serialize, Debug)] pub struct PayeezyCard { @@ -66,7 +97,7 @@ pub struct PayeezyPaymentsRequest { pub merchant_ref: String, pub transaction_type: PayeezyTransactionType, pub method: PayeezyPaymentMethodType, - pub amount: i64, + pub amount: String, pub currency_code: String, pub credit_card: PayeezyPaymentMethod, pub stored_credentials: Option, @@ -95,10 +126,12 @@ pub enum Initiator { CardHolder, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayeezyPaymentsRequest { +impl TryFrom<&PayeezyRouterData<&types::PaymentsAuthorizeRouterData>> for PayeezyPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.payment_method { + fn try_from( + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.payment_method { diesel_models::enums::PaymentMethod::Card => get_card_specific_payment_data(item), diesel_models::enums::PaymentMethod::CardRedirect @@ -119,14 +152,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayeezyPaymentsRequest { } fn get_card_specific_payment_data( - item: &types::PaymentsAuthorizeRouterData, + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - let merchant_ref = item.attempt_id.to_string(); + let merchant_ref = item.router_data.attempt_id.to_string(); let method = PayeezyPaymentMethodType::CreditCard; - let amount = item.request.amount; - let currency_code = item.request.currency.to_string(); + let amount = item.amount.clone(); + let currency_code = item.router_data.request.currency.to_string(); let credit_card = get_payment_method_data(item)?; - let (transaction_type, stored_credentials) = get_transaction_type_and_stored_creds(item)?; + let (transaction_type, stored_credentials) = + get_transaction_type_and_stored_creds(item.router_data)?; Ok(PayeezyPaymentsRequest { merchant_ref, transaction_type, @@ -135,7 +169,7 @@ fn get_card_specific_payment_data( currency_code, credit_card, stored_credentials, - reference: item.connector_request_reference_id.clone(), + reference: item.router_data.connector_request_reference_id.clone(), }) } fn get_transaction_type_and_stored_creds( @@ -201,9 +235,9 @@ fn is_mandate_payment( } fn get_payment_method_data( - item: &types::PaymentsAuthorizeRouterData, + item: &PayeezyRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result> { - match item.request.payment_method_data { + match item.router_data.request.payment_method_data { api::PaymentMethodData::Card(ref card) => { let card_type = PayeezyCardType::try_from(card.get_card_issuer()?)?; let payeezy_card = PayeezyCard { @@ -305,16 +339,20 @@ pub struct PayeezyCaptureOrVoidRequest { currency_code: String, } -impl TryFrom<&types::PaymentsCaptureRouterData> for PayeezyCaptureOrVoidRequest { +impl TryFrom<&PayeezyRouterData<&types::PaymentsCaptureRouterData>> + for PayeezyCaptureOrVoidRequest +{ type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + fn try_from( + item: &PayeezyRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { let metadata: PayeezyPaymentsMetadata = - utils::to_connector_meta(item.request.connector_meta.clone()) + utils::to_connector_meta(item.router_data.request.connector_meta.clone()) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { transaction_type: PayeezyTransactionType::Capture, - amount: item.request.amount_to_capture.to_string(), - currency_code: item.request.currency.to_string(), + amount: item.amount.clone(), + currency_code: item.router_data.request.currency.to_string(), transaction_tag: metadata.transaction_tag, }) } @@ -338,6 +376,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for PayeezyCaptureOrVoidRequest { }) } } + #[derive(Debug, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] pub enum PayeezyTransactionType { @@ -442,16 +481,18 @@ pub struct PayeezyRefundRequest { currency_code: String, } -impl TryFrom<&types::RefundsRouterData> for PayeezyRefundRequest { +impl TryFrom<&PayeezyRouterData<&types::RefundsRouterData>> for PayeezyRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { + fn try_from( + item: &PayeezyRouterData<&types::RefundsRouterData>, + ) -> Result { let metadata: PayeezyPaymentsMetadata = - utils::to_connector_meta(item.request.connector_metadata.clone()) + utils::to_connector_meta(item.router_data.request.connector_metadata.clone()) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { transaction_type: PayeezyTransactionType::Refund, - amount: item.request.refund_amount.to_string(), - currency_code: item.request.currency.to_string(), + amount: item.amount.clone(), + currency_code: item.router_data.request.currency.to_string(), transaction_tag: metadata.transaction_tag, }) } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index e0d6229c004c..ef10c6d00878 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -97,6 +97,7 @@ impl ConnectorCommon for Payme { "{}, additional info: {}", response.status_error_details, response.status_additional_info )), + attempt_status: None, }) } } @@ -149,6 +150,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::CaptureBuyerRequest::try_from(req)?; @@ -172,7 +174,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), ), AuthenticationType::NoThreeDs => None, @@ -252,6 +256,7 @@ impl fn get_request_body( &self, req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let amount = req.request.get_amount()?; let currency = req.request.get_currency()?; @@ -282,7 +287,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), ); @@ -377,6 +382,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::Pay3dsRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -402,7 +408,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -479,6 +485,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -510,7 +517,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::PaymeQuerySaleRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -594,7 +604,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -687,7 +700,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), @@ -792,7 +808,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = payme::PaymeQueryTransactionRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( @@ -873,7 +892,9 @@ impl ConnectorIntegration for types::ErrorResponse { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: pay_sale_response.status_error_details.to_owned(), status_code: http_code, + attempt_status: None, } } } @@ -308,6 +309,7 @@ impl From<(&SaleQuery, u16)> for types::ErrorResponse { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: sale_query_response.sale_error_text.clone(), status_code: http_code, + attempt_status: None, } } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 854d48dcaadc..d4ab481eb9de 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -91,6 +91,7 @@ impl Paypal { code: response.name, message: response.message.clone(), reason: error_reason.or(Some(response.message)), + attempt_status: None, }) } } @@ -203,6 +204,7 @@ impl ConnectorCommon for Paypal { code: response.name, message: response.message.clone(), reason, + attempt_status: None, }) } } @@ -281,6 +283,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = paypal::PaypalAuthUpdateRequest::try_from(req)?; let paypal_req = types::RequestBody::log_and_get_request_body( @@ -302,7 +305,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -412,7 +419,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -714,7 +724,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), @@ -874,7 +887,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; let paypal_req = types::RequestBody::log_and_get_request_body( diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 6433735f7c53..9a8d4734f837 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -96,6 +96,7 @@ impl ConnectorCommon for Payu { code: response.status.status_code, message: response.status.status_desc, reason: response.status.code_literal, + attempt_status: None, }) } } @@ -243,6 +244,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = payu::PayuAuthUpdateRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -265,7 +267,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payu::PayuPaymentsCaptureRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -439,7 +445,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = payu::PayuPaymentsRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -539,7 +548,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = payu::PayuRefundRequest::try_from(req)?; let payu_req = types::RequestBody::log_and_get_request_body( @@ -626,7 +638,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzPaymentsRequest::try_from(req)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -218,7 +220,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let redirect_payload: powertranz::RedirectResponsePayload = req .request @@ -308,7 +313,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -370,6 +375,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -393,7 +399,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -481,7 +490,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = powertranz::PowertranzBaseRequest::try_from(req)?; let powertranz_req = types::RequestBody::log_and_get_request_body( @@ -542,7 +554,9 @@ impl ConnectorIntegration>() .join(", "), ), + attempt_status: None, } }) } else if !ISO_SUCCESS_CODES.contains(&item.iso_response_code.as_str()) { @@ -452,6 +453,7 @@ fn build_error_response( code: item.iso_response_code.clone(), message: item.response_message.clone(), reason: Some(item.response_message.clone()), + attempt_status: None, }) } else { None diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 0e8d5100ea35..417c34207e05 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Prophetpay { code: response.code, message: response.message, reason: response.reason, + attempt_status: None, }) } } @@ -164,6 +165,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), @@ -195,7 +197,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -329,7 +334,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), @@ -417,7 +425,9 @@ impl ConnectorIntegration { logger::error!(deserialization_error =? error_msg); @@ -182,6 +183,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -211,7 +213,7 @@ impl let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?; - let body = types::PaymentsAuthorizeType::get_request_body(self, req)? + let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = @@ -232,7 +234,9 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -492,6 +496,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -521,7 +526,7 @@ impl "/v1/payments/{}/capture", req.request.connector_transaction_id ); - let body = types::PaymentsCaptureType::get_request_body(self, req)? + let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)? .ok_or(errors::ConnectorError::RequestEncodingFailed)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = @@ -540,7 +545,9 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -630,6 +637,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), @@ -655,7 +663,7 @@ impl services::ConnectorIntegration status_code: item.http_code, message: item.response.status.status.unwrap_or_default(), reason: data.failure_message.to_owned(), + attempt_status: None, }), ), _ => { @@ -497,6 +498,7 @@ impl status_code: item.http_code, message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, + attempt_status: None, }), ), }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index a17546711f14..98eb895db548 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -99,6 +99,7 @@ impl ConnectorCommon for Shift4 { .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), message: response.error.message, reason: None, + attempt_status: None, }) } } @@ -185,6 +186,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -248,7 +250,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -495,7 +500,9 @@ impl .content_type(request::ContentType::FormUrlEncoded) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body(self, req)?) + .body(types::PaymentsInitType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -555,6 +562,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; let req = types::RequestBody::log_and_get_request_body( @@ -580,7 +588,7 @@ impl self, req, connectors, )?) .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -635,6 +643,7 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = shift4::Shift4RefundRequest::try_from(req)?; let shift4_req = types::RequestBody::log_and_get_request_body( @@ -657,7 +666,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = square::SquareTokenRequest::try_from(req)?; @@ -267,7 +269,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -412,6 +416,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = square::SquarePaymentsRequest::try_from(req)?; @@ -438,7 +443,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = square::SquareRefundRequest::try_from(req)?; let square_req = types::RequestBody::log_and_get_request_body( @@ -724,7 +732,9 @@ impl ConnectorIntegration api::CurrencyUnit { + api::CurrencyUnit::Base + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -105,6 +109,7 @@ impl ConnectorCommon for Stax { .change_context(errors::ConnectorError::ResponseDeserializationFailed)? .to_owned(), ), + attempt_status: None, }) } } @@ -156,6 +161,7 @@ impl fn get_request_body( &self, req: &types::ConnectorCustomerRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stax::StaxCustomerRequest::try_from(req)?; @@ -182,7 +188,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -245,6 +253,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stax::StaxTokenRequest::try_from(req)?; @@ -267,7 +276,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -346,8 +357,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = stax::StaxPaymentsRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &req_obj, @@ -372,7 +390,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_req = stax::StaxCaptureRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = stax::StaxCaptureRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &connector_req, utils::Encode::::encode_to_string_of_json, @@ -525,7 +552,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let req_obj = stax::StaxRefundRequest::try_from(req)?; + let connector_router_data = stax::StaxRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = stax::StaxRefundRequest::try_from(&connector_router_data)?; let stax_req = types::RequestBody::log_and_get_request_body( &req_obj, utils::Encode::::encode_to_string_of_json, @@ -678,7 +714,9 @@ impl ConnectorIntegration { + pub amount: f64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for StaxRouterData +{ + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_f64(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} + #[derive(Debug, Serialize)] pub struct StaxPaymentsRequestMetaData { tax: i64, @@ -26,21 +57,23 @@ pub struct StaxPaymentsRequest { idempotency_id: Option, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { +impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - if item.request.currency != enums::Currency::USD { + fn try_from( + item: &StaxRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + if item.router_data.request.currency != enums::Currency::USD { Err(errors::ConnectorError::NotSupported { - message: item.request.currency.to_string(), + message: item.router_data.request.currency.to_string(), connector: "Stax", })? } - let total = utils::to_currency_base_unit_asf64(item.request.amount, item.request.currency)?; + let total = item.amount; - match item.request.payment_method_data.clone() { + match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(_) => { - let pm_token = item.get_payment_method_token()?; - let pre_auth = !item.request.is_auto_capture()?; + let pm_token = item.router_data.get_payment_method_token()?; + let pre_auth = !item.router_data.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, @@ -52,14 +85,14 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { Err(errors::ConnectorError::InvalidWalletToken)? } }), - idempotency_id: Some(item.connector_request_reference_id.clone()), + idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) } api::PaymentMethodData::BankDebit( api_models::payments::BankDebitData::AchBankDebit { .. }, ) => { - let pm_token = item.get_payment_method_token()?; - let pre_auth = !item.request.is_auto_capture()?; + let pm_token = item.router_data.get_payment_method_token()?; + let pre_auth = !item.router_data.request.is_auto_capture()?; Ok(Self { meta: StaxPaymentsRequestMetaData { tax: 0 }, total, @@ -71,7 +104,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { Err(errors::ConnectorError::InvalidWalletToken)? } }), - idempotency_id: Some(item.connector_request_reference_id.clone()), + idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) } api::PaymentMethodData::BankDebit(_) @@ -347,13 +380,12 @@ pub struct StaxCaptureRequest { total: Option, } -impl TryFrom<&types::PaymentsCaptureRouterData> for StaxCaptureRequest { +impl TryFrom<&StaxRouterData<&types::PaymentsCaptureRouterData>> for StaxCaptureRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - let total = utils::to_currency_base_unit_asf64( - item.request.amount_to_capture, - item.request.currency, - )?; + fn try_from( + item: &StaxRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let total = item.amount; Ok(Self { total: Some(total) }) } } @@ -365,15 +397,10 @@ pub struct StaxRefundRequest { pub total: f64, } -impl TryFrom<&types::RefundsRouterData> for StaxRefundRequest { +impl TryFrom<&StaxRouterData<&types::RefundsRouterData>> for StaxRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - Ok(Self { - total: utils::to_currency_base_unit_asf64( - item.request.refund_amount, - item.request.currency, - )?, - }) + fn try_from(item: &StaxRouterData<&types::RefundsRouterData>) -> Result { + Ok(Self { total: item.amount }) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index e3551306e673..3f1263657e83 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -144,6 +144,7 @@ impl fn get_request_body( &self, req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; let pre_processing_request = types::RequestBody::log_and_get_request_body( @@ -171,7 +172,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), )) @@ -225,6 +226,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -269,6 +271,7 @@ impl fn get_request_body( &self, req: &types::ConnectorCustomerRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CustomerRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -294,7 +297,9 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body(self, req)?) + .body(types::ConnectorCustomerType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -351,6 +356,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -395,6 +401,7 @@ impl fn get_request_body( &self, req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::TokenRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -416,7 +423,9 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body(self, req)?) + .body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -473,6 +482,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -523,6 +533,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CaptureRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -546,7 +557,9 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -603,6 +616,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -667,7 +681,9 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body(self, req)?) + .body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -743,6 +759,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -805,6 +822,7 @@ impl fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { match &req.request.payment_method_data { api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { @@ -837,7 +855,9 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -897,6 +917,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -944,6 +965,7 @@ impl fn get_request_body( &self, req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::CancelRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -964,7 +986,9 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body(self, req)?) + .body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -1016,6 +1040,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -1077,6 +1102,7 @@ impl types::SetupMandateRequestData, types::PaymentsResponseData, >, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req = stripe::SetupIntentRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -1102,7 +1128,7 @@ impl .url(&Verify::get_url(self, req, connectors)?) .attach_default_headers() .headers(Verify::get_headers(self, req, connectors)?) - .body(Verify::get_request_body(self, req)?) + .body(Verify::get_request_body(self, req, connectors)?) .build(), )) } @@ -1170,6 +1196,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } @@ -1212,6 +1239,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_request = stripe::RefundRequest::try_from(req)?; let stripe_req = types::RequestBody::log_and_get_request_body( @@ -1234,7 +1262,9 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { let stripe_req = stripe::Evidence::try_from(req)?; let stripe_req_string = types::RequestBody::log_and_get_request_body( @@ -1707,7 +1744,9 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body(self, req)?) + .body(types::SubmitEvidenceType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) } @@ -1761,6 +1800,7 @@ impl }) .unwrap_or(message) }), + attempt_status: None, }) } } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index fa9d7d617041..a783fd23fe19 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2475,6 +2475,7 @@ impl }) .or(Some(error.message.clone())), status_code: item.http_code, + attempt_status: None, }); let connector_metadata = diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 912f1575e1e0..7509131afeef 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -138,6 +138,7 @@ impl ConnectorCommon for Trustpay { .map(|error_code_message| error_code_message.error_code) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: reason.or(response_data.description), + attempt_status: None, }) } Err(error_msg) => { @@ -235,6 +236,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = trustpay::TrustpayAuthUpdateRequest::try_from(req)?; let trustpay_req = types::RequestBody::log_and_get_request_body( @@ -256,7 +258,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let currency = req.request.get_currency()?; let amount = req @@ -473,7 +480,7 @@ impl self, req, connectors, )?) .body(types::PaymentsPreProcessingType::get_request_body( - self, req, + self, req, connectors, )?) .build(), ); @@ -550,6 +557,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let amount = req .request @@ -596,7 +604,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), @@ -703,7 +714,9 @@ impl ConnectorIntegration TryFrom CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysPaymentsRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -168,7 +169,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysSyncRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -247,7 +251,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_req = tsys::TsysPaymentsCaptureRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -328,7 +335,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysPaymentsCancelRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -404,7 +414,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysRefundRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -482,7 +495,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = tsys::TsysSyncRequest::try_from(req)?; let tsys_req = types::RequestBody::log_and_get_request_body( @@ -559,7 +575,9 @@ impl ConnectorIntegration Option>; } +pub trait PaymentMethodTokenizationRequestData { + fn get_browser_info(&self) -> Result; +} + +impl PaymentMethodTokenizationRequestData for types::PaymentMethodTokenizationData { + fn get_browser_info(&self) -> Result { + self.browser_info + .clone() + .ok_or_else(missing_field_err("browser_info")) + } +} + impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn is_auto_capture(&self) -> Result { match self.capture_method { @@ -1077,7 +1089,7 @@ pub fn get_amount_as_string( currency: diesel_models::enums::Currency, ) -> Result> { let amount = match currency_unit { - types::api::CurrencyUnit::Minor => to_currency_lower_unit(amount.to_string(), currency)?, + types::api::CurrencyUnit::Minor => amount.to_string(), types::api::CurrencyUnit::Base => to_currency_base_unit(amount, currency)?, }; Ok(amount) diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index e1af4c008b12..3697b8c8923f 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -129,6 +129,7 @@ impl ConnectorCommon for Volt { code: response.exception.message.to_string(), message: response.exception.message.clone(), reason: Some(reason), + attempt_status: None, }) } } @@ -173,6 +174,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let req_obj = volt::VoltAuthUpdateRequest::try_from(req)?; let volt_req = types::RequestBody::log_and_get_request_body( @@ -195,7 +197,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), @@ -292,7 +297,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -434,7 +442,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), @@ -524,7 +535,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -342,7 +347,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -419,7 +427,9 @@ impl .headers(types::PayoutRecipientType::get_headers( self, req, connectors, )?) - .body(types::PayoutRecipientType::get_request_body(self, req)?) + .body(types::PayoutRecipientType::get_request_body( + self, req, connectors, + )?) .build(); Ok(Some(request)) @@ -512,6 +522,7 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutCreateRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -532,7 +543,9 @@ impl services::ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = wise::WisePayoutFulfillRequest::try_from(req)?; let wise_req = types::RequestBody::log_and_get_request_body( @@ -630,7 +644,9 @@ impl services::ConnectorIntegration, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = worldline::ApproveRequest::try_from(req)?; @@ -406,7 +407,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = worldline::WorldlineRouterData::try_from(( &self.get_currency_unit(), @@ -524,7 +528,9 @@ impl ConnectorIntegration, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = worldline::WorldlineRefundRequest::try_from(req)?; let refund_req = types::RequestBody::log_and_get_request_body( @@ -610,7 +617,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = worldpay::WorldpayRouterData::try_from(( &self.get_currency_unit(), @@ -462,7 +466,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_request = WorldpayRefundRequest::try_from(req)?; let fiserv_refund_request = types::RequestBody::log_and_get_request_body( @@ -549,7 +556,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), @@ -246,7 +248,9 @@ impl ConnectorIntegration, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), @@ -438,7 +443,9 @@ impl ConnectorIntegration { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -342,12 +341,8 @@ impl api_models::payments::BankTransferData::Pse { .. } => { ZenPaymentChannels::PclBoacompraPse } - api_models::payments::BankTransferData::SepaBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::SepaBankTransfer { .. } + | api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::PermataBankTransfer { .. } | api_models::payments::BankTransferData::BcaBankTransfer { .. } @@ -356,10 +351,9 @@ impl | api_models::payments::BankTransferData::CimbVaBankTransfer { .. } | api_models::payments::BankTransferData::DanamonVaBankTransfer { .. } | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -489,12 +483,8 @@ impl api_models::payments::WalletData::WeChatPayRedirect(_) | api_models::payments::WalletData::PaypalRedirect(_) | api_models::payments::WalletData::ApplePay(_) - | api_models::payments::WalletData::GooglePay(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) | api_models::payments::WalletData::MomoRedirect(_) @@ -514,10 +504,9 @@ impl | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) | api_models::payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; let terminal_uuid = session_data @@ -719,10 +708,9 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } } } @@ -736,13 +724,8 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::Sofort { .. } | api_models::payments::BankRedirectData::BancontactCard { .. } | api_models::payments::BankRedirectData::Blik { .. } - | api_models::payments::BankRedirectData::Trustly { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ) - .into()) - } - api_models::payments::BankRedirectData::Eps { .. } + | api_models::payments::BankRedirectData::Trustly { .. } + | api_models::payments::BankRedirectData::Eps { .. } | api_models::payments::BankRedirectData::Giropay { .. } | api_models::payments::BankRedirectData::Przelewy24 { .. } | api_models::payments::BankRedirectData::Bizum {} @@ -754,10 +737,9 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::OpenBankingUk { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -776,10 +758,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for ZenPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -794,10 +775,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for ZenPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -811,10 +791,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -825,19 +804,13 @@ impl TryFrom<&api_models::payments::GiftCardData> for ZenPaymentsRequest { type Error = error_stack::Report; fn try_from(value: &api_models::payments::GiftCardData) -> Result { match value { - api_models::payments::GiftCardData::PaySafeCard {} => { + api_models::payments::GiftCardData::PaySafeCard {} + | api_models::payments::GiftCardData::Givex(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) .into()) } - api_models::payments::GiftCardData::Givex(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } - .into()) - } } } } diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 02db8b1754ed..7b20c3865d15 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -13,6 +13,7 @@ pub(crate) const ALPHABETS: [char; 62] = [ pub const REQUEST_TIME_OUT: u64 = 30; pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; +pub const REQUEST_TIMEOUT_PAYMENT_NOT_FOUND: &str = "Timed out ,payment not found"; pub const REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC: &str = "This Payment has been moved to failed as there is no response from the connector"; @@ -46,3 +47,8 @@ pub(crate) const QR_IMAGE_DATA_SOURCE_STRING: &str = "data:image/png;base64"; pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32"; pub(crate) const METRICS_HOST_TAG_NAME: &str = "host"; +pub const MAX_ROUTING_CONFIGS_PER_MERCHANT: usize = 100; +pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; + +pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; +pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a3bb3c78915c..817fafdae520 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -8,6 +8,7 @@ pub mod customers; pub mod disputes; pub mod errors; pub mod files; +pub mod gsm; pub mod mandate; pub mod metrics; pub mod payment_link; @@ -16,6 +17,7 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod routing; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index cd1243dd3688..e1e5ea744e2f 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1,6 +1,8 @@ +use std::str::FromStr; + use api_models::{ admin::{self as admin_types}, - enums as api_enums, + enums as api_enums, routing as routing_types, }; use common_utils::{ crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue}, @@ -18,6 +20,7 @@ use crate::{ core::{ errors::{self, RouterResponse, RouterResult, StorageErrorExt}, payments::helpers, + routing::helpers as routing_helpers, utils as core_utils, }, db::StorageInterface, @@ -89,7 +92,7 @@ pub async fn create_merchant_account( .transpose()?; if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -178,7 +181,10 @@ pub async fn create_merchant_account( .await?, return_url: req.return_url.map(|a| a.to_string()), webhook_details, - routing_algorithm: req.routing_algorithm, + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), sub_merchants_enabled: req.sub_merchants_enabled, parent_merchant_id, enable_payment_response_hash, @@ -470,7 +476,7 @@ pub async fn merchant_account_update( } if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -756,6 +762,9 @@ pub async fn create_payment_connector( ) .await?; + let routable_connector = + api_enums::RoutableConnectors::from_str(&req.connector_name.to_string()).ok(); + let business_profile = state .store .find_business_profile_by_profile_id(&profile_id) @@ -828,6 +837,37 @@ pub async fn create_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + // The purpose of this merchant account update is just to update the + // merchant account `modified_at` field for KGraph cache invalidation + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + locker_id: None, + payment_response_hash_key: None, + primary_business_details: None, + metadata: None, + publishable_key: None, + redirect_to_merchant_with_http_post: None, + routing_algorithm: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + state + .store + .update_specific_fields_in_merchant(merchant_id, merchant_account_update, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error updating the merchant account when creating payment connector")?; + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -852,7 +892,7 @@ pub async fn create_payment_connector( connector_label: Some(connector_label), business_country: req.business_country, business_label: req.business_label.clone(), - business_sub_label: req.business_sub_label, + business_sub_label: req.business_sub_label.clone(), created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), id: None, @@ -873,6 +913,9 @@ pub async fn create_payment_connector( pm_auth_config: req.pm_auth_config.clone(), }; + let mut default_routing_config = + routing_helpers::get_merchant_default_config(&*state.store, merchant_id).await?; + let mca = state .store .insert_merchant_connector_account(merchant_connector_account, &key_store) @@ -884,6 +927,28 @@ pub async fn create_payment_connector( }, )?; + if let Some(routable_connector_val) = routable_connector { + let choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: routable_connector_val, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Some(mca.merchant_connector_id.clone()), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: req.business_sub_label, + }; + + if !default_routing_config.contains(&choice) { + default_routing_config.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + merchant_id, + default_routing_config, + ) + .await?; + } + } + metrics::MCA_CREATE.add( &metrics::CONTEXT, 1, @@ -1020,8 +1085,8 @@ pub async fn update_payment_connector( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, - test_mode: mca.test_mode, - disabled: mca.disabled, + test_mode: req.test_mode, + disabled: req.disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, @@ -1248,7 +1313,7 @@ pub async fn create_business_profile( .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; if let Some(ref routing_algorithm) = request.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -1360,7 +1425,7 @@ pub async fn update_business_profile( .transpose()?; if let Some(ref routing_algorithm) = request.routing_algorithm { - let _: api::RoutingAlgorithm = routing_algorithm + let _: api_models::routing::RoutingAlgorithm = routing_algorithm .clone() .parse_value("RoutingAlgorithm") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -1434,6 +1499,10 @@ pub(crate) fn validate_auth_and_metadata_type( authorizedotnet::transformers::AuthorizedotnetAuthType::try_from(val)?; Ok(()) } + // api_enums::Connector::Bankofamerica => { + // bankofamerica::transformers::BankofamericaAuthType::try_from(val)?; + // Ok(()) + // } Added as template code for future usage api_enums::Connector::Bitpay => { bitpay::transformers::BitpayAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 7bda894826a1..c1ddc43cd65d 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -294,10 +294,10 @@ pub async fn retrieve_api_key( #[instrument(skip_all)] pub async fn update_api_key( state: AppState, - merchant_id: &str, - key_id: &str, api_key: api::UpdateApiKeyRequest, ) -> RouterResponse { + let merchant_id = api_key.merchant_id.clone(); + let key_id = api_key.key_id.clone(); let store = state.store.as_ref(); let api_key = store @@ -313,7 +313,7 @@ pub async fn update_api_key( { let expiry_reminder_days = state.conf.api_keys.expiry_reminder_days.clone(); - let task_id = generate_task_id_for_api_key_expiry_workflow(key_id); + let task_id = generate_task_id_for_api_key_expiry_workflow(&key_id); // In order to determine how to update the existing process in the process_tracker table, // we need access to the current entry in the table. let existing_process_tracker_task = store @@ -339,7 +339,7 @@ pub async fn update_api_key( // If an expiry is set to 'never' else { // Process exist in process, revoke it - revoke_api_key_expiry_task(store, key_id) + revoke_api_key_expiry_task(store, &key_id) .await .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/cache.rs b/crates/router/src/core/cache.rs index cba9a5ec303f..a8ca8395a670 100644 --- a/crates/router/src/core/cache.rs +++ b/crates/router/src/core/cache.rs @@ -10,7 +10,7 @@ pub async fn invalidate( key: &str, ) -> CustomResult, errors::ApiErrorResponse> { let store = state.store.as_ref(); - let result = publish_into_redact_channel(store, CacheKind::All(key.into())) + let result = publish_into_redact_channel(store, [CacheKind::All(key.into())]) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 1c062b7035af..dc1d56721e88 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -325,3 +325,49 @@ pub mod error_stack_parsing { } #[cfg(feature = "detailed_errors")] pub use error_stack_parsing::*; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum RoutingError { + #[error("Merchant routing algorithm not found in cache")] + CacheMiss, + #[error("Final connector selection failed")] + ConnectorSelectionFailed, + #[error("[DSL] Missing required field in payment data: '{field_name}'")] + DslMissingRequiredField { field_name: String }, + #[error("The lock on the DSL cache is most probably poisoned")] + DslCachePoisoned, + #[error("Expected DSL to be saved in DB but did not find")] + DslMissingInDb, + #[error("Unable to parse DSL from JSON")] + DslParsingError, + #[error("Failed to initialize DSL backend")] + DslBackendInitError, + #[error("Error updating merchant with latest dsl cache contents")] + DslMerchantUpdateError, + #[error("Error executing the DSL")] + DslExecutionError, + #[error("Final connector selection failed")] + DslFinalConnectorSelectionFailed, + #[error("[DSL] Received incorrect selection algorithm as DSL output")] + DslIncorrectSelectionAlgorithm, + #[error("there was an error saving/retrieving values from the kgraph cache")] + KgraphCacheFailure, + #[error("failed to refresh the kgraph cache")] + KgraphCacheRefreshFailed, + #[error("there was an error during the kgraph analysis phase")] + KgraphAnalysisError, + #[error("'profile_id' was not provided")] + ProfileIdMissing, + #[error("the profile was not found in the database")] + ProfileNotFound, + #[error("failed to fetch the fallback config for the merchant")] + FallbackConfigFetchFailed, + #[error("Invalid connector name received: '{0}'")] + InvalidConnectorName(String), + #[error("The routing algorithm in merchant account had invalid structure")] + InvalidRoutingAlgorithmStructure, + #[error("Volume split failed")] + VolumeSplitFailed, + #[error("Unable to parse metadata")] + MetadataParsingError, +} diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs new file mode 100644 index 000000000000..d25860674570 --- /dev/null +++ b/crates/router/src/core/gsm.rs @@ -0,0 +1,137 @@ +use api_models::gsm as gsm_api_types; +use diesel_models::gsm as storage; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use crate::{ + core::{ + errors, + errors::{RouterResponse, StorageErrorExt}, + }, + db::gsm::GsmInterface, + services, + types::{self, transformers::ForeignInto}, + AppState, +}; + +#[instrument(skip_all)] +pub async fn create_gsm_rule( + state: AppState, + gsm_rule: gsm_api_types::GsmCreateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + GsmInterface::add_gsm_rule(db, gsm_rule.foreign_into()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: "GSM with given key already exists in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn retrieve_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmRetrieveRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmRetrieveRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + GsmInterface::find_gsm_rule(db, connector.to_string(), flow, sub_flow, code, message) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn update_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmUpdateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmUpdateRequest { + connector, + flow, + sub_flow, + code, + message, + decision, + status, + router_error, + step_up_possible, + } = gsm_request; + GsmInterface::update_gsm_rule( + db, + connector.to_string(), + flow, + sub_flow, + code, + message, + storage::GatewayStatusMappingUpdate { + decision: decision.map(|d| d.to_string()), + status, + router_error: Some(router_error), + step_up_possible, + }, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while updating Gsm rule") + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn delete_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmDeleteRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmDeleteRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + match GsmInterface::delete_gsm_rule( + db, + connector.to_string(), + flow.to_owned(), + sub_flow.to_owned(), + code.to_owned(), + message.to_owned(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while Deleting Gsm rule") + { + Ok(is_deleted) => { + if is_deleted { + Ok(services::ApplicationResponse::Json( + gsm_api_types::GsmDeleteResponse { + gsm_rule_delete: true, + connector, + flow, + sub_flow, + code, + }, + )) + } else { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed while Deleting Gsm rule, got response as false") + } + } + Err(err) => Err(err), + } +} diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2c51fa0c3cbb..2ea6a4d7f219 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,6 +1,12 @@ use api_models::admin as admin_types; -use common_utils::ext_traits::AsyncExt; +use common_utils::{ + consts::{ + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + }, + ext_traits::ValueExt, +}; use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -43,6 +49,11 @@ pub async fn intiate_payment_link_flow( .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_link_id = payment_intent + .payment_link_id + .get_required_value("payment_link_id") + .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; + helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ @@ -55,20 +66,10 @@ pub async fn intiate_payment_link_flow( "create payment link", )?; - let fulfillment_time = payment_intent - .payment_link_id - .as_ref() - .async_and_then(|pli| async move { - db.find_payment_link_by_payment_link_id(pli) - .await - .ok()? - .fulfilment_time - .ok_or(errors::ApiErrorResponse::PaymentNotFound) - .ok() - }) + let payment_link = db + .find_payment_link_by_payment_link_id(&payment_link_id) .await - .get_required_value("fulfillment_time") - .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; let payment_link_config = merchant_account .payment_link_config @@ -81,12 +82,7 @@ pub async fn intiate_payment_link_flow( }) .transpose()?; - let order_details = payment_intent - .order_details - .get_required_value("order_details") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "order_details", - })?; + let order_details = validate_order_details(payment_intent.order_details)?; let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { payment_create_return_url @@ -104,25 +100,45 @@ pub async fn intiate_payment_link_flow( payment_intent.client_secret, )?; + let (default_sdk_theme, default_background_color) = + (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + let payment_details = api_models::payments::PaymentLinkDetails { amount: payment_intent.amount, currency, payment_id: payment_intent.payment_id, - merchant_name: merchant_account.merchant_name, + merchant_name: payment_link.custom_merchant_name.unwrap_or( + merchant_account + .merchant_name + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(), + ), order_details, return_url, - expiry: fulfillment_time, + expiry: payment_link.fulfilment_time, pub_key, client_secret, merchant_logo: payment_link_config .clone() - .map(|pl_metadata| pl_metadata.merchant_logo.unwrap_or_default()) + .map(|pl_config| { + pl_config + .merchant_logo + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) + }) .unwrap_or_default(), max_items_visible_after_collapse: 3, + sdk_theme: payment_link_config.clone().and_then(|pl_config| { + pl_config + .color_scheme + .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) + }), }; let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css(payment_link_config.clone()); + let css_script = get_color_scheme_css( + payment_link_config.clone(), + default_background_color.to_string(), + ); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), @@ -149,38 +165,21 @@ fn get_js_script( fn get_color_scheme_css( payment_link_config: Option, + default_primary_color: String, ) -> String { - let (default_primary_color, default_accent_color, default_secondary_color) = ( - "#C6C7C8".to_string(), - "#6A8EF5".to_string(), - "#0C48F6".to_string(), - ); - - let (primary_color, primary_accent_color, secondary_color) = payment_link_config + let background_primary_color = payment_link_config .and_then(|pl_config| { pl_config.color_scheme.map(|color| { - ( - color.primary_color.unwrap_or(default_primary_color.clone()), - color - .primary_accent_color - .unwrap_or(default_accent_color.clone()), - color - .secondary_color - .unwrap_or(default_secondary_color.clone()), - ) + color + .background_primary_color + .unwrap_or(default_primary_color.clone()) }) }) - .unwrap_or(( - default_primary_color, - default_accent_color, - default_secondary_color, - )); + .unwrap_or(default_primary_color); format!( ":root {{ - --primary-color: {primary_color}; - --primary-accent-color: {primary_accent_color}; - --secondary-color: {secondary_color}; + --primary-color: {background_primary_color}; }}" ) } @@ -203,3 +202,36 @@ fn validate_sdk_requirements( })?; Ok((pub_key, currency, client_secret)) } + +fn validate_order_details( + order_details: Option>>, +) -> Result< + Option>, + error_stack::Report, +> { + let order_details = order_details + .map(|order_details| { + order_details + .iter() + .map(|data| { + data.to_owned() + .parse_value("OrderDetailsWithAmount") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "OrderDetailsWithAmount", + }) + .attach_printable("Unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + }) + .transpose()?; + + let updated_order_details = order_details.map(|mut order_details| { + for order in order_details.iter_mut() { + if order.product_img_link.is_none() { + order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + } + } + order_details + }); + Ok(updated_order_details) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 462a11d2567e..67410cac8418 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -112,8 +112,8 @@ } #hyper-checkout-merchant-image > img { - height: 48px; - width: 48px; + height: 40px; + width: 40px; } #hyper-checkout-cart-image { @@ -175,8 +175,8 @@ } .hyper-checkout-cart-product-image { - height: 72px; - width: 72px; + height: 56px; + width: 56px; } .hyper-checkout-card-item-name { @@ -234,13 +234,21 @@ background-color: var(--primary-color); box-shadow: 0px 1px 10px #f2f2f2; display: flex; + flex-flow: column; align-items: center; justify-content: center; } #payment-form-wrap { - min-width: 584px; - padding: 50px; + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; + } + + .powered-by-hyper { + margin-top: 20px; } #hyper-checkout-sdk-header { @@ -295,28 +303,13 @@ margin-top: 10px; } - .checkoutButton { - height: 48px; - border-radius: 25px; - width: 100%; - border: transparent; - background: var(--secondary-color); - color: #ffffff; - font-weight: 600; - cursor: pointer; - } - .page-spinner, .page-spinner::before, - .page-spinner::after, - .spinner, - .spinner:before, - .spinner:after { + .page-spinner::after { border-radius: 50%; } - .page-spinner, - .spinner { + .page-spinner { color: #ffffff; font-size: 22px; text-indent: -99999px; @@ -331,9 +324,7 @@ } .page-spinner::before, - .page-spinner::after, - .spinner:before, - .spinner:after { + .page-spinner::after { position: absolute; content: ""; } @@ -405,19 +396,6 @@ } } - .spinner:before { - width: 10.4px; - height: 20.4px; - background: var(--primary-color); - border-radius: 20.4px 0 0 20.4px; - top: -0.2px; - left: -0.2px; - -webkit-transform-origin: 10.4px 10.2px; - transform-origin: 10.4px 10.2px; - -webkit-animation: loading 2s infinite ease 1.5s; - animation: loading 2s infinite ease 1.5s; - } - #payment-message { font-size: 12px; font-weight: 500; @@ -426,19 +404,6 @@ font-family: "Montserrat"; } - .spinner:after { - width: 10.4px; - height: 10.2px; - background: var(--primary-color); - border-radius: 0 10.2px 10.2px 0; - top: -0.1px; - left: 10.2px; - -webkit-transform-origin: 0px 10.2px; - transform-origin: 0px 10.2px; - -webkit-animation: loading 2s infinite ease; - animation: loading 2s infinite ease; - } - #payment-form { max-width: 560px; width: 100%; @@ -447,11 +412,6 @@ } @media only screen and (max-width: 1200px) { - .checkoutButton { - width: 95%; - background-color: var(--primary-color); - } - .hyper-checkout { flex-flow: column; margin: 0; @@ -627,16 +587,16 @@ @@ -700,7 +660,7 @@
-
+
-
+ +
+ + + + + + + + + + + + + + + + +
- - + function showSDK(e) { + if (window.state.isMobileView) { + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } + setPageLoading(true); + checkStatus() + .then((res) => { + if (res.showSdk) { + renderPaymentDetails(); + renderCart(); + renderSDKHeader(); + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + } else { + show("#hyper-checkout-status"); + show("#hyper-footer"); + } + }) + .catch((err) => {}) + .finally(() => { + setPageLoading(false); + }); + } + + window.addEventListener("resize", (event) => { + const currentHeight = window.innerHeight; + const currentWidth = window.innerWidth; + if (currentWidth <= 1200 && window.state.prevWidth > 1200) { + hide("#hyper-checkout-cart"); + } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { + show("#hyper-checkout-cart"); + } + + window.state.prevHeight = currentHeight; + window.state.prevWidth = currentWidth; + window.state.isMobileView = currentWidth <= 1200; + }); + + diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 422c3fa19881..b19b381af507 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -13,7 +13,10 @@ use diesel_models::enums; use crate::{ core::{errors::RouterResult, payments::helpers}, routes::AppState, - types::api::{self, payments}, + types::{ + api::{self, payments}, + domain, + }, }; pub struct Oss; @@ -25,6 +28,7 @@ pub trait PaymentMethodRetrieve { state: &AppState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; } @@ -35,6 +39,7 @@ impl PaymentMethodRetrieve for Oss { state: &AppState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)> { match pm_data { pm_opt @ Some(pm @ api::PaymentMethodData::Card(_)) => { @@ -44,6 +49,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::Card, pm, + merchant_key_store, ) .await?; @@ -64,6 +70,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::BankTransfer, pm, + merchant_key_store, ) .await?; @@ -76,6 +83,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::Wallet, pm, + merchant_key_store, ) .await?; @@ -88,6 +96,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, enums::PaymentMethod::BankRedirect, pm, + merchant_key_store, ) .await?; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2161ab69222e..234323f0179a 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -31,7 +31,10 @@ use crate::{ transformers::{self as payment_methods}, vault, }, - payments::helpers, + payments::{ + helpers, + routing::{self, SessionFlowRoutingInput}, + }, }, db, logger, pii::prelude::*, @@ -42,7 +45,7 @@ use crate::{ }, services, types::{ - api::{self, PaymentMethodCreateExt}, + api::{self, routing as routing_types, PaymentMethodCreateExt}, domain::{ self, types::{decrypt, encrypt_optional, AsyncLift}, @@ -933,6 +936,135 @@ pub async fn list_payment_methods( .await?; } + if let Some((payment_attempt, payment_intent)) = + payment_attempt.as_ref().zip(payment_intent.as_ref()) + { + let routing_enabled_pms = HashSet::from([ + api_enums::PaymentMethod::BankTransfer, + api_enums::PaymentMethod::BankDebit, + api_enums::PaymentMethod::BankRedirect, + ]); + + let routing_enabled_pm_types = HashSet::from([ + api_enums::PaymentMethodType::GooglePay, + api_enums::PaymentMethodType::ApplePay, + api_enums::PaymentMethodType::Klarna, + api_enums::PaymentMethodType::Paypal, + ]); + + let mut chosen = Vec::::new(); + for intermediate in &response { + if routing_enabled_pm_types.contains(&intermediate.payment_method_type) + || routing_enabled_pms.contains(&intermediate.payment_method) + { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.clone().conf.connectors, + &intermediate.connector, + api::GetToken::from(intermediate.payment_method_type), + None, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("invalid connector name received")?; + + chosen.push(api::SessionConnectorData { + payment_method_type: intermediate.payment_method_type, + connector: connector_data, + business_sub_label: None, + }); + } + } + let sfr = SessionFlowRoutingInput { + state: &state, + country: shipping_address.clone().and_then(|ad| ad.country), + key_store: &key_store, + merchant_account: &merchant_account, + payment_attempt, + payment_intent, + chosen, + }; + let result = routing::perform_session_flow_routing(sfr) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing session flow routing")?; + + response.retain(|intermediate| { + if !routing_enabled_pm_types.contains(&intermediate.payment_method_type) + && !routing_enabled_pms.contains(&intermediate.payment_method) + { + return true; + } + + if let Some(choice) = result.get(&intermediate.payment_method_type) { + intermediate.connector == choice.connector.connector_name.to_string() + } else { + false + } + }); + + let mut routing_info: storage::PaymentRoutingInfo = payment_attempt + .straight_through_algorithm + .clone() + .map(|val| val.parse_value("PaymentRoutingInfo")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid PaymentRoutingInfo format found in payment attempt")? + .unwrap_or_else(|| storage::PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }); + + let mut pre_routing_results: HashMap< + api_enums::PaymentMethodType, + routing_types::RoutableConnectorChoice, + > = HashMap::new(); + + for (pm_type, choice) in result { + let routable_choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: choice + .connector + .connector_name + .to_string() + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("")?, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: choice.connector.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: choice.sub_label, + }; + + pre_routing_results.insert(pm_type, routable_choice); + } + + routing_info.pre_routing_results = Some(pre_routing_results); + + let encoded = utils::Encode::::encode_to_value(&routing_info) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize payment routing info to value")?; + + let attempt_update = storage::PaymentAttemptUpdate::UpdateTrackers { + payment_token: None, + connector: None, + straight_through_algorithm: Some(encoded), + amount_capturable: None, + updated_by: merchant_account.storage_scheme.to_string(), + merchant_connector_id: None, + }; + + state + .store + .update_payment_attempt_with_attempt_id( + payment_attempt.clone(), + attempt_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + let req = api_models::payments::PaymentsRequest::foreign_from(( payment_attempt.as_ref(), shipping_address.as_ref(), @@ -1877,7 +2009,7 @@ pub async fn list_customer_payment_method( let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token).await? + get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? } else { None }; @@ -1972,6 +2104,7 @@ async fn get_card_details( key: &[u8], state: &routes::AppState, hyperswitch_token: &str, + key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult> { let mut _card_decrypted = decrypt::(pm.payment_method_data.clone(), key) @@ -1988,7 +2121,7 @@ async fn get_card_details( }); Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm).await?, + get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, )) } @@ -1996,6 +2129,7 @@ pub async fn get_lookup_key_from_locker( state: &routes::AppState, payment_token: &str, pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2010,9 +2144,15 @@ pub async fn get_lookup_key_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Get Card Details Failed")?; let card = card_detail.clone(); - let resp = - BasiliskCardSupport::create_payment_method_data_in_locker(state, payment_token, card, pm) - .await?; + + let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( + state, + payment_token, + card, + pm, + merchant_key_store, + ) + .await?; Ok(resp) } @@ -2045,6 +2185,7 @@ pub async fn get_lookup_key_for_payout_method( Some(payout_token.to_string()), &pm_parsed, Some(pm.customer_id.to_owned()), + key_store, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -2058,110 +2199,16 @@ pub async fn get_lookup_key_for_payout_method( } } -pub struct BasiliskCardSupport; +pub struct TempLockerCardSupport; -#[cfg(not(feature = "basilisk"))] -impl BasiliskCardSupport { - async fn create_payment_method_data_in_locker( - state: &routes::AppState, - payment_token: &str, - card: api::CardDetailFromLocker, - pm: &storage::PaymentMethod, - ) -> errors::RouterResult { - let card_number = card.card_number.clone().get_required_value("card_number")?; - let card_exp_month = card - .expiry_month - .clone() - .expose_option() - .get_required_value("expiry_month")?; - let card_exp_year = card - .expiry_year - .clone() - .expose_option() - .get_required_value("expiry_year")?; - let card_holder_name = card - .card_holder_name - .clone() - .expose_option() - .unwrap_or_default(); - let value1 = payment_methods::mk_card_value1( - card_number, - card_exp_year, - card_exp_month, - Some(card_holder_name), - None, - None, - None, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - let value2 = payment_methods::mk_card_value2( - None, - None, - None, - Some(pm.customer_id.to_string()), - Some(pm.payment_method_id.to_string()), - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let value1 = vault::VaultPaymentMethod::Card(value1); - let value2 = vault::VaultPaymentMethod::Card(value2); - - let value1 = utils::Encode::::encode_to_string_of_json(&value1) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value1 construction failed when saving card to locker")?; - - let value2 = utils::Encode::::encode_to_string_of_json(&value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value2 construction failed when saving card to locker")?; - - let db_value = vault::MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Mock tokenize value construction failed when saving card to locker", - )?; - - let db = &*state.store; - - let already_present = db.find_config_by_key(payment_token).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: payment_token.to_string(), - config: value_string, - }; - - db.insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - - db.update_config_by_key(payment_token, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization db update failed")?; - } - - Ok(card) - } -} - -#[cfg(feature = "basilisk")] -impl BasiliskCardSupport { +impl TempLockerCardSupport { #[instrument(skip_all)] - async fn create_payment_method_data_in_locker( + async fn create_payment_method_data_in_temp_locker( state: &routes::AppState, payment_token: &str, card: api::CardDetailFromLocker, pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card_number = card.card_number.clone().get_required_value("card_number")?; let card_exp_month = card @@ -2211,8 +2258,14 @@ impl BasiliskCardSupport { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Wrapped value2 construction failed when saving card to locker")?; - let lookup_key = - vault::create_tokenize(state, value1, Some(value2), payment_token.to_string()).await?; + let lookup_key = vault::create_tokenize( + state, + value1, + Some(value2), + payment_token.to_string(), + merchant_key_store.key.get_inner(), + ) + .await?; vault::add_delete_tokenized_data_task( &*state.store, &lookup_key, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index d16269deb9b2..5ad78c9d730e 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1,34 +1,30 @@ -use common_utils::generate_id_with_default_len; -#[cfg(feature = "basilisk")] -use error_stack::report; -use error_stack::{IntoReport, ResultExt}; +use common_utils::{ + crypto::{DecodeMessage, EncodeMessage, GcmAes256}, + ext_traits::BytesExt, + generate_id_with_default_len, +}; +use error_stack::{report, IntoReport, ResultExt}; #[cfg(feature = "basilisk")] use josekit::jwe; use masking::PeekInterface; use router_env::{instrument, tracing}; -#[cfg(feature = "basilisk")] use scheduler::{types::process_data, utils as process_tracker_utils}; -#[cfg(feature = "basilisk")] -use crate::routes::metrics; #[cfg(feature = "payouts")] use crate::types::api::payouts; use crate::{ - configs::settings, + consts, core::errors::{self, CustomResult, RouterResult}, - logger, routes, + db, logger, routes, + routes::metrics, types::{ - api, - storage::{self, enums}, + api, domain, + storage::{self, enums, ProcessTrackerExt}, }, utils::{self, StringExt}, }; #[cfg(feature = "basilisk")] -use crate::{core::payment_methods::transformers as payment_methods, services, utils::BytesExt}; -#[cfg(feature = "basilisk")] -use crate::{db, types::storage::ProcessTrackerExt}; - -#[cfg(feature = "basilisk")] +use crate::{core::payment_methods::transformers as payment_methods, services, settings}; const VAULT_SERVICE_NAME: &str = "CARD"; #[cfg(feature = "basilisk")] const VAULT_VERSION: &str = "0"; @@ -622,196 +618,15 @@ pub struct MockTokenizeDBValue { pub struct Vault; -#[cfg(not(feature = "basilisk"))] impl Vault { #[instrument(skip_all)] pub async fn get_payment_method_data_from_locker( state: &routes::AppState, lookup_key: &str, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payment_method, supp_data) = - api::PaymentMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payment Method from Values")?; - - Ok((Some(payment_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn get_payout_method_data_from_temporary_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payout_method, supp_data) = - api::PayoutMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payout Method from Values")?; - - Ok((Some(payout_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn store_payout_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payout_method: &api::PayoutMethodData, - customer_id: Option, - ) -> RouterResult { - let value1 = payout_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payout_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payout method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn store_payment_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payment_method: &api::PaymentMethodData, - customer_id: Option, - _pm: enums::PaymentMethod, - ) -> RouterResult { - let value1 = payment_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payment_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value12 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payment method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn delete_locker_payment_method_by_lookup_key( - state: &routes::AppState, - lookup_key: &Option, - ) { - let db = &*state.store; - if let Some(id) = lookup_key { - match db.delete_config_by_key(id).await { - Ok(_) => logger::info!("Card Deleted from locker mock up"), - Err(err) => logger::error!("Err: Card Delete from locker Failed : {}", err), - } - } - } -} - -#[cfg(feature = "basilisk")] -impl Vault { - #[instrument(skip_all)] - pub async fn get_payment_method_data_from_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let de_tokenize = get_tokenized_data(state, lookup_key, true).await?; + let de_tokenize = + get_tokenized_data(state, lookup_key, true, merchant_key_store.key.get_inner()).await?; let (payment_method, customer_id) = api::PaymentMethodData::from_values(de_tokenize.value1, de_tokenize.value2) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -827,6 +642,7 @@ impl Vault { payment_method: &api::PaymentMethodData, customer_id: Option, pm: enums::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let value1 = payment_method .get_value1(customer_id.clone()) @@ -840,7 +656,14 @@ impl Vault { let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - let lookup_key = create_tokenize(state, value1, Some(value2), lookup_key).await?; + let lookup_key = create_tokenize( + state, + value1, + Some(value2), + lookup_key, + merchant_key_store.key.get_inner(), + ) + .await?; add_delete_tokenized_data_task(&*state.store, &lookup_key, pm).await?; metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); Ok(lookup_key) @@ -851,8 +674,10 @@ impl Vault { pub async fn get_payout_method_data_from_temporary_locker( state: &routes::AppState, lookup_key: &str, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, SupplementaryVaultData)> { - let de_tokenize = get_tokenized_data(state, lookup_key, true).await?; + let de_tokenize = + get_tokenized_data(state, lookup_key, true, merchant_key_store.key.get_inner()).await?; let (payout_method, supp_data) = api::PayoutMethodData::from_values(de_tokenize.value1, de_tokenize.value2) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -868,6 +693,7 @@ impl Vault { token_id: Option, payout_method: &api::PayoutMethodData, customer_id: Option, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let value1 = payout_method .get_value1(customer_id.clone()) @@ -881,7 +707,14 @@ impl Vault { let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - let lookup_key = create_tokenize(state, value1, Some(value2), lookup_key).await?; + let lookup_key = create_tokenize( + state, + value1, + Some(value2), + lookup_key, + merchant_key_store.key.get_inner(), + ) + .await?; // add_delete_tokenized_data_task(&*state.store, &lookup_key, pm).await?; // scheduler_metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); Ok(lookup_key) @@ -893,31 +726,334 @@ impl Vault { lookup_key: &Option, ) { if let Some(lookup_key) = lookup_key { - let delete_resp = delete_tokenized_data(state, lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully") - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp) - } - } - Err(err) => logger::error!("Err: Deleting Card From Locker : {:?}", err), - } + delete_tokenized_data(state, lookup_key) + .await + .map(|_| logger::info!("Card From locker deleted Successfully")) + .map_err(|err| logger::error!("Error: Deleting Card From Redis Locker : {:?}", err)) + .ok(); } } } //------------------------------------------------TokenizeService------------------------------------------------ -pub fn get_key_id(keys: &settings::Jwekey) -> &str { - let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources - if key_identifier == "1" { - &keys.locker_key_identifier1 - } else { - &keys.locker_key_identifier2 + +#[inline(always)] +fn get_redis_locker_key(lookup_key: &str) -> String { + format!("{}_{}", consts::LOCKER_REDIS_PREFIX, lookup_key) +} + +#[instrument(skip(state, value1, value2))] +pub async fn create_tokenize( + state: &routes::AppState, + value1: String, + value2: Option, + lookup_key: String, + encryption_key: &masking::Secret>, +) -> RouterResult { + let redis_key = get_redis_locker_key(lookup_key.as_str()); + let func = || async { + metrics::CREATED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let payload_to_be_encrypted = api::TokenizePayloadRequest { + value1: value1.clone(), + value2: value2.clone().unwrap_or_default(), + lookup_key: lookup_key.clone(), + service_name: VAULT_SERVICE_NAME.to_string(), + }; + + let payload = utils::Encode::::encode_to_string_of_json( + &payload_to_be_encrypted, + ) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let encrypted_payload = GcmAes256 + .encode_message(encryption_key.peek().as_ref(), payload.as_bytes()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode redis temp locker data")?; + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + redis_conn + .set_key_if_not_exists_with_expiry( + redis_key.as_str(), + bytes::Bytes::from(encrypted_payload), + Some(i64::from(consts::LOCKER_REDIS_EXPIRY_SECONDS)), + ) + .await + .map(|_| lookup_key.clone()) + .map_err(|err| { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + err + }) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error from redis locker") + }; + + match func().await { + Ok(s) => { + logger::info!( + "Insert payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_create_tokenize(state, value1, value2, lookup_key).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +#[instrument(skip(state))] +pub async fn get_tokenized_data( + state: &routes::AppState, + lookup_key: &str, + _should_get_value2: bool, + encryption_key: &masking::Secret>, +) -> RouterResult { + let redis_key = get_redis_locker_key(lookup_key); + let func = || async { + metrics::GET_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.get_key::(redis_key.as_str()).await; + + match response { + Ok(resp) => { + let decrypted_payload = GcmAes256 + .decode_message( + encryption_key.peek().as_ref(), + masking::Secret::new(resp.into()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to decode redis temp locker data")?; + + let get_response: api::TokenizePayloadRequest = + bytes::Bytes::from(decrypted_payload) + .parse_struct("TokenizePayloadRequest") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Error getting TokenizePayloadRequest from tokenize response", + )?; + + Ok(get_response) + } + Err(err) => { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + Err(err).change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".into(), + }) + } + } + }; + + match func().await { + Ok(s) => { + logger::info!( + "Fetch payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_get_tokenized_data(state, lookup_key, _should_get_value2).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +#[instrument(skip(state))] +pub async fn delete_tokenized_data(state: &routes::AppState, lookup_key: &str) -> RouterResult<()> { + let redis_key = get_redis_locker_key(lookup_key); + let func = || async { + metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.delete_key(redis_key.as_str()).await; + + match response { + Ok(redis_interface::DelReply::KeyDeleted) => Ok(()), + Ok(redis_interface::DelReply::KeyNotDeleted) => { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Token invalid or expired") + } + Err(err) => { + metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable_lazy(|| { + format!("Failed to delete from redis locker: {err:?}") + }) + } + } + }; + match func().await { + Ok(s) => { + logger::info!( + "Delete payload in redis locker successful with lookup key: {:?}", + redis_key + ); + Ok(s) + } + Err(err) => { + logger::error!("Redis Temp locker Failed: {:?}", err); + + #[cfg(feature = "basilisk")] + return old_delete_tokenized_data(state, lookup_key).await; + + #[cfg(not(feature = "basilisk"))] + Err(err) + } + } +} + +// ********************************************** PROCESS TRACKER ********************************************** + +pub async fn add_delete_tokenized_data_task( + db: &dyn db::StorageInterface, + lookup_key: &str, + pm: enums::PaymentMethod, +) -> RouterResult<()> { + let runner = "DELETE_TOKENIZE_DATA_WORKFLOW"; + let current_time = common_utils::date_time::now(); + let tracking_data = serde_json::to_value(storage::TokenizeCoreWorkflow { + lookup_key: lookup_key.to_owned(), + pm, + }) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("unable to convert into value {lookup_key:?}"))?; + + let schedule_time = get_delete_tokenize_schedule_time(db, &pm, 0).await; + + let process_tracker_entry = storage::ProcessTrackerNew { + id: format!("{runner}_{lookup_key}"), + name: Some(String::from(runner)), + tag: vec![String::from("BASILISK-V3")], + runner: Some(String::from(runner)), + retry_count: 0, + schedule_time, + rule: String::new(), + tracking_data, + business_status: String::from("Pending"), + status: enums::ProcessTrackerStatus::New, + event: vec![], + created_at: current_time, + updated_at: current_time, + }; + let response = db.insert_process(process_tracker_entry).await; + response.map(|_| ()).or_else(|err| { + if err.current_context().is_db_unique_violation() { + Ok(()) + } else { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + } + }) +} + +pub async fn start_tokenize_data_workflow( + state: &routes::AppState, + tokenize_tracker: &storage::ProcessTracker, +) -> Result<(), errors::ProcessTrackerError> { + let db = &*state.store; + let delete_tokenize_data = serde_json::from_value::( + tokenize_tracker.tracking_data.clone(), + ) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "unable to convert into DeleteTokenizeByTokenRequest {:?}", + tokenize_tracker.tracking_data + ) + })?; + + match delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await { + Ok(()) => { + logger::info!("Card From locker deleted Successfully"); + //mark task as finished + let id = tokenize_tracker.id.clone(); + tokenize_tracker + .clone() + .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) + .await?; + } + Err(err) => { + logger::error!("Err: Deleting Card From Locker : {:?}", err); + retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) + .await?; + metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); + } } + Ok(()) +} + +pub async fn get_delete_tokenize_schedule_time( + db: &dyn db::StorageInterface, + pm: &enums::PaymentMethod, + retry_count: i32, +) -> Option { + let redis_mapping = db::get_and_deserialize_key( + db, + &format!("pt_mapping_delete_{pm}_tokenize_data"), + "PaymentMethodsPTMapping", + ) + .await; + let mapping = match redis_mapping { + Ok(x) => x, + Err(err) => { + logger::info!("Redis Mapping Error: {}", err); + process_data::PaymentMethodsPTMapping::default() + } + }; + let time_delta = process_tracker_utils::get_pm_schedule_time(mapping, pm, retry_count + 1); + + process_tracker_utils::get_time_from_delta(time_delta) } +pub async fn retry_delete_tokenize( + db: &dyn db::StorageInterface, + pm: &enums::PaymentMethod, + pt: storage::ProcessTracker, +) -> Result<(), errors::ProcessTrackerError> { + let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; + + match schedule_time { + Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + None => { + pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) + .await + } + } +} + +// Fallback logic of old temp locker needs to be removed later + #[cfg(feature = "basilisk")] async fn get_locker_jwe_keys( keys: &settings::ActiveKmsSecrets, @@ -936,13 +1072,13 @@ async fn get_locker_jwe_keys( } #[cfg(feature = "basilisk")] -pub async fn create_tokenize( +#[instrument(skip(state, value1, value2))] +pub async fn old_create_tokenize( state: &routes::AppState, value1: String, value2: Option, lookup_key: String, ) -> RouterResult { - metrics::CREATED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); let payload_to_be_encrypted = api::TokenizePayloadRequest { value1, value2: value2.unwrap_or_default(), @@ -1017,7 +1153,7 @@ pub async fn create_tokenize( } #[cfg(feature = "basilisk")] -pub async fn get_tokenized_data( +pub async fn old_get_tokenized_data( state: &routes::AppState, lookup_key: &str, should_get_value2: bool, @@ -1096,10 +1232,10 @@ pub async fn get_tokenized_data( } #[cfg(feature = "basilisk")] -pub async fn delete_tokenized_data( +pub async fn old_delete_tokenized_data( state: &routes::AppState, lookup_key: &str, -) -> RouterResult { +) -> RouterResult<()> { metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); let payload_to_be_encrypted = api::DeleteTokenizeByTokenRequest { lookup_key: lookup_key.to_string(), @@ -1136,11 +1272,11 @@ pub async fn delete_tokenized_data( .attach_printable("Error while making /tokenize/delete/token call to the locker")?; match response { Ok(r) => { - let delete_response = std::str::from_utf8(&r.response) + let _delete_response = std::str::from_utf8(&r.response) .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Decoding Failed for basilisk delete response")?; - Ok(delete_response.to_string()) + Ok(()) } Err(err) => { metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); @@ -1151,133 +1287,12 @@ pub async fn delete_tokenized_data( } } -// ********************************************** PROCESS TRACKER ********************************************** #[cfg(feature = "basilisk")] -pub async fn add_delete_tokenized_data_task( - db: &dyn db::StorageInterface, - lookup_key: &str, - pm: enums::PaymentMethod, -) -> RouterResult<()> { - let runner = "DELETE_TOKENIZE_DATA_WORKFLOW"; - let current_time = common_utils::date_time::now(); - let tracking_data = serde_json::to_value(storage::TokenizeCoreWorkflow { - lookup_key: lookup_key.to_owned(), - pm, - }) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("unable to convert into value {lookup_key:?}"))?; - - let schedule_time = get_delete_tokenize_schedule_time(db, &pm, 0).await; - - let process_tracker_entry = storage::ProcessTrackerNew { - id: format!("{runner}_{lookup_key}"), - name: Some(String::from(runner)), - tag: vec![String::from("BASILISK-V3")], - runner: Some(String::from(runner)), - retry_count: 0, - schedule_time, - rule: String::new(), - tracking_data, - business_status: String::from("Pending"), - status: enums::ProcessTrackerStatus::New, - event: vec![], - created_at: current_time, - updated_at: current_time, - }; - let response = db.insert_process(process_tracker_entry).await; - response.map(|_| ()).or_else(|err| { - if err.current_context().is_db_unique_violation() { - Ok(()) - } else { - Err(report!(errors::ApiErrorResponse::InternalServerError)) - } - }) -} - -#[cfg(feature = "basilisk")] -pub async fn start_tokenize_data_workflow( - state: &routes::AppState, - tokenize_tracker: &storage::ProcessTracker, -) -> Result<(), errors::ProcessTrackerError> { - let db = &*state.store; - let delete_tokenize_data = serde_json::from_value::( - tokenize_tracker.tracking_data.clone(), - ) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!( - "unable to convert into DeleteTokenizeByTokenRequest {:?}", - tokenize_tracker.tracking_data - ) - })?; - - let delete_resp = delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully"); - //mark task as finished - let id = tokenize_tracker.id.clone(); - tokenize_tracker - .clone() - .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) - .await?; - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp); - retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) - .await?; - metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); - } - } - Err(err) => { - logger::error!("Err: Deleting Card From Locker : {:?}", err); - retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) - .await?; - metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); - } - } - Ok(()) -} - -#[cfg(feature = "basilisk")] -pub async fn get_delete_tokenize_schedule_time( - db: &dyn db::StorageInterface, - pm: &enums::PaymentMethod, - retry_count: i32, -) -> Option { - let redis_mapping = db::get_and_deserialize_key( - db, - &format!("pt_mapping_delete_{pm}_tokenize_data"), - "PaymentMethodsPTMapping", - ) - .await; - let mapping = match redis_mapping { - Ok(x) => x, - Err(err) => { - logger::info!("Redis Mapping Error: {}", err); - process_data::PaymentMethodsPTMapping::default() - } - }; - let time_delta = process_tracker_utils::get_pm_schedule_time(mapping, pm, retry_count + 1); - - process_tracker_utils::get_time_from_delta(time_delta) -} - -#[cfg(feature = "basilisk")] -pub async fn retry_delete_tokenize( - db: &dyn db::StorageInterface, - pm: &enums::PaymentMethod, - pt: storage::ProcessTracker, -) -> Result<(), errors::ProcessTrackerError> { - let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; - - match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, - None => { - pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) - .await - } +pub fn get_key_id(keys: &settings::Jwekey) -> &str { + let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources + if key_identifier == "1" { + &keys.locker_key_identifier1 + } else { + &keys.locker_key_identifier2 } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f26b91479ece..a114b20380bf 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3,11 +3,12 @@ pub mod customers; pub mod flows; pub mod helpers; pub mod operations; +pub mod routing; pub mod tokenization; pub mod transformers; pub mod types; -use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; +use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; use api_models::{ enums, @@ -35,6 +36,7 @@ pub use self::operations::{ use self::{ flows::{ConstructFlowSpecificData, Feature}, operations::{payment_complete_authorize, BoxedOperation, Operation}, + routing::{self as self_routing, SessionFlowRoutingInput}, }; use super::errors::StorageErrorExt; use crate::{ @@ -49,8 +51,11 @@ use crate::{ routes::{metrics, payment_methods::ParentPaymentMethodToken, AppState}, services::{self, api::Authenticate}, types::{ - self as router_types, api, domain, + self as router_types, + api::{self, ConnectorCallType}, + domain, storage::{self, enums as storage_enums}, + transformers::ForeignTryInto, }, utils::{ add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt, @@ -69,6 +74,7 @@ pub async fn payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResult<( PaymentData, @@ -136,34 +142,18 @@ where &merchant_account, &key_store, &mut payment_data, + eligible_connectors, ) .await?; - let schedule_time = match &connector { - Some(api::ConnectorCallType::Single(connector_data)) => { - if should_add_task_to_process_tracker(&payment_data) { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector_data.connector.id(), - &merchant_account.merchant_id, - 0, - ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - } - } - _ => None, - }; + let should_add_task_to_process_tracker = should_add_task_to_process_tracker(&payment_data); payment_data = tokenize_in_router_when_confirm_false( state, &operation, &mut payment_data, &validate_result, + &key_store, ) .await?; @@ -171,7 +161,21 @@ where let mut external_latency = None; if let Some(connector_details) = connector { payment_data = match connector_details { - api::ConnectorCallType::Single(connector) => { + api::ConnectorCallType::PreDetermined(connector) => { + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; let router_data = call_connector_service( state, &merchant_account, @@ -186,6 +190,57 @@ where header_payload, ) .await?; + let operation = Box::new(PaymentResponse); + let db = &*state.store; + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( + db, + &validate_result.payment_id, + payment_data, + router_data, + merchant_account.storage_scheme, + ) + .await? + } + + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); + + let connector_data = get_connector_data(&mut connectors)?; + + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector_data.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( + state, + &merchant_account, + &key_store, + connector_data, + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, + ) + .await?; let operation = Box::new(PaymentResponse); let db = &*state.store; @@ -205,7 +260,7 @@ where .await? } - api::ConnectorCallType::Multiple(connectors) => { + api::ConnectorCallType::SessionMultiple(connectors) => { call_multiple_connectors_service( state, &merchant_account, @@ -258,6 +313,17 @@ where )) } +#[inline] +pub fn get_connector_data( + connectors: &mut IntoIter, +) -> RouterResult { + connectors + .next() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Connector not found in connectors iterator") +} + #[allow(clippy::too_many_arguments)] pub async fn payments_core( state: AppState, @@ -267,6 +333,7 @@ pub async fn payments_core( req: Req, auth_flow: services::AuthFlow, call_connector_action: CallConnectorAction, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResponse where @@ -287,6 +354,12 @@ where // To perform router related operation for PaymentResponse PaymentResponse: Operation, { + let eligible_routable_connectors = eligible_connectors.map(|connectors| { + connectors + .into_iter() + .flat_map(|c| c.foreign_try_into()) + .collect() + }); let (payment_data, req, customer, connector_http_status_code, external_latency) = payments_operation_core::<_, _, _, _, Ctx>( &state, @@ -296,6 +369,7 @@ where req, call_connector_action, auth_flow, + eligible_routable_connectors, header_payload, ) .await?; @@ -470,6 +544,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom payment_confirm_req, services::api::AuthFlow::Merchant, connector_action, + None, HeaderPayload::default(), ) .await @@ -565,11 +640,11 @@ impl PaymentRedirectFlow for PaymentRedirectSyn payment_sync_req, services::api::AuthFlow::Merchant, connector_action, + None, HeaderPayload::default(), ) .await } - fn generate_response( &self, payments_response: api_models::payments::PaymentsResponse, @@ -632,12 +707,18 @@ where ) .await?; + if payment_data.payment_attempt.merchant_connector_id.is_none() { + payment_data.payment_attempt.merchant_connector_id = + merchant_connector_account.get_mca_id(); + } + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, payment_data, validate_result, &merchant_connector_account, + key_store, ) .await?; @@ -1320,6 +1401,7 @@ pub async fn get_connector_tokenization_action_when_confirm_true( payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(PaymentData, TokenizationAction)> where F: Send + Clone, @@ -1382,7 +1464,12 @@ where TokenizationAction::TokenizeInRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; TokenizationAction::SkipConnectorTokenization @@ -1392,7 +1479,12 @@ where TokenizationAction::TokenizeInConnectorAndRouter => { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; @@ -1428,6 +1520,7 @@ pub async fn tokenize_in_router_when_confirm_false( operation: &BoxedOperation<'_, F, Req, Ctx>, payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> where F: Send + Clone, @@ -1437,7 +1530,12 @@ where let payment_data = if !is_operation_confirm(operation) { let (_operation, payment_method_data) = operation .to_domain()? - .make_pm_data(state, payment_data, validate_result.storage_scheme) + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) .await?; payment_data.payment_method_data = payment_method_data; payment_data @@ -1480,7 +1578,6 @@ where pub payment_intent: storage::PaymentIntent, pub payment_attempt: storage::PaymentAttempt, pub multiple_capture_data: Option, - pub connector_response: storage::ConnectorResponse, pub amount: api::Amount, pub mandate_id: Option, pub mandate_connector: Option, @@ -1573,10 +1670,7 @@ pub fn should_call_connector( !matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::Failed | storage_enums::IntentStatus::Succeeded - ) && payment_data - .connector_response - .authentication_data - .is_none() + ) && payment_data.payment_attempt.authentication_data.is_none() } "PaymentStatus" => { matches!( @@ -1837,7 +1931,7 @@ pub fn update_straight_through_routing( where F: Send + Clone, { - let _: api::RoutingAlgorithm = request_straight_through + let _: api_models::routing::RoutingAlgorithm = request_straight_through .clone() .parse_value("RoutingAlgorithm") .attach_printable("Invalid straight through routing rules format")?; @@ -1854,7 +1948,8 @@ pub async fn get_connector_choice( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, -) -> RouterResult> + eligible_connectors: Option>, +) -> RouterResult> where F: Send + Clone, Ctx: PaymentMethodRetrieve, @@ -1863,7 +1958,7 @@ where .to_domain()? .get_connector( merchant_account, - state, + &state.clone(), req, &payment_data.payment_intent, key_store, @@ -1872,39 +1967,132 @@ where let connector = if should_call_connector(operation, payment_data) { Some(match connector_choice { - api::ConnectorChoice::SessionMultiple(session_connectors) => { - api::ConnectorCallType::Multiple(session_connectors) + api::ConnectorChoice::SessionMultiple(connectors) => { + let routing_output = perform_session_token_routing( + state.clone(), + merchant_account, + key_store, + payment_data, + connectors, + ) + .await?; + api::ConnectorCallType::SessionMultiple(routing_output) } - api::ConnectorChoice::StraightThrough(straight_through) => connector_selection( - state, - merchant_account, - payment_data, - Some(straight_through), - )?, + api::ConnectorChoice::StraightThrough(straight_through) => { + connector_selection( + state, + merchant_account, + key_store, + payment_data, + Some(straight_through), + eligible_connectors, + ) + .await? + } api::ConnectorChoice::Decide => { - connector_selection(state, merchant_account, payment_data, None)? + connector_selection( + state, + merchant_account, + key_store, + payment_data, + None, + eligible_connectors, + ) + .await? } }) - } else if let api::ConnectorChoice::StraightThrough(val) = connector_choice { - update_straight_through_routing(payment_data, val) + } else if let api::ConnectorChoice::StraightThrough(algorithm) = connector_choice { + update_straight_through_routing(payment_data, algorithm) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update straight through routing algorithm")?; + None } else { None }; - Ok(connector) } -pub fn connector_selection( +pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, -) -> RouterResult + eligible_connectors: Option>, +) -> RouterResult +where + F: Send + Clone, +{ + let request_straight_through: Option = + request_straight_through + .map(|val| val.parse_value("RoutingAlgorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid straight through routing rules format")?; + + let mut routing_data = storage::RoutingData { + routed_through: payment_data.payment_attempt.connector.clone(), + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: payment_data.payment_attempt.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + business_sub_label: payment_data.payment_attempt.business_sub_label.clone(), + algorithm: request_straight_through.clone(), + routing_info: payment_data + .payment_attempt + .straight_through_algorithm + .clone() + .map(|val| val.parse_value("PaymentRoutingInfo")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid straight through algorithm format found in payment attempt")? + .unwrap_or_else(|| storage::PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }), + }; + + let decided_connector = decide_connector( + state.clone(), + merchant_account, + key_store, + payment_data, + request_straight_through, + &mut routing_data, + eligible_connectors, + ) + .await?; + + let encoded_info = + Encode::::encode_to_value(&routing_data.routing_info) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error serializing payment routing info to serde value")?; + + payment_data.payment_attempt.connector = routing_data.routed_through; + #[cfg(feature = "connector_choice_mca_id")] + { + payment_data.payment_attempt.merchant_connector_id = routing_data.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + payment_data.payment_attempt.business_sub_label = routing_data.business_sub_label; + } + payment_data.payment_attempt.straight_through_algorithm = Some(encoded_info); + + Ok(decided_connector) +} + +pub async fn decide_connector( + state: AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + request_straight_through: Option, + routing_data: &mut storage::RoutingData, + eligible_connectors: Option>, +) -> RouterResult where F: Send + Clone, { @@ -1920,111 +2108,424 @@ where payment_data.payment_attempt.merchant_connector_id.clone(), ) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("invalid connector name received in payment attempt")?; + .attach_printable("Invalid connector name received in 'routed_through'")?; - return Ok(api::ConnectorCallType::Single(connector_data)); + routing_data.routed_through = Some(connector_name.clone()); + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); } - let request_straight_through = request_straight_through - .map(|val| val.parse_value::("StraightThroughAlgorithm")) - .transpose() + if let Some(mandate_connector_details) = payment_data.mandate_connector.as_ref() { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &mandate_connector_details.connector, + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + mandate_connector_details.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid straight through routing rules format") - .transpose(); + .attach_printable("Invalid connector name received in 'routed_through'")?; - let payment_routing_algorithm = request_straight_through.or(payment_data + routing_data.routed_through = Some(mandate_connector_details.connector.clone()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = + mandate_connector_details.merchant_connector_id.clone(); + } + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); + } + + if let Some((pre_routing_results, storage_pm_type)) = routing_data + .routing_info + .pre_routing_results + .as_ref() + .zip(payment_data.payment_attempt.payment_method_type.as_ref()) + { + if let Some(choice) = pre_routing_results.get(storage_pm_type) { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &choice.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + choice.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = choice.merchant_connector_id.clone(); + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = choice.sub_label.clone(); + } + return Ok(api::ConnectorCallType::PreDetermined(connector_data)); + } + } + + if let Some(routing_algorithm) = request_straight_through { + let (mut connectors, check_eligibility) = + routing::perform_straight_through_routing(&routing_algorithm, payment_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; + + if check_eligibility { + connectors = routing::perform_eligibility_analysis_with_fallback( + &state.clone(), + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed eligibility analysis and fallback")?; + } + + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); + + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id.clone(), + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label.clone(); + } + routing_data.routing_info.algorithm = Some(routing_algorithm); + return Ok(api::ConnectorCallType::Retryable(connector_data)); + } + + if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm { + let (mut connectors, check_eligibility) = + routing::perform_straight_through_routing(routing_algorithm, payment_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; + + if check_eligibility { + connectors = routing::perform_eligibility_analysis_with_fallback( + &state, + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed eligibility analysis and fallback")?; + } + + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); + + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label; + } + return Ok(api::ConnectorCallType::Retryable(connector_data)); + } + + route_connector_v1( + &state, + merchant_account, + key_store, + payment_data, + routing_data, + eligible_connectors, + ) + .await +} + +pub fn should_add_task_to_process_tracker(payment_data: &PaymentData) -> bool { + let connector = payment_data.payment_attempt.connector.as_deref(); + + !matches!( + (payment_data.payment_attempt.payment_method, connector), + ( + Some(storage_enums::PaymentMethod::BankTransfer), + Some("stripe") + ) + ) +} + +pub async fn perform_session_token_routing( + state: AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + connectors: Vec, +) -> RouterResult> +where + F: Clone, +{ + let routing_info: Option = payment_data .payment_attempt .straight_through_algorithm .clone() - .map(|val| val.parse_value::("RoutingAlgorithm")) + .map(|val| val.parse_value("PaymentRoutingInfo")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid straight through algorithm format in payment attempt") - .transpose()); + .attach_printable("invalid payment routing info format found in payment attempt")?; - let routing_algorithm = payment_routing_algorithm - .or(merchant_account - .routing_algorithm - .clone() - .map(|merchant_routing_algorithm| { - merchant_routing_algorithm - .parse_value::("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::InternalServerError) // Deserialization failed - .attach_printable("Unable to deserialize merchant routing algorithm") - })) - .get_required_value("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::PreconditionFailed { - message: "no routing algorithm has been configured".to_string(), - })??; + if let Some(storage::PaymentRoutingInfo { + pre_routing_results: Some(pre_routing_results), + .. + }) = routing_info + { + let mut payment_methods: rustc_hash::FxHashMap< + (String, enums::PaymentMethodType), + api::SessionConnectorData, + > = rustc_hash::FxHashMap::from_iter(connectors.iter().map(|c| { + ( + ( + c.connector.connector_name.to_string(), + c.payment_method_type, + ), + c.clone(), + ) + })); - let mut routing_data = storage::RoutingData { - routed_through: payment_data.payment_attempt.connector.clone(), - algorithm: Some(routing_algorithm), - }; + let mut final_list: Vec = Vec::new(); + for (routed_pm_type, choice) in pre_routing_results.into_iter() { + if let Some(session_connector_data) = + payment_methods.remove(&(choice.to_string(), routed_pm_type)) + { + final_list.push(session_connector_data); + } + } + + if !final_list.is_empty() { + return Ok(final_list); + } + } - let (decided_connector, connector_id) = decide_connector(state, &mut routing_data)?; + let routing_enabled_pms = std::collections::HashSet::from([ + enums::PaymentMethodType::GooglePay, + enums::PaymentMethodType::ApplePay, + enums::PaymentMethodType::Klarna, + enums::PaymentMethodType::Paypal, + ]); + + let mut chosen = Vec::::new(); + for connector_data in &connectors { + if routing_enabled_pms.contains(&connector_data.payment_method_type) { + chosen.push(connector_data.clone()); + } + } + let sfr = SessionFlowRoutingInput { + state: &state, + country: payment_data + .address + .billing + .as_ref() + .and_then(|address| address.address.as_ref()) + .and_then(|details| details.country), + key_store, + merchant_account, + payment_attempt: &payment_data.payment_attempt, + payment_intent: &payment_data.payment_intent, - let encoded_algorithm = routing_data - .algorithm - .map(|algo| Encode::::encode_to_value(&algo)) - .transpose() + chosen, + }; + let result = self_routing::perform_session_flow_routing(sfr) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize routing algorithm to serde value")?; + .attach_printable("error performing session flow routing")?; + + let mut final_list: Vec = Vec::new(); + + #[cfg(not(feature = "connector_choice_mca_id"))] + for mut connector_data in connectors { + if !routing_enabled_pms.contains(&connector_data.payment_method_type) { + final_list.push(connector_data); + } else if let Some(choice) = result.get(&connector_data.payment_method_type) { + if connector_data.connector.connector_name == choice.connector.connector_name { + connector_data.business_sub_label = choice.sub_label.clone(); + final_list.push(connector_data); + } + } + } - payment_data.payment_attempt.connector = routing_data.routed_through; - payment_data.payment_attempt.straight_through_algorithm = encoded_algorithm; - payment_data.payment_attempt.merchant_connector_id = connector_id; + #[cfg(feature = "connector_choice_mca_id")] + for connector_data in connectors { + if !routing_enabled_pms.contains(&connector_data.payment_method_type) { + final_list.push(connector_data); + } else if let Some(choice) = result.get(&connector_data.payment_method_type) { + if connector_data.connector.connector_name == choice.connector.connector_name { + final_list.push(connector_data); + } + } + } - Ok(decided_connector) + Ok(final_list) } -pub fn decide_connector( +pub async fn route_connector_v1( state: &AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, -) -> RouterResult<(api::ConnectorCallType, Option)> { - let routing_algorithm = routing_data - .algorithm + eligible_connectors: Option>, +) -> RouterResult +where + F: Send + Clone, +{ + #[cfg(not(feature = "business_profile_routing"))] + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm .clone() - .get_required_value("Routing algorithm")?; + .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode merchant routing algorithm ref")? + .unwrap_or_default(); - let (connector_name, merchant_connector_id) = match routing_algorithm { - api::StraightThroughAlgorithm::Single(routable_connector_choice) => { - match routable_connector_choice { - api_models::admin::RoutableConnectorChoice::ConnectorName(routable_connector) => { - (routable_connector.to_string(), None) - } - api_models::admin::RoutableConnectorChoice::ConnectorId { - merchant_connector_id, - connector, - } => (connector.to_string(), Some(merchant_connector_id)), - } - } + #[cfg(feature = "business_profile_routing")] + let algorithm_ref: api::routing::RoutingAlgorithmRef = { + let profile_id = payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + business_profile + .routing_algorithm + .clone() + .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode merchant routing algorithm ref")? + .unwrap_or_default() }; - let connector_data = api::ConnectorData::get_connector_by_name( - &state.conf.connectors, - &connector_name, - api::GetToken::Connector, - merchant_connector_id.clone(), + let connectors = routing::perform_static_routing_v1( + state, + &merchant_account.merchant_id, + algorithm_ref, + payment_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let connectors = routing::perform_eligibility_analysis_with_fallback( + &state.clone(), + key_store, + merchant_account.modified_at.assume_utc().unix_timestamp(), + connectors, + payment_data, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), ) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid connector name received in routing algorithm")?; + .attach_printable("failed eligibility analysis and fallback")?; - routing_data.routed_through = Some(connector_name); - Ok(( - api::ConnectorCallType::Single(connector_data), - merchant_connector_id, - )) -} + let first_connector_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("Empty connector list returned")? + .clone(); -pub fn should_add_task_to_process_tracker(payment_data: &PaymentData) -> bool { - let connector = payment_data.payment_attempt.connector.as_deref(); + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); - !matches!( - (payment_data.payment_attempt.payment_method, connector), - ( - Some(storage_enums::PaymentMethod::BankTransfer), - Some("stripe") - ) - ) + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label; + } + + let connector_data = connectors + .into_iter() + .map(|conn| { + api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &conn.connector.to_string(), + api::GetToken::Connector, + #[cfg(feature = "connector_choice_mca_id")] + conn.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + Ok(ConnectorCallType::Retryable(connector_data)) } diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index 887b2b8f411e..af10e91b5a08 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -173,6 +173,7 @@ pub async fn refresh_connector_auth( message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, + attempt_status: None, }; Ok(Err(error_response)) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c55df8e35d6e..0b253cdc6079 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -144,6 +144,7 @@ impl default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, + connector::Bankofamerica, connector::Bitpay, connector::Boku, connector::Cashtocode, @@ -212,6 +213,7 @@ default_imp_for_webhook_source_verification!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Braintree, @@ -290,6 +292,7 @@ default_imp_for_create_customer!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -366,6 +369,7 @@ default_imp_for_connector_redirect_response!( connector::Aci, connector::Adyen, connector::Bitpay, + connector::Bankofamerica, connector::Boku, connector::Cashtocode, connector::Coinbase, @@ -416,6 +420,7 @@ default_imp_for_connector_request_id!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -496,6 +501,7 @@ default_imp_for_accept_dispute!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -596,6 +602,7 @@ default_imp_for_file_upload!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -673,6 +680,7 @@ default_imp_for_submit_evidence!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -750,6 +758,7 @@ default_imp_for_defend_dispute!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -828,6 +837,7 @@ default_imp_for_pre_processing_steps!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -886,6 +896,7 @@ default_imp_for_payouts!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -964,6 +975,7 @@ default_imp_for_payouts_create!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1045,6 +1057,7 @@ default_imp_for_payouts_eligibility!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1123,6 +1136,7 @@ default_imp_for_payouts_fulfill!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1201,6 +1215,7 @@ default_imp_for_payouts_cancel!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1280,6 +1295,7 @@ default_imp_for_payouts_quote!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1359,6 +1375,7 @@ default_imp_for_payouts_recipient!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1437,6 +1454,7 @@ default_imp_for_approve!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -1516,6 +1534,7 @@ default_imp_for_reject!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, + connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 2c77184b9bd4..e27fe54c0ed0 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -95,37 +95,56 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - let save_payment_result = tokenization::save_payment_method( - state, - connector, - resp.to_owned(), - maybe_customer, - merchant_account, - self.request.payment_method_type, - key_store, - ) - .await; - - let pm_id = match save_payment_result { - Ok(payment_method_id) => Ok(payment_method_id), - Err(error) => { - if resp.request.setup_mandate_details.clone().is_some() { - Err(error) - } else { - logger::error!(save_payment_method_error=?error); - Ok(None) + if resp.request.setup_mandate_details.clone().is_some() { + let payment_method_id = tokenization::save_payment_method( + state, + connector, + resp.to_owned(), + maybe_customer, + merchant_account, + self.request.payment_method_type, + key_store, + ) + .await?; + Ok(mandate::mandate_procedure( + state, + resp, + maybe_customer, + payment_method_id, + connector.merchant_connector_id.clone(), + ) + .await?) + } else { + let connector = connector.clone(); + let response = resp.clone(); + let maybe_customer = maybe_customer.clone(); + let merchant_account = merchant_account.clone(); + let key_store = key_store.clone(); + let state = state.clone(); + + logger::info!("Initiating async call to save_payment_method in locker"); + + tokio::spawn(async move { + logger::info!("Starting async call to save_payment_method in locker"); + + let result = tokenization::save_payment_method( + &state, + &connector, + response, + &maybe_customer, + &merchant_account, + self.request.payment_method_type, + &key_store, + ) + .await; + + if let Err(err) = result { + logger::error!("Asynchronously saving card in locker failed : {:?}", err); } - } - }?; + }); - Ok(mandate::mandate_procedure( - state, - resp, - maybe_customer, - pm_id, - connector.merchant_connector_id.clone(), - ) - .await?) + Ok(resp) + } } else { Ok(self.clone()) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index af67d30ec6c3..4ee2fd4b94d3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -221,8 +221,6 @@ pub async fn create_or_update_address_for_payment_by_request( None => match req_address { Some(address) => { // generate a new address here - let customer_id = customer_id.get_required_value("customer_id")?; - let address_details = address.address.clone().unwrap_or_default(); Some( db.insert_address_for_payments( @@ -282,7 +280,6 @@ pub async fn create_or_find_address_for_payment_by_request( None => match req_address { Some(address) => { // generate a new address here - let customer_id = customer_id.get_required_value("customer_id")?; let address_details = address.address.clone().unwrap_or_default(); Some( @@ -317,7 +314,7 @@ pub async fn get_domain_address_for_payments( address_details: api_models::payments::AddressDetails, address: &api_models::payments::Address, merchant_id: &str, - customer_id: &str, + customer_id: Option<&String>, payment_id: &str, key: &[u8], storage_scheme: enums::MerchantStorageScheme, @@ -332,7 +329,7 @@ pub async fn get_domain_address_for_payments( .async_lift(|inner| types::encrypt_optional(inner, key)) .await?, country_code: address.phone.as_ref().and_then(|a| a.country_code.clone()), - customer_id: customer_id.to_string(), + customer_id: customer_id.cloned(), merchant_id: merchant_id.to_string(), address_id: generate_id(consts::ID_LENGTH, "add"), city: address_details.city, @@ -402,6 +399,7 @@ pub async fn get_token_pm_type_mandate_details( request: &api::PaymentsRequest, mandate_type: Option, merchant_account: &domain::MerchantAccount, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( Option, Option, @@ -430,7 +428,13 @@ pub async fn get_token_pm_type_mandate_details( recurring_mandate_payment_data, payment_method_type_, mandate_connector, - ) = get_token_for_recurring_mandate(state, request, merchant_account).await?; + ) = get_token_for_recurring_mandate( + state, + request, + merchant_account, + merchant_key_store, + ) + .await?; Ok(( token_, payment_method_, @@ -455,6 +459,7 @@ pub async fn get_token_for_recurring_mandate( state: &AppState, req: &api::PaymentsRequest, merchant_account: &domain::MerchantAccount, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( Option, Option, @@ -504,7 +509,9 @@ pub async fn get_token_for_recurring_mandate( }; if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method { - let _ = cards::get_lookup_key_from_locker(state, &token, &payment_method).await?; + let _ = + cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store) + .await?; if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if pm != payment_method.payment_method { @@ -763,25 +770,14 @@ fn validate_new_mandate_request( } pub fn validate_customer_id_mandatory_cases( - has_shipping: bool, - has_billing: bool, has_setup_future_usage: bool, customer_id: &Option, ) -> RouterResult<()> { - match ( - has_shipping, - has_billing, - has_setup_future_usage, - customer_id, - ) { - (true, _, _, None) | (_, true, _, None) | (_, _, true, None) => { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: "customer_id is mandatory when shipping or billing \ - address is given or when setup_future_usage is given" - .to_string(), - }) - .into_report() - } + match (has_setup_future_usage, customer_id) { + (true, None) => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "customer_id is mandatory when setup_future_usage is given".to_string(), + }) + .into_report(), _ => Ok(()), } } @@ -1334,6 +1330,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, payment_data: &mut PaymentData, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, @@ -1387,6 +1384,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( state, &hyperswitch_token, + merchant_key_store, ) .await .attach_printable( @@ -1416,6 +1414,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( &updated_pm, payment_data.payment_intent.customer_id.to_owned(), enums::PaymentMethod::Card, + merchant_key_store, ) .await?; Some(updated_pm) @@ -1456,6 +1455,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( state, &payment_data.payment_intent, &payment_data.payment_attempt, + merchant_key_store, ) .await?; @@ -1475,6 +1475,7 @@ pub async fn store_in_vault_and_generate_ppmt( payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, payment_method: enums::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult { let router_token = vault::Vault::store_payment_method_data_in_locker( state, @@ -1482,6 +1483,7 @@ pub async fn store_in_vault_and_generate_ppmt( payment_method_data, payment_intent.customer_id.to_owned(), payment_method, + merchant_key_store, ) .await?; let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); @@ -1505,6 +1507,7 @@ pub async fn store_payment_method_data_in_vault( payment_intent: &PaymentIntent, payment_method: enums::PaymentMethod, payment_method_data: &api::PaymentMethodData, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { if should_store_payment_method_data_in_vault( &state.conf.temp_locker_enable_config, @@ -1517,6 +1520,7 @@ pub async fn store_payment_method_data_in_vault( payment_intent, payment_attempt, payment_method, + merchant_key_store, ) .await?; @@ -2994,60 +2998,6 @@ impl AttemptType { } } } - - #[instrument(skip_all)] - pub async fn get_or_insert_connector_response( - &self, - payment_attempt: &PaymentAttempt, - db: &dyn StorageInterface, - storage_scheme: storage::enums::MerchantStorageScheme, - ) -> RouterResult { - match self { - Self::New => db - .insert_connector_response( - payments::PaymentCreate::make_connector_response(payment_attempt), - storage_scheme, - ) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { - payment_id: payment_attempt.payment_id.clone(), - }), - Self::SameOld => db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound), - } - } - - #[instrument(skip_all)] - pub async fn get_connector_response( - &self, - db: &dyn StorageInterface, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> RouterResult { - match self { - Self::New => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("Precondition failed, the attempt type should not be `New`"), - Self::SameOld => db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - payment_id, - merchant_id, - attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound), - } - } } #[inline(always)] diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index d198cd562a79..ad747ac2792a 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -123,6 +123,7 @@ pub trait Domain: Send + Sync { state: &'a AppState, payment_data: &mut PaymentData, storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, @@ -233,11 +234,12 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } } @@ -282,6 +284,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, Option, @@ -343,6 +346,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, Option, @@ -394,6 +398,7 @@ where _state: &'a AppState, _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRejectRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index a1d50a9049aa..d5d0d2d01765 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -88,6 +88,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -130,8 +131,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id @@ -162,16 +161,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let redirect_response = request .feature_metadata .as_ref() @@ -225,7 +214,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, @@ -301,12 +289,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 43fdc440e64d..f734afef7826 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -106,15 +106,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let currency = payment_attempt.currency.get_required_value("currency")?; let amount = payment_attempt.amount.into(); @@ -161,7 +152,7 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, + sessions_token: vec![], card_cvc: None, creds_identifier, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 1cfcbce5532f..6e794b1ba618 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -3,7 +3,6 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; -use diesel_models::connector_response::ConnectorResponse; use error_stack::ResultExt; use router_env::{instrument, tracing}; @@ -20,7 +19,7 @@ use crate::{ types::{ api::{self, PaymentIdTypeExt}, domain, - storage::{self, enums, payment_attempt::PaymentAttemptExt, ConnectorResponseExt}, + storage::{self, enums, payment_attempt::PaymentAttemptExt}, }, utils::OptionExt, }; @@ -89,9 +88,7 @@ impl helpers::validate_capture_method(capture_method)?; - let (multiple_capture_data, connector_response) = if capture_method - == enums::CaptureMethod::ManualMultiple - { + let multiple_capture_data = if capture_method == enums::CaptureMethod::ManualMultiple { let amount_to_capture = request .amount_to_capture .get_required_value("amount_to_capture")?; @@ -121,37 +118,13 @@ impl .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { payment_id: payment_id.clone(), })?; - let new_connector_response = db - .insert_connector_response( - ConnectorResponse::make_new_connector_response( - capture.payment_id.clone(), - capture.merchant_id.clone(), - capture.capture_id.clone(), - Some(capture.connector.clone()), - storage_scheme.to_string(), - ), - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { payment_id })?; - ( - Some(MultipleCaptureData::new_for_create( - previous_captures, - capture, - )), - new_connector_response, - ) + + Some(MultipleCaptureData::new_for_create( + previous_captures, + capture, + )) } else { - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - (None, connector_response) + None }; currency = payment_attempt.currency.get_required_value("currency")?; @@ -223,7 +196,6 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, sessions_token: vec![], card_cvc: None, creds_identifier, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 0e357f08734e..038d34ea290f 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -87,6 +87,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -139,8 +140,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id @@ -171,16 +170,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let redirect_response = request .feature_metadata .as_ref() @@ -220,7 +209,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, @@ -296,12 +284,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; Ok((op, payment_method_data)) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 9e1f12b6bca2..21f7db3d0b41 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -69,6 +69,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ); let (mut payment_intent, mandate_details) = @@ -156,77 +157,49 @@ impl }) .map(|x| x.transpose()); - let (mut payment_attempt, shipping_address, billing_address, connector_response) = - match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let attempt_type = helpers::AttemptType::SameOld; - - let attempt_id = payment_intent.active_attempt.get_id(); - let connector_response_fut = attempt_type.get_connector_response( + let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + let (payment_attempt, shipping_address, billing_address, _) = futures::try_join!( + payment_attempt_fut, + shipping_address_fut, + billing_address_fut, + config_update_fut + )?; + + (payment_attempt, shipping_address, billing_address) + } + _ => { + let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( + payment_attempt_fut, + shipping_address_fut, + billing_address_fut, + config_update_fut + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + // 3 + request, + payment_intent, + payment_attempt, db, - &payment_intent.payment_id, - &payment_intent.merchant_id, - attempt_id.as_str(), storage_scheme, - ); - - let (payment_attempt, shipping_address, billing_address, connector_response, _) = - futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - connector_response_fut, - config_update_fut - )?; - - ( - payment_attempt, - shipping_address, - billing_address, - connector_response, ) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - // 3 - request, - payment_intent, - payment_attempt, - db, - storage_scheme, - ) - .await?; - - let connector_response = attempt_type - .get_or_insert_connector_response(&payment_attempt, db, storage_scheme) - .await?; + .await?; - ( - payment_attempt, - shipping_address, - billing_address, - connector_response, - ) - } - }; + (payment_attempt, shipping_address, billing_address) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -284,8 +257,6 @@ impl amount = payment_attempt.amount.into(); helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id @@ -355,7 +326,6 @@ impl payment_intent, payment_attempt, currency, - connector_response, amount, email: request.email.clone(), mandate_id: None, @@ -425,12 +395,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, key_store).await?; utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2d31f82aeb00..97bb84371306 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -62,7 +62,7 @@ impl let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (payment_intent, payment_attempt, connector_response); + let (payment_intent, payment_attempt); let money @ (amount, currency) = payments_create_request_validation(request)?; @@ -107,6 +107,7 @@ impl request, mandate_type, merchant_account, + merchant_key_store, ) .await?; @@ -195,16 +196,6 @@ impl payment_id: payment_id.clone(), })?; - connector_response = db - .insert_connector_response( - Self::make_connector_response(&payment_attempt), - storage_scheme, - ) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { - payment_id: payment_id.clone(), - })?; - let mandate_id = request .mandate_id .as_ref() @@ -299,7 +290,6 @@ impl disputes: vec![], attempts: None, force_sync: None, - connector_response, sessions_token: vec![], card_cvc: request.card_cvc.clone(), creds_identifier, @@ -353,11 +343,12 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] @@ -538,8 +529,6 @@ impl ValidateRequest storage::ConnectorResponseNew { - storage::ConnectorResponseNew { - payment_id: payment_attempt.payment_id.clone(), - merchant_id: payment_attempt.merchant_id.clone(), - attempt_id: payment_attempt.attempt_id.clone(), - created_at: payment_attempt.created_at, - modified_at: payment_attempt.modified_at, - connector_name: payment_attempt.connector.clone(), - connector_transaction_id: None, - authentication_data: None, - encoded_data: None, - updated_by: payment_attempt.updated_by.clone(), - } - } - #[instrument(skip_all)] pub async fn get_ephemeral_key( request: &api::PaymentsRequest, @@ -813,6 +784,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + custom_merchant_name: payment_link_object.custom_merchant_name, }; let payment_link_db = db .insert_payment_link(payment_link_req) diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 6d97f7b66cd1..7e4fe0951b03 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -7,7 +7,7 @@ use error_stack::ResultExt; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; -use super::{BoxedOperation, Domain, GetTracker, PaymentCreate, UpdateTracker, ValidateRequest}; +use super::{BoxedOperation, Domain, GetTracker, UpdateTracker, ValidateRequest}; use crate::{ consts, core::{ @@ -89,7 +89,7 @@ impl let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (payment_intent, payment_attempt, connector_response); + let (payment_intent, payment_attempt); let payment_id = payment_id .get_payment_intent_id() @@ -135,19 +135,6 @@ impl } }?; - connector_response = match db - .insert_connector_response( - PaymentCreate::make_connector_response(&payment_attempt), - storage_scheme, - ) - .await - { - Ok(connector_resp) => Ok(connector_resp), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - let creds_identifier = request .merchant_connector_details .as_ref() @@ -180,7 +167,6 @@ impl mandate_connector: None, setup_mandate: request.mandate_data.clone().map(Into::into), token: request.payment_token.clone(), - connector_response, payment_method_data: request.payment_method_data.clone(), confirm: Some(true), address: types::PaymentAddress::default(), @@ -297,11 +283,12 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::VerifyRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } async fn get_connector<'a>( diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 415ab3eccfe7..a6c2561aaeed 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -104,15 +104,6 @@ impl ) .await?; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let currency = payment_attempt.currency.get_required_value("currency")?; let amount = payment_attempt.amount.into(); @@ -147,7 +138,7 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, + sessions_token: vec![], card_cvc: None, creds_identifier: None, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 1467da7f816d..77c344949660 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -296,10 +296,7 @@ async fn payment_response_update_tracker( router_data: types::RouterData, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { - let (capture_update, mut payment_attempt_update, connector_response_update) = match router_data - .response - .clone() - { + let (capture_update, mut payment_attempt_update) = match router_data.response.clone() { Err(err) => { let (capture_update, attempt_update) = match payment_data.multiple_capture_data { Some(multiple_capture_data) => { @@ -326,7 +323,7 @@ async fn payment_response_update_tracker( match err.status_code { // marking failure for 2xx because this is genuine payment failure 200..=299 => storage::enums::AttemptStatus::Failure, - _ => payment_data.payment_attempt.status, + _ => router_data.status, } } else { match err.status_code { @@ -356,14 +353,7 @@ async fn payment_response_update_tracker( ) } }; - ( - capture_update, - attempt_update, - Some(storage::ConnectorResponseUpdate::ErrorUpdate { - connector_name: Some(router_data.connector.clone()), - updated_by: storage_scheme.to_string(), - }), - ) + (capture_update, attempt_update) } Ok(payments_response) => match payments_response { types::PaymentsResponseData::PreProcessingResponse { @@ -394,7 +384,7 @@ async fn payment_response_update_tracker( updated_by: storage_scheme.to_string(), }; - (None, Some(payment_attempt_update), None) + (None, Some(payment_attempt_update)) } types::PaymentsResponseData::TransactionResponse { resource_id, @@ -409,8 +399,7 @@ async fn payment_response_update_tracker( | types::ResponseId::EncodedData(id) => Some(id), }; - let encoded_data = payment_data.connector_response.encoded_data.clone(); - let connector_name = router_data.connector.clone(); + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); let authentication_data = redirection_data .map(|data| utils::Encode::::encode_to_value(&data)) @@ -478,23 +467,13 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + authentication_data, + encoded_data, }), ), }; - let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate { - connector_transaction_id, - authentication_data, - encoded_data, - connector_name: Some(connector_name), - updated_by: storage_scheme.to_string(), - }; - - ( - capture_updates, - payment_attempt_update, - Some(connector_response_update), - ) + (capture_updates, payment_attempt_update) } types::PaymentsResponseData::TransactionUnresolvedResponse { resource_id, @@ -519,14 +498,13 @@ async fn payment_response_update_tracker( connector_response_reference_id, updated_by: storage_scheme.to_string(), }), - None, ) } - types::PaymentsResponseData::SessionResponse { .. } => (None, None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None, None), + types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -535,13 +513,9 @@ async fn payment_response_update_tracker( &multiple_capture_data, capture_sync_response_list, )?; - ( - Some((multiple_capture_data, capture_update_list)), - None, - None, - ) + (Some((multiple_capture_data, capture_update_list)), None) } - None => (None, None, None), + None => (None, None), }, }, }; @@ -571,40 +545,18 @@ async fn payment_response_update_tracker( // Stage 1 let payment_attempt = payment_data.payment_attempt.clone(); - let connector_response = payment_data.connector_response.clone(); - - let payment_attempt_fut = Box::pin(async move { - Ok::<_, error_stack::Report>(match payment_attempt_update { - Some(payment_attempt_update) => db - .update_payment_attempt_with_attempt_id( - payment_attempt, - payment_attempt_update, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_attempt, - }) - }); - - let connector_response_fut = Box::pin(async move { - Ok::<_, error_stack::Report>(match connector_response_update { - Some(connector_response_update) => db - .update_connector_response( - connector_response, - connector_response_update, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => connector_response, - }) - }); - - let (payment_attempt, connector_response) = - futures::try_join!(payment_attempt_fut, connector_response_fut)?; - payment_data.payment_attempt = payment_attempt; - payment_data.connector_response = connector_response; + + payment_data.payment_attempt = match payment_attempt_update { + Some(payment_attempt_update) => db + .update_payment_attempt_with_attempt_id( + payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => payment_attempt, + }; let amount_captured = get_total_amount_captured( router_data.request, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index cf16a053592b..52677ab3cc8d 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -126,20 +126,6 @@ impl payment_intent.shipping_address_id = shipping_address.clone().map(|x| x.address_id); payment_intent.billing_address_id = billing_address.clone().map(|x| x.address_id); - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - let customer_details = payments::CustomerDetails { customer_id: payment_intent.customer_id.clone(), name: None, @@ -190,7 +176,6 @@ impl disputes: vec![], attempts: None, sessions_token: vec![], - connector_response, card_cvc: None, creds_identifier, pm_token: None, @@ -318,6 +303,7 @@ where _state: &'b AppState, _payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsSessionRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 227e7e2f90db..5578f6b3dc15 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -126,20 +126,6 @@ impl ..CustomerDetails::default() }; - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - Ok(( Box::new(self), PaymentData { @@ -150,7 +136,6 @@ impl email: None, mandate_id: None, mandate_connector: None, - connector_response, setup_mandate: None, token: payment_attempt.payment_token.clone(), address: PaymentAddress { @@ -282,6 +267,7 @@ where state: &'a AppState, payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, Option, @@ -293,7 +279,7 @@ where .map(|connector_name| connector_name == *"bluesnap".to_string()) .unwrap_or(false) { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } else { Ok((Box::new(self), None)) } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d20830d9bc6b..83e7131b2675 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -95,11 +95,12 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] @@ -225,7 +226,7 @@ async fn get_tracker_for_sync< PaymentData, Option, )> { - let (payment_intent, payment_attempt, currency, amount); + let (payment_intent, mut payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -249,18 +250,7 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - let mut connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentNotFound) - .attach_printable("Database error when finding connector response")?; - - connector_response.encoded_data = request.param.clone(); + payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -348,7 +338,7 @@ async fn get_tracker_for_sync< format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {payment_id_str}", &merchant_account.merchant_id) }); - let contains_encoded_data = connector_response.encoded_data.is_some(); + let contains_encoded_data = payment_attempt.encoded_data.is_some(); let creds_identifier = request .merchant_connector_details @@ -372,7 +362,6 @@ async fn get_tracker_for_sync< PaymentData { flow: PhantomData, payment_intent, - connector_response, currency, amount, email: None, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index a77ede0e6f6a..0a49c830b732 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -106,6 +106,7 @@ impl request, mandate_type.clone(), merchant_account, + key_store, ) .await?; @@ -146,8 +147,6 @@ impl if request.confirm.unwrap_or(false) { helpers::validate_customer_id_mandatory_cases( - request.shipping.is_some(), - request.billing.is_some(), request.setup_future_usage.is_some(), &payment_intent .customer_id @@ -220,20 +219,6 @@ impl )?; } - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_intent.payment_id, - &payment_intent.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .map_err(|error| { - error - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Database error when finding connector response") - })?; - let mandate_id = request .mandate_id .as_ref() @@ -342,7 +327,6 @@ impl refunds: vec![], disputes: vec![], attempts: None, - connector_response, sessions_token: vec![], card_cvc: request.card_cvc.clone(), creds_identifier, @@ -396,11 +380,12 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data).await + helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs new file mode 100644 index 000000000000..4134ddf65ea0 --- /dev/null +++ b/crates/router/src/core/payments/routing.rs @@ -0,0 +1,950 @@ +mod transformers; + +use std::{ + collections::hash_map, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use api_models::{ + admin as admin_api, + enums::{self as api_enums, CountryAlpha2}, + routing::ConnectorSelection, +}; +use common_utils::static_cache::StaticCache; +use diesel_models::enums as storage_enums; +use error_stack::{IntoReport, ResultExt}; +use euclid::{ + backend::{self, inputs as dsl_inputs, EuclidBackend}, + dssa::graph::{self as euclid_graph, Memoization}, + enums as euclid_enums, + frontend::ast, +}; +use kgraph_utils::{ + mca as mca_graph, + transformers::{IntoContext, IntoDirValue}, +}; +use masking::PeekInterface; +use rand::{ + distributions::{self, Distribution}, + SeedableRng, +}; +use rustc_hash::FxHashMap; + +#[cfg(not(feature = "business_profile_routing"))] +use crate::utils::StringExt; +use crate::{ + core::{ + errors as oss_errors, errors, payments as payments_oss, routing::helpers as routing_helpers, + }, + logger, + types::{ + api, api::routing as routing_types, domain, storage as oss_storage, + transformers::ForeignInto, + }, + utils::{OptionExt, ValueExt}, + AppState, +}; + +pub(super) enum CachedAlgorithm { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Advanced(backend::VirInterpreterBackend), +} + +pub struct SessionFlowRoutingInput<'a> { + pub state: &'a AppState, + pub country: Option, + pub key_store: &'a domain::MerchantKeyStore, + pub merchant_account: &'a domain::MerchantAccount, + pub payment_attempt: &'a oss_storage::PaymentAttempt, + pub payment_intent: &'a oss_storage::PaymentIntent, + pub chosen: Vec, +} + +pub struct SessionRoutingPmTypeInput<'a> { + state: &'a AppState, + key_store: &'a domain::MerchantKeyStore, + merchant_last_modified: i64, + attempt_id: &'a str, + routing_algorithm: &'a MerchantAccountRoutingAlgorithm, + backend_input: dsl_inputs::BackendInput, + allowed_connectors: FxHashMap, + #[cfg(feature = "business_profile_routing")] + profile_id: Option, +} +static ROUTING_CACHE: StaticCache = StaticCache::new(); +static KGRAPH_CACHE: StaticCache> = StaticCache::new(); + +type RoutingResult = oss_errors::CustomResult; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum MerchantAccountRoutingAlgorithm { + V1(routing_types::RoutingAlgorithmRef), +} + +impl Default for MerchantAccountRoutingAlgorithm { + fn default() -> Self { + Self::V1(routing_types::RoutingAlgorithmRef::default()) + } +} + +pub fn make_dsl_input( + payment_data: &payments_oss::PaymentData, +) -> RoutingResult +where + F: Clone, +{ + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: payment_data + .setup_mandate + .as_ref() + .and_then(|mandate_data| { + mandate_data + .customer_acceptance + .clone() + .map(|cat| match cat.acceptance_type { + data_models::mandates::AcceptanceType::Online => { + euclid_enums::MandateAcceptanceType::Online + } + data_models::mandates::AcceptanceType::Offline => { + euclid_enums::MandateAcceptanceType::Offline + } + }) + }), + mandate_type: payment_data + .setup_mandate + .as_ref() + .and_then(|mandate_data| { + mandate_data.mandate_type.clone().map(|mt| match mt { + data_models::mandates::MandateDataType::SingleUse(_) => { + euclid_enums::MandateType::SingleUse + } + data_models::mandates::MandateDataType::MultiUse(_) => { + euclid_enums::MandateType::MultiUse + } + }) + }), + payment_type: Some(payment_data.setup_mandate.clone().map_or_else( + || euclid_enums::PaymentType::NonMandate, + |_| euclid_enums::PaymentType::SetupMandate, + )), + }; + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: payment_data.payment_attempt.payment_method, + payment_method_type: payment_data.payment_attempt.payment_method_type, + card_network: payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api::PaymentMethodData::Card(card) => card.card_network.clone(), + + _ => None, + }), + }; + + let payment_input = dsl_inputs::PaymentInput { + amount: payment_data.payment_intent.amount, + card_bin: payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api::PaymentMethodData::Card(card) => { + Some(card.card_number.peek().chars().take(6).collect()) + } + _ => None, + }), + currency: payment_data.currency, + authentication_type: payment_data.payment_attempt.authentication_type, + capture_method: payment_data + .payment_attempt + .capture_method + .and_then(|cm| cm.foreign_into()), + business_country: payment_data + .payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: payment_data + .address + .billing + .as_ref() + .and_then(|bic| bic.address.as_ref()) + .and_then(|add| add.country) + .map(api_enums::Country::from_alpha2), + business_label: payment_data.payment_intent.business_label.clone(), + setup_future_usage: payment_data.payment_intent.setup_future_usage, + }; + + let metadata = payment_data + .payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + Ok(dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }) +} + +pub async fn perform_static_routing_v1( + state: &AppState, + merchant_id: &str, + algorithm_ref: routing_types::RoutingAlgorithmRef, + payment_data: &mut payments_oss::PaymentData, +) -> RoutingResult> { + let algorithm_id = if let Some(id) = algorithm_ref.algorithm_id { + id + } else { + let fallback_config = + routing_helpers::get_merchant_default_config(&*state.clone().store, merchant_id) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + + return Ok(fallback_config); + }; + let key = ensure_algorithm_cached_v1( + state, + merchant_id, + algorithm_ref.timestamp, + &algorithm_id, + #[cfg(feature = "business_profile_routing")] + payment_data.payment_intent.profile_id.clone(), + ) + .await?; + let cached_algorithm: Arc = ROUTING_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + + Ok(match cached_algorithm.as_ref() { + CachedAlgorithm::Single(conn) => vec![(**conn).clone()], + + CachedAlgorithm::Priority(plist) => plist.clone(), + + CachedAlgorithm::VolumeSplit(splits) => perform_volume_split(splits.to_vec(), None) + .change_context(errors::RoutingError::ConnectorSelectionFailed)?, + + CachedAlgorithm::Advanced(interpreter) => { + let backend_input = make_dsl_input(payment_data)?; + + execute_dsl_and_get_connector_v1(backend_input, interpreter)? + } + }) +} + +async fn ensure_algorithm_cached_v1( + state: &AppState, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult { + #[cfg(feature = "business_profile_routing")] + let key = { + let profile_id = profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + format!("routing_config_{merchant_id}_{profile_id}") + }; + + #[cfg(not(feature = "business_profile_routing"))] + let key = format!("dsl_{merchant_id}"); + + let present = ROUTING_CACHE + .present(&key) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + + let expired = ROUTING_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error checking expiry of DSL in cache")?; + + if !present || expired { + refresh_routing_cache_v1( + state, + key.clone(), + algorithm_id, + timestamp, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + }; + + Ok(key) +} + +pub fn perform_straight_through_routing( + algorithm: &routing_types::StraightThroughAlgorithm, + payment_data: &payments_oss::PaymentData, +) -> RoutingResult<(Vec, bool)> { + Ok(match algorithm { + routing_types::StraightThroughAlgorithm::Single(conn) => ( + vec![(**conn).clone()], + payment_data.creds_identifier.is_none(), + ), + + routing_types::StraightThroughAlgorithm::Priority(conns) => (conns.clone(), true), + + routing_types::StraightThroughAlgorithm::VolumeSplit(splits) => ( + perform_volume_split(splits.to_vec(), None) + .change_context(errors::RoutingError::ConnectorSelectionFailed) + .attach_printable( + "Volume Split connector selection error in straight through routing", + )?, + true, + ), + }) +} + +fn execute_dsl_and_get_connector_v1( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> RoutingResult> { + let routing_output: routing_types::RoutingAlgorithm = interpreter + .execute(backend_input) + .map(|out| out.connector_selection.foreign_into()) + .into_report() + .change_context(errors::RoutingError::DslExecutionError)?; + + Ok(match routing_output { + routing_types::RoutingAlgorithm::Priority(plist) => plist, + + routing_types::RoutingAlgorithm::VolumeSplit(splits) => perform_volume_split(splits, None) + .change_context(errors::RoutingError::DslFinalConnectorSelectionFailed)?, + + _ => Err(errors::RoutingError::DslIncorrectSelectionAlgorithm) + .into_report() + .attach_printable("Unsupported algorithm received as a result of static routing")?, + }) +} + +pub async fn refresh_routing_cache_v1( + state: &AppState, + key: String, + algorithm_id: &str, + timestamp: i64, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult<()> { + #[cfg(feature = "business_profile_routing")] + let algorithm = { + let algorithm = state + .store + .find_routing_algorithm_by_profile_id_algorithm_id( + &profile_id.unwrap_or_default(), + algorithm_id, + ) + .await + .change_context(errors::RoutingError::DslMissingInDb)?; + let algorithm: routing_types::RoutingAlgorithm = algorithm + .algorithm_data + .parse_value("RoutingAlgorithm") + .change_context(errors::RoutingError::DslParsingError)?; + algorithm + }; + + #[cfg(not(feature = "business_profile_routing"))] + let algorithm = { + let config = state + .store + .find_config_by_key(algorithm_id) + .await + .change_context(errors::RoutingError::DslMissingInDb) + .attach_printable("DSL not found in DB")?; + + let algorithm: routing_types::RoutingAlgorithm = config + .config + .parse_struct("Program") + .change_context(errors::RoutingError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + algorithm + }; + let cached_algorithm = match algorithm { + routing_types::RoutingAlgorithm::Single(conn) => CachedAlgorithm::Single(conn), + routing_types::RoutingAlgorithm::Priority(plist) => CachedAlgorithm::Priority(plist), + routing_types::RoutingAlgorithm::VolumeSplit(splits) => { + CachedAlgorithm::VolumeSplit(splits) + } + routing_types::RoutingAlgorithm::Advanced(program) => { + let interpreter = backend::VirInterpreterBackend::with_program(program) + .into_report() + .change_context(errors::RoutingError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + + CachedAlgorithm::Advanced(interpreter) + } + }; + + ROUTING_CACHE + .save(key, cached_algorithm, timestamp) + .into_report() + .change_context(errors::RoutingError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + + Ok(()) +} + +pub fn perform_volume_split( + mut splits: Vec, + rng_seed: Option<&str>, +) -> RoutingResult> { + let weights: Vec = splits.iter().map(|sp| sp.split).collect(); + let weighted_index = distributions::WeightedIndex::new(weights) + .into_report() + .change_context(errors::RoutingError::VolumeSplitFailed) + .attach_printable("Error creating weighted distribution for volume split")?; + + let idx = if let Some(seed) = rng_seed { + let mut hasher = hash_map::DefaultHasher::new(); + seed.hash(&mut hasher); + let hash = hasher.finish(); + + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(hash); + weighted_index.sample(&mut rng) + } else { + let mut rng = rand::thread_rng(); + weighted_index.sample(&mut rng) + }; + + splits + .get(idx) + .ok_or(errors::RoutingError::VolumeSplitFailed) + .into_report() + .attach_printable("Volume split index lookup failed")?; + + // Panic Safety: We have performed a `get(idx)` operation just above which will + // ensure that the index is always present, else throw an error. + let removed = splits.remove(idx); + splits.insert(0, removed); + + Ok(splits.into_iter().map(|sp| sp.connector).collect()) +} + +pub async fn get_merchant_kgraph<'a>( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult>> { + #[cfg(feature = "business_profile_routing")] + let key = { + let profile_id = profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + format!("kgraph_{}_{profile_id}", key_store.merchant_id) + }; + + #[cfg(not(feature = "business_profile_routing"))] + let key = format!("kgraph_{}", key_store.merchant_id); + + let kgraph_present = KGRAPH_CACHE + .present(&key) + .into_report() + .change_context(errors::RoutingError::KgraphCacheFailure) + .attach_printable("when checking kgraph presence")?; + + let kgraph_expired = KGRAPH_CACHE + .expired(&key, merchant_last_modified) + .into_report() + .change_context(errors::RoutingError::KgraphCacheFailure) + .attach_printable("when checking kgraph expiry")?; + + if !kgraph_present || kgraph_expired { + refresh_kgraph_cache( + state, + key_store, + merchant_last_modified, + key.clone(), + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + } + + let cached_kgraph = KGRAPH_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("when retrieving kgraph")?; + + Ok(cached_kgraph) +} + +pub async fn refresh_kgraph_cache( + state: &AppState, + key_store: &domain::MerchantKeyStore, + timestamp: i64, + key: String, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult<()> { + let mut merchant_connector_accounts = state + .store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + &key_store.merchant_id, + false, + key_store, + ) + .await + .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; + + merchant_connector_accounts + .retain(|mca| mca.connector_type != storage_enums::ConnectorType::PaymentVas); + + #[cfg(feature = "business_profile_routing")] + let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( + merchant_connector_accounts, + profile_id, + ); + + let api_mcas: Vec = merchant_connector_accounts + .into_iter() + .map(|acct| acct.try_into()) + .collect::>() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; + + let kgraph = mca_graph::make_mca_graph(api_mcas) + .into_report() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed) + .attach_printable("when construction kgraph")?; + + KGRAPH_CACHE + .save(key, kgraph, timestamp) + .into_report() + .change_context(errors::RoutingError::KgraphCacheRefreshFailed) + .attach_printable("when saving kgraph to cache")?; + + Ok(()) +} + +async fn perform_kgraph_filtering( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + backend_input: dsl_inputs::BackendInput, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let context = euclid_graph::AnalysisContext::from_dir_values( + backend_input + .into_context() + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?, + ); + let cached_kgraph = get_merchant_kgraph( + state, + key_store, + merchant_last_modified, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await?; + + let mut final_selection = Vec::::new(); + for choice in chosen { + let routable_connector = choice.connector; + let euclid_choice: ast::ConnectorChoice = choice.clone().foreign_into(); + let dir_val = euclid_choice + .into_dir_value() + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?; + let kgraph_eligible = cached_kgraph + .check_value_validity(dir_val, &context, &mut Memoization::new()) + .into_report() + .change_context(errors::RoutingError::KgraphAnalysisError)?; + + let filter_eligible = + eligible_connectors.map_or(true, |list| list.contains(&routable_connector)); + + if kgraph_eligible && filter_eligible { + final_selection.push(choice); + } + } + + Ok(final_selection) +} + +pub async fn perform_eligibility_analysis( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let backend_input = make_dsl_input(payment_data)?; + + perform_kgraph_filtering( + state, + key_store, + merchant_last_modified, + chosen, + backend_input, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await +} + +pub async fn perform_fallback_routing( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option<&Vec>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let fallback_config = + routing_helpers::get_merchant_default_config(&*state.store, &key_store.merchant_id) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let backend_input = make_dsl_input(payment_data)?; + + perform_kgraph_filtering( + state, + key_store, + merchant_last_modified, + fallback_config, + backend_input, + eligible_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await +} + +pub async fn perform_eligibility_analysis_with_fallback( + state: &AppState, + key_store: &domain::MerchantKeyStore, + merchant_last_modified: i64, + chosen: Vec, + payment_data: &payments_oss::PaymentData, + eligible_connectors: Option>, + #[cfg(feature = "business_profile_routing")] profile_id: Option, +) -> RoutingResult> { + let mut final_selection = perform_eligibility_analysis( + state, + key_store, + merchant_last_modified, + chosen, + payment_data, + eligible_connectors.as_ref(), + #[cfg(feature = "business_profile_routing")] + profile_id.clone(), + ) + .await?; + + let fallback_selection = perform_fallback_routing( + state, + key_store, + merchant_last_modified, + payment_data, + eligible_connectors.as_ref(), + #[cfg(feature = "business_profile_routing")] + profile_id, + ) + .await; + + final_selection.append( + &mut fallback_selection + .unwrap_or_default() + .iter() + .filter(|&routable_connector_choice| { + !final_selection.contains(routable_connector_choice) + }) + .cloned() + .collect::>(), + ); + + let final_selected_connectors = final_selection + .iter() + .map(|item| item.connector) + .collect::>(); + logger::debug!(final_selected_connectors_for_routing=?final_selected_connectors, "List of final selected connectors for routing"); + + Ok(final_selection) +} + +pub async fn perform_session_flow_routing( + session_input: SessionFlowRoutingInput<'_>, +) -> RoutingResult> { + let mut pm_type_map: FxHashMap> = + FxHashMap::default(); + let merchant_last_modified = session_input + .merchant_account + .modified_at + .assume_utc() + .unix_timestamp(); + + #[cfg(feature = "business_profile_routing")] + let routing_algorithm: MerchantAccountRoutingAlgorithm = { + let profile_id = session_input + .payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?; + + let business_profile = session_input + .state + .store + .find_business_profile_by_profile_id(&profile_id) + .await + .change_context(errors::RoutingError::ProfileNotFound)?; + + business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("MerchantAccountRoutingAlgorithm")) + .transpose() + .change_context(errors::RoutingError::InvalidRoutingAlgorithmStructure)? + .unwrap_or_default() + }; + + #[cfg(not(feature = "business_profile_routing"))] + let routing_algorithm: MerchantAccountRoutingAlgorithm = { + session_input + .merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("MerchantAccountRoutingAlgorithm")) + .transpose() + .change_context(errors::RoutingError::InvalidRoutingAlgorithmStructure)? + .unwrap_or_default() + }; + + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: None, + payment_method_type: None, + card_network: None, + }; + + let payment_input = dsl_inputs::PaymentInput { + amount: session_input.payment_intent.amount, + currency: session_input + .payment_intent + .currency + .get_required_value("Currency") + .change_context(errors::RoutingError::DslMissingRequiredField { + field_name: "currency".to_string(), + })?, + authentication_type: session_input.payment_attempt.authentication_type, + card_bin: None, + capture_method: session_input + .payment_attempt + .capture_method + .and_then(|cm| cm.foreign_into()), + business_country: session_input + .payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: session_input + .country + .map(storage_enums::Country::from_alpha2), + business_label: session_input.payment_intent.business_label.clone(), + setup_future_usage: session_input.payment_intent.setup_future_usage, + }; + + let metadata = session_input + .payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(?err); + None + }); + + let mut backend_input = dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }, + }; + + for connector_data in session_input.chosen.iter() { + pm_type_map + .entry(connector_data.payment_method_type) + .or_default() + .insert( + connector_data.connector.connector_name.to_string(), + connector_data.connector.get_token.clone(), + ); + } + + let mut result: FxHashMap = + FxHashMap::default(); + + for (pm_type, allowed_connectors) in pm_type_map { + let euclid_pmt: euclid_enums::PaymentMethodType = pm_type; + let euclid_pm: euclid_enums::PaymentMethod = euclid_pmt.into(); + + backend_input.payment_method.payment_method = Some(euclid_pm); + backend_input.payment_method.payment_method_type = Some(euclid_pmt); + + let session_pm_input = SessionRoutingPmTypeInput { + state: session_input.state, + key_store: session_input.key_store, + merchant_last_modified, + attempt_id: &session_input.payment_attempt.attempt_id, + routing_algorithm: &routing_algorithm, + backend_input: backend_input.clone(), + allowed_connectors, + #[cfg(feature = "business_profile_routing")] + profile_id: session_input.payment_intent.clone().profile_id, + }; + let maybe_choice = perform_session_routing_for_pm_type(session_pm_input).await?; + + // (connector, sub_label) + if let Some(data) = maybe_choice { + result.insert( + pm_type, + routing_types::SessionRoutingChoice { + connector: data.0, + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: data.1, + payment_method_type: pm_type, + }, + ); + } + } + + Ok(result) +} + +async fn perform_session_routing_for_pm_type( + session_pm_input: SessionRoutingPmTypeInput<'_>, +) -> RoutingResult)>> { + let merchant_id = &session_pm_input.key_store.merchant_id; + + let chosen_connectors = match session_pm_input.routing_algorithm { + MerchantAccountRoutingAlgorithm::V1(algorithm_ref) => { + if let Some(ref algorithm_id) = algorithm_ref.algorithm_id { + let key = ensure_algorithm_cached_v1( + &session_pm_input.state.clone(), + merchant_id, + algorithm_ref.timestamp, + algorithm_id, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + + let cached_algorithm = ROUTING_CACHE + .retrieve(&key) + .into_report() + .change_context(errors::RoutingError::CacheMiss) + .attach_printable("unable to retrieve cached routing algorithm")?; + + match cached_algorithm.as_ref() { + CachedAlgorithm::Single(conn) => vec![(**conn).clone()], + CachedAlgorithm::Priority(plist) => plist.clone(), + CachedAlgorithm::VolumeSplit(splits) => { + perform_volume_split(splits.to_vec(), Some(session_pm_input.attempt_id)) + .change_context(errors::RoutingError::ConnectorSelectionFailed)? + } + CachedAlgorithm::Advanced(interpreter) => execute_dsl_and_get_connector_v1( + session_pm_input.backend_input.clone(), + interpreter, + )?, + } + } else { + routing_helpers::get_merchant_default_config( + &*session_pm_input.state.clone().store, + merchant_id, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)? + } + } + }; + + let mut final_selection = perform_kgraph_filtering( + &session_pm_input.state.clone(), + session_pm_input.key_store, + session_pm_input.merchant_last_modified, + chosen_connectors, + session_pm_input.backend_input.clone(), + None, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + + if final_selection.is_empty() { + let fallback = routing_helpers::get_merchant_default_config( + &*session_pm_input.state.clone().store, + merchant_id, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + + final_selection = perform_kgraph_filtering( + &session_pm_input.state.clone(), + session_pm_input.key_store, + session_pm_input.merchant_last_modified, + fallback, + session_pm_input.backend_input, + None, + #[cfg(feature = "business_profile_routing")] + session_pm_input.profile_id.clone(), + ) + .await?; + } + + let mut final_choice: Option<(api::ConnectorData, Option)> = None; + + for selection in final_selection { + let connector_name = selection.connector.to_string(); + if let Some(get_token) = session_pm_input.allowed_connectors.get(&connector_name) { + let connector_data = api::ConnectorData::get_connector_by_name( + &session_pm_input.state.clone().conf.connectors, + &connector_name, + get_token.clone(), + #[cfg(feature = "connector_choice_mca_id")] + selection.merchant_connector_id, + #[cfg(not(feature = "connector_choice_mca_id"))] + None, + ) + .change_context(errors::RoutingError::InvalidConnectorName(connector_name))?; + #[cfg(not(feature = "connector_choice_mca_id"))] + let sub_label = selection.sub_label; + #[cfg(feature = "connector_choice_mca_id")] + let sub_label = None; + + final_choice = Some((connector_data, sub_label)); + break; + } + } + + Ok(final_choice) +} diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs new file mode 100644 index 000000000000..de94a36248ff --- /dev/null +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -0,0 +1,121 @@ +use api_models::{self, enums as api_enums, routing as routing_types}; +use diesel_models::enums as storage_enums; +use euclid::{enums as dsl_enums, frontend::ast as dsl_ast}; + +use crate::types::transformers::{ForeignFrom, ForeignInto}; + +impl ForeignFrom for dsl_ast::ConnectorChoice { + fn foreign_from(from: routing_types::RoutableConnectorChoice) -> Self { + Self { + // #[cfg(feature = "backwards_compatibility")] + // choice_kind: from.choice_kind.foreign_into(), + connector: from.connector.foreign_into(), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: from.sub_label, + } + } +} + +impl ForeignFrom for Option { + fn foreign_from(value: storage_enums::CaptureMethod) -> Self { + match value { + storage_enums::CaptureMethod::Automatic => Some(dsl_enums::CaptureMethod::Automatic), + storage_enums::CaptureMethod::Manual => Some(dsl_enums::CaptureMethod::Manual), + _ => None, + } + } +} + +impl ForeignFrom for dsl_enums::MandateAcceptanceType { + fn foreign_from(from: api_models::payments::AcceptanceType) -> Self { + match from { + api_models::payments::AcceptanceType::Online => Self::Online, + api_models::payments::AcceptanceType::Offline => Self::Offline, + } + } +} + +impl ForeignFrom for dsl_enums::MandateType { + fn foreign_from(from: api_models::payments::MandateType) -> Self { + match from { + api_models::payments::MandateType::MultiUse(_) => Self::MultiUse, + api_models::payments::MandateType::SingleUse(_) => Self::SingleUse, + } + } +} + +impl ForeignFrom for dsl_enums::MandateType { + fn foreign_from(from: storage_enums::MandateDataType) -> Self { + match from { + storage_enums::MandateDataType::MultiUse(_) => Self::MultiUse, + storage_enums::MandateDataType::SingleUse(_) => Self::SingleUse, + } + } +} + +impl ForeignFrom for dsl_enums::Connector { + fn foreign_from(from: api_enums::RoutableConnectors) -> Self { + match from { + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + api_enums::RoutableConnectors::DummyConnector7 => Self::DummyConnector7, + api_enums::RoutableConnectors::Aci => Self::Aci, + api_enums::RoutableConnectors::Adyen => Self::Adyen, + api_enums::RoutableConnectors::Airwallex => Self::Airwallex, + api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, + api_enums::RoutableConnectors::Bitpay => Self::Bitpay, + api_enums::RoutableConnectors::Bambora => Self::Bambora, + api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, + api_enums::RoutableConnectors::Boku => Self::Boku, + api_enums::RoutableConnectors::Braintree => Self::Braintree, + api_enums::RoutableConnectors::Cashtocode => Self::Cashtocode, + api_enums::RoutableConnectors::Checkout => Self::Checkout, + api_enums::RoutableConnectors::Coinbase => Self::Coinbase, + api_enums::RoutableConnectors::Cryptopay => Self::Cryptopay, + api_enums::RoutableConnectors::Cybersource => Self::Cybersource, + api_enums::RoutableConnectors::Dlocal => Self::Dlocal, + api_enums::RoutableConnectors::Fiserv => Self::Fiserv, + api_enums::RoutableConnectors::Forte => Self::Forte, + api_enums::RoutableConnectors::Globalpay => Self::Globalpay, + api_enums::RoutableConnectors::Globepay => Self::Globepay, + api_enums::RoutableConnectors::Gocardless => Self::Gocardless, + api_enums::RoutableConnectors::Helcim => Self::Helcim, + api_enums::RoutableConnectors::Iatapay => Self::Iatapay, + api_enums::RoutableConnectors::Klarna => Self::Klarna, + api_enums::RoutableConnectors::Mollie => Self::Mollie, + api_enums::RoutableConnectors::Multisafepay => Self::Multisafepay, + api_enums::RoutableConnectors::Nexinets => Self::Nexinets, + api_enums::RoutableConnectors::Nmi => Self::Nmi, + api_enums::RoutableConnectors::Noon => Self::Noon, + api_enums::RoutableConnectors::Nuvei => Self::Nuvei, + api_enums::RoutableConnectors::Opennode => Self::Opennode, + api_enums::RoutableConnectors::Payme => Self::Payme, + api_enums::RoutableConnectors::Paypal => Self::Paypal, + api_enums::RoutableConnectors::Payu => Self::Payu, + api_enums::RoutableConnectors::Powertranz => Self::Powertranz, + api_enums::RoutableConnectors::Rapyd => Self::Rapyd, + api_enums::RoutableConnectors::Shift4 => Self::Shift4, + api_enums::RoutableConnectors::Square => Self::Square, + api_enums::RoutableConnectors::Stax => Self::Stax, + api_enums::RoutableConnectors::Stripe => Self::Stripe, + api_enums::RoutableConnectors::Trustpay => Self::Trustpay, + api_enums::RoutableConnectors::Tsys => Self::Tsys, + api_enums::RoutableConnectors::Volt => Self::Volt, + api_enums::RoutableConnectors::Wise => Self::Wise, + api_enums::RoutableConnectors::Worldline => Self::Worldline, + api_enums::RoutableConnectors::Worldpay => Self::Worldpay, + api_enums::RoutableConnectors::Zen => Self::Zen, + } + } +} diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f7831465e1ce..794180e2112e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1,6 +1,7 @@ use common_utils::{ext_traits::ValueExt, pii}; use error_stack::{report, ResultExt}; use masking::ExposeInterface; +use router_env::{instrument, tracing}; use super::helpers; use crate::{ @@ -20,6 +21,7 @@ use crate::{ utils::OptionExt, }; +#[instrument(skip_all)] pub async fn save_payment_method( state: &AppState, connector: &api::ConnectorData, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 2fcd792eca83..6c6b4ae9339f 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -346,7 +346,7 @@ pub fn payments_to_payments_response( connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, connector_http_status_code: Option, external_latency: Option, - is_latency_header_enabled: Option, + _is_latency_header_enabled: Option, ) -> RouterResponse where Op: Debug, @@ -451,23 +451,17 @@ where payment_confirm_source.to_string(), )) } - if Some(true) == is_latency_header_enabled { - headers.extend( - external_latency - .map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())]) - .unwrap_or_default(), - ); - } + + headers.extend( + external_latency + .map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())]) + .unwrap_or_default(), + ); + let output = Ok(match payment_request { Some(_request) => { - if payments::is_start_pay(&operation) - && payment_data - .connector_response - .authentication_data - .is_some() - { - let redirection_data = payment_data - .connector_response + if payments::is_start_pay(&operation) && payment_attempt.authentication_data.is_some() { + let redirection_data = payment_attempt .authentication_data .get_required_value("redirection_data")?; @@ -523,16 +517,15 @@ where display_to_timestamp: wait_screen_data.display_to_timestamp, } })) - .or(payment_data - .connector_response - .authentication_data - .map(|_| api_models::payments::NextActionData::RedirectToUrl { + .or(payment_attempt.authentication_data.as_ref().map(|_| { + api_models::payments::NextActionData::RedirectToUrl { redirect_to_url: helpers::create_startpay_url( server, &payment_attempt, &payment_intent, ), - })); + } + })); }; // next action check for third party sdk session (for ex: Apple pay through trustpay has third party sdk session response) @@ -1056,7 +1049,7 @@ impl TryFrom> for types::PaymentsSyncData } None => types::ResponseId::NoResponseId, }, - encoded_data: payment_data.connector_response.encoded_data, + encoded_data: payment_data.payment_attempt.encoded_data, capture_method: payment_data.payment_attempt.capture_method, connector_meta: payment_data.payment_attempt.connector_metadata, sync_type: match payment_data.multiple_capture_data { @@ -1356,7 +1349,7 @@ impl TryFrom> for types::CompleteAuthoriz browser_info, email: payment_data.email, payment_method_data: payment_data.payment_method_data, - connector_transaction_id: payment_data.connector_response.connector_transaction_id, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, redirect_response, connector_meta: payment_data.payment_attempt.connector_metadata, }) diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index ddb2a017e35a..f1136a35a65a 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -112,7 +112,7 @@ where // Validate create request let (payout_id, payout_method_data) = - validator::validate_create_request(&state, &merchant_account, &req).await?; + validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries let mut payout_data = payout_create_db_entries( @@ -403,6 +403,7 @@ pub async fn payouts_fulfill_core( &payout_attempt.merchant_id, &payout_attempt.payout_id, Some(&payout_data.payouts.payout_type), + &key_store, ) .await? .get_required_value("payout_method_data")?, @@ -458,6 +459,7 @@ pub async fn call_connector_payout( &payout_attempt.merchant_id, &payout_attempt.payout_id, Some(&payouts.payout_type), + key_store, ) .await? .get_required_value("payout_method_data")?, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9890cd9d5efd..39079ea36cd6 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -28,6 +28,7 @@ use crate::{ utils::{self, OptionExt}, }; +#[allow(clippy::too_many_arguments)] pub async fn make_payout_method_data<'a>( state: &'a AppState, payout_method_data: Option<&api::PayoutMethodData>, @@ -36,6 +37,7 @@ pub async fn make_payout_method_data<'a>( merchant_id: &str, payout_id: &str, payout_type: Option<&api_enums::PayoutType>, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { let db = &*state.store; let hyperswitch_token = if let Some(payout_token) = payout_token { @@ -67,14 +69,16 @@ pub async fn make_payout_method_data<'a>( match (payout_method_data.to_owned(), hyperswitch_token) { // Get operation (None, Some(payout_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payout_method_data_from_temporary_locker( - state, - &payout_token, - ) - .await - .attach_printable( - "Payout method for given token not found or there was a problem fetching it", - )?; + let (pm, supplementary_data) = + vault::Vault::get_payout_method_data_from_temporary_locker( + state, + &payout_token, + merchant_key_store, + ) + .await + .attach_printable( + "Payout method for given token not found or there was a problem fetching it", + )?; utils::when( supplementary_data .customer_id @@ -93,6 +97,7 @@ pub async fn make_payout_method_data<'a>( payout_token.to_owned(), payout_method, Some(customer_id.to_owned()), + merchant_key_store, ) .await?; diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index c815d91e41dd..3793ee523dc3 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -57,6 +57,7 @@ pub async fn validate_create_request( state: &AppState, merchant_account: &domain::MerchantAccount, req: &payouts::PayoutCreateRequest, + merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(String, Option)> { let merchant_id = &merchant_account.merchant_id; @@ -103,6 +104,7 @@ pub async fn validate_create_request( &merchant_account.merchant_id, payout_id.as_ref(), req.payout_type.as_ref(), + merchant_key_store, ) .await? } diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index fcda3c8daf03..a42e46ca62d5 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -476,14 +476,13 @@ pub async fn sync_refund_with_gateway( pub async fn refund_update_core( state: AppState, merchant_account: domain::MerchantAccount, - refund_id: &str, req: refunds::RefundUpdateRequest, ) -> RouterResponse { let db = state.store.as_ref(); let refund = db .find_refund_by_merchant_id_refund_id( &merchant_account.merchant_id, - refund_id, + &req.refund_id, merchant_account.storage_scheme, ) .await @@ -501,7 +500,9 @@ pub async fn refund_update_core( ) .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("Unable to update refund with refund_id: {refund_id}"))?; + .attach_printable_lazy(|| { + format!("Unable to update refund with refund_id: {}", req.refund_id) + })?; Ok(services::ApplicationResponse::Json(response.foreign_into())) } @@ -698,7 +699,7 @@ pub async fn refund_list( pub async fn refund_filter_list( state: AppState, merchant_account: domain::MerchantAccount, - req: api_models::refunds::TimeRange, + req: api_models::payments::TimeRange, ) -> RouterResponse { let db = state.store; let filter_list = db diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs new file mode 100644 index 000000000000..723611ed5009 --- /dev/null +++ b/crates/router/src/core/routing.rs @@ -0,0 +1,713 @@ +pub mod helpers; +pub mod transformers; + +use api_models::routing::{self as routing_types, RoutingAlgorithmId}; +#[cfg(feature = "business_profile_routing")] +use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +#[cfg(not(feature = "business_profile_routing"))] +use common_utils::ext_traits::{Encode, StringExt}; +#[cfg(not(feature = "business_profile_routing"))] +use diesel_models::configs; +#[cfg(feature = "business_profile_routing")] +use diesel_models::routing_algorithm::RoutingAlgorithm; +use error_stack::{IntoReport, ResultExt}; +use rustc_hash::FxHashSet; + +#[cfg(feature = "business_profile_routing")] +use crate::core::utils::validate_and_get_business_profile; +#[cfg(feature = "business_profile_routing")] +use crate::types::transformers::{ForeignInto, ForeignTryInto}; +use crate::{ + consts, + core::errors::{RouterResponse, StorageErrorExt}, + routes::AppState, + types::domain, + utils::{self, OptionExt, ValueExt}, +}; +#[cfg(not(feature = "business_profile_routing"))] +use crate::{core::errors, services::api as service_api, types::storage}; +#[cfg(feature = "business_profile_routing")] +use crate::{errors, services::api as service_api}; + +pub async fn retrieve_merchant_routing_dictionary( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, +) -> RouterResponse { + #[cfg(feature = "business_profile_routing")] + { + let routing_metadata = state + .store + .list_routing_algorithm_metadata_by_merchant_id( + &merchant_account.merchant_id, + i64::from(query_params.limit.unwrap_or_default()), + i64::from(query_params.offset.unwrap_or_default()), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + let result = routing_metadata + .into_iter() + .map(ForeignInto::foreign_into) + .collect::>(); + + Ok(service_api::ApplicationResponse::Json( + routing_types::RoutingKind::RoutingAlgorithm(result), + )) + } + #[cfg(not(feature = "business_profile_routing"))] + Ok(service_api::ApplicationResponse::Json( + routing_types::RoutingKind::Config( + helpers::get_merchant_routing_dictionary( + state.store.as_ref(), + &merchant_account.merchant_id, + ) + .await?, + ), + )) +} + +pub async fn create_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + request: routing_types::RoutingConfigRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + + let name = request + .name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "name" }) + .attach_printable("Name of config not given")?; + + let description = request + .description + .get_required_value("description") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "description", + }) + .attach_printable("Description of config not given")?; + + let algorithm = request + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Algorithm of config not given")?; + + let algorithm_id = common_utils::generate_id( + consts::ROUTING_CONFIG_ID_LENGTH, + &format!("routing_{}", &merchant_account.merchant_id), + ); + + #[cfg(feature = "business_profile_routing")] + { + let profile_id = request + .profile_id + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }) + .attach_printable("Profile_id not provided")?; + + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await?; + + helpers::validate_connectors_in_routing_config( + db, + &key_store, + &merchant_account.merchant_id, + &profile_id, + &algorithm, + ) + .await?; + + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id: algorithm_id.clone(), + profile_id, + merchant_id: merchant_account.merchant_id, + name: name.clone(), + description: Some(description.clone()), + kind: algorithm.get_kind().foreign_into(), + algorithm_data: serde_json::json!(algorithm), + created_at: timestamp, + modified_at: timestamp, + }; + let record = db + .insert_routing_algorithm(algo) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let new_record = record.foreign_into(); + + Ok(service_api::ApplicationResponse::Json(new_record)) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let algorithm_str = + utils::Encode::::encode_to_string_of_json(&algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing algorithm to string")?; + + let mut algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + utils::when( + merchant_dictionary.records.len() >= consts::MAX_ROUTING_CONFIGS_PER_MERCHANT, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!("Reached the maximum number of routing configs ({}), please delete some to create new ones", consts::MAX_ROUTING_CONFIGS_PER_MERCHANT), + }) + .into_report() + }, + )?; + let timestamp = common_utils::date_time::now_unix_timestamp(); + let records_are_empty = merchant_dictionary.records.is_empty(); + + let new_record = routing_types::RoutingDictionaryRecord { + id: algorithm_id.clone(), + name: name.clone(), + kind: algorithm.get_kind(), + description: description.clone(), + created_at: timestamp, + modified_at: timestamp, + }; + merchant_dictionary.records.push(new_record.clone()); + + let new_algorithm_config = configs::ConfigNew { + key: algorithm_id.clone(), + config: algorithm_str, + }; + + db.insert_config(new_algorithm_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to save new routing algorithm config to DB")?; + + if records_are_empty { + merchant_dictionary.active_id = Some(algorithm_id.clone()); + algorithm_ref.update_algorithm_id(algorithm_id); + helpers::update_merchant_active_algorithm_ref(db, &key_store, algorithm_ref).await?; + } + + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + + Ok(service_api::ApplicationResponse::Json(new_record)) + } +} + +pub async fn link_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, + algorithm_id: String, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let routing_algorithm = db + .find_routing_algorithm_by_algorithm_id_merchant_id( + &algorithm_id, + &merchant_account.merchant_id, + ) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let business_profile = validate_and_get_business_profile( + db, + Some(&routing_algorithm.profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: routing_algorithm.profile_id.clone(), + })?; + + let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + + utils::when( + routing_ref.algorithm_id == Some(algorithm_id.clone()), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + .into_report() + }, + )?; + + routing_ref.update_algorithm_id(algorithm_id); + helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) + .await?; + + Ok(service_api::ApplicationResponse::Json( + routing_algorithm.foreign_into(), + )) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let mut routing_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + + utils::when( + routing_ref.algorithm_id == Some(algorithm_id.clone()), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + .into_report() + }, + )?; + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let modified_at = common_utils::date_time::now_unix_timestamp(); + let record = merchant_dictionary + .records + .iter_mut() + .find(|rec| rec.id == algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Record with given ID not found for routing config activation")?; + + record.modified_at = modified_at; + merchant_dictionary.active_id = Some(record.id.clone()); + let response = record.clone(); + routing_ref.update_algorithm_id(algorithm_id); + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} + +pub async fn retrieve_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + algorithm_id: RoutingAlgorithmId, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let routing_algorithm = db + .find_routing_algorithm_by_algorithm_id_merchant_id( + &algorithm_id.0, + &merchant_account.merchant_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + validate_and_get_business_profile( + db, + Some(&routing_algorithm.profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let response = routing_algorithm + .foreign_try_into() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse routing algorithm")?; + Ok(service_api::ApplicationResponse::Json(response)) + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let record = merchant_dictionary + .records + .into_iter() + .find(|rec| rec.id == algorithm_id.0) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Algorithm with the given ID not found in the merchant dictionary")?; + + let algorithm_config = db + .find_config_by_key(&algorithm_id.0) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("Routing config not found in DB")?; + + let algorithm: routing_types::RoutingAlgorithm = algorithm_config + .config + .parse_struct("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error deserializing routing algorithm config")?; + + let response = routing_types::MerchantRoutingAlgorithm { + id: record.id, + name: record.name, + description: record.description, + algorithm, + created_at: record.created_at, + modified_at: record.modified_at, + }; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} +pub async fn unlink_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, + #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + #[cfg(feature = "business_profile_routing")] + { + let profile_id = request + .profile_id + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }) + .attach_printable("Profile_id not provided")?; + let business_profile = + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await?; + match business_profile { + Some(business_profile) => { + let routing_algo_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from merchant account", + )? + .unwrap_or_default(); + + let timestamp = common_utils::date_time::now_unix_timestamp(); + + match routing_algo_ref.algorithm_id { + Some(algorithm_id) => { + let routing_algorithm: routing_types::RoutingAlgorithmRef = + routing_types::RoutingAlgorithmRef { + algorithm_id: None, + timestamp, + config_algo_id: routing_algo_ref.config_algo_id.clone(), + surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id, + }; + + let record = db + .find_routing_algorithm_by_profile_id_algorithm_id( + &profile_id, + &algorithm_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + let response = record.foreign_into(); + helpers::update_business_profile_active_algorithm_ref( + db, + business_profile, + routing_algorithm, + ) + .await?; + Ok(service_api::ApplicationResponse::Json(response)) + } + None => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + }) + .into_report()?, + } + } + None => Err(errors::ApiErrorResponse::InvalidRequestData { + message: "The business_profile is not present".to_string(), + } + .into()), + } + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let mut merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let routing_algo_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + let timestamp = common_utils::date_time::now_unix_timestamp(); + + utils::when(routing_algo_ref.algorithm_id.is_none(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + }) + .into_report() + })?; + let routing_algorithm: routing_types::RoutingAlgorithmRef = + routing_types::RoutingAlgorithmRef { + algorithm_id: None, + timestamp, + config_algo_id: routing_algo_ref.config_algo_id.clone(), + surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id, + }; + + let active_algorithm_id = merchant_dictionary + .active_id + .or(routing_algo_ref.algorithm_id.clone()) + .ok_or(errors::ApiErrorResponse::PreconditionFailed { + // When the merchant_dictionary doesn't have any active algorithm and merchant_account doesn't have any routing_algorithm configured + message: "Algorithm is already inactive".to_string(), + }) + .into_report()?; + + let record = merchant_dictionary + .records + .iter_mut() + .find(|rec| rec.id == active_algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("Record with the given ID not found for de-activation")?; + + let response = record.clone(); + + merchant_dictionary.active_id = None; + + helpers::update_merchant_routing_dictionary( + db, + &merchant_account.merchant_id, + merchant_dictionary, + ) + .await?; + + let ref_value = + Encode::::encode_to_value(&routing_algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed converting routing algorithm ref to json value")?; + + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + locker_id: None, + metadata: None, + routing_algorithm: Some(ref_value), + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + db.update_specific_fields_in_merchant( + &key_store.merchant_id, + merchant_account_update, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in merchant account")?; + + Ok(service_api::ApplicationResponse::Json(response)) + } +} + +pub async fn update_default_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + updated_config: Vec, +) -> RouterResponse> { + let db = state.store.as_ref(); + let default_config = + helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; + + utils::when(default_config.len() != updated_config.len(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "current config and updated config have different lengths".to_string(), + }) + .into_report() + })?; + + let existing_set: FxHashSet = + FxHashSet::from_iter(default_config.iter().map(|c| c.to_string())); + let updated_set: FxHashSet = + FxHashSet::from_iter(updated_config.iter().map(|c| c.to_string())); + + let symmetric_diff: Vec = existing_set + .symmetric_difference(&updated_set) + .cloned() + .collect(); + + utils::when(!symmetric_diff.is_empty(), || { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector mismatch between old and new configs ({})", + symmetric_diff.join(", ") + ), + }) + .into_report() + })?; + + helpers::update_merchant_default_config( + db, + &merchant_account.merchant_id, + updated_config.clone(), + ) + .await?; + + Ok(service_api::ApplicationResponse::Json(updated_config)) +} + +pub async fn retrieve_default_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse> { + let db = state.store.as_ref(); + + helpers::get_merchant_default_config(db, &merchant_account.merchant_id) + .await + .map(service_api::ApplicationResponse::Json) +} + +pub async fn retrieve_linked_routing_config( + state: AppState, + merchant_account: domain::MerchantAccount, + #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery, +) -> RouterResponse { + let db = state.store.as_ref(); + + #[cfg(feature = "business_profile_routing")] + { + let business_profiles = if let Some(profile_id) = query_params.profile_id { + validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) + .await? + .map(|profile| vec![profile]) + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id, + })? + } else { + db.list_business_profile_by_merchant_id(&merchant_account.merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)? + }; + + let mut active_algorithms = Vec::new(); + + for business_profile in business_profiles { + let routing_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from merchant account", + )? + .unwrap_or_default(); + + if let Some(algorithm_id) = routing_ref.algorithm_id { + let record = db + .find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &algorithm_id, + &business_profile.profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + active_algorithms.push(record.foreign_into()); + } + } + + Ok(service_api::ApplicationResponse::Json( + routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms), + )) + } + #[cfg(not(feature = "business_profile_routing"))] + { + let merchant_dictionary = + helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; + + let algorithm = if let Some(algorithm_id) = merchant_dictionary.active_id { + let record = merchant_dictionary + .records + .into_iter() + .find(|rec| rec.id == algorithm_id) + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound) + .into_report() + .attach_printable("record for active algorithm not found in merchant dictionary")?; + + let config = db + .find_config_by_key(&algorithm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error finding routing config in db")?; + + let the_algorithm: routing_types::RoutingAlgorithm = config + .config + .parse_struct("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse routing algorithm")?; + + Some(routing_types::MerchantRoutingAlgorithm { + id: record.id, + name: record.name, + description: record.description, + algorithm: the_algorithm, + created_at: record.created_at, + modified_at: record.modified_at, + }) + } else { + None + }; + + let response = routing_types::LinkedRoutingConfigRetrieveResponse::MerchantAccountBased( + routing_types::RoutingRetrieveResponse { algorithm }, + ); + + Ok(service_api::ApplicationResponse::Json(response)) + } +} diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs new file mode 100644 index 000000000000..6eec39f53bc6 --- /dev/null +++ b/crates/router/src/core/routing/helpers.rs @@ -0,0 +1,479 @@ +//! Analysis for usage of all helper functions for use case of routing +//! +//! Functions that are used to perform the retrieval of merchant's +//! routing dict, configs, defaults +use api_models::routing as routing_types; +use common_utils::ext_traits::Encode; +use diesel_models::{ + business_profile::{BusinessProfile, BusinessProfileUpdateInternal}, + configs, +}; +use error_stack::ResultExt; +use rustc_hash::FxHashSet; + +use crate::{ + core::errors::{self, RouterResult}, + db::StorageInterface, + types::{domain, storage}, + utils::{self, StringExt}, +}; + +/// provides the complete merchant routing dictionary that is basically a list of all the routing +/// configs a merchant configured with an active_id field that specifies the current active routing +/// config +pub async fn get_merchant_routing_dictionary( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult { + let key = get_routing_dictionary_key(merchant_id); + let maybe_dict = db.find_config_by_key(&key).await; + + match maybe_dict { + Ok(config) => config + .config + .parse_struct("RoutingDictionary") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant routing dictionary has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_dictionary = routing_types::RoutingDictionary { + merchant_id: merchant_id.to_string(), + active_id: None, + records: Vec::new(), + }; + + let serialized = + utils::Encode::::encode_to_string_of_json( + &new_dictionary, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing newly created merchant dictionary")?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting new routing dictionary for merchant")?; + + Ok(new_dictionary) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching routing dictionary for merchant"), + } +} + +/// Provides us with all the configured configs of the Merchant in the ascending time configured +/// manner and chooses the first of them +pub async fn get_merchant_default_config( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult> { + let key = get_default_config_key(merchant_id); + let maybe_config = db.find_config_by_key(&key).await; + + match maybe_config { + Ok(config) => config + .config + .parse_struct("Vec") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant default config has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_config_conns = Vec::::new(); + let serialized = + utils::Encode::>::encode_to_string_of_json( + &new_config_conns, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Error while creating and serializing new merchant default config", + )?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting new default routing config into DB")?; + + Ok(new_config_conns) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching default config for merchant"), + } +} + +/// Merchant's already created config can be updated and this change will be reflected +/// in DB as well for the particular updated config +pub async fn update_merchant_default_config( + db: &dyn StorageInterface, + merchant_id: &str, + connectors: Vec, +) -> RouterResult<()> { + let key = get_default_config_key(merchant_id); + let config_str = + Encode::>::encode_to_string_of_json( + &connectors, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize merchant default routing config during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(config_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating the default routing config in DB")?; + + Ok(()) +} + +pub async fn update_merchant_routing_dictionary( + db: &dyn StorageInterface, + merchant_id: &str, + dictionary: routing_types::RoutingDictionary, +) -> RouterResult<()> { + let key = get_routing_dictionary_key(merchant_id); + let dictionary_str = + Encode::::encode_to_string_of_json(&dictionary) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing dictionary during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(dictionary_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error saving routing dictionary to DB")?; + + Ok(()) +} + +pub async fn update_routing_algorithm( + db: &dyn StorageInterface, + algorithm_id: String, + algorithm: routing_types::RoutingAlgorithm, +) -> RouterResult<()> { + let algorithm_str = + Encode::::encode_to_string_of_json(&algorithm) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize routing algorithm to string")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(algorithm_str), + }; + + db.update_config_by_key(&algorithm_id, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating the routing algorithm in DB")?; + + Ok(()) +} + +/// This will help make one of all configured algorithms to be in active state for a particular +/// merchant +pub async fn update_merchant_active_algorithm_ref( + db: &dyn StorageInterface, + key_store: &domain::MerchantKeyStore, + algorithm_id: routing_types::RoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_value = Encode::::encode_to_value(&algorithm_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed converting routing algorithm ref to json value")?; + + let merchant_account_update = storage::MerchantAccountUpdate::Update { + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + locker_id: None, + metadata: None, + routing_algorithm: Some(ref_value), + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + default_profile: None, + payment_link_config: None, + }; + + db.update_specific_fields_in_merchant( + &key_store.merchant_id, + merchant_account_update, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in merchant account")?; + + Ok(()) +} + +pub async fn update_business_profile_active_algorithm_ref( + db: &dyn StorageInterface, + current_business_profile: BusinessProfile, + algorithm_id: routing_types::RoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_val = Encode::::encode_to_value(&algorithm_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert routing ref to value")?; + + let business_profile_update = BusinessProfileUpdateInternal { + profile_name: None, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + routing_algorithm: Some(ref_val), + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + applepay_verified_domains: None, + modified_at: None, + is_recon_enabled: None, + }; + db.update_business_profile_by_profile_id(current_business_profile, business_profile_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref in business profile")?; + Ok(()) +} + +pub async fn get_merchant_connector_agnostic_mandate_config( + db: &dyn StorageInterface, + merchant_id: &str, +) -> RouterResult> { + let key = get_pg_agnostic_mandate_config_key(merchant_id); + let maybe_config = db.find_config_by_key(&key).await; + + match maybe_config { + Ok(config) => config + .config + .parse_struct("Vec") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("pg agnostic mandate config has invalid structure"), + + Err(e) if e.current_context().is_db_not_found() => { + let new_mandate_config: Vec = Vec::new(); + + let serialized = + utils::Encode::>::encode_to_string_of_json( + &new_mandate_config, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error serializing newly created pg agnostic mandate config")?; + + let new_config = configs::ConfigNew { + key, + config: serialized, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting new pg agnostic mandate config in db")?; + + Ok(new_mandate_config) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching pg agnostic mandate config for merchant from db"), + } +} + +pub async fn update_merchant_connector_agnostic_mandate_config( + db: &dyn StorageInterface, + merchant_id: &str, + mandate_config: Vec, +) -> RouterResult> { + let key = get_pg_agnostic_mandate_config_key(merchant_id); + let mandate_config_str = + Encode::>::encode_to_string_of_json( + &mandate_config, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to serialize pg agnostic mandate config during update")?; + + let config_update = configs::ConfigUpdate::Update { + config: Some(mandate_config_str), + }; + + db.update_config_by_key(&key, config_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error saving pg agnostic mandate config to db")?; + + Ok(mandate_config) +} + +pub async fn validate_connectors_in_routing_config( + db: &dyn StorageInterface, + key_store: &domain::MerchantKeyStore, + merchant_id: &str, + profile_id: &str, + routing_algorithm: &routing_types::RoutingAlgorithm, +) -> RouterResult<()> { + let all_mcas = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_id.to_string(), + })?; + + #[cfg(feature = "connector_choice_mca_id")] + let name_mca_id_set = all_mcas + .iter() + .filter(|mca| mca.profile_id.as_deref() == Some(profile_id)) + .map(|mca| (&mca.connector_name, &mca.merchant_connector_id)) + .collect::>(); + + let name_set = all_mcas + .iter() + .filter(|mca| mca.profile_id.as_deref() == Some(profile_id)) + .map(|mca| &mca.connector_name) + .collect::>(); + + #[cfg(feature = "connector_choice_mca_id")] + let check_connector_choice = |choice: &routing_types::RoutableConnectorChoice| { + if let Some(ref mca_id) = choice.merchant_connector_id { + error_stack::ensure!( + name_mca_id_set.contains(&(&choice.connector.to_string(), mca_id)), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' and merchant connector account id '{}' not found for the given profile", + choice.connector, + mca_id, + ) + } + ); + } else { + error_stack::ensure!( + name_set.contains(&choice.connector.to_string()), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' not found for the given profile", + choice.connector, + ) + } + ); + } + + Ok(()) + }; + + #[cfg(not(feature = "connector_choice_mca_id"))] + let check_connector_choice = |choice: &routing_types::RoutableConnectorChoice| { + error_stack::ensure!( + name_set.contains(&choice.connector.to_string()), + errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "connector with name '{}' not found for the given profile", + choice.connector, + ) + } + ); + + Ok(()) + }; + + match routing_algorithm { + routing_types::RoutingAlgorithm::Single(choice) => { + check_connector_choice(choice)?; + } + + routing_types::RoutingAlgorithm::Priority(list) => { + for choice in list { + check_connector_choice(choice)?; + } + } + + routing_types::RoutingAlgorithm::VolumeSplit(splits) => { + for split in splits { + check_connector_choice(&split.connector)?; + } + } + + routing_types::RoutingAlgorithm::Advanced(program) => { + let check_connector_selection = + |selection: &routing_types::ConnectorSelection| -> RouterResult<()> { + match selection { + routing_types::ConnectorSelection::VolumeSplit(splits) => { + for split in splits { + check_connector_choice(&split.connector)?; + } + } + + routing_types::ConnectorSelection::Priority(list) => { + for choice in list { + check_connector_choice(choice)?; + } + } + } + + Ok(()) + }; + + check_connector_selection(&program.default_selection)?; + + for rule in &program.rules { + check_connector_selection(&rule.connector_selection)?; + } + } + } + + Ok(()) +} + +/// Provides the identifier for the specific merchant's routing_dictionary_key +#[inline(always)] +pub fn get_routing_dictionary_key(merchant_id: &str) -> String { + format!("routing_dict_{merchant_id}") +} + +/// Provides the identifier for the specific merchant's agnostic_mandate_config +#[inline(always)] +pub fn get_pg_agnostic_mandate_config_key(merchant_id: &str) -> String { + format!("pg_agnostic_mandate_{merchant_id}") +} + +/// Provides the identifier for the specific merchant's default_config +#[inline(always)] +pub fn get_default_config_key(merchant_id: &str) -> String { + format!("routing_default_{merchant_id}") +} +pub fn get_payment_config_routing_id(merchant_id: &str) -> String { + format!("payment_config_id_{merchant_id}") +} + +pub fn get_payment_method_surcharge_routing_id(merchant_id: &str) -> String { + format!("payment_method_surcharge_id_{merchant_id}") +} diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs new file mode 100644 index 000000000000..e5f1f1e1d5f0 --- /dev/null +++ b/crates/router/src/core/routing/transformers.rs @@ -0,0 +1,86 @@ +use api_models::routing::{ + MerchantRoutingAlgorithm, RoutingAlgorithm as Algorithm, RoutingAlgorithmKind, + RoutingDictionaryRecord, +}; +use common_utils::ext_traits::ValueExt; +use diesel_models::{ + enums as storage_enums, + routing_algorithm::{RoutingAlgorithm, RoutingProfileMetadata}, +}; + +use crate::{ + core::errors, + types::transformers::{ForeignFrom, ForeignInto, ForeignTryFrom}, +}; + +impl ForeignFrom for RoutingDictionaryRecord { + fn foreign_from(value: RoutingProfileMetadata) -> Self { + Self { + id: value.algorithm_id, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + name: value.name, + + kind: value.kind.foreign_into(), + description: value.description.unwrap_or_default(), + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + } + } +} + +impl ForeignFrom for RoutingDictionaryRecord { + fn foreign_from(value: RoutingAlgorithm) -> Self { + Self { + id: value.algorithm_id, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + name: value.name, + kind: value.kind.foreign_into(), + description: value.description.unwrap_or_default(), + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + } + } +} + +impl ForeignTryFrom for MerchantRoutingAlgorithm { + type Error = error_stack::Report; + + fn foreign_try_from(value: RoutingAlgorithm) -> Result { + Ok(Self { + id: value.algorithm_id, + name: value.name, + #[cfg(feature = "business_profile_routing")] + profile_id: value.profile_id, + description: value.description.unwrap_or_default(), + algorithm: value + .algorithm_data + .parse_value::("RoutingAlgorithm")?, + created_at: value.created_at.assume_utc().unix_timestamp(), + modified_at: value.modified_at.assume_utc().unix_timestamp(), + }) + } +} + +impl ForeignFrom for RoutingAlgorithmKind { + fn foreign_from(value: storage_enums::RoutingAlgorithmKind) -> Self { + match value { + storage_enums::RoutingAlgorithmKind::Single => Self::Single, + storage_enums::RoutingAlgorithmKind::Priority => Self::Priority, + storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, + storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced, + } + } +} + +impl ForeignFrom for storage_enums::RoutingAlgorithmKind { + fn foreign_from(value: RoutingAlgorithmKind) -> Self { + match value { + RoutingAlgorithmKind::Single => Self::Single, + RoutingAlgorithmKind::Priority => Self::Priority, + RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, + RoutingAlgorithmKind::Advanced => Self::Advanced, + } + } +} diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index fa700b4cd663..e643e0455b8b 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -1,5 +1,4 @@ pub mod utils; -use actix_web::web; use api_models::verifications::{self, ApplepayMerchantResponse}; use common_utils::{errors::CustomResult, ext_traits::Encode}; use error_stack::ResultExt; @@ -18,7 +17,7 @@ const APPLEPAY_INTERNAL_MERCHANT_NAME: &str = "Applepay_merchant"; pub async fn verify_merchant_creds_for_applepay( state: AppState, _req: &actix_web::HttpRequest, - body: web::Json, + body: verifications::ApplepayMerchantVerificationRequest, kms_config: &kms::KmsConfig, merchant_id: String, ) -> CustomResult< diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index eb2e19081ff3..ba4d7f6549e7 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -98,6 +98,7 @@ pub async fn payments_incoming_webhook_flow< }, services::AuthFlow::Merchant, consume_or_trigger_flow, + None, HeaderPayload::default(), ) .await; @@ -579,6 +580,7 @@ async fn bank_transfer_webhook_flow api::MerchantWebhookConfig { - std::collections::HashSet::from([ - api::IncomingWebhookEvent::PaymentIntentSuccess, - api::IncomingWebhookEvent::PaymentIntentFailure, - api::IncomingWebhookEvent::PaymentIntentProcessing, - api::IncomingWebhookEvent::PaymentIntentCancelled, - api::IncomingWebhookEvent::PaymentActionRequired, - api::IncomingWebhookEvent::RefundSuccess, - ]) -} - const IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = "irrelevant_payment_id_in_source_verification_flow"; const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = @@ -30,38 +20,40 @@ const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str = "irrelevant_connector_request_reference_id_in_source_verification_flow"; -/// Check whether the merchant has configured to process the webhook `event` for the `connector` +/// Check whether the merchant has configured to disable the webhook `event` for the `connector` /// First check for the key "whconf_{merchant_id}_{connector_id}" in redis, -/// if not found, fetch from configs table in database, if not found use default -pub async fn lookup_webhook_event( +/// if not found, fetch from configs table in database +pub async fn is_webhook_event_disabled( db: &dyn StorageInterface, connector_id: &str, merchant_id: &str, event: &api::IncomingWebhookEvent, ) -> bool { - let redis_key = format!("whconf_{merchant_id}_{connector_id}"); - let merchant_webhook_config_result = - get_and_deserialize_key(db, &redis_key, "MerchantWebhookConfig") - .await - .map(|h| &h | &default_webhook_config()); + let redis_key = format!("whconf_disabled_events_{merchant_id}_{connector_id}"); + let merchant_webhook_disable_config_result: CustomResult< + api::MerchantWebhookConfig, + redis_interface::errors::RedisError, + > = get_and_deserialize_key(db, &redis_key, "MerchantWebhookConfig").await; - match merchant_webhook_config_result { + match merchant_webhook_disable_config_result { Ok(merchant_webhook_config) => merchant_webhook_config.contains(event), Err(..) => { //if failed to fetch from redis. fetch from db and populate redis db.find_config_by_key(&redis_key) .await .map(|config| { - if let Ok(set) = - serde_json::from_str::(&config.config) - { - &set | &default_webhook_config() - } else { - default_webhook_config() + match serde_json::from_str::(&config.config) { + Ok(set) => set.contains(event), + Err(err) => { + logger::warn!(?err, "error while parsing merchant webhook config"); + false + } } }) - .unwrap_or_else(|_| default_webhook_config()) - .contains(event) + .unwrap_or_else(|err| { + logger::warn!(?err, "error while fetching merchant webhook config"); + false + }) } } } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index f5647b3d1778..9687f7f97c92 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -5,13 +5,13 @@ pub mod cache; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; pub mod customers; pub mod dispute; pub mod ephemeral_key; pub mod events; pub mod file; pub mod fraud_check; +pub mod gsm; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -24,6 +24,9 @@ pub mod payout_attempt; pub mod payouts; pub mod refund; pub mod reverse_lookup; +pub mod routing_algorithm; +pub mod user; +pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, @@ -50,7 +53,6 @@ pub trait StorageInterface: + api_keys::ApiKeyInterface + configs::ConfigInterface + capture::CaptureInterface - + connector_response::ConnectorResponseInterface + customers::CustomerInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface @@ -75,8 +77,13 @@ pub trait StorageInterface: + MasterKeyInterface + payment_link::PaymentLinkInterface + RedisConnInterface + + RequestIdStore + business_profile::BusinessProfileInterface + organization::OrganizationInterface + + routing_algorithm::RoutingAlgorithmInterface + + gsm::GsmInterface + + user::UserInterface + + user_role::UserRoleInterface + 'static { fn get_scheduler_db(&self) -> Box; @@ -116,6 +123,25 @@ impl StorageInterface for MockDb { } } +pub trait RequestIdStore { + fn add_request_id(&mut self, _request_id: String) {} + fn get_request_id(&self) -> Option { + None + } +} + +impl RequestIdStore for MockDb {} + +impl RequestIdStore for Store { + fn add_request_id(&mut self, request_id: String) { + self.request_id = Some(request_id) + } + + fn get_request_id(&self) -> Option { + self.request_id.clone() + } +} + pub async fn get_and_deserialize_key( db: &dyn StorageInterface, key: &str, diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 20f7bdb9120f..9244fc022d9e 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -763,7 +763,8 @@ impl AddressInterface for MockDb { .await .iter_mut() .find(|address| { - address.customer_id == customer_id && address.merchant_id == merchant_id + address.customer_id == Some(customer_id.to_string()) + && address.merchant_id == merchant_id }) .map(|a| { let address_updated = diff --git a/crates/router/src/db/cache.rs b/crates/router/src/db/cache.rs index 06ab85591a93..0688665f0c4c 100644 --- a/crates/router/src/db/cache.rs +++ b/crates/router/src/db/cache.rs @@ -100,9 +100,9 @@ where Ok(data) } -pub async fn publish_into_redact_channel<'a>( +pub async fn publish_into_redact_channel<'a, K: IntoIterator> + Send>( store: &dyn StorageInterface, - key: CacheKind<'a>, + keys: K, ) -> CustomResult { let redis_conn = store .get_redis_conn() @@ -111,10 +111,18 @@ pub async fn publish_into_redact_channel<'a>( )) .attach_printable("Failed to get redis connection")?; - redis_conn - .publish(consts::PUB_SUB_CHANNEL, key) - .await - .change_context(errors::StorageError::KVError) + let futures = keys.into_iter().map(|key| async { + redis_conn + .clone() + .publish(consts::PUB_SUB_CHANNEL, key) + .await + .change_context(errors::StorageError::KVError) + }); + + Ok(futures::future::try_join_all(futures) + .await? + .iter() + .sum::()) } pub async fn publish_and_redact<'a, T, F, Fut>( @@ -127,6 +135,21 @@ where Fut: futures::Future> + Send, { let data = fun().await?; - publish_into_redact_channel(store, key).await?; + publish_into_redact_channel(store, [key]).await?; + Ok(data) +} + +pub async fn publish_and_redact_multiple<'a, T, F, Fut, K>( + store: &dyn StorageInterface, + keys: K, + fun: F, +) -> CustomResult +where + F: FnOnce() -> Fut + Send, + Fut: futures::Future> + Send, + K: IntoIterator> + Send, +{ + let data = fun().await?; + publish_into_redact_channel(store, keys).await?; Ok(data) } diff --git a/crates/router/src/db/connector_response.rs b/crates/router/src/db/connector_response.rs deleted file mode 100644 index 354231d136ec..000000000000 --- a/crates/router/src/db/connector_response.rs +++ /dev/null @@ -1,343 +0,0 @@ -use error_stack::{IntoReport, ResultExt}; -use router_env::{instrument, tracing}; - -use super::{MockDb, Store}; -use crate::{ - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums}, -}; - -#[async_trait::async_trait] -pub trait ConnectorResponseInterface { - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; - - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - payment_attempt: storage_type::ConnectorResponseUpdate, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult; -} - -#[cfg(not(feature = "kv_store"))] -mod storage { - use error_stack::IntoReport; - use router_env::{instrument, tracing}; - - use super::Store; - use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums}, - }; - - #[async_trait::async_trait] - impl super::ConnectorResponseInterface for Store { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - connector_response - .insert(&conn) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage_type::ConnectorResponse::find_by_payment_id_merchant_id_attempt_id( - &conn, - payment_id, - merchant_id, - attempt_id, - ) - .await - .map_err(Into::into) - .into_report() - } - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - this.update(&conn, connector_response_update) - .await - .map_err(Into::into) - .into_report() - } - } -} - -#[cfg(feature = "kv_store")] -mod storage { - - use diesel_models::enums as storage_enums; - use error_stack::{IntoReport, ResultExt}; - use redis_interface::HsetnxReply; - use router_env::{instrument, tracing}; - use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; - - use super::Store; - use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage::{self as storage_type, enums, kv}, - utils::db_utils, - }; - - #[async_trait::async_trait] - impl super::ConnectorResponseInterface for Store { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - connector_response: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => connector_response - .insert(&conn) - .await - .map_err(Into::into) - .into_report(), - storage_enums::MerchantStorageScheme::RedisKv => { - let merchant_id = &connector_response.merchant_id; - let payment_id = &connector_response.payment_id; - let attempt_id = &connector_response.attempt_id; - - let key = format!("mid_{merchant_id}_pid_{payment_id}"); - let field = format!("connector_resp_{merchant_id}_{payment_id}_{attempt_id}"); - - let created_connector_resp = storage_type::ConnectorResponse { - id: Default::default(), - payment_id: connector_response.payment_id.clone(), - merchant_id: connector_response.merchant_id.clone(), - attempt_id: connector_response.attempt_id.clone(), - created_at: connector_response.created_at, - modified_at: connector_response.modified_at, - connector_name: connector_response.connector_name.clone(), - connector_transaction_id: connector_response - .connector_transaction_id - .clone(), - authentication_data: connector_response.authentication_data.clone(), - encoded_data: connector_response.encoded_data.clone(), - updated_by: storage_scheme.to_string(), - }; - - let redis_entry = kv::TypedSql { - op: kv::DBOperation::Insert { - insertable: kv::Insertable::ConnectorResponse( - connector_response.clone(), - ), - }, - }; - - match kv_wrapper::( - self, - KvOperation::HSetNx(&field, &created_connector_resp, redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hsetnx() - { - Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { - entity: "address", - key: Some(key), - }) - .into_report(), - Ok(HsetnxReply::KeySet) => Ok(created_connector_resp), - Err(er) => Err(er).change_context(errors::StorageError::KVError), - } - } - } - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - payment_id: &str, - merchant_id: &str, - attempt_id: &str, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - let database_call = || async { - storage_type::ConnectorResponse::find_by_payment_id_merchant_id_attempt_id( - &conn, - payment_id, - merchant_id, - attempt_id, - ) - .await - .map_err(Into::into) - .into_report() - }; - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => database_call().await, - storage_enums::MerchantStorageScheme::RedisKv => { - let key = format!("mid_{merchant_id}_pid_{payment_id}"); - let field = format!("connector_resp_{merchant_id}_{payment_id}_{attempt_id}"); - - db_utils::try_redis_get_else_try_database_get( - async { - kv_wrapper( - self, - KvOperation::::HGet(&field), - key, - ) - .await? - .try_into_hget() - }, - database_call, - ) - .await - } - } - } - - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - match storage_scheme { - storage_enums::MerchantStorageScheme::PostgresOnly => this - .update(&conn, connector_response_update) - .await - .map_err(Into::into) - .into_report(), - storage_enums::MerchantStorageScheme::RedisKv => { - let key = format!("mid_{}_pid_{}", this.merchant_id, this.payment_id); - let updated_connector_response = connector_response_update - .clone() - .apply_changeset(this.clone()); - let redis_value = serde_json::to_string(&updated_connector_response) - .into_report() - .change_context(errors::StorageError::KVError)?; - let field = format!( - "connector_resp_{}_{}_{}", - &updated_connector_response.merchant_id, - &updated_connector_response.payment_id, - &updated_connector_response.attempt_id - ); - - let redis_entry = kv::TypedSql { - op: kv::DBOperation::Update { - updatable: kv::Updateable::ConnectorResponseUpdate( - kv::ConnectorResponseUpdateMems { - orig: this, - update_data: connector_response_update, - }, - ), - }, - }; - - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::( - (&field, redis_value), - redis_entry, - ), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - - Ok(updated_connector_response) - } - } - } - } -} - -#[async_trait::async_trait] -impl ConnectorResponseInterface for MockDb { - #[instrument(skip_all)] - async fn insert_connector_response( - &self, - new: storage_type::ConnectorResponseNew, - storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let mut connector_response = self.connector_response.lock().await; - let response = storage_type::ConnectorResponse { - id: connector_response - .len() - .try_into() - .into_report() - .change_context(errors::StorageError::MockDbError)?, - payment_id: new.payment_id, - merchant_id: new.merchant_id, - attempt_id: new.attempt_id, - created_at: new.created_at, - modified_at: new.modified_at, - connector_name: new.connector_name, - connector_transaction_id: new.connector_transaction_id, - authentication_data: new.authentication_data, - encoded_data: new.encoded_data, - updated_by: storage_scheme.to_string(), - }; - connector_response.push(response.clone()); - Ok(response) - } - - #[instrument(skip_all)] - async fn find_connector_response_by_payment_id_merchant_id_attempt_id( - &self, - _payment_id: &str, - _merchant_id: &str, - _attempt_id: &str, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - // [#172]: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - // safety: interface only used for testing - #[allow(clippy::unwrap_used)] - async fn update_connector_response( - &self, - this: storage_type::ConnectorResponse, - connector_response_update: storage_type::ConnectorResponseUpdate, - _storage_scheme: enums::MerchantStorageScheme, - ) -> CustomResult { - let mut connector_response = self.connector_response.lock().await; - let response = connector_response - .iter_mut() - .find(|item| item.id == this.id) - .unwrap(); - *response = connector_response_update.apply_changeset(response.clone()); - Ok(response.clone()) - } -} diff --git a/crates/router/src/db/gsm.rs b/crates/router/src/db/gsm.rs new file mode 100644 index 000000000000..b623bdc2bcf5 --- /dev/null +++ b/crates/router/src/db/gsm.rs @@ -0,0 +1,180 @@ +use diesel_models::gsm as storage; +use error_stack::IntoReport; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait GsmInterface { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult; + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult; + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl GsmInterface for Store { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + rule.insert(&conn).await.map_err(Into::into).into_report() + } + + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::GatewayStatusMap::retrieve_decision( + &conn, connector, flow, sub_flow, code, message, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::GatewayStatusMap::find(&conn, connector, flow, sub_flow, code, message) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::GatewayStatusMap::update(&conn, connector, flow, sub_flow, code, message, data) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::GatewayStatusMap::delete(&conn, connector, flow, sub_flow, code, message) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl GsmInterface for MockDb { + async fn add_gsm_rule( + &self, + _rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_gsm_decision( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + _data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_gsm_rule( + &self, + _connector: String, + _flow: String, + _sub_flow: String, + _code: String, + _message: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index e0bff7d9069c..0d3ce99b948d 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -399,19 +399,17 @@ async fn publish_and_redact_merchant_account_cache( store: &dyn super::StorageInterface, merchant_account: &storage::MerchantAccount, ) -> CustomResult<(), errors::StorageError> { - super::cache::publish_into_redact_channel( - store, - CacheKind::Accounts(merchant_account.merchant_id.as_str().into()), - ) - .await?; - merchant_account + let publishable_key = merchant_account .publishable_key .as_ref() - .async_map(|pub_key| async { - super::cache::publish_into_redact_channel(store, CacheKind::Accounts(pub_key.into())) - .await - }) - .await - .transpose()?; + .map(|publishable_key| CacheKind::Accounts(publishable_key.into())); + + let mut cache_keys = vec![CacheKind::Accounts( + merchant_account.merchant_id.as_str().into(), + )]; + + cache_keys.extend(publishable_key.into_iter()); + + super::cache::publish_into_redact_channel(store, cache_keys).await?; Ok(()) } diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 9ff3f5121082..ecf52531f28a 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -290,21 +290,40 @@ impl MerchantConnectorAccountInterface for Store { merchant_connector_id: &str, key_store: &domain::MerchantKeyStore, ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( - &conn, - merchant_id, - merchant_connector_id, - ) - .await - .map_err(Into::into) - .into_report() - .async_and_then(|item| async { - item.convert(key_store.key.get_inner()) + let find_call = || async { + let conn = connection::pg_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( + &conn, + merchant_id, + merchant_connector_id, + ) + .await + .map_err(Into::into) + .into_report() + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert(key_store.key.get_inner()) .await .change_context(errors::StorageError::DecryptionError) - }) - .await + } + + #[cfg(feature = "accounts_cache")] + { + super::cache::get_or_populate_in_memory( + self, + &format!("{}_{}", merchant_id, merchant_connector_id), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await? + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + } } async fn insert_merchant_connector_account( @@ -367,6 +386,9 @@ impl MerchantConnectorAccountInterface for Store { "profile_id".to_string(), ))?; + let _merchant_id = this.merchant_id.clone(); + let _merchant_connector_id = this.merchant_connector_id.clone(); + let update_call = || async { let conn = connection::pg_connection_write(self).await?; Conversion::convert(this) @@ -386,9 +408,17 @@ impl MerchantConnectorAccountInterface for Store { #[cfg(feature = "accounts_cache")] { - super::cache::publish_and_redact( + // Redact both the caches as any one or both might be used because of backwards compatibility + super::cache::publish_and_redact_multiple( self, - cache::CacheKind::Accounts(format!("{}_{}", _profile_id, _connector_name).into()), + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id, _connector_name).into(), + ), + cache::CacheKind::Accounts( + format!("{}_{}", _merchant_id, _merchant_connector_id).into(), + ), + ], update_call, ) .await diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index a6133edad673..c9b9f8ac55f5 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -78,7 +78,7 @@ pub trait RefundInterface { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult; @@ -232,7 +232,7 @@ mod storage { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -707,7 +707,7 @@ mod storage { async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -979,7 +979,7 @@ impl RefundInterface for MockDb { async fn filter_refund_by_meta_constraints( &self, _merchant_id: &str, - refund_details: &api_models::refunds::TimeRange, + refund_details: &api_models::payments::TimeRange, _storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult { let refunds = self.refunds.lock().await; diff --git a/crates/router/src/db/routing_algorithm.rs b/crates/router/src/db/routing_algorithm.rs new file mode 100644 index 000000000000..58550b2f01fa --- /dev/null +++ b/crates/router/src/db/routing_algorithm.rs @@ -0,0 +1,199 @@ +use diesel_models::routing_algorithm as routing_storage; +use error_stack::IntoReport; +use storage_impl::mock_db::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +type StorageResult = CustomResult; + +#[async_trait::async_trait] +pub trait RoutingAlgorithmInterface { + async fn insert_routing_algorithm( + &self, + routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult; + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> StorageResult; + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult; + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult; + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult>; + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult>; +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for Store { + async fn insert_routing_algorithm( + &self, + routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_algorithm + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_by_algorithm_id_profile_id( + &conn, + algorithm_id, + profile_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_by_algorithm_id_merchant_id( + &conn, + algorithm_id, + merchant_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> StorageResult { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::find_metadata_by_algorithm_id_profile_id( + &conn, + algorithm_id, + profile_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::list_metadata_by_profile_id( + &conn, profile_id, limit, offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> StorageResult> { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::list_metadata_by_merchant_id( + &conn, + merchant_id, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for MockDb { + async fn insert_routing_algorithm( + &self, + _routing_algorithm: routing_storage::RoutingAlgorithm, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + _profile_id: &str, + _algorithm_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + _algorithm_id: &str, + _merchant_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + _algorithm_id: &str, + _profile_id: &str, + ) -> StorageResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + _profile_id: &str, + _limit: i64, + _offset: i64, + ) -> StorageResult> { + Err(errors::StorageError::MockDbError)? + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + _merchant_id: &str, + _limit: i64, + _offset: i64, + ) -> StorageResult> { + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs new file mode 100644 index 000000000000..6bb1d9e50b6a --- /dev/null +++ b/crates/router/src/db/user.rs @@ -0,0 +1,265 @@ +use diesel_models::user as storage; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait UserInterface { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult; + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult; + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult; + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult; + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl UserInterface for Store { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + user_data + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_by_user_email(&conn, user_email) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_by_user_id(&conn, user_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::update_by_user_id(&conn, user_id, user) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::delete_by_user_id(&conn, user_id) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl UserInterface for MockDb { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + let mut users = self.users.lock().await; + if users + .iter() + .any(|user| user.email == user_data.email || user.user_id == user_data.user_id) + { + Err(errors::StorageError::DuplicateValue { + entity: "email or user_id", + key: None, + })? + } + let time_now = common_utils::date_time::now(); + let user = storage::User { + id: users + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: user_data.user_id, + email: user_data.email, + name: user_data.name, + password: user_data.password, + is_verified: user_data.is_verified, + created_at: user_data.created_at.unwrap_or(time_now), + last_modified_at: user_data.created_at.unwrap_or(time_now), + }; + users.push(user.clone()); + Ok(user) + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + let users = self.users.lock().await; + let user_email_pii: common_utils::pii::Email = user_email + .to_string() + .try_into() + .map_err(|_| errors::StorageError::MockDbError)?; + users + .iter() + .find(|user| user.email == user_email_pii) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for email = {user_email}" + )) + .into(), + ) + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + let users = self.users.lock().await; + users + .iter() + .find(|user| user.user_id == user_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + update_user: storage::UserUpdate, + ) -> CustomResult { + let mut users = self.users.lock().await; + users + .iter_mut() + .find(|user| user.user_id == user_id) + .map(|user| { + *user = match &update_user { + storage::UserUpdate::VerifyUser => storage::User { + is_verified: true, + ..user.to_owned() + }, + storage::UserUpdate::AccountUpdate { + name, + password, + is_verified, + } => storage::User { + name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), + password: password.clone().unwrap_or(user.password.clone()), + is_verified: is_verified.unwrap_or(user.is_verified), + ..user.to_owned() + }, + }; + user.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let mut users = self.users.lock().await; + let user_index = users + .iter() + .position(|user| user.user_id == user_id) + .ok_or(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )))?; + users.remove(user_index); + Ok(true) + } +} +#[cfg(feature = "kafka_events")] +#[async_trait::async_trait] +impl UserInterface for super::KafkaStore { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + self.diesel_store.insert_user(user_data).await + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_email(user_email).await + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_id(user_id).await + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_user_id(user_id, user) + .await + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.delete_user_by_user_id(user_id).await + } +} diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs new file mode 100644 index 000000000000..37e38e8afca7 --- /dev/null +++ b/crates/router/src/db/user_role.rs @@ -0,0 +1,255 @@ +use diesel_models::user_role as storage; +use error_stack::{IntoReport, ResultExt}; + +use super::MockDb; +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait UserRoleInterface { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult; + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult; + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult; + async fn delete_user_role(&self, user_id: &str) -> CustomResult; + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl UserRoleInterface for Store { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + user_role + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::find_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::update_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + update, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::delete_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::list_by_user_id(&conn, user_id.to_owned()) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl UserRoleInterface for MockDb { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + if user_roles + .iter() + .any(|user_role_inner| user_role_inner.user_id == user_role.user_id) + { + Err(errors::StorageError::DuplicateValue { + entity: "user_id", + key: None, + })? + } + let user_role = storage::UserRole { + id: user_roles + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: user_role.user_id, + merchant_id: user_role.merchant_id, + role_id: user_role.role_id, + status: user_role.status, + created_by: user_role.created_by, + created_at: user_role.created_at, + last_modified_at: user_role.last_modified_at, + last_modified_by: user_role.last_modified_by, + org_id: user_role.org_id, + }; + user_roles.push(user_role.clone()); + Ok(user_role) + } + + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + let user_roles = self.user_roles.lock().await; + user_roles + .iter() + .find(|user_role| user_role.user_id == user_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id}" + )) + .into(), + ) + } + + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + user_roles + .iter_mut() + .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id) + .map(|user_role| { + *user_role = match &update { + storage::UserRoleUpdate::UpdateRole { + role_id, + modified_by, + } => storage::UserRole { + role_id: role_id.to_string(), + last_modified_by: modified_by.to_string(), + ..user_role.to_owned() + }, + storage::UserRoleUpdate::UpdateStatus { + status, + modified_by, + } => storage::UserRole { + status: status.to_owned(), + last_modified_by: modified_by.to_owned(), + ..user_role.to_owned() + }, + }; + user_role.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } + + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + let mut user_roles = self.user_roles.lock().await; + let user_role_index = user_roles + .iter() + .position(|user_role| user_role.user_id == user_id) + .ok_or(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id}" + )))?; + user_roles.remove(user_role_index); + Ok(true) + } + + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + let user_roles = self.user_roles.lock().await; + + Ok(user_roles + .iter() + .cloned() + .filter_map(|ele| { + if ele.user_id == user_id { + return Some(ele); + } + None + }) + .collect()) + } +} + +#[cfg(feature = "kafka_events")] +#[async_trait::async_trait] +impl UserRoleInterface for super::KafkaStore { + async fn insert_user_role( + &self, + user_role: storage::UserRoleNew, + ) -> CustomResult { + self.diesel_store.insert_user_role(user_role).await + } + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: storage::UserRoleUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) + .await + } + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_role_by_user_id(user_id).await + } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + self.diesel_store.delete_user_role(user_id).await + } + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_user_id(user_id).await + } +} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 24315026e821..8576d014544b 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -1,9 +1,25 @@ +use actix_web::HttpRequest; +pub use common_utils::events::{ApiEventMetric, ApiEventsType}; +use common_utils::impl_misc_api_event_type; use router_env::{tracing_actix_web::RequestId, types::FlowMetric}; use serde::Serialize; use time::OffsetDateTime; use super::{EventType, RawEvent}; -use crate::services::authentication::AuthenticationType; +#[cfg(feature = "dummy_connector")] +use crate::routes::dummy_connector::types::{ + DummyConnectorPaymentCompleteRequest, DummyConnectorPaymentConfirmRequest, + DummyConnectorPaymentRequest, DummyConnectorPaymentResponse, + DummyConnectorPaymentRetrieveRequest, DummyConnectorRefundRequest, + DummyConnectorRefundResponse, DummyConnectorRefundRetrieveRequest, +}; +use crate::{ + core::payments::PaymentsRedirectResponseData, + services::{authentication::AuthenticationType, ApplicationResponse, PaymentLinkFormData}, + types::api::{ + AttachEvidenceRequest, Config, ConfigUpdate, CreateFileRequest, DisputeId, FileId, + }, +}; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct ApiEvent { @@ -15,11 +31,17 @@ pub struct ApiEvent { #[serde(flatten)] auth_type: AuthenticationType, request: serde_json::Value, + user_agent: Option, + ip_addr: Option, + url_path: String, response: Option, + #[serde(flatten)] + event_type: ApiEventsType, hs_latency: Option, } impl ApiEvent { + #[allow(clippy::too_many_arguments)] pub fn new( api_flow: &impl FlowMetric, request_id: &RequestId, @@ -29,6 +51,8 @@ impl ApiEvent { response: Option, hs_latency: Option, auth_type: AuthenticationType, + event_type: ApiEventsType, + http_req: &HttpRequest, ) -> Self { Self { api_flow: api_flow.to_string(), @@ -38,8 +62,18 @@ impl ApiEvent { status_code, request, response, - hs_latency, auth_type, + ip_addr: http_req + .connection_info() + .realip_remote_addr() + .map(ToOwned::to_owned), + user_agent: http_req + .headers() + .get("user-agent") + .and_then(|user_agent_value| user_agent_value.to_str().ok().map(ToOwned::to_owned)), + url_path: http_req.path().to_string(), + event_type, + hs_latency, } } } @@ -55,3 +89,35 @@ impl TryFrom for RawEvent { }) } } + +impl ApiEventMetric for ApplicationResponse { + fn get_api_event_type(&self) -> Option { + match self { + Self::Json(r) => r.get_api_event_type(), + Self::JsonWithHeaders((r, _)) => r.get_api_event_type(), + _ => None, + } + } +} +impl_misc_api_event_type!( + Config, + CreateFileRequest, + FileId, + AttachEvidenceRequest, + DisputeId, + PaymentLinkFormData, + PaymentsRedirectResponseData, + ConfigUpdate +); + +#[cfg(feature = "dummy_connector")] +impl_misc_api_event_type!( + DummyConnectorPaymentCompleteRequest, + DummyConnectorPaymentRequest, + DummyConnectorPaymentResponse, + DummyConnectorPaymentRetrieveRequest, + DummyConnectorPaymentConfirmRequest, + DummyConnectorRefundRetrieveRequest, + DummyConnectorRefundResponse, + DummyConnectorRefundRequest +); diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 11efec64055b..38efe8b75134 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -141,6 +141,8 @@ pub fn mk_app( .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) + .service(routes::Routing::server(state.clone())) + .service(routes::Gsm::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index a5bce200889b..dbcd8cbe4ce2 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -305,7 +305,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::RequiredFieldInfo, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, - api_models::refunds::TimeRange, + api_models::payments::TimeRange, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, api_models::mandates::MandateCardDetails, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 307797e8ac9d..47b9f23cf8cb 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -10,6 +10,7 @@ pub mod disputes; pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +pub mod gsm; pub mod health; pub mod lock_utils; pub mod mandates; @@ -20,6 +21,8 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +#[cfg(feature = "olap")] +pub mod routing; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; pub mod webhooks; @@ -28,11 +31,13 @@ pub mod webhooks; pub use self::app::DummyConnector; #[cfg(feature = "payouts")] pub use self::app::Payouts; +#[cfg(feature = "olap")] +pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, + Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Refunds, Webhooks, }; #[cfg(feature = "stripe")] diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a93556202aab..9153e9e747f6 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -388,15 +388,15 @@ pub async fn merchant_account_toggle_kv( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::ConfigKeyUpdate; - let payload = json_payload.into_inner(); - let merchant_id = path.into_inner(); + let mut payload = json_payload.into_inner(); + payload.merchant_id = path.into_inner(); api::server_wrap( flow, state, &req, - (merchant_id, payload), - |state, _, (merchant_id, payload)| kv_for_merchant(state, merchant_id, payload.kv_enabled), + payload, + |state, _, payload| kv_for_merchant(state, payload.merchant_id, payload.kv_enabled), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, ) diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 6057b4c5db24..c2e289cd0f7e 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -124,16 +124,16 @@ pub async fn api_key_update( ) -> impl Responder { let flow = Flow::ApiKeyUpdate; let (merchant_id, key_id) = path.into_inner(); - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); + payload.key_id = key_id; + payload.merchant_id = merchant_id; api::server_wrap( flow, state, &req, - (&merchant_id, &key_id, payload), - |state, _, (merchant_id, key_id, payload)| { - api_keys::update_api_key(state, merchant_id, key_id, payload) - }, + payload, + |state, _, payload| api_keys::update_api_key(state, payload), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, ) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5b16e93404ae..ec87fcdc3900 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -14,10 +14,12 @@ use tokio::sync::oneshot; use super::dummy_connector::*; #[cfg(feature = "payouts")] use super::payouts::*; +#[cfg(feature = "olap")] +use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -78,7 +80,9 @@ impl AppStateInfo for AppState { } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); + self.store.add_request_id(request_id.to_string()) } + fn add_merchant_id(&mut self, merchant_id: Option) { self.api_client.add_merchant_id(merchant_id); } @@ -274,6 +278,43 @@ impl Payments { } } +#[cfg(feature = "olap")] +pub struct Routing; + +#[cfg(feature = "olap")] +impl Routing { + pub fn server(state: AppState) -> Scope { + web::scope("/routing") + .app_data(web::Data::new(state.clone())) + .service( + web::resource("/active") + .route(web::get().to(cloud_routing::routing_retrieve_linked_config)), + ) + .service( + web::resource("") + .route(web::get().to(cloud_routing::routing_retrieve_dictionary)) + .route(web::post().to(cloud_routing::routing_create_config)), + ) + .service( + web::resource("/default") + .route(web::get().to(cloud_routing::routing_retrieve_default_config)) + .route(web::post().to(cloud_routing::routing_update_default_config)), + ) + .service( + web::resource("/deactivate") + .route(web::post().to(cloud_routing::routing_unlink_config)), + ) + .service( + web::resource("/{algorithm_id}") + .route(web::get().to(cloud_routing::routing_retrieve_config)), + ) + .service( + web::resource("/{algorithm_id}/activate") + .route(web::post().to(cloud_routing::routing_link_config)), + ) + } +} + pub struct Customers; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -625,6 +666,20 @@ impl BusinessProfile { } } +pub struct Gsm; + +#[cfg(feature = "olap")] +impl Gsm { + pub fn server(state: AppState) -> Scope { + web::scope("/gsm") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(create_gsm_rule))) + .service(web::resource("/get").route(web::post().to(get_gsm_rule))) + .service(web::resource("/update").route(web::post().to(update_gsm_rule))) + .service(web::resource("/delete").route(web::post().to(delete_gsm_rule))) + } +} + #[cfg(all(feature = "olap", feature = "kms"))] pub struct Verify; diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs index 52a7f7f77c9a..7d2aad7e3482 100644 --- a/crates/router/src/routes/dummy_connector.rs +++ b/crates/router/src/routes/dummy_connector.rs @@ -10,7 +10,7 @@ use crate::{ mod consts; mod core; mod errors; -mod types; +pub mod types; mod utils; #[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs new file mode 100644 index 000000000000..02d943792dba --- /dev/null +++ b/crates/router/src/routes/gsm.rs @@ -0,0 +1,93 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models::gsm as gsm_api_types; +use router_env::{instrument, tracing, Flow}; + +use super::app::AppState; +use crate::{ + core::{api_locking, gsm}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleCreate))] +pub async fn create_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleCreate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::create_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleRetrieve))] +pub async fn get_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let gsm_retrieve_req = json_payload.into_inner(); + let flow = Flow::GsmRuleRetrieve; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + gsm_retrieve_req, + |state, _, gsm_retrieve_req| gsm::retrieve_gsm_rule(state, gsm_retrieve_req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleUpdate))] +pub async fn update_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleUpdate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::update_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleDelete))] +pub async fn delete_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleDelete; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, _, payload| gsm::delete_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 5be361098bcc..4e6fc1870f56 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -22,6 +22,8 @@ pub enum ApiIdentifier { Verification, ApiKeys, PaymentLink, + Routing, + Gsm, } impl From for ApiIdentifier { @@ -33,6 +35,17 @@ impl From for ApiIdentifier { | Flow::MerchantsAccountDelete | Flow::MerchantAccountList => Self::MerchantAccount, + Flow::RoutingCreateConfig + | Flow::RoutingLinkConfig + | Flow::RoutingUnlinkConfig + | Flow::RoutingRetrieveConfig + | Flow::RoutingRetrieveActiveConfig + | Flow::RoutingRetrieveDefaultConfig + | Flow::RoutingRetrieveDictionary + | Flow::RoutingUpdateConfig + | Flow::RoutingUpdateDefaultConfig + | Flow::RoutingDeleteConfig => Self::Routing, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate @@ -117,6 +130,10 @@ impl From for ApiIdentifier { Flow::Verification => Self::Verification, Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, + Flow::GsmRuleCreate + | Flow::GsmRuleRetrieve + | Flow::GsmRuleUpdate + | Flow::GsmRuleDelete => Self::Gsm, } } } diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 4bc05826a3e4..5ed73df1c175 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -178,6 +178,7 @@ pub async fn payments_start( req, api::AuthFlow::Client, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -244,6 +245,7 @@ pub async fn payments_retrieve( req, auth_flow, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -305,6 +307,7 @@ pub async fn payments_retrieve_with_gateway_creds( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -509,6 +512,7 @@ pub async fn payments_capture( payload, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -564,6 +568,7 @@ pub async fn payments_connector_session( payload, api::AuthFlow::Client, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -774,6 +779,7 @@ pub async fn payments_cancel( req, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, HeaderPayload::default(), ) }, @@ -897,6 +903,7 @@ where // the operation are flow agnostic, and the flow is only required in the post_update_tracker // Thus the flow can be generated just before calling the connector instead of explicitly passing it here. + let eligible_connectors = req.connector.clone(); match req.payment_type.unwrap_or_default() { api_models::enums::PaymentType::Normal | api_models::enums::PaymentType::RecurringMandate @@ -916,6 +923,7 @@ where req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, header_payload, ) .await @@ -936,6 +944,7 @@ where req, auth_flow, payments::CallConnectorAction::Trigger, + eligible_connectors, header_payload, ) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 4c4121b5d532..c20f3fbf975d 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -161,13 +161,14 @@ pub async fn refunds_update( path: web::Path, ) -> HttpResponse { let flow = Flow::RefundsUpdate; - let refund_id = path.into_inner(); + let mut refund_update_req = json_payload.into_inner(); + refund_update_req.refund_id = path.into_inner(); api::server_wrap( flow, state, &req, - json_payload.into_inner(), - |state, auth, req| refund_update_core(state, auth.merchant_account, &refund_id, req), + refund_update_req, + |state, auth, req| refund_update_core(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, ) @@ -225,7 +226,7 @@ pub async fn refunds_list( pub async fn refunds_filter_list( state: web::Data, req: HttpRequest, - payload: web::Json, + payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsList; api::server_wrap( diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs new file mode 100644 index 000000000000..9252c360a9ce --- /dev/null +++ b/crates/router/src/routes/routing.rs @@ -0,0 +1,298 @@ +//! Analysis for usage of Routing in Payment flows +//! +//! Functions that are used to perform the api level configuration, retrieval, updation +//! of Routing configs. +use actix_web::{web, HttpRequest, Responder}; +use api_models::routing as routing_types; +#[cfg(feature = "business_profile_routing")] +use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +use router_env::{ + tracing::{self, instrument}, + Flow, +}; + +use crate::{ + core::{api_locking, routing}, + routes::AppState, + services::{api as oss_api, authentication as oss_auth, authentication as auth}, +}; + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_create_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::RoutingCreateConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, payload| { + routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_link_config( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let flow = Flow::RoutingLinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + path.into_inner(), + |state, auth: oss_auth::AuthenticationData, algorithm_id| { + routing::link_routing_config( + state, + auth.merchant_account, + #[cfg(not(feature = "business_profile_routing"))] + auth.key_store, + algorithm_id.0, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_config( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let algorithm_id = path.into_inner(); + let flow = Flow::RoutingRetrieveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + algorithm_id, + |state, auth: oss_auth::AuthenticationData, algorithm_id| { + routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_dictionary( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] query: web::Query, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + let flow = Flow::RoutingRetrieveDictionary; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + query.into_inner(), + |state, auth: oss_auth::AuthenticationData, query_params| { + routing::retrieve_merchant_routing_dictionary( + state, + auth.merchant_account, + query_params, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingRetrieveDictionary; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_unlink_config( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] payload: web::Json< + routing_types::RoutingConfigRequest, + >, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + let flow = Flow::RoutingUnlinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, payload_req| { + routing::unlink_routing_config(state, auth.merchant_account, payload_req) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingUnlinkConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_update_default_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json>, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingUpdateDefaultConfig, + state, + &req, + json_payload.into_inner(), + |state, auth: oss_auth::AuthenticationData, updated_config| { + routing::update_default_routing_config(state, auth.merchant_account, updated_config) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_default_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingRetrieveDefaultConfig, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_default_routing_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_linked_config( + state: web::Data, + req: HttpRequest, + #[cfg(feature = "business_profile_routing")] query: web::Query, +) -> impl Responder { + #[cfg(feature = "business_profile_routing")] + { + use crate::services::authentication::AuthenticationData; + let flow = Flow::RoutingRetrieveActiveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + query.into_inner(), + |state, auth: AuthenticationData, query_params| { + routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } + + #[cfg(not(feature = "business_profile_routing"))] + { + let flow = Flow::RoutingRetrieveActiveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: oss_auth::AuthenticationData, _| { + routing::retrieve_linked_routing_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } +} diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index a0861f2b14d7..2ad061848c92 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -22,7 +22,7 @@ pub async fn apple_pay_merchant_registration( flow, state, &req, - json_payload, + json_payload.into_inner(), |state, _, body| { verification::verify_merchant_creds_for_applepay( state.clone(), diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index fe4201d7c018..321bf909ea0c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -34,7 +34,7 @@ use crate::{ errors::{self, CustomResult}, payments, }, - events::api_logs::ApiEvent, + events::api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, logger, routes::{ app::AppStateInfo, @@ -136,6 +136,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, + _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(None) } @@ -226,6 +227,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny { connector_integration.get_5xx_error_response(body)? } - _ => connector_integration.get_error_response(body)?, + _ => { + let error_res = + connector_integration.get_error_response(body)?; + if let Some(status) = error_res.attempt_status { + router_data.status = status; + }; + error_res + } }; router_data.response = Err(error); @@ -420,6 +430,7 @@ where message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, + attempt_status: None, }; router_data.response = Err(error_response); router_data.connector_http_status_code = Some(504); @@ -758,8 +769,8 @@ where F: Fn(A, U, T) -> Fut, 'b: 'a, Fut: Future, E>>, - Q: Serialize + Debug + 'a, - T: Debug + Serialize, + Q: Serialize + Debug + 'a + ApiEventMetric, + T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + error_stack::Context, OErr: ResponseError + error_stack::Context, @@ -780,6 +791,8 @@ where .attach_printable("Failed to serialize json request") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?; + let mut event_type = payload.get_api_event_type(); + // Currently auth failures are not recorded as API events let (auth_out, auth_type) = api_auth .authenticate_and_fetch(request.headers(), &request_state) @@ -841,6 +854,7 @@ where } } } + event_type = res.get_api_event_type().or(event_type); metrics::request::track_response_status_code(res) } @@ -856,6 +870,8 @@ where serialized_response, overhead_latency, auth_type, + event_type.unwrap_or(ApiEventsType::Miscellaneous), + request, ); match api_event.clone().try_into() { Ok(event) => { @@ -887,8 +903,8 @@ pub async fn server_wrap<'a, A, T, U, Q, F, Fut, E>( where F: Fn(A, U, T) -> Fut, Fut: Future, E>>, - Q: Serialize + Debug + 'a, - T: Debug + Serialize, + Q: Serialize + Debug + ApiEventMetric + 'a, + T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, ApplicationResponse: Debug, E: ErrorSwitch + error_stack::Context, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 261195d166cb..f2e86a4bf335 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -923,6 +923,7 @@ pub struct ErrorResponse { pub message: String, pub reason: Option, pub status_code: u16, + pub attempt_status: Option, } impl ErrorResponse { @@ -938,6 +939,7 @@ impl ErrorResponse { .error_message(), reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + attempt_status: None, } } } @@ -980,6 +982,7 @@ impl From for ErrorResponse { errors::ApiErrorResponse::ExternalConnectorError { status_code, .. } => status_code, _ => 500, }, + attempt_status: None, } } } @@ -1190,3 +1193,5 @@ impl } } } + +pub type GsmResponse = storage::GatewayStatusMap; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 8f5a0f8a59f2..e815740cac48 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -11,6 +11,7 @@ pub mod payment_methods; pub mod payments; pub mod payouts; pub mod refunds; +pub mod routing; pub mod webhooks; use std::{fmt::Debug, str::FromStr}; @@ -38,6 +39,13 @@ pub trait ConnectorAccessToken: { } +#[derive(Clone)] +pub enum ConnectorCallType { + PreDetermined(ConnectorData), + Retryable(Vec), + SessionMultiple(Vec), +} + #[derive(Clone, Debug)] pub struct VerifyWebhookSource; @@ -103,6 +111,7 @@ pub trait ConnectorCommon { code: consts::NO_ERROR_CODE.to_string(), message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, + attempt_status: None, }) } } @@ -218,12 +227,6 @@ pub enum PayoutConnectorChoice { Decide, } -#[derive(Clone)] -pub enum ConnectorCallType { - Multiple(Vec), - Single(ConnectorData), -} - #[cfg(feature = "payouts")] #[derive(Clone)] pub enum PayoutConnectorCallType { @@ -231,12 +234,6 @@ pub enum PayoutConnectorCallType { Single(PayoutConnectorData), } -impl ConnectorCallType { - pub fn is_single(&self) -> bool { - matches!(self, Self::Single(_)) - } -} - #[cfg(feature = "payouts")] impl PayoutConnectorData { pub fn get_connector_by_name( @@ -307,6 +304,7 @@ impl ConnectorData { enums::Connector::Airwallex => Ok(Box::new(&connector::Airwallex)), enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), + // enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), Added as template code for future usage enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)), enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)), enums::Connector::Boku => Ok(Box::new(&connector::Boku)), diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 258a3d566dde..6bbe9149f4d7 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -4,8 +4,8 @@ pub use api_models::admin::{ MerchantAccountResponse, MerchantAccountUpdate, MerchantConnectorCreate, MerchantConnectorDeleteResponse, MerchantConnectorDetails, MerchantConnectorDetailsWrap, MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, MerchantId, - PaymentMethodsEnabled, PayoutRoutingAlgorithm, PayoutStraightThroughAlgorithm, - RoutingAlgorithm, StraightThroughAlgorithm, ToggleKVRequest, ToggleKVResponse, WebhookDetails, + PaymentMethodsEnabled, PayoutRoutingAlgorithm, PayoutStraightThroughAlgorithm, ToggleKVRequest, + ToggleKVResponse, WebhookDetails, }; use common_utils::ext_traits::ValueExt; use error_stack::ResultExt; diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 2050b4149ef8..32430c0918a2 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -10,6 +10,12 @@ newtype!( derives = (Debug, Clone, Serialize) ); +impl common_utils::events::ApiEventMetric for CustomerResponse { + fn get_api_event_type(&self) -> Option { + self.0.get_api_event_type() + } +} + pub(crate) trait CustomerRequestExt: Sized { fn validate(self) -> RouterResult; } diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index e5bf1d8dd1bf..5acb66b5068e 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,12 +1,11 @@ use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByDateRequest, DeleteTokenizeByTokenRequest, - GetTokenizePayloadRequest, GetTokenizePayloadResponse, PaymentMethodCreate, - PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, PaymentMethodListRequest, - PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, - TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, - TokenizedWalletValue1, TokenizedWalletValue2, + CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, + GetTokenizePayloadResponse, PaymentMethodCreate, PaymentMethodDeleteResponse, PaymentMethodId, + PaymentMethodList, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, + PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, + TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, }; use error_stack::report; diff --git a/crates/router/src/types/api/routing.rs b/crates/router/src/types/api/routing.rs new file mode 100644 index 000000000000..faafac76e3dc --- /dev/null +++ b/crates/router/src/types/api/routing.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "backwards_compatibility")] +pub use api_models::routing::RoutableChoiceKind; +pub use api_models::{ + enums as api_enums, + routing::{ + ConnectorVolumeSplit, DetailedConnectorChoice, RoutableConnectorChoice, RoutingAlgorithm, + RoutingAlgorithmKind, RoutingAlgorithmRef, RoutingConfigRequest, RoutingDictionary, + RoutingDictionaryRecord, StraightThroughAlgorithm, + }, +}; + +use super::types::api as api_oss; + +pub struct SessionRoutingChoice { + pub connector: api_oss::ConnectorData, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, + pub payment_method_type: api_enums::PaymentMethodType, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConnectorVolumeSplitV0 { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum RoutingAlgorithmV0 { + Single(Box), + Priority(Vec), + VolumeSplit(Vec), + Custom { timestamp: i64 }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FrmRoutingAlgorithm { + pub data: String, + #[serde(rename = "type")] + pub algorithm_type: String, +} diff --git a/crates/router/src/types/domain/address.rs b/crates/router/src/types/domain/address.rs index 008cead1ebeb..ddf9c2152e94 100644 --- a/crates/router/src/types/domain/address.rs +++ b/crates/router/src/types/domain/address.rs @@ -35,7 +35,7 @@ pub struct Address { #[serde(skip_serializing)] #[serde(with = "custom_serde::iso8601")] pub modified_at: PrimitiveDateTime, - pub customer_id: String, + pub customer_id: Option, pub merchant_id: String, pub payment_id: Option, pub updated_by: String, diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 92ead76e9137..e3e19323357b 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -4,13 +4,13 @@ pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; -pub mod connector_response; pub mod customers; pub mod dispute; pub mod enums; pub mod ephemeral_key; pub mod events; pub mod file; +pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; pub mod locker_mock_up; @@ -21,6 +21,9 @@ pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_link; pub mod payment_method; +pub mod routing_algorithm; +use std::collections::HashMap; + pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; pub use scheduler::db::process_tracker; pub mod reverse_lookup; @@ -29,6 +32,8 @@ pub mod payout_attempt; pub mod payouts; mod query; pub mod refund; +pub mod user; +pub mod user_role; pub use data_models::payments::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, @@ -37,15 +42,67 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, connector_response::*, - customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, + ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, + reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; +use crate::types::api::routing; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RoutingData { pub routed_through: Option, - pub algorithm: Option, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub business_sub_label: Option, + pub routing_info: PaymentRoutingInfo, + pub algorithm: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(from = "PaymentRoutingInfoSerde", into = "PaymentRoutingInfoSerde")] +pub struct PaymentRoutingInfo { + pub algorithm: Option, + pub pre_routing_results: + Option>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentRoutingInfoInner { + pub algorithm: Option, + pub pre_routing_results: + Option>, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum PaymentRoutingInfoSerde { + OnlyAlgorithm(Box), + WithDetails(Box), +} + +impl From for PaymentRoutingInfo { + fn from(value: PaymentRoutingInfoSerde) -> Self { + match value { + PaymentRoutingInfoSerde::OnlyAlgorithm(algo) => Self { + algorithm: Some(*algo), + pre_routing_results: None, + }, + PaymentRoutingInfoSerde::WithDetails(details) => Self { + algorithm: details.algorithm, + pre_routing_results: details.pre_routing_results, + }, + } + } +} + +impl From for PaymentRoutingInfoSerde { + fn from(value: PaymentRoutingInfo) -> Self { + Self::WithDetails(Box::new(PaymentRoutingInfoInner { + algorithm: value.algorithm, + pre_routing_results: value.pre_routing_results, + })) + } } diff --git a/crates/router/src/types/storage/connector_response.rs b/crates/router/src/types/storage/connector_response.rs deleted file mode 100644 index c93c231e3d1c..000000000000 --- a/crates/router/src/types/storage/connector_response.rs +++ /dev/null @@ -1,41 +0,0 @@ -pub use diesel_models::{ - connector_response::{ - ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate, - ConnectorResponseUpdateInternal, - }, - enums::MerchantStorageScheme, -}; - -pub trait ConnectorResponseExt { - fn make_new_connector_response( - payment_id: String, - merchant_id: String, - attempt_id: String, - connector: Option, - storage_scheme: String, - ) -> ConnectorResponseNew; -} - -impl ConnectorResponseExt for ConnectorResponse { - fn make_new_connector_response( - payment_id: String, - merchant_id: String, - attempt_id: String, - connector: Option, - storage_scheme: String, - ) -> ConnectorResponseNew { - let now = common_utils::date_time::now(); - ConnectorResponseNew { - payment_id, - merchant_id, - attempt_id, - created_at: now, - modified_at: now, - connector_name: connector, - connector_transaction_id: None, - authentication_data: None, - encoded_data: None, - updated_by: storage_scheme, - } - } -} diff --git a/crates/router/src/types/storage/gsm.rs b/crates/router/src/types/storage/gsm.rs new file mode 100644 index 000000000000..bcea00e90910 --- /dev/null +++ b/crates/router/src/types/storage/gsm.rs @@ -0,0 +1,4 @@ +pub use diesel_models::gsm::{ + GatewayStatusMap, GatewayStatusMapperUpdateInternal, GatewayStatusMappingNew, + GatewayStatusMappingUpdate, +}; diff --git a/crates/router/src/types/storage/kv.rs b/crates/router/src/types/storage/kv.rs index 2afc73e6637d..6bb6c38e7b26 100644 --- a/crates/router/src/types/storage/kv.rs +++ b/crates/router/src/types/storage/kv.rs @@ -1,4 +1,4 @@ pub use diesel_models::kv::{ - AddressUpdateMems, ConnectorResponseUpdateMems, DBOperation, Insertable, - PaymentAttemptUpdateMems, PaymentIntentUpdateMems, RefundUpdateMems, TypedSql, Updateable, + AddressUpdateMems, DBOperation, Insertable, PaymentAttemptUpdateMems, PaymentIntentUpdateMems, + RefundUpdateMems, TypedSql, Updateable, }; diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index bdfa8dc5b5ff..4d5667700122 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -27,7 +27,7 @@ pub trait RefundDbExt: Sized { async fn filter_by_meta_constraints( conn: &PgPooledConn, merchant_id: &str, - refund_list_details: &api_models::refunds::TimeRange, + refund_list_details: &api_models::payments::TimeRange, ) -> CustomResult; async fn get_refunds_count( @@ -114,7 +114,7 @@ impl RefundDbExt for Refund { async fn filter_by_meta_constraints( conn: &PgPooledConn, merchant_id: &str, - refund_list_details: &api_models::refunds::TimeRange, + refund_list_details: &api_models::payments::TimeRange, ) -> CustomResult { let start_time = refund_list_details.start_time; diff --git a/crates/router/src/types/storage/routing_algorithm.rs b/crates/router/src/types/storage/routing_algorithm.rs new file mode 100644 index 000000000000..8022ab075ec4 --- /dev/null +++ b/crates/router/src/types/storage/routing_algorithm.rs @@ -0,0 +1,3 @@ +pub use diesel_models::routing_algorithm::{ + RoutingAlgorithm, RoutingAlgorithmMetadata, RoutingProfileMetadata, +}; diff --git a/crates/router/src/types/storage/user.rs b/crates/router/src/types/storage/user.rs new file mode 100644 index 000000000000..17dc9d365243 --- /dev/null +++ b/crates/router/src/types/storage/user.rs @@ -0,0 +1 @@ +pub use diesel_models::user::*; diff --git a/crates/router/src/types/storage/user_role.rs b/crates/router/src/types/storage/user_role.rs new file mode 100644 index 000000000000..780b9b2971db --- /dev/null +++ b/crates/router/src/types/storage/user_role.rs @@ -0,0 +1 @@ +pub use diesel_models::user_role::*; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index d38497c7100a..1cd016de18e6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1,6 +1,6 @@ // use actix_web::HttpMessage; use actix_web::http::header::HeaderMap; -use api_models::{enums as api_enums, payments}; +use api_models::{enums as api_enums, gsm as gsm_api_types, payments, routing::ConnectorSelection}; use common_utils::{ consts::X_HS_LATENCY, crypto::Encryptable, @@ -8,14 +8,15 @@ use common_utils::{ pii, }; use diesel_models::enums as storage_enums; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; +use euclid::enums as dsl_enums; use masking::{ExposeInterface, PeekInterface}; use super::domain; use crate::{ core::errors, services::authentication::get_header_value_by_key, - types::{api as api_types, storage}, + types::{api as api_types, api::routing as routing_types, storage}, }; pub trait ForeignInto { @@ -169,6 +170,154 @@ impl ForeignFrom for api_models::payments::Manda } } +impl ForeignTryFrom for api_enums::RoutableConnectors { + type Error = error_stack::Report; + + fn foreign_try_from(from: api_enums::Connector) -> Result { + Ok(match from { + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + api_enums::Connector::Aci => Self::Aci, + api_enums::Connector::Adyen => Self::Adyen, + api_enums::Connector::Airwallex => Self::Airwallex, + api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, + api_enums::Connector::Bitpay => Self::Bitpay, + api_enums::Connector::Bambora => Self::Bambora, + api_enums::Connector::Bluesnap => Self::Bluesnap, + api_enums::Connector::Boku => Self::Boku, + api_enums::Connector::Braintree => Self::Braintree, + api_enums::Connector::Cashtocode => Self::Cashtocode, + api_enums::Connector::Checkout => Self::Checkout, + api_enums::Connector::Coinbase => Self::Coinbase, + api_enums::Connector::Cryptopay => Self::Cryptopay, + api_enums::Connector::Cybersource => Self::Cybersource, + api_enums::Connector::Dlocal => Self::Dlocal, + api_enums::Connector::Fiserv => Self::Fiserv, + api_enums::Connector::Forte => Self::Forte, + api_enums::Connector::Globalpay => Self::Globalpay, + api_enums::Connector::Globepay => Self::Globepay, + api_enums::Connector::Gocardless => Self::Gocardless, + api_enums::Connector::Helcim => Self::Helcim, + api_enums::Connector::Iatapay => Self::Iatapay, + api_enums::Connector::Klarna => Self::Klarna, + api_enums::Connector::Mollie => Self::Mollie, + api_enums::Connector::Multisafepay => Self::Multisafepay, + api_enums::Connector::Nexinets => Self::Nexinets, + api_enums::Connector::Nmi => Self::Nmi, + api_enums::Connector::Noon => Self::Noon, + api_enums::Connector::Nuvei => Self::Nuvei, + api_enums::Connector::Opennode => Self::Opennode, + api_enums::Connector::Payme => Self::Payme, + api_enums::Connector::Paypal => Self::Paypal, + api_enums::Connector::Payu => Self::Payu, + api_enums::Connector::Plaid => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "plaid is not a routable connector".to_string(), + }) + .into_report()? + } + api_enums::Connector::Powertranz => Self::Powertranz, + api_enums::Connector::Rapyd => Self::Rapyd, + api_enums::Connector::Shift4 => Self::Shift4, + api_enums::Connector::Signifyd => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "signifyd is not a routable connector".to_string(), + }) + .into_report()? + } + api_enums::Connector::Square => Self::Square, + api_enums::Connector::Stax => Self::Stax, + api_enums::Connector::Stripe => Self::Stripe, + api_enums::Connector::Trustpay => Self::Trustpay, + api_enums::Connector::Tsys => Self::Tsys, + api_enums::Connector::Volt => Self::Volt, + api_enums::Connector::Wise => Self::Wise, + api_enums::Connector::Worldline => Self::Worldline, + api_enums::Connector::Worldpay => Self::Worldpay, + api_enums::Connector::Zen => Self::Zen, + }) + } +} + +impl ForeignFrom for api_enums::RoutableConnectors { + fn foreign_from(from: dsl_enums::Connector) -> Self { + match from { + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector1 => Self::DummyConnector1, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector2 => Self::DummyConnector2, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector3 => Self::DummyConnector3, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector4 => Self::DummyConnector4, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector5 => Self::DummyConnector5, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector6 => Self::DummyConnector6, + #[cfg(feature = "dummy_connector")] + dsl_enums::Connector::DummyConnector7 => Self::DummyConnector7, + dsl_enums::Connector::Aci => Self::Aci, + dsl_enums::Connector::Adyen => Self::Adyen, + dsl_enums::Connector::Airwallex => Self::Airwallex, + dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, + dsl_enums::Connector::Bitpay => Self::Bitpay, + dsl_enums::Connector::Bambora => Self::Bambora, + dsl_enums::Connector::Bluesnap => Self::Bluesnap, + dsl_enums::Connector::Boku => Self::Boku, + dsl_enums::Connector::Braintree => Self::Braintree, + dsl_enums::Connector::Cashtocode => Self::Cashtocode, + dsl_enums::Connector::Checkout => Self::Checkout, + dsl_enums::Connector::Coinbase => Self::Coinbase, + dsl_enums::Connector::Cryptopay => Self::Cryptopay, + dsl_enums::Connector::Cybersource => Self::Cybersource, + dsl_enums::Connector::Dlocal => Self::Dlocal, + dsl_enums::Connector::Fiserv => Self::Fiserv, + dsl_enums::Connector::Forte => Self::Forte, + dsl_enums::Connector::Globalpay => Self::Globalpay, + dsl_enums::Connector::Globepay => Self::Globepay, + dsl_enums::Connector::Gocardless => Self::Gocardless, + dsl_enums::Connector::Helcim => Self::Helcim, + dsl_enums::Connector::Iatapay => Self::Iatapay, + dsl_enums::Connector::Klarna => Self::Klarna, + dsl_enums::Connector::Mollie => Self::Mollie, + dsl_enums::Connector::Multisafepay => Self::Multisafepay, + dsl_enums::Connector::Nexinets => Self::Nexinets, + dsl_enums::Connector::Nmi => Self::Nmi, + dsl_enums::Connector::Noon => Self::Noon, + dsl_enums::Connector::Nuvei => Self::Nuvei, + dsl_enums::Connector::Opennode => Self::Opennode, + dsl_enums::Connector::Payme => Self::Payme, + dsl_enums::Connector::Paypal => Self::Paypal, + dsl_enums::Connector::Payu => Self::Payu, + dsl_enums::Connector::Powertranz => Self::Powertranz, + dsl_enums::Connector::Rapyd => Self::Rapyd, + dsl_enums::Connector::Shift4 => Self::Shift4, + dsl_enums::Connector::Square => Self::Square, + dsl_enums::Connector::Stax => Self::Stax, + dsl_enums::Connector::Stripe => Self::Stripe, + dsl_enums::Connector::Trustpay => Self::Trustpay, + dsl_enums::Connector::Tsys => Self::Tsys, + dsl_enums::Connector::Volt => Self::Volt, + dsl_enums::Connector::Wise => Self::Wise, + dsl_enums::Connector::Worldline => Self::Worldline, + dsl_enums::Connector::Worldpay => Self::Worldpay, + dsl_enums::Connector::Zen => Self::Zen, + } + } +} + impl ForeignFrom for api_models::payments::MandateAmountData { fn foreign_from(from: storage_enums::MandateAmountData) -> Self { Self { @@ -862,6 +1011,16 @@ impl From for payments::AddressDetails { } } +impl ForeignFrom for routing_types::RoutingAlgorithm { + fn foreign_from(value: ConnectorSelection) -> Self { + match value { + ConnectorSelection::Priority(connectors) => Self::Priority(connectors), + + ConnectorSelection::VolumeSplit(splits) => Self::VolumeSplit(splits), + } + } +} + impl ForeignFrom for diesel_models::organization::OrganizationNew { @@ -872,3 +1031,19 @@ impl ForeignFrom } } } + +impl ForeignFrom for storage::GatewayStatusMappingNew { + fn foreign_from(value: gsm_api_types::GsmCreateRequest) -> Self { + Self { + connector: value.connector.to_string(), + flow: value.flow, + sub_flow: value.sub_flow, + code: value.code, + message: value.message, + decision: value.decision.to_string(), + status: value.status, + router_error: value.router_error, + step_up_possible: value.step_up_possible, + } + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 84a75d397e31..558044028f7a 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -401,6 +401,7 @@ pub fn handle_json_response_deserialization_failure( code: consts::NO_ERROR_CODE.to_string(), message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), reason: Some(response_data), + attempt_status: None, }) } } @@ -566,7 +567,7 @@ impl CustomerAddress for api_models::customers::CustomerRequest { .async_lift(|inner| encrypt_optional(inner, key)) .await?, country_code: self.phone_country_code.clone(), - customer_id: customer_id.to_string(), + customer_id: Some(customer_id.to_string()), merchant_id: merchant_id.to_string(), address_id: generate_id(consts::ID_LENGTH, "add"), payment_id: None, diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 540f2d68dd61..f41b300c5127 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -69,6 +69,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { tracking_data.clone(), payment_flows::CallConnectorAction::Trigger, services::AuthFlow::Client, + None, api::HeaderPayload::default(), ) .await?; diff --git a/crates/router/src/workflows/tokenized_data.rs b/crates/router/src/workflows/tokenized_data.rs index 2f5d33173276..0674982f92fe 100644 --- a/crates/router/src/workflows/tokenized_data.rs +++ b/crates/router/src/workflows/tokenized_data.rs @@ -1,14 +1,13 @@ use scheduler::consumer::workflows::ProcessTrackerWorkflow; -#[cfg(feature = "basilisk")] -use crate::core::payment_methods::vault; -use crate::{errors, logger::error, routes::AppState, types::storage}; +use crate::{ + core::payment_methods::vault, errors, logger::error, routes::AppState, types::storage, +}; pub struct DeleteTokenizeDataWorkflow; #[async_trait::async_trait] impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { - #[cfg(feature = "basilisk")] async fn execute_workflow<'a>( &'a self, state: &'a AppState, @@ -17,15 +16,6 @@ impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { Ok(vault::start_tokenize_data_workflow(state, &process).await?) } - #[cfg(not(feature = "basilisk"))] - async fn execute_workflow<'a>( - &'a self, - _state: &'a AppState, - _process: storage::ProcessTracker, - ) -> Result<(), errors::ProcessTrackerError> { - Ok(()) - } - async fn error_handler<'a>( &'a self, _state: &'a AppState, diff --git a/crates/router/tests/connectors/bankofamerica.rs b/crates/router/tests/connectors/bankofamerica.rs new file mode 100644 index 000000000000..ce264cbccc86 --- /dev/null +++ b/crates/router/tests/connectors/bankofamerica.rs @@ -0,0 +1,420 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct BankofamericaTest; +impl ConnectorActions for BankofamericaTest {} +impl utils::Connector for BankofamericaTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Bankofamerica; + types::api::ConnectorData { + connector: Box::new(&Bankofamerica), + connector_name: types::Connector::DummyConnector1, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .bankofamerica + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "bankofamerica".to_string() + } +} + +static CONNECTOR: BankofamericaTest = BankofamericaTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index ed06312b77ac..03b6181b8a89 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -11,6 +11,7 @@ mod adyen; mod airwallex; mod authorizedotnet; mod bambora; +mod bankofamerica; mod bitpay; mod bluesnap; mod boku; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 0966db95a42f..f8f6039d6d36 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -183,4 +183,9 @@ api_key="API Key" api_key="API Key" [prophetpay] -api_key="API Key" \ No newline at end of file +api_key="API Key" + +[bankofamerica] +api_key = "MyApiKey" +key1 = "Merchant id" +api_secret = "Secret key" diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 551960ac1380..d2d6c48507e5 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -369,6 +369,7 @@ async fn payments_create_core() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await @@ -539,6 +540,7 @@ async fn payments_create_core_adyen_no_redirect() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 96ed131dc6f8..ed8827a910be 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -135,6 +135,7 @@ async fn payments_create_core() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await @@ -313,6 +314,7 @@ async fn payments_create_core_adyen_no_redirect() { req, services::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, + None, api::HeaderPayload::default(), ) .await diff --git a/crates/router_env/src/logger/formatter.rs b/crates/router_env/src/logger/formatter.rs index ce2fd74e0e87..4fd94c221637 100644 --- a/crates/router_env/src/logger/formatter.rs +++ b/crates/router_env/src/logger/formatter.rs @@ -51,6 +51,8 @@ const REQUEST_METHOD: &str = "request_method"; const REQUEST_URL_PATH: &str = "request_url_path"; const REQUEST_ID: &str = "request_id"; const WORKFLOW_ID: &str = "workflow_id"; +const GLOBAL_ID: &str = "global_id"; +const SESSION_ID: &str = "session_id"; /// Set of predefined implicit keys. pub static IMPLICIT_KEYS: Lazy> = Lazy::new(|| { @@ -85,6 +87,8 @@ pub static EXTRA_IMPLICIT_KEYS: Lazy> = Lazy::new(|| set.insert(REQUEST_METHOD); set.insert(REQUEST_URL_PATH); set.insert(REQUEST_ID); + set.insert(GLOBAL_ID); + set.insert(SESSION_ID); set.insert(WORKFLOW_ID); set diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d63ddce58f30..0c9751aee440 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,26 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + /// Routing create flow, + RoutingCreateConfig, + /// Routing link config + RoutingLinkConfig, + /// Routing link config + RoutingUnlinkConfig, + /// Routing retrieve config + RoutingRetrieveConfig, + /// Routing retrieve active config + RoutingRetrieveActiveConfig, + /// Routing retrieve default config + RoutingRetrieveDefaultConfig, + /// Routing retrieve dictionary + RoutingRetrieveDictionary, + /// Routing update config + RoutingUpdateConfig, + /// Routing update default config + RoutingUpdateDefaultConfig, + /// Routing delete config + RoutingDeleteConfig, /// Incoming Webhook Receive IncomingWebhookReceive, /// Validate payment method flow @@ -215,6 +235,14 @@ pub enum Flow { BusinessProfileList, /// Different verification flows Verification, + /// Gsm Rule Creation flow + GsmRuleCreate, + /// Gsm Rule Retrieve flow + GsmRuleRetrieve, + /// Gsm Rule Update flow + GsmRuleUpdate, + /// Gsm Rule Delete flow + GsmRuleDelete, } /// diff --git a/crates/storage_impl/src/connector_response.rs b/crates/storage_impl/src/connector_response.rs deleted file mode 100644 index 7d4ff6df94d9..000000000000 --- a/crates/storage_impl/src/connector_response.rs +++ /dev/null @@ -1,5 +0,0 @@ -use diesel_models::connector_response::ConnectorResponse; - -use crate::redis::kv_store::KvStorePartition; - -impl KvStorePartition for ConnectorResponse {} diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 17d432c7932b..00d8703940c7 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -8,7 +8,6 @@ use redis::{kv_store::RedisConnInterface, RedisStore}; mod address; pub mod config; pub mod connection; -mod connector_response; pub mod database; pub mod errors; mod lookup; @@ -31,6 +30,7 @@ pub struct RouterStore { db_store: T, cache_store: RedisStore, master_encryption_key: StrongSecret>, + pub request_id: Option, } #[async_trait::async_trait] @@ -103,6 +103,7 @@ impl RouterStore { db_store, cache_store, master_encryption_key: encryption_key, + request_id: None, }) } @@ -128,6 +129,7 @@ impl RouterStore { db_store, cache_store, master_encryption_key: encryption_key, + request_id: None, }) } } @@ -138,6 +140,7 @@ pub struct KVRouterStore { drainer_stream_name: String, drainer_num_partitions: u8, ttl_for_kv: u32, + pub request_id: Option, } #[async_trait::async_trait] @@ -179,11 +182,14 @@ impl KVRouterStore { drainer_num_partitions: u8, ttl_for_kv: u32, ) -> Self { + let request_id = store.request_id.clone(); + Self { router_store: store, drainer_stream_name, drainer_num_partitions, ttl_for_kv, + request_id, } } @@ -203,6 +209,9 @@ impl KVRouterStore { where R: crate::redis::kv_store::KvStorePartition, { + let global_id = format!("{}", partition_key); + let request_id = self.request_id.clone().unwrap_or_default(); + let shard_key = R::shard_key(partition_key, self.drainer_num_partitions); let stream_name = self.get_drainer_stream_name(&shard_key); self.router_store @@ -212,7 +221,7 @@ impl KVRouterStore { &stream_name, &redis_interface::RedisEntryId::AutoGeneratedID, redis_entry - .to_field_value_pairs() + .to_field_value_pairs(request_id, global_id) .change_context(RedisError::JsonSerializationFailed)?, ) .await diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 76bdb1160ccc..4cdf8e2456bb 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -27,7 +27,6 @@ pub struct MockDb { pub customers: Arc>>, pub refunds: Arc>>, pub processes: Arc>>, - pub connector_response: Arc>>, pub redis: Arc, pub api_keys: Arc>>, pub ephemeral_keys: Arc>>, @@ -42,6 +41,8 @@ pub struct MockDb { pub reverse_lookups: Arc>>, pub payment_link: Arc>>, pub organizations: Arc>>, + pub users: Arc>>, + pub user_roles: Arc>>, } impl MockDb { @@ -57,7 +58,6 @@ impl MockDb { customers: Default::default(), refunds: Default::default(), processes: Default::default(), - connector_response: Default::default(), redis: Arc::new( RedisStore::new(redis) .await @@ -76,6 +76,8 @@ impl MockDb { reverse_lookups: Default::default(), payment_link: Default::default(), organizations: Default::default(), + users: Default::default(), + user_roles: Default::default(), }) } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index e3047221b6f5..21002917df83 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1062,7 +1062,6 @@ impl DataModelExt for PaymentAttemptNew { connector_response_reference_id: self.connector_response_reference_id, multiple_capture_count: self.multiple_capture_count, amount_capturable: self.amount_capturable, - updated_by: self.updated_by, authentication_data: self.authentication_data, encoded_data: self.encoded_data, @@ -1244,6 +1243,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1259,6 +1260,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, }, Self::UnresolvedResponseUpdate { status, @@ -1481,6 +1484,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, } => Self::ResponseUpdate { status, connector, @@ -1496,6 +1501,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + authentication_data, + encoded_data, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, diff --git a/crates/test_utils/README.md b/crates/test_utils/README.md index 1e92174b3337..2edbc7104c25 100644 --- a/crates/test_utils/README.md +++ b/crates/test_utils/README.md @@ -28,9 +28,16 @@ Required fields: Optional fields: +- `--delay` -- To add a delay between requests in milliseconds. + - Maximum delay is 4294967295 milliseconds or 4294967.295 seconds or 71616 minutes or 1193.6 hours or 49.733 days + - Example: `--delay 1000` (for 1 second delay) - `--folder` -- To run individual folders in the collection - Use double quotes to specify folder name. If you wish to run multiple folders, separate them with a comma (`,`) - Example: `--folder "QuickStart"` or `--folder "Health check,QuickStart"` +- `--header` -- If you wish to add custom headers to the requests, you can pass them as a string + - Example: `--header "key:value"` + - If you want to pass multiple custom headers, you can pass multiple `--header` flags + - Example: `--header "key1:value1" --header "key2:value2"` - `--verbose` -- A boolean to print detailed logs (requests and responses) **Note:** Passing `--verbose` will also print the connector as well as admin API keys in the logs. So, make sure you don't push the commands with `--verbose` to any public repository. diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index d774e2530e9d..9562972c126e 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -17,6 +17,7 @@ pub struct ConnectorAuthentication { pub airwallex: Option, pub authorizedotnet: Option, pub bambora: Option, + pub bankofamerica: Option, pub bitpay: Option, pub bluesnap: Option, pub boku: Option, diff --git a/crates/test_utils/src/main.rs b/crates/test_utils/src/main.rs index 637122e468e6..22c91e063d8f 100644 --- a/crates/test_utils/src/main.rs +++ b/crates/test_utils/src/main.rs @@ -3,10 +3,10 @@ use std::process::{exit, Command}; use test_utils::newman_runner; fn main() { - let mut newman_command: Command = newman_runner::command_generate(); + let mut runner = newman_runner::generate_newman_command(); // Execute the newman command - let output = newman_command.spawn(); + let output = runner.newman_command.spawn(); let mut child = match output { Ok(child) => child, Err(err) => { @@ -16,6 +16,30 @@ fn main() { }; let status = child.wait(); + if runner.file_modified_flag { + let git_status = Command::new("git") + .args([ + "restore", + format!("{}/event.prerequest.js", runner.collection_path).as_str(), + ]) + .output(); + + match git_status { + Ok(output) => { + if output.status.success() { + let stdout_str = String::from_utf8_lossy(&output.stdout); + println!("Git command executed successfully: {stdout_str}"); + } else { + let stderr_str = String::from_utf8_lossy(&output.stderr); + eprintln!("Git command failed with error: {stderr_str}"); + } + } + Err(e) => { + eprintln!("Error running Git: {e}"); + } + } + } + let exit_code = match status { Ok(exit_status) => { if exit_status.success() { diff --git a/crates/test_utils/src/newman_runner.rs b/crates/test_utils/src/newman_runner.rs index c51556f8f255..a6e0268e2c29 100644 --- a/crates/test_utils/src/newman_runner.rs +++ b/crates/test_utils/src/newman_runner.rs @@ -1,22 +1,28 @@ -use std::{env, process::Command}; +use std::{env, io::Write, path::Path, process::Command}; use clap::{arg, command, Parser}; use masking::PeekInterface; use crate::connector_auth::{ConnectorAuthType, ConnectorAuthenticationMap}; - #[derive(Parser)] #[command(version, about = "Postman collection runner using newman!", long_about = None)] struct Args { /// Admin API Key of the environment - #[arg(short, long = "admin_api_key")] + #[arg(short, long)] admin_api_key: String, /// Base URL of the Hyperswitch environment - #[arg(short, long = "base_url")] + #[arg(short, long)] base_url: String, /// Name of the connector - #[arg(short, long = "connector_name")] + #[arg(short, long)] connector_name: String, + /// Custom headers + #[arg(short = 'H', long = "header")] + custom_headers: Option>, + /// Minimum delay in milliseconds to be added before sending a request + /// By default, 7 milliseconds will be the delay + #[arg(short, long, default_value_t = 7)] + delay_request: u32, /// Folder name of specific tests #[arg(short, long = "folder")] folders: Option, @@ -25,6 +31,12 @@ struct Args { verbose: bool, } +pub struct ReturnArgs { + pub newman_command: Command, + pub file_modified_flag: bool, + pub collection_path: String, +} + // Just by the name of the connector, this function generates the name of the collection dir // Example: CONNECTOR_NAME="stripe" -> OUTPUT: postman/collection-dir/stripe #[inline] @@ -32,7 +44,29 @@ fn get_path(name: impl AsRef) -> String { format!("postman/collection-dir/{}", name.as_ref()) } -pub fn command_generate() -> Command { +// This function currently allows you to add only custom headers. +// In future, as we scale, this can be modified based on the need +fn insert_content(dir: T, content_to_insert: U) -> std::io::Result<()> +where + T: AsRef, + U: AsRef, +{ + let file_name = "event.prerequest.js"; + let file_path = dir.as_ref().join(file_name); + + // Open the file in write mode or create it if it doesn't exist + let mut file = std::fs::OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(file_path)?; + + write!(file, "{}", content_to_insert.as_ref())?; + + Ok(()) +} + +pub fn generate_newman_command() -> ReturnArgs { let args = Args::parse(); let connector_name = args.connector_name; @@ -129,7 +163,10 @@ pub fn command_generate() -> Command { ]); } - newman_command.arg("--delay-request").arg("7"); // 7 milli seconds delay + newman_command.args([ + "--delay-request", + format!("{}", &args.delay_request).as_str(), + ]); newman_command.arg("--color").arg("on"); @@ -151,5 +188,24 @@ pub fn command_generate() -> Command { newman_command.arg("--verbose"); } - newman_command + let mut modified = false; + if let Some(headers) = &args.custom_headers { + for header in headers { + if let Some((key, value)) = header.split_once(':') { + let content_to_insert = + format!(r#"pm.request.headers.add({{key: "{key}", value: "{value}"}});"#); + if insert_content(&collection_path, &content_to_insert).is_ok() { + modified = true; + } + } else { + eprintln!("Invalid header format: {}", header); + } + } + } + + ReturnArgs { + newman_command, + file_modified_flag: modified, + collection_path, + } } diff --git a/docs/imgs/aws_button.png b/docs/imgs/aws_button.png new file mode 100644 index 000000000000..4c8c2c20e097 Binary files /dev/null and b/docs/imgs/aws_button.png differ diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 7cdbc8dd6fdd..352c4ff551bc 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -64,6 +64,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -130,6 +131,7 @@ cards = [ "airwallex", "authorizedotnet", "bambora", + "bankofamerica", "bitpay", "bluesnap", "boku", diff --git a/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql b/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql new file mode 100644 index 000000000000..2cace88297db --- /dev/null +++ b/migrations/2023-10-19-101558_create_routing_algorithm_table/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE routing_algorithm; +DROP TYPE "RoutingAlgorithmKind"; diff --git a/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql b/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql new file mode 100644 index 000000000000..361194561227 --- /dev/null +++ b/migrations/2023-10-19-101558_create_routing_algorithm_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here + +CREATE TYPE "RoutingAlgorithmKind" AS ENUM ('single', 'priority', 'volume_split', 'advanced'); + +CREATE TABLE routing_algorithm ( + algorithm_id VARCHAR(64) PRIMARY KEY, + profile_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + name VARCHAR(64) NOT NULL, + description VARCHAR(256), + kind "RoutingAlgorithmKind" NOT NULL, + algorithm_data JSONB NOT NULL, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP NOT NULL +); + +CREATE INDEX routing_algorithm_profile_id_modified_at ON routing_algorithm (profile_id, modified_at DESC); + +CREATE INDEX routing_algorithm_merchant_id_modified_at ON routing_algorithm (merchant_id, modified_at DESC); diff --git a/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql new file mode 100644 index 000000000000..84f009021df8 --- /dev/null +++ b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN custom_merchant_name; diff --git a/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql new file mode 100644 index 000000000000..c4fa756e57a0 --- /dev/null +++ b/migrations/2023-10-25-070909_add_merchant_custom_name_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN custom_merchant_name VARCHAR(64); \ No newline at end of file diff --git a/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql new file mode 100644 index 000000000000..b148e66d8750 --- /dev/null +++ b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE address ALTER COLUMN customer_id SET NOT NULL; \ No newline at end of file diff --git a/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql new file mode 100644 index 000000000000..98826c41e79c --- /dev/null +++ b/migrations/2023-11-02-074243_make_customer_id_nullable_in_address_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE address ALTER COLUMN customer_id DROP NOT NULL; \ No newline at end of file diff --git a/migrations/2023-11-06-110233_create_user_table/down.sql b/migrations/2023-11-06-110233_create_user_table/down.sql new file mode 100644 index 000000000000..0172a87499bb --- /dev/null +++ b/migrations/2023-11-06-110233_create_user_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users; \ No newline at end of file diff --git a/migrations/2023-11-06-110233_create_user_table/up.sql b/migrations/2023-11-06-110233_create_user_table/up.sql new file mode 100644 index 000000000000..410436c461ce --- /dev/null +++ b/migrations/2023-11-06-110233_create_user_table/up.sql @@ -0,0 +1,14 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + is_verified bool NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS user_id_index ON users (user_id); +CREATE UNIQUE INDEX IF NOT EXISTS user_email_index ON users (email); \ No newline at end of file diff --git a/migrations/2023-11-06-113726_create_user_roles_table/down.sql b/migrations/2023-11-06-113726_create_user_roles_table/down.sql new file mode 100644 index 000000000000..5e6350de9e70 --- /dev/null +++ b/migrations/2023-11-06-113726_create_user_roles_table/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +-- Drop the table +DROP TABLE IF EXISTS user_roles; \ No newline at end of file diff --git a/migrations/2023-11-06-113726_create_user_roles_table/up.sql b/migrations/2023-11-06-113726_create_user_roles_table/up.sql new file mode 100644 index 000000000000..768306721626 --- /dev/null +++ b/migrations/2023-11-06-113726_create_user_roles_table/up.sql @@ -0,0 +1,18 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS user_roles ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + role_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + status VARCHAR(64) NOT NULL, + created_by VARCHAR(64) NOT NULL, + last_modified_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_at TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT user_merchant_unique UNIQUE (user_id, merchant_id) +); + + +CREATE INDEX IF NOT EXISTS user_id_roles_index ON user_roles (user_id); +CREATE INDEX IF NOT EXISTS user_mid_roles_index ON user_roles (merchant_id); \ No newline at end of file diff --git a/migrations/2023-11-07-110139_add_gsm_table/down.sql b/migrations/2023-11-07-110139_add_gsm_table/down.sql new file mode 100644 index 000000000000..e1cdd5d4133d --- /dev/null +++ b/migrations/2023-11-07-110139_add_gsm_table/down.sql @@ -0,0 +1,2 @@ +-- Tables +DROP TABLE gateway_status_map; diff --git a/migrations/2023-11-07-110139_add_gsm_table/up.sql b/migrations/2023-11-07-110139_add_gsm_table/up.sql new file mode 100644 index 000000000000..9dfa68b01af9 --- /dev/null +++ b/migrations/2023-11-07-110139_add_gsm_table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here +-- Tables +CREATE TABLE IF NOT EXISTS gateway_status_map ( + connector VARCHAR(64) NOT NULL, + flow VARCHAR(64) NOT NULL, + sub_flow VARCHAR(64) NOT NULL, + code VARCHAR(255) NOT NULL, + message VARCHAR(1024), + status VARCHAR(64) NOT NULL, + router_error VARCHAR(64), + decision VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + last_modified TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + step_up_possible BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (connector, flow, sub_flow, code, message) +); diff --git a/migrations/2023-11-08-144951_drop_connector_response_table/down.sql b/migrations/2023-11-08-144951_drop_connector_response_table/down.sql new file mode 100644 index 000000000000..ff9ca4e4f9c7 --- /dev/null +++ b/migrations/2023-11-08-144951_drop_connector_response_table/down.sql @@ -0,0 +1,34 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE connector_response ( + id SERIAL PRIMARY KEY, + payment_id VARCHAR(255) NOT NULL, + merchant_id VARCHAR(255) NOT NULL, + txn_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + connector_name VARCHAR(32) NOT NULL, + connector_transaction_id VARCHAR(255), + authentication_data JSON, + encoded_data TEXT +); + +CREATE UNIQUE INDEX connector_response_id_index ON connector_response (payment_id, merchant_id, txn_id); + +ALTER TABLE connector_response ALTER COLUMN connector_name DROP NOT NULL; +ALTER TABLE connector_response RENAME COLUMN txn_id TO attempt_id; +ALTER TABLE connector_response + ALTER COLUMN payment_id TYPE VARCHAR(64), + ALTER COLUMN merchant_id TYPE VARCHAR(64), + ALTER COLUMN attempt_id TYPE VARCHAR(64), + ALTER COLUMN connector_name TYPE VARCHAR(64), + ALTER COLUMN connector_transaction_id TYPE VARCHAR(128); + + + +ALTER TABLE connector_response +ALTER COLUMN modified_at DROP DEFAULT; + +ALTER TABLE connector_response +ALTER COLUMN created_at DROP DEFAULT; + +ALTER TABLE connector_response ADD column updated_by VARCHAR(32) NOT NULL DEFAULT 'postgres_only'; diff --git a/migrations/2023-11-08-144951_drop_connector_response_table/up.sql b/migrations/2023-11-08-144951_drop_connector_response_table/up.sql new file mode 100644 index 000000000000..0059a6b38dc1 --- /dev/null +++ b/migrations/2023-11-08-144951_drop_connector_response_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +DROP TABLE connector_response; --NOT to run in deployment envs \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 45d0bde9d323..23f8f1b3628b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -3898,8 +3898,8 @@ "adyen", "airwallex", "authorizedotnet", - "bitpay", "bambora", + "bitpay", "bluesnap", "boku", "braintree", @@ -7809,15 +7809,11 @@ "PaymentLinkColorSchema": { "type": "object", "properties": { - "primary_color": { - "type": "string", - "nullable": true - }, - "primary_accent_color": { + "background_primary_color": { "type": "string", "nullable": true }, - "secondary_color": { + "sdk_theme": { "type": "string", "nullable": true } @@ -7866,6 +7862,11 @@ "merchant_custom_domain_name": { "type": "string", "nullable": true + }, + "custom_merchant_name": { + "type": "string", + "description": "Custom merchant name for payment link", + "nullable": true } } }, @@ -11218,12 +11219,12 @@ "start_time": { "type": "string", "format": "date-time", - "description": "The start time to filter refunds list or to get list of filters. To get list of filters start time is needed to be passed" + "description": "The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed" }, "end_time": { "type": "string", "format": "date-time", - "description": "The end time to filter refunds list or to get list of filters. If not passed the default time is now", + "description": "The end time to filter payments list or to get list of filters. If not passed the default time is now", "nullable": true } } diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 144a35f773aa..09772bd13de5 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario11-Create Partial Capture payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index a6a8150c2404..8fca41333799 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index b9e2faa143c9..be7b29473334 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -42,7 +42,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json index 50cb0663b403..b9b658979cf2 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index b080ff1a6b95..8cf69c5039f6 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json index 07ffc4eedefc..d54ac18d3c50 100644 --- a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Payments - Create/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json index 6e9db26a339d..ad8aa7b2ae06 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "13", "card_exp_year": "2023", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json index 0b35b7a4e92b..ab5943ac13ac 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 5b606850fd2e..c3b86fd9b2d3 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index 144a35f773aa..09772bd13de5 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -34,7 +34,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index 09987daa71ec..d7582d82ddea 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -35,7 +35,7 @@ "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4012000033330026", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", diff --git a/postman/collection-dir/paypal/event.prerequest.js b/postman/collection-dir/paypal/event.prerequest.js index e69de29bb2d1..f4c9a7648646 100644 --- a/postman/collection-dir/paypal/event.prerequest.js +++ b/postman/collection-dir/paypal/event.prerequest.js @@ -0,0 +1,27 @@ +const path = pm.request.url.toString(); +const isPostRequest = pm.request.method.toString() === "POST"; +const isPaymentCreation = path.match(/\/payments$/) && isPostRequest; + +if (isPaymentCreation) { + try { + const request = JSON.parse(pm.request.body.toJSON().raw); + + // Attach routing + const routing = { type: "single", data: "paypal" }; + request["routing"] = routing; + + let updatedRequest = { + mode: "raw", + raw: JSON.stringify(request), + options: { + raw: { + language: "json", + }, + }, + }; + pm.request.body.update(updatedRequest); + } catch (error) { + console.error("Failed to inject routing in the request"); + console.error(error); + } +} \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js new file mode 100644 index 000000000000..ffcdd527d07c --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json new file mode 100644 index 000000000000..21f054843897 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -0,0 +1,98 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1, + "customer_id": "bernard123", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_type": "debit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "24", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..20626ecd2a9c --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have "connector_transaction_id" +pm.test( + "[POST]::/payments - Content check if 'connector_transaction_id' exists", + function () { + pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/MerchantAccounts/.meta.json b/postman/collection-dir/stripe/MerchantAccounts/.meta.json index ef71d120e462..02ea600d2eb8 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/.meta.json +++ b/postman/collection-dir/stripe/MerchantAccounts/.meta.json @@ -2,6 +2,7 @@ "childrenOrder": [ "Merchant Account - Create", "Merchant Account - Retrieve", + "Merchant Account - List", "Merchant Account - Update" ] } diff --git a/postman/collection-json/multisafepay.postman_collection.json b/postman/collection-json/multisafepay.postman_collection.json index 8ba70b7c654f..50a2655c5b05 100644 --- a/postman/collection-json/multisafepay.postman_collection.json +++ b/postman/collection-json/multisafepay.postman_collection.json @@ -532,7 +532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"routing\":{\"type\":\"single\",\"data\":\"multisafepay\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1875,6 +1875,233 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario6- Create payment with Invalid Merchant ID", + "item": [ + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"wrongAPIKey\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate error message in the JSON Body", + "pm.test(\"[POST]::/payments - Validate error message\", function () {", + " pm.expect(jsonData.error_message).to.not.be.null", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"routing\":{\"type\":\"single\",\"data\":\"multisafepay\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"someone\",\"last_name\":\"happy\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index bd6459e5c708..4849a27fe051 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -4,7 +4,33 @@ "listen": "prerequest", "script": { "exec": [ - "" + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "", + "if (isPaymentCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"paypal\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject routing in the request\");", + " console.error(error);", + " }", + "}" ], "type": "text/javascript" } @@ -523,7 +549,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -782,7 +808,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1459,7 +1485,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1751,7 +1777,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2389,7 +2415,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2695,7 +2721,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3356,7 +3382,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3833,7 +3859,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4310,7 +4336,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5613,7 +5639,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5879,7 +5905,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6340,7 +6366,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6732,7 +6758,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6993,7 +7019,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 393b2979289b..5d308dd0fe53 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -81,19 +81,19 @@ "name": "MerchantAccounts", "item": [ { - "name": "Merchant Account - List", + "name": "Merchant Account - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -105,6 +105,19 @@ " jsonData = pm.response.json();", "} catch (e) { }", "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", @@ -130,6 +143,36 @@ " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -157,55 +200,53 @@ } ] }, - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, "url": { - "raw": "{{baseUrl}}/accounts/list", + "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ - "accounts", - "list" - ], - "query": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false - } - ], - "variable": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "description": "(Required) - Organization id" - } + "accounts" ] }, - "description": "List merchant accounts for an organization" + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." }, "response": [] }, { - "name": "Merchant Account - Create", + "name": "Merchant Account - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -215,20 +256,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", + "} catch (e) {}", "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", @@ -255,36 +283,6 @@ " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", " );", "}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", - " console.log(", - " \"- use {{organization_id}} as collection variable for value\",", - " jsonData.organization_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/accounts - Organization id is generated\",", - " function () {", - " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ "" ], "type": "text/javascript" @@ -312,53 +310,48 @@ } ] }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" - }, "url": { - "raw": "{{baseUrl}}/accounts", + "raw": "{{baseUrl}}/accounts/:id", "host": [ "{{baseUrl}}" ], "path": [ - "accounts" + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } ] }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + "description": "Retrieve a merchant account details." }, "response": [] }, { - "name": "Merchant Account - Retrieve", + "name": "Merchant Account - List", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", @@ -368,7 +361,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", "if (jsonData?.api_key) {", @@ -430,23 +423,30 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/:id", + "raw": "{{baseUrl}}/accounts/list", "host": [ "{{baseUrl}}" ], "path": [ "accounts", - ":id" + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "disabled": false + } ], "variable": [ { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" } ] }, - "description": "Retrieve a merchant account details." + "description": "List merchant accounts for an organization" }, "response": [] }, @@ -8669,6 +8669,266 @@ } ] }, + { + "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index bcd02f6cbd68..9fdc57bf3c81 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp