diff --git a/.deepsource.toml b/.deepsource.toml index d3932afd2315..640298afbdbe 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -13,4 +13,4 @@ name = "rust" enabled = true [analyzers.meta] -msrv = "1.65.0" +msrv = "1.70.0" diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index d6b3d98b8c82..052afd5d25c1 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -114,7 +114,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.65 + toolchain: "1.70" - uses: Swatinem/rust-cache@v2.7.0 with: @@ -124,7 +124,6 @@ jobs: uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack - version: 0.6.5 - name: Deny warnings shell: bash diff --git a/.github/workflows/CI-push.yml b/.github/workflows/CI-push.yml index 90b301bbd9e5..c946513f723b 100644 --- a/.github/workflows/CI-push.yml +++ b/.github/workflows/CI-push.yml @@ -61,7 +61,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.65 + toolchain: "1.70" - uses: Swatinem/rust-cache@v2.7.0 with: @@ -71,7 +71,6 @@ jobs: uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack - version: 0.6.5 - name: Deny warnings shell: bash diff --git a/.typos.toml b/.typos.toml index 40acb1305892..2d2e165544af 100644 --- a/.typos.toml +++ b/.typos.toml @@ -2,6 +2,7 @@ check-filename = true [default.extend-identifiers] +"ABD" = "ABD" # Aberdeenshire, UK ISO 3166-2 code BA = "BA" # Bosnia and Herzegovina country code CAF = "CAF" # Central African Republic country code flate2 = "flate2" diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b1713ddab3..0d19ed47c70d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,112 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.03.05.0 + +### Features + +- **connector:** [PLACETOPAY] Fix refund request and status mapping ([#3894](https://github.com/juspay/hyperswitch/pull/3894)) ([`5eff9d4`](https://github.com/juspay/hyperswitch/commit/5eff9d47d3e53d380ef792a8fbdf06ecf78d3d16)) +- **webhooks:** Implement automatic retries for failed webhook deliveries using scheduler ([#3842](https://github.com/juspay/hyperswitch/pull/3842)) ([`5bb67c7`](https://github.com/juspay/hyperswitch/commit/5bb67c7dcc22f9cee51adf501bdd8455b41548db)) + +### Bug Fixes + +- **connector:** [Volt] Fix status mapping for Volt ([#3915](https://github.com/juspay/hyperswitch/pull/3915)) ([`f132527`](https://github.com/juspay/hyperswitch/commit/f132527490a7d8cd8469573d8e6856f33974959f)) +- **router:** [nuvei] Nuvei recurring MIT fix and mandatory details fix ([#3602](https://github.com/juspay/hyperswitch/pull/3602)) ([`aa001b4`](https://github.com/juspay/hyperswitch/commit/aa001b4579a6be022b46eb0cc3e65c52ec9d10bb)) + +### Refactors + +- **api_keys:** Provide identifier for api key in the expiry reminder email ([#3888](https://github.com/juspay/hyperswitch/pull/3888)) ([`901d61b`](https://github.com/juspay/hyperswitch/commit/901d61bc0ddb4b2ad742de927126f468629a79af)) +- **connectors:** [Checkout] PII data masking ([#3775](https://github.com/juspay/hyperswitch/pull/3775)) ([`6076eb0`](https://github.com/juspay/hyperswitch/commit/6076eb01ca80ae2d06218a09d2a69f01d78cdec4)) +- **test_utils:** Use json to run collection and add run time edit ([#3807](https://github.com/juspay/hyperswitch/pull/3807)) ([`a1d63d4`](https://github.com/juspay/hyperswitch/commit/a1d63d4b8be273c525aac76f22cf3bda25719f28)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`cd7040f`](https://github.com/juspay/hyperswitch/commit/cd7040fa8cad2e69a53e3ed609c9eb8a8a17495a)) +- Upgrade msrv to 1.70 ([#3938](https://github.com/juspay/hyperswitch/pull/3938)) ([`0e60083`](https://github.com/juspay/hyperswitch/commit/0e600837f77af0443335deb0c73d6f3b2bda5ac2)) + +**Full Changelog:** [`2024.03.04.0...2024.03.05.0`](https://github.com/juspay/hyperswitch/compare/2024.03.04.0...2024.03.05.0) + +- - - + +## 2024.03.04.0 + +### Features + +- **address:** Add payment method billing details ([#3812](https://github.com/juspay/hyperswitch/pull/3812)) ([`33f0741`](https://github.com/juspay/hyperswitch/commit/33f07419abb7adc9198c67604f4d0bebab9faeb4)) +- **core:** Diesel models and db interface changes for authentication table ([#3859](https://github.com/juspay/hyperswitch/pull/3859)) ([`8162668`](https://github.com/juspay/hyperswitch/commit/816266819928477738f70b782eab0e26b600b171)) + +### Bug Fixes + +- **connector:** [BOA/CYB] Pass ucaf for apple pay mastercard users ([#3899](https://github.com/juspay/hyperswitch/pull/3899)) ([`f95beaa`](https://github.com/juspay/hyperswitch/commit/f95beaa189f17a6e117971a749e2b4595e1e2fc3)) +- **mandates:** Remove validation for `mandate_data` object in payments create request ([#3860](https://github.com/juspay/hyperswitch/pull/3860)) ([`49d2298`](https://github.com/juspay/hyperswitch/commit/49d22981026e0bc5105aca842a3be6533bbbd477)) +- **payment_methods:** Insert `locker_id` as null in case of payment method not getting stored in locker ([#3919](https://github.com/juspay/hyperswitch/pull/3919)) ([`9917dd0`](https://github.com/juspay/hyperswitch/commit/9917dd065444d66628039b19df7cd8e7d5c107db)) +- **wasm:** [Adyen] update connector account configs and integration bugs ([#3910](https://github.com/juspay/hyperswitch/pull/3910)) ([`34f7705`](https://github.com/juspay/hyperswitch/commit/34f7705c44f5551ccc34a54b70867177909079a7)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`cb5761b`](https://github.com/juspay/hyperswitch/commit/cb5761be47fa5a9f6a1e0abb135369de96a116fa)) +- Adding addition fields from psql to kafka event for analytics usecase ([#3815](https://github.com/juspay/hyperswitch/pull/3815)) ([`cc0d006`](https://github.com/juspay/hyperswitch/commit/cc0d00633058277e6f49f352e8d158554c864038)) + +**Full Changelog:** [`2024.03.01.0...2024.03.04.0`](https://github.com/juspay/hyperswitch/compare/2024.03.01.0...2024.03.04.0) + +- - - + +## 2024.03.01.0 + +### Features + +- **roles:** Add groups for `get_from_token` api ([#3872](https://github.com/juspay/hyperswitch/pull/3872)) ([`b0b9bfa`](https://github.com/juspay/hyperswitch/commit/b0b9bfa731695b530cdcdeaeba29dc0f88bd8887)) +- Add unresponsive timeout for fred ([#3369](https://github.com/juspay/hyperswitch/pull/3369)) ([`26fb96e`](https://github.com/juspay/hyperswitch/commit/26fb96eeaaaffb4e4f87a644a3f7cc920e4b2057)) + +### Bug Fixes + +- **connector:** [adyen] production endpoints and mappings ([#3900](https://github.com/juspay/hyperswitch/pull/3900)) ([`8933ddf`](https://github.com/juspay/hyperswitch/commit/8933ddff66901027b22bb01424a528d20b54adad)) + +### Refactors + +- **connector:** CANCEL button after redirection is enabled for card 3ds ([#3829](https://github.com/juspay/hyperswitch/pull/3829)) ([`e003958`](https://github.com/juspay/hyperswitch/commit/e003958ff31ea0f1e0cddb3d2369945e8d2a2353)) +- **core:** Status mapping for Capture for 429 http code ([#3897](https://github.com/juspay/hyperswitch/pull/3897)) ([`9b5f26a`](https://github.com/juspay/hyperswitch/commit/9b5f26a5d29fe8d297cb8651b53be5cfba275276)) +- **roles:** Add more checks in create, update role APIs and change the response type ([#3896](https://github.com/juspay/hyperswitch/pull/3896)) ([`0136523`](https://github.com/juspay/hyperswitch/commit/0136523f38b7ceda0022843779ba922d612423a6)) +- **router:** Add parent caller function for DB ([#3838](https://github.com/juspay/hyperswitch/pull/3838)) ([`0936b02`](https://github.com/juspay/hyperswitch/commit/0936b02ade7f57eaa0213c4f4422bff7c9bb4de2)) + +### Miscellaneous Tasks + +- **configs:** [Cashtocode] wasm changes for AUD, INR, JPY, NZD, ZAR currency ([#3892](https://github.com/juspay/hyperswitch/pull/3892)) ([`de7f400`](https://github.com/juspay/hyperswitch/commit/de7f400c07d85b97340255556b39383648a0fd9f)) +- **dispute:** Adding disputeamount as int type ([#3886](https://github.com/juspay/hyperswitch/pull/3886)) ([`7db499d`](https://github.com/juspay/hyperswitch/commit/7db499d8a9388b9a3674f7fa130bc389151840ec)) + +**Full Changelog:** [`2024.02.29.0...2024.03.01.0`](https://github.com/juspay/hyperswitch/compare/2024.02.29.0...2024.03.01.0) + +- - - + +## 2024.02.29.0 + +### Features + +- **analytics:** + - Adding metric api for dispute analytics ([#3810](https://github.com/juspay/hyperswitch/pull/3810)) ([`de6b16b`](https://github.com/juspay/hyperswitch/commit/de6b16bed98280a4ed8fc8cdad920a759662aa19)) + - Add force retrieve call for force retrieve calls ([#3565](https://github.com/juspay/hyperswitch/pull/3565)) ([`032d58c`](https://github.com/juspay/hyperswitch/commit/032d58cdbbf388cf25cbf2e43b0117b83f7d076d)) +- **payment_methods:** Add default payment method column in customers table and last used column in payment_methods table ([#3790](https://github.com/juspay/hyperswitch/pull/3790)) ([`f3931cf`](https://github.com/juspay/hyperswitch/commit/f3931cf484f61a4d9c107c362d0f3f6ee872e0e7)) +- **payouts:** Implement Smart Retries for Payout ([#3580](https://github.com/juspay/hyperswitch/pull/3580)) ([`8b32dff`](https://github.com/juspay/hyperswitch/commit/8b32dffe324a4cdbfde173cffe3fad2e839a52aa)) + +### Bug Fixes + +- **tests/postman/adyen:** Enable sepa payment method type for payout flows ([#3861](https://github.com/juspay/hyperswitch/pull/3861)) ([`53559c2`](https://github.com/juspay/hyperswitch/commit/53559c22527dde9536aa493ad7cd3bf353335c1a)) + +### Refactors + +- **connector:** + - [Gocardless] Mask PII data ([#3844](https://github.com/juspay/hyperswitch/pull/3844)) ([`2f3ec7f`](https://github.com/juspay/hyperswitch/commit/2f3ec7f951967359d3995f743a486f3b380dd1f8)) + - [Mollie] Mask PII data ([#3856](https://github.com/juspay/hyperswitch/pull/3856)) ([`ffbe042`](https://github.com/juspay/hyperswitch/commit/ffbe042fdccde4a721d329d6b85c95203234368e)) +- **payment_link:** Add Miscellaneous charges in cart ([#3645](https://github.com/juspay/hyperswitch/pull/3645)) ([`15b367e`](https://github.com/juspay/hyperswitch/commit/15b367eb792448fb3f3312484ab13dd8241d4a14)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`5c91a94`](https://github.com/juspay/hyperswitch/commit/5c91a9440e098490cc00a54ead34989da81babc0)) + +**Full Changelog:** [`2024.02.28.0...2024.02.29.0`](https://github.com/juspay/hyperswitch/compare/2024.02.28.0...2024.02.29.0) + +- - - + ## 2024.02.28.0 ### Features diff --git a/Cargo.lock b/Cargo.lock index 2c9293c1701f..60bc557972c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2610,9 +2610,9 @@ dependencies = [ [[package]] name = "fred" -version = "7.1.0" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9282e65613822eea90c99872c51afa1de61542215cb11f91456a93f50a5a131a" +checksum = "b99c2b48934cd02a81032dd7428b7ae831a27794275bc94eba367418db8a9e55" dependencies = [ "arc-swap", "async-trait", @@ -4977,14 +4977,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.6" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.9", - "regex-syntax 0.7.5", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -4998,13 +4998,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.9" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", ] [[package]] @@ -5031,6 +5031,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "rend" version = "0.4.1" @@ -6356,6 +6362,7 @@ dependencies = [ "clap", "masking", "rand 0.8.5", + "regex", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 21e01cf4d3c0..82687a32ba6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = ["crates/*"] package.edition = "2021" -package.rust-version = "1.65" +package.rust-version = "1.70" package.license = "Apache-2.0" [profile.release] diff --git a/INSTALL_dependencies.sh b/INSTALL_dependencies.sh index 5c4886115204..288dea1bd44f 100755 --- a/INSTALL_dependencies.sh +++ b/INSTALL_dependencies.sh @@ -9,7 +9,7 @@ if [[ "${TRACE-0}" == "1" ]]; then set -o xtrace fi -RUST_MSRV=1.65.0 +RUST_MSRV=1.70.0 _DB_NAME="hyperswitch_db" _DB_USER="db_user" _DB_PASS="db_password" diff --git a/config/config.example.toml b/config/config.example.toml index abe3f6b4e087..dd05eb61d470 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -54,9 +54,10 @@ use_legacy_version = false # Resp protocol for fred crate (set this to tr stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. -max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. -default_command_timeout = 0 # An optional timeout to apply to all commands. -max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 30 # An optional timeout to apply to all commands. In seconds +unresponsive_timeout = 10 # An optional timeout for Unresponsive commands in seconds. This should be less than default_command_timeout. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. # This section provides configs for currency conversion api [forex_api] @@ -409,6 +410,10 @@ afterpay_clearpay = { fields = { stripe = [ # payment_method_type = afterpay_cle payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility [pm_filters.adyen] +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} +ideal = { country = "NL", currency = "EUR" } online_banking_fpx = { country = "MY", currency = "MYR" } online_banking_thailand = { country = "TH", currency = "THB" } touch_n_go = { country = "MY", currency = "MYR" } diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index c8c118d98273..990796c79bcc 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -169,9 +169,10 @@ use_legacy_version = false # RESP p stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. -max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. -default_command_timeout = 0 # An optional timeout to apply to all commands. -max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 30 # An optional timeout to apply to all commands. In seconds +unresponsive_timeout = 10 # An optional timeout for Unresponsive commands in seconds. This should be less than default_command_timeout. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. cluster_enabled = true # boolean cluster_urls = ["redis.cluster.uri-1:8080", "redis.cluster.uri-2:4115"] # List of redis cluster urls diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index dba591ae6792..dd755c9dd226 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -1,7 +1,7 @@ [bank_config] eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" -ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,ing,knab,n26,nationale_nederlanden,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot,yoursafe" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" @@ -174,7 +174,7 @@ google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT, ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -193,12 +193,13 @@ oxxo = { country = "MX", currency = "MXN" } pay_bright = { country = "CA", currency = "CAD" } pay_easy = { country = "JP", currency = "JPY" } pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } + swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 48bb9d33e3f2..9c2f25b24ff4 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -1,7 +1,7 @@ [bank_config] eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" -ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,ing,knab,n26,nationale_nederlanden,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot,yoursafe" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" @@ -17,8 +17,8 @@ payout_connector_list = "wise" [connectors] aci.base_url = "https://eu-test.oppwa.com/" -adyen.base_url = "https://checkout-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/" +adyen.secondary_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" 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" @@ -189,7 +189,7 @@ google_pay = { country = "AE,AG,AL,AO,AR,AS,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CL,CO, ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,BE,CA,CH,DE,DK,ES,FI,FR,GB,IE,IT,NL,NO,PL,PT,SE,GB,US", currency = "AUD,CAD,CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -213,7 +213,7 @@ permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE,GB", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 8723f871c349..5c052e5c85f7 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -1,7 +1,7 @@ [bank_config] eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" -ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,ing,knab,n26,nationale_nederlanden,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot,yoursafe" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" @@ -189,7 +189,7 @@ google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT, ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -213,7 +213,7 @@ permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } diff --git a/config/development.toml b/config/development.toml index d69719bd4194..8b3c036af4d1 100644 --- a/config/development.toml +++ b/config/development.toml @@ -44,7 +44,8 @@ stream_read_count = 1 auto_pipeline = true disable_auto_backpressure = false max_in_flight_commands = 5000 -default_command_timeout = 0 +default_command_timeout = 30 +unresponsive_timeout = 10 max_feed_count = 200 @@ -71,7 +72,6 @@ mock_locker = true basilisk_host = "" locker_enabled = true - [forex_api] call_delay = 21600 local_fetch_retry_count = 5 @@ -256,7 +256,7 @@ adyen = { banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_ban [bank_config.ideal] stripe = { banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" } -adyen = { banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" } +adyen = { banks = "abn_amro,asn_bank,bunq,ing,knab,n26,nationale_nederlanden,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot, yoursafe" } [bank_config.online_banking_czech_republic] adyen = { banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" } @@ -314,14 +314,14 @@ mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } mb_way = { country = "PT", currency = "EUR" } -klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} affirm = { country = "US", currency = "USD" } afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } pay_bright = { country = "CA", currency = "CAD" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } giropay = { country = "DE", currency = "EUR" } eps = { country = "AT", currency = "EUR" } -sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} ideal = { country = "NL", currency = "EUR" } blik = {country = "PL", currency = "PLN"} trustly = {country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 8170132bb85a..a2beb445b7af 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -78,7 +78,8 @@ stream_read_count = 1 auto_pipeline = true disable_auto_backpressure = false max_in_flight_commands = 5000 -default_command_timeout = 0 +default_command_timeout = 30 +unresponsive_timeout = 10 max_feed_count = 200 [cors] @@ -305,6 +306,11 @@ family_mart = {country = "JP", currency = "JPY"} seicomart = {country = "JP", currency = "JPY"} pay_easy = {country = "JP", currency = "JPY"} boleto = { country = "BR", currency = "BRL" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} + [pm_filters.volt] open_banking_uk = {country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT,NL,PT,SI,SK,BG,CZ,DK,HU,NO,PL,RO,SE,AU,BR", currency = "EUR,GBP,DKK,NOK,PLN,SEK,AUD,BRL"} diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/dispute_analytics.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/dispute_analytics.sql deleted file mode 100644 index 92a748ff4890..000000000000 --- a/crates/analytics/docs/clickhouse/cluster_setup/scripts/dispute_analytics.sql +++ /dev/null @@ -1,142 +0,0 @@ -CREATE TABLE hyperswitch.dispute_queue on cluster '{cluster}' ( - `dispute_id` String, - `amount` String, - `currency` String, - `dispute_stage` LowCardinality(String), - `dispute_status` LowCardinality(String), - `payment_id` String, - `attempt_id` String, - `merchant_id` String, - `connector_status` String, - `connector_dispute_id` String, - `connector_reason` Nullable(String), - `connector_reason_code` Nullable(String), - `challenge_required_by` Nullable(DateTime) CODEC(T64, LZ4), - `connector_created_at` Nullable(DateTime) CODEC(T64, LZ4), - `connector_updated_at` Nullable(DateTime) CODEC(T64, LZ4), - `created_at` DateTime CODEC(T64, LZ4), - `modified_at` DateTime CODEC(T64, LZ4), - `connector` LowCardinality(String), - `evidence` Nullable(String), - `profile_id` Nullable(String), - `merchant_connector_id` Nullable(String), - `sign_flag` Int8 -) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', -kafka_topic_list = 'hyperswitch-dispute-events', -kafka_group_name = 'hyper-c1', -kafka_format = 'JSONEachRow', -kafka_handle_error_mode = 'stream'; - - -CREATE MATERIALIZED VIEW hyperswitch.dispute_mv on cluster '{cluster}' TO hyperswitch.dispute ( - `dispute_id` String, - `amount` String, - `currency` String, - `dispute_stage` LowCardinality(String), - `dispute_status` LowCardinality(String), - `payment_id` String, - `attempt_id` String, - `merchant_id` String, - `connector_status` String, - `connector_dispute_id` String, - `connector_reason` Nullable(String), - `connector_reason_code` Nullable(String), - `challenge_required_by` Nullable(DateTime64(3)), - `connector_created_at` Nullable(DateTime64(3)), - `connector_updated_at` Nullable(DateTime64(3)), - `created_at` DateTime64(3), - `modified_at` DateTime64(3), - `connector` LowCardinality(String), - `evidence` Nullable(String), - `profile_id` Nullable(String), - `merchant_connector_id` Nullable(String), - `inserted_at` DateTime64(3), - `sign_flag` Int8 -) AS -SELECT - dispute_id, - amount, - currency, - dispute_stage, - dispute_status, - payment_id, - attempt_id, - merchant_id, - connector_status, - connector_dispute_id, - connector_reason, - connector_reason_code, - challenge_required_by, - connector_created_at, - connector_updated_at, - created_at, - modified_at, - connector, - evidence, - profile_id, - merchant_connector_id, - now() as inserted_at, - sign_flag -FROM - hyperswitch.dispute_queue -WHERE length(_error) = 0; - - -CREATE TABLE hyperswitch.dispute_clustered on cluster '{cluster}' ( - `dispute_id` String, - `amount` String, - `currency` String, - `dispute_stage` LowCardinality(String), - `dispute_status` LowCardinality(String), - `payment_id` String, - `attempt_id` String, - `merchant_id` String, - `connector_status` String, - `connector_dispute_id` String, - `connector_reason` Nullable(String), - `connector_reason_code` Nullable(String), - `challenge_required_by` Nullable(DateTime) CODEC(T64, LZ4), - `connector_created_at` Nullable(DateTime) CODEC(T64, LZ4), - `connector_updated_at` Nullable(DateTime) CODEC(T64, LZ4), - `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), - `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), - `connector` LowCardinality(String), - `evidence` String DEFAULT '{}' CODEC(T64, LZ4), - `profile_id` Nullable(String), - `merchant_connector_id` Nullable(String), - `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), - `sign_flag` Int8 - INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, - INDEX disputeStatusIndex dispute_status TYPE bloom_filter GRANULARITY 1, - INDEX disputeStageIndex dispute_stage TYPE bloom_filter GRANULARITY 1 -) ENGINE = ReplicatedCollapsingMergeTree( - '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/dispute_clustered', - '{replica}', - dispute_status -) -PARTITION BY toStartOfDay(created_at) -ORDER BY - (created_at, merchant_id, dispute_id) -TTL created_at + toIntervalMonth(6); - - -CREATE MATERIALIZED VIEW hyperswitch.dispute_parse_errors on cluster '{cluster}' -( - `topic` String, - `partition` Int64, - `offset` Int64, - `raw` String, - `error` String -) -ENGINE = MergeTree -ORDER BY (topic, partition, offset) -SETTINGS index_granularity = 8192 AS -SELECT - _topic AS topic, - _partition AS partition, - _offset AS offset, - _raw_message AS raw, - _error AS error -FROM hyperswitch.dispute_queue -WHERE length(_error) > 0 -; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/scripts/api_events.sql b/crates/analytics/docs/clickhouse/scripts/api_events.sql index 49a6472eaa4d..e466fc56cb6e 100644 --- a/crates/analytics/docs/clickhouse/scripts/api_events.sql +++ b/crates/analytics/docs/clickhouse/scripts/api_events.sql @@ -58,7 +58,7 @@ CREATE TABLE api_events_dist ( `hs_latency` Nullable(UInt128), `http_method` LowCardinality(String), `url_path` String, - `dispute_id` Nullable(String) + `dispute_id` Nullable(String), INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 diff --git a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql index 276e311e57a9..8b7715044c1c 100644 --- a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql +++ b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql @@ -29,6 +29,15 @@ CREATE TABLE payment_attempts_queue ( `created_at` DateTime CODEC(T64, LZ4), `last_synced` Nullable(DateTime) CODEC(T64, LZ4), `modified_at` DateTime CODEC(T64, LZ4), + `payment_method_data` Nullable(String), + `error_reason` Nullable(String), + `multiple_capture_count` Nullable(Int16), + `amount_capturable` Nullable(UInt64) , + `merchant_connector_id` Nullable(String), + `net_amount` Nullable(UInt64) , + `unified_code` Nullable(String), + `unified_message` Nullable(String), + `mandate_data` Nullable(String), `sign_flag` Int8 ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-payment-attempt-events', @@ -67,6 +76,15 @@ CREATE TABLE payment_attempt_dist ( `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), `last_synced` Nullable(DateTime) CODEC(T64, LZ4), `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `payment_method_data` Nullable(String), + `error_reason` Nullable(String), + `multiple_capture_count` Nullable(Int16), + `amount_capturable` Nullable(UInt64) , + `merchant_connector_id` Nullable(String), + `net_amount` Nullable(UInt64) , + `unified_code` Nullable(String), + `unified_message` Nullable(String), + `mandate_data` Nullable(String), `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), `sign_flag` Int8, INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, @@ -115,6 +133,15 @@ CREATE MATERIALIZED VIEW kafka_parse_pa TO payment_attempt_dist ( `capture_on` Nullable(DateTime64(3)), `last_synced` Nullable(DateTime64(3)), `modified_at` DateTime64(3), + `payment_method_data` Nullable(String), + `error_reason` Nullable(String), + `multiple_capture_count` Nullable(Int16), + `amount_capturable` Nullable(UInt64) , + `merchant_connector_id` Nullable(String), + `net_amount` Nullable(UInt64) , + `unified_code` Nullable(String), + `unified_message` Nullable(String), + `mandate_data` Nullable(String), `inserted_at` DateTime64(3), `sign_flag` Int8 ) AS @@ -149,6 +176,15 @@ SELECT capture_on, last_synced, modified_at, + payment_method_data, + error_reason, + multiple_capture_count, + amount_capturable, + merchant_connector_id, + net_amount, + unified_code, + unified_message, + mandate_data, now() as inserted_at, sign_flag FROM diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index fed029f2edb3..6ebac7c1d921 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -23,7 +23,7 @@ use crate::{ metrics::{latency::LatencyAvg, ApiEventMetricRow}, }, connector_events::events::ConnectorEventsResult, - disputes::filters::DisputeFilterRow, + disputes::{filters::DisputeFilterRow, metrics::DisputeMetricRow}, outgoing_webhook_event::events::OutgoingWebhookLogsResult, sdk_events::events::SdkEventsResult, types::TableEngine, @@ -170,6 +170,7 @@ impl super::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics { } impl super::disputes::filters::DisputeFilterAnalytics for ClickhouseClient {} +impl super::disputes::metrics::DisputeMetricAnalytics for ClickhouseClient {} #[derive(Debug, serde::Serialize)] struct CkhQuery { @@ -278,6 +279,17 @@ impl TryInto for serde_json::Value { )) } } +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse DisputeMetricRow in clickhouse results", + )) + } +} impl TryInto for serde_json::Value { type Error = Report; diff --git a/crates/analytics/src/disputes.rs b/crates/analytics/src/disputes.rs index b6d7e6280c73..8cf1b3db0fd8 100644 --- a/crates/analytics/src/disputes.rs +++ b/crates/analytics/src/disputes.rs @@ -1,5 +1,9 @@ +pub mod accumulators; mod core; - pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulators::{DisputeMetricAccumulator, DisputeMetricsAccumulator}; -pub use self::core::get_filters; +pub trait DisputeAnalytics: metrics::DisputeMetricAnalytics {} +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/analytics/src/disputes/accumulators.rs b/crates/analytics/src/disputes/accumulators.rs new file mode 100644 index 000000000000..1997d75d3230 --- /dev/null +++ b/crates/analytics/src/disputes/accumulators.rs @@ -0,0 +1,100 @@ +use api_models::analytics::disputes::DisputeMetricsBucketValue; +use diesel_models::enums as storage_enums; + +use super::metrics::DisputeMetricRow; +#[derive(Debug, Default)] +pub struct DisputeMetricsAccumulator { + pub disputes_status_rate: RateAccumulator, + pub total_amount_disputed: SumAccumulator, + pub total_dispute_lost_amount: SumAccumulator, +} +#[derive(Debug, Default)] +pub struct RateAccumulator { + pub won_count: i64, + pub challenged_count: i64, + pub lost_count: i64, + pub total: i64, +} +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option, +} + +pub trait DisputeMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl DisputeMetricAccumulator for SumAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) { + self.total = match ( + self.total, + metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.total.and_then(|i| u64::try_from(i).ok()) + } +} + +impl DisputeMetricAccumulator for RateAccumulator { + type MetricOutput = Option<(Option, Option, Option, Option)>; + + fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) { + if let Some(ref dispute_status) = metrics.dispute_status { + if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeChallenged { + self.challenged_count += metrics.count.unwrap_or_default(); + } + if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeWon { + self.won_count += metrics.count.unwrap_or_default(); + } + if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeLost { + self.lost_count += metrics.count.unwrap_or_default(); + } + }; + + self.total += metrics.count.unwrap_or_default(); + } + + fn collect(self) -> Self::MetricOutput { + if self.total <= 0 { + Some((None, None, None, None)) + } else { + Some(( + u64::try_from(self.challenged_count).ok(), + u64::try_from(self.won_count).ok(), + u64::try_from(self.lost_count).ok(), + u64::try_from(self.total).ok(), + )) + } + } +} + +impl DisputeMetricsAccumulator { + pub fn collect(self) -> DisputeMetricsBucketValue { + let (challenge_rate, won_rate, lost_rate, total_dispute) = + self.disputes_status_rate.collect().unwrap_or_default(); + DisputeMetricsBucketValue { + disputes_challenged: challenge_rate, + disputes_won: won_rate, + disputes_lost: lost_rate, + total_amount_disputed: self.total_amount_disputed.collect(), + total_dispute_lost_amount: self.total_dispute_lost_amount.collect(), + total_dispute, + } + } +} diff --git a/crates/analytics/src/disputes/core.rs b/crates/analytics/src/disputes/core.rs index 8ccbbdea6d26..ae013aa81d17 100644 --- a/crates/analytics/src/disputes/core.rs +++ b/crates/analytics/src/disputes/core.rs @@ -1,15 +1,125 @@ +use std::collections::HashMap; + use api_models::analytics::{ - disputes::DisputeDimensions, DisputeFilterValue, DisputeFiltersResponse, - GetDisputeFilterRequest, + disputes::{ + DisputeDimensions, DisputeMetrics, DisputeMetricsBucketIdentifier, + DisputeMetricsBucketResponse, + }, + AnalyticsMetadata, DisputeFilterValue, DisputeFiltersResponse, GetDisputeFilterRequest, + GetDisputeMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, }; -use error_stack::ResultExt; -use super::filters::{get_dispute_filter_for_dimension, DisputeFilterRow}; +use super::{ + filters::{get_dispute_filter_for_dimension, DisputeFilterRow}, + DisputeMetricsAccumulator, +}; use crate::{ + disputes::DisputeMetricAccumulator, errors::{AnalyticsError, AnalyticsResult}, - AnalyticsProvider, + metrics, AnalyticsProvider, }; +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &String, + req: GetDisputeMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + DisputeMetricsBucketIdentifier, + DisputeMetricsAccumulator, + > = HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_dispute_query", + refund_metric = metric_type.as_ref() + ); + // Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference + // We can optimize away this clone once that is fixed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_dispute_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + DisputeMetrics::DisputeStatusMetric => metrics_builder + .disputes_status_rate + .add_metrics_bucket(&value), + DisputeMetrics::TotalAmountDisputed => metrics_builder + .total_amount_disputed + .add_metrics_bucket(&value), + DisputeMetrics::TotalDisputeLostAmount => metrics_builder + .total_dispute_lost_amount + .add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| DisputeMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + pub async fn get_filters( pool: &AnalyticsProvider, req: GetDisputeFilterRequest, @@ -76,9 +186,7 @@ pub async fn get_filters( .change_context(AnalyticsError::UnknownError)? .into_iter() .filter_map(|fil: DisputeFilterRow| match dim { - DisputeDimensions::DisputeStatus => fil.dispute_status, DisputeDimensions::DisputeStage => fil.dispute_stage, - DisputeDimensions::ConnectorStatus => fil.connector_status, DisputeDimensions::Connector => fil.connector, }) .collect::>(); diff --git a/crates/analytics/src/disputes/metrics.rs b/crates/analytics/src/disputes/metrics.rs new file mode 100644 index 000000000000..4963626d0faf --- /dev/null +++ b/crates/analytics/src/disputes/metrics.rs @@ -0,0 +1,119 @@ +mod dispute_status_metric; +mod total_amount_disputed; +mod total_dispute_lost_amount; + +use api_models::{ + analytics::{ + disputes::{ + DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier, + }, + Granularity, + }, + payments::TimeRange, +}; +use diesel_models::enums as storage_enums; +use time::PrimitiveDateTime; + +use self::{ + dispute_status_metric::DisputeStatusMetric, total_amount_disputed::TotalAmountDisputed, + total_dispute_lost_amount::TotalDisputeLostAmount, +}; +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; +#[derive(Debug, Eq, PartialEq, serde::Deserialize)] +pub struct DisputeMetricRow { + pub dispute_stage: Option>, + pub dispute_status: Option>, + pub connector: Option, + pub total: Option, + pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait DisputeMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait DisputeMetric +where + T: AnalyticsDataSource + DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl DisputeMetric for DisputeMetrics +where + T: AnalyticsDataSource + DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::TotalAmountDisputed => { + TotalAmountDisputed::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::DisputeStatusMetric => { + DisputeStatusMetric::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::TotalDisputeLostAmount => { + TotalDisputeLostAmount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/disputes/metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs new file mode 100644 index 000000000000..5b97021beccb --- /dev/null +++ b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs @@ -0,0 +1,119 @@ +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct DisputeStatusMetric {} + +#[async_trait::async_trait] +impl super::DisputeMetric for DisputeStatusMetric +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::Dispute); + + for dim in dimensions { + query_builder.add_select_column(dim).switch()?; + } + + query_builder.add_select_column("dispute_status").switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions { + query_builder.add_group_by_clause(dim).switch()?; + } + + query_builder + .add_group_by_clause("dispute_status") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/disputes/metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs new file mode 100644 index 000000000000..cbba553aa85f --- /dev/null +++ b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs @@ -0,0 +1,116 @@ +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct TotalAmountDisputed {} + +#[async_trait::async_trait] +impl super::DisputeMetric for TotalAmountDisputed +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Dispute); + + for dim in dimensions { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "dispute_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + query_builder + .add_filter_clause("dispute_status", "dispute_won") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs new file mode 100644 index 000000000000..c7be2ab1a982 --- /dev/null +++ b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct TotalDisputeLostAmount {} + +#[async_trait::async_trait] +impl super::DisputeMetric for TotalDisputeLostAmount +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Dispute); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "dispute_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause("dispute_status", "dispute_lost") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/disputes/types.rs b/crates/analytics/src/disputes/types.rs new file mode 100644 index 000000000000..762e8d27554f --- /dev/null +++ b/crates/analytics/src/disputes/types.rs @@ -0,0 +1,29 @@ +use api_models::analytics::disputes::{DisputeDimensions, DisputeFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for DisputeFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(DisputeDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + + if !self.dispute_stage.is_empty() { + builder + .add_filter_in_range_clause(DisputeDimensions::DisputeStage, &self.dispute_stage) + .attach_printable("Error adding dispute stage filter")?; + } + + Ok(()) + } +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 0fb8d9eea6e6..c9752bd27a61 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -15,6 +15,7 @@ pub mod sdk_events; mod sqlx; mod types; use api_event::metrics::{ApiEventMetric, ApiEventMetricRow}; +use disputes::metrics::{DisputeMetric, DisputeMetricRow}; pub use types::AnalyticsDomain; pub mod lambda_utils; pub mod utils; @@ -25,6 +26,7 @@ use api_models::analytics::{ api_event::{ ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, }, + disputes::{DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier}, payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, sdk_events::{ @@ -393,6 +395,106 @@ impl AnalyticsProvider { .await } + pub async fn get_dispute_metrics( + &self, + metric: &DisputeMetrics, + dimensions: &[DisputeDimensions], + merchant_id: &str, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each refund metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres disputes analytics metrics") + } + _ => {} + }; + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres disputes analytics metrics") + } + _ => {} + }; + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + pub async fn get_sdk_event_metrics( &self, metric: &SdkEventMetrics, diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index bc21ab8f0f4d..27e4154a1a98 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -11,7 +11,8 @@ use api_models::{ Granularity, }, enums::{ - AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, + AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, PaymentMethod, + PaymentMethodType, }, refunds::RefundStatus, }; @@ -363,8 +364,6 @@ impl_to_sql_for_to_string!( PaymentDimensions, &PaymentDistributions, RefundDimensions, - &DisputeDimensions, - DisputeDimensions, PaymentMethod, PaymentMethodType, AuthenticationType, @@ -386,6 +385,8 @@ impl_to_sql_for_to_string!(&SdkEventDimensions, SdkEventDimensions, SdkEventName impl_to_sql_for_to_string!(&ApiEventDimensions, ApiEventDimensions); +impl_to_sql_for_to_string!(&DisputeDimensions, DisputeDimensions, DisputeStage); + #[derive(Debug)] pub enum FilterTypes { Equal, diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 0aeffeafa9e1..e88fe519c3cb 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -1,6 +1,9 @@ use std::{fmt::Display, str::FromStr}; -use api_models::analytics::refunds::RefundType; +use api_models::{ + analytics::refunds::RefundType, + enums::{DisputeStage, DisputeStatus}, +}; use common_utils::errors::{CustomResult, ParsingError}; use diesel_models::enums::{ AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, @@ -89,6 +92,8 @@ db_type!(AttemptStatus); db_type!(PaymentMethod, TEXT); db_type!(RefundStatus); db_type!(RefundType); +db_type!(DisputeStage); +db_type!(DisputeStatus); impl<'q, Type> Encode<'q, Postgres> for DBEnumWrapper where @@ -145,6 +150,7 @@ impl super::payments::distribution::PaymentDistributionAnalytics for SqlxClient impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} impl super::disputes::filters::DisputeFilterAnalytics for SqlxClient {} +impl super::disputes::metrics::DisputeMetricAnalytics for SqlxClient {} #[async_trait::async_trait] impl AnalyticsDataSource for SqlxClient { @@ -454,6 +460,48 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::filters::DisputeFilterRow { }) } } +impl<'a> FromRow<'a, PgRow> for super::disputes::metrics::DisputeMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let dispute_stage: Option> = + row.try_get("dispute_stage").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let dispute_status: Option> = + row.try_get("dispute_status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + // Removing millisecond precision to get accurate diffs against clickhouse + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + dispute_stage, + dispute_status, + connector, + total, + count, + start_bucket, + end_bucket, + }) + } +} impl ToSql for PrimitiveDateTime { fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index c12f8e7adfe9..1434c357798a 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -5,7 +5,7 @@ use masking::Secret; use self::{ api_event::{ApiEventDimensions, ApiEventMetrics}, - disputes::DisputeDimensions, + disputes::{DisputeDimensions, DisputeMetrics}, payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, sdk_events::{SdkEventDimensions, SdkEventMetrics}, @@ -271,3 +271,17 @@ pub struct DisputeFilterValue { pub dimension: DisputeDimensions, pub values: Vec, } + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetDisputeMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: disputes::DisputeFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs index 4b4b7ba3830f..edb85c129e67 100644 --- a/crates/api_models/src/analytics/disputes.rs +++ b/crates/api_models/src/analytics/disputes.rs @@ -1,4 +1,10 @@ -use super::NameDescription; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; +use crate::enums::DisputeStage; #[derive( Clone, @@ -15,9 +21,7 @@ use super::NameDescription; #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum DisputeMetrics { - DisputesChallenged, - DisputesWon, - DisputesLost, + DisputeStatusMetric, TotalAmountDisputed, TotalDisputeLostAmount, } @@ -42,8 +46,6 @@ pub enum DisputeDimensions { // Do not change the order of these enums // Consult the Dashboard FE folks since these also affects the order of metrics on FE Connector, - DisputeStatus, - ConnectorStatus, DisputeStage, } @@ -64,3 +66,70 @@ impl From for NameDescription { } } } + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct DisputeFilters { + #[serde(default)] + pub dispute_stage: Vec, + pub connector: Vec, +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct DisputeMetricsBucketIdentifier { + pub dispute_stage: Option, + pub connector: Option, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl Hash for DisputeMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.dispute_stage.hash(state); + self.connector.hash(state); + self.time_bucket.hash(state); + } +} +impl PartialEq for DisputeMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +impl DisputeMetricsBucketIdentifier { + pub fn new( + dispute_stage: Option, + connector: Option, + normalized_time_range: TimeRange, + ) -> Self { + Self { + dispute_stage, + connector, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +#[derive(Debug, serde::Serialize)] +pub struct DisputeMetricsBucketValue { + pub disputes_challenged: Option, + pub disputes_won: Option, + pub disputes_lost: Option, + pub total_amount_disputed: Option, + pub total_dispute_lost_amount: Option, + pub total_dispute: Option, +} +#[derive(Debug, serde::Serialize)] +pub struct DisputeMetricsBucketResponse { + #[serde(flatten)] + pub values: DisputeMetricsBucketValue, + #[serde(flatten)] + pub dimensions: DisputeMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index d8c864746a8f..eeb10e622860 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -73,6 +73,9 @@ pub struct CustomerResponse { /// object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, + /// The identifier for the default payment method. + #[schema(max_length = 64, example = "pm_djh2837dwduh890123")] + pub default_payment_method_id: Option, } #[derive(Default, Clone, Debug, Deserialize, Serialize)] diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 45851e4c11bb..60c48f3d03cb 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -434,6 +434,9 @@ pub enum BankNames { TsbBank, TescoBank, UlsterBank, + Yoursafe, + N26, + NationaleNederlanden, } #[derive( diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 59b2d54016aa..218881389dbf 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -97,7 +97,8 @@ impl_misc_api_event_type!( ConnectorEventsRequest, OutgoingWebhookLogsRequest, GetDisputeFilterRequest, - DisputeFiltersResponse + DisputeFiltersResponse, + GetDisputeMetricRequest ); #[cfg(feature = "stripe")] diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 32d3dc30bd8d..92f699339008 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -2,7 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::{ payment_methods::{ - CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, + CustomerDefaultPaymentMethodResponse, CustomerPaymentMethodsListResponse, + DefaultPaymentMethod, PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ @@ -95,6 +96,16 @@ impl ApiEventMetric for PaymentMethodResponse { impl ApiEventMetric for PaymentMethodUpdate {} +impl ApiEventMetric for DefaultPaymentMethod { + 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 PaymentMethodDeleteResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethod { @@ -121,6 +132,16 @@ impl ApiEventMetric for PaymentMethodListRequest { impl ApiEventMetric for PaymentMethodListResponse {} +impl ApiEventMetric for CustomerDefaultPaymentMethodResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.default_payment_method_id.clone().unwrap_or_default(), + payment_method: Some(self.payment_method), + payment_method_type: self.payment_method_type, + }) + } +} + impl ApiEventMetric for PaymentListFilterConstraints { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index f46c5cf312b0..0d42d1de7d67 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -2,8 +2,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ role::{ - CreateRoleRequest, GetRoleRequest, ListRolesResponse, RoleInfoResponse, - RoleInfoWithPermissionsResponse, UpdateRoleRequest, + CreateRoleRequest, GetRoleFromTokenResponse, GetRoleRequest, ListRolesResponse, + RoleInfoResponse, RoleInfoWithGroupsResponse, RoleInfoWithPermissionsResponse, + UpdateRoleRequest, }, AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, TransferOrgOwnershipRequest, UpdateUserRoleRequest, @@ -20,5 +21,7 @@ common_utils::impl_misc_api_event_type!( CreateRoleRequest, UpdateRoleRequest, ListRolesResponse, - RoleInfoResponse + RoleInfoResponse, + GetRoleFromTokenResponse, + RoleInfoWithGroupsResponse ); diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 83e1a2c4887a..35193b958f2b 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -185,6 +185,10 @@ pub struct PaymentMethodResponse { #[cfg(feature = "payouts")] #[schema(value_type = Option)] pub bank_transfer: Option, + + #[schema(value_type = Option, example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_used_at: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -550,6 +554,10 @@ pub struct PaymentMethodListRequest { /// Indicates whether the payment method is eligible for card netwotks #[schema(value_type = Option>, example = json!(["visa", "mastercard"]))] pub card_networks: Option>, + + /// Indicates the limit of last used payment methods + #[schema(example = 1)] + pub limit: Option, } impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { @@ -618,6 +626,9 @@ impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { Some(inner) => inner.push(map.next_value()?), None => output.card_networks = Some(vec![map.next_value()?]), }, + "limit" => { + set_or_reject_duplicate(&mut output.limit, "limit", map.next_value()?)?; + } _ => {} } } @@ -731,12 +742,30 @@ pub struct PaymentMethodDeleteResponse { #[schema(example = true)] pub deleted: bool, } +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct CustomerDefaultPaymentMethodResponse { + /// The unique identifier of the Payment method + #[schema(example = "card_rGK4Vi5iSW70MY7J2mIy")] + pub default_payment_method_id: Option, + /// The unique identifier of the customer. + #[schema(example = "cus_meowerunwiuwiwqw")] + pub customer_id: String, + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method: api_enums::PaymentMethod, + /// This is a sub-category of payment method. + #[schema(value_type = Option,example = "credit")] + pub payment_method_type: Option, +} #[derive(Debug, Clone, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethod { /// Token for payment method in temporary card locker which gets refreshed often #[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")] pub payment_token: String, + /// The unique identifier of the customer. + #[schema(example = "pm_iouuy468iyuowqs")] + pub payment_method_id: String, /// The unique identifier of the customer. #[schema(example = "cus_meowerunwiuwiwqw")] @@ -798,6 +827,14 @@ pub struct CustomerPaymentMethod { /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, + + /// A timestamp (ISO 8601 code) that determines when the payment method was last used + #[schema(value_type = Option,example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_used_at: Option, + /// Indicates if the payment method has been set to default or not + #[schema(example = true)] + pub default_payment_method_set: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] @@ -810,6 +847,11 @@ pub struct PaymentMethodId { pub payment_method_id: String, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct DefaultPaymentMethod { + pub customer_id: String, + pub payment_method_id: String, +} //------------------------------------------------TokenizeService------------------------------------------------ #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenizePayloadEncrypted { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 91a289652b87..9a3afd2f0ff2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -253,7 +253,7 @@ pub struct PaymentsRequest { #[schema(value_type = Option, example = "no_three_ds", default = "three_ds")] pub authentication_type: Option, - /// The billing details of the customer + /// The billing details of the payment. This address will be used for invoicing. pub billing: Option
, /// A timestamp (ISO 8601 code) that determines when the payment should be captured. @@ -309,7 +309,7 @@ pub struct PaymentsRequest { /// The payment method information provided for making a payment #[schema(example = "bank_transfer")] - pub payment_method_data: Option, + pub payment_method_data: Option, /// The payment method that is to be used #[schema(value_type = Option, example = "card")] @@ -1049,6 +1049,16 @@ pub enum BankDebitData { }, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] +pub struct PaymentMethodDataRequest { + #[serde(flatten)] + pub payment_method_data: PaymentMethodData, + /// billing details for the payment method. + /// This billing details will be passed to the processor as billing address. + /// If not passed, then payment.billing will be considered + pub billing: Option
, +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PaymentMethodData { @@ -1921,20 +1931,28 @@ pub enum VoucherData { pub enum PaymentMethodDataResponse { #[serde(rename = "card")] Card(Box), - BankTransfer, - Wallet, - PayLater, - Paypal, - BankRedirect, - Crypto, - BankDebit, - MandatePayment, - Reward, - Upi, - Voucher, - GiftCard, - CardRedirect, - CardToken, + BankTransfer {}, + Wallet {}, + PayLater {}, + Paypal {}, + BankRedirect {}, + Crypto {}, + BankDebit {}, + MandatePayment {}, + Reward {}, + Upi {}, + Voucher {}, + GiftCard {}, + CardRedirect {}, + CardToken {}, +} + +#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct PaymentMethodDataResponseWithBilling { + // The struct is flattened in order to provide backwards compatibility + #[serde(flatten)] + pub payment_method_data: PaymentMethodDataResponse, + pub billing: Option
, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -2369,7 +2387,7 @@ pub struct PaymentsResponse { /// The payment method information provided for making a payment #[schema(value_type = Option, example = "bank_transfer")] #[auth_based] - pub payment_method_data: Option, + pub payment_method_data: Option, /// Provide a reference to a stored payment method #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] @@ -2790,19 +2808,19 @@ impl From for PaymentMethodDataResponse { fn from(payment_method_data: AdditionalPaymentData) -> Self { match payment_method_data { AdditionalPaymentData::Card(card) => Self::Card(Box::new(CardResponse::from(*card))), - AdditionalPaymentData::PayLater {} => Self::PayLater, - AdditionalPaymentData::Wallet {} => Self::Wallet, - AdditionalPaymentData::BankRedirect { .. } => Self::BankRedirect, - AdditionalPaymentData::Crypto {} => Self::Crypto, - AdditionalPaymentData::BankDebit {} => Self::BankDebit, - AdditionalPaymentData::MandatePayment {} => Self::MandatePayment, - AdditionalPaymentData::Reward {} => Self::Reward, - AdditionalPaymentData::Upi {} => Self::Upi, - AdditionalPaymentData::BankTransfer {} => Self::BankTransfer, - AdditionalPaymentData::Voucher {} => Self::Voucher, - AdditionalPaymentData::GiftCard {} => Self::GiftCard, - AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, - AdditionalPaymentData::CardToken {} => Self::CardToken, + AdditionalPaymentData::PayLater {} => Self::PayLater {}, + AdditionalPaymentData::Wallet {} => Self::Wallet {}, + AdditionalPaymentData::BankRedirect { .. } => Self::BankRedirect {}, + AdditionalPaymentData::Crypto {} => Self::Crypto {}, + AdditionalPaymentData::BankDebit {} => Self::BankDebit {}, + AdditionalPaymentData::MandatePayment {} => Self::MandatePayment {}, + AdditionalPaymentData::Reward {} => Self::Reward {}, + AdditionalPaymentData::Upi {} => Self::Upi {}, + AdditionalPaymentData::BankTransfer {} => Self::BankTransfer {}, + AdditionalPaymentData::Voucher {} => Self::Voucher {}, + AdditionalPaymentData::GiftCard {} => Self::GiftCard {}, + AdditionalPaymentData::CardRedirect {} => Self::CardRedirect {}, + AdditionalPaymentData::CardToken {} => Self::CardToken {}, } } } diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs index fafb8fede0bc..6d0bb2154d94 100644 --- a/crates/api_models/src/user_role/role.rs +++ b/crates/api_models/src/user_role/role.rs @@ -23,6 +23,13 @@ pub struct GetGroupsQueryParam { pub groups: Option, } +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum GetRoleFromTokenResponse { + Permissions(Vec), + Groups(Vec), +} + #[derive(Debug, serde::Serialize)] #[serde(untagged)] pub enum RoleInfoResponse { diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 37aaac42e0df..25e1f5827ad3 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -160,9 +160,9 @@ pub struct OutgoingWebhook { /// This is specific to the flow, for ex: it will be `PaymentsResponse` for payments flow pub content: OutgoingWebhookContent, - #[serde(default, with = "custom_serde::iso8601")] /// The time at which webhook was sent + #[serde(default, with = "custom_serde::iso8601")] pub timestamp: PrimitiveDateTime, } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e0b6a1cc5a12..6b25b692c12a 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2172,6 +2172,91 @@ pub enum ApplePayFlow { Manual, } +#[derive( + Clone, + Debug, + Eq, + Default, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, + Copy, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticationStatus { + #[default] + Started, + Pending, + Success, + Failed, +} + +impl AuthenticationStatus { + pub fn is_terminal_status(&self) -> bool { + match self { + Self::Started | Self::Pending => false, + Self::Success | Self::Failed => true, + } + } + + pub fn is_failed(&self) -> bool { + self == &Self::Failed + } +} + +#[derive( + Clone, + Debug, + Eq, + Default, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, + Copy, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DecoupledAuthenticationType { + #[default] + Challenge, + Frictionless, +} + +#[derive( + Clone, + Debug, + Eq, + Default, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, + Copy, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticationLifecycleStatus { + Used, + #[default] + Unused, + Expired, +} + #[derive( Clone, Copy, diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs index c3cbaab4ab33..2af5b88b337b 100644 --- a/crates/connector_configs/src/common_config.rs +++ b/crates/connector_configs/src/common_config.rs @@ -81,6 +81,7 @@ pub struct ApiModelMetaData { pub google_pay: Option, pub apple_pay: Option, pub apple_pay_combined: Option, + pub endpoint_prefix: Option, } #[serde_with::skip_serializing_none] @@ -173,4 +174,5 @@ pub struct DashboardMetaData { pub google_pay: Option, pub apple_pay: Option, pub apple_pay_combined: Option, + pub endpoint_prefix: Option, } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index f0997d53107e..357ee0cb73f6 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -74,6 +74,7 @@ pub struct ConfigMetadata { pub google_pay: Option, pub apple_pay: Option, pub merchant_id: Option, + pub endpoint_prefix: Option, } #[serde_with::skip_serializing_none] diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs index 80332612c13a..5942a9e01ae1 100644 --- a/crates/connector_configs/src/response_modifier.rs +++ b/crates/connector_configs/src/response_modifier.rs @@ -293,6 +293,10 @@ impl ConnectorApiIntegrationPayload { Some(meta_data) => meta_data.terminal_id, _ => None, }; + let endpoint_prefix = match response.metadata.clone() { + Some(meta_data) => meta_data.endpoint_prefix, + _ => None, + }; let apple_pay = match response.metadata.clone() { Some(meta_data) => meta_data.apple_pay, _ => None, @@ -315,6 +319,7 @@ impl ConnectorApiIntegrationPayload { account_name, terminal_id, merchant_id, + endpoint_prefix, }; DashboardRequestPayload { diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs index be68a0c5f94b..735178cd676a 100644 --- a/crates/connector_configs/src/transformer.rs +++ b/crates/connector_configs/src/transformer.rs @@ -186,6 +186,7 @@ impl DashboardRequestPayload { merchant_account_id: None, merchant_id: None, merchant_config_currency: None, + endpoint_prefix: None, }; let meta_data = match request.metadata { Some(data) => data, @@ -196,6 +197,7 @@ impl DashboardRequestPayload { let merchant_account_id = meta_data.merchant_account_id.clone(); let merchant_id = meta_data.merchant_id.clone(); let terminal_id = meta_data.terminal_id.clone(); + let endpoint_prefix = meta_data.endpoint_prefix.clone(); let apple_pay = meta_data.apple_pay; let apple_pay_combined = meta_data.apple_pay_combined; let merchant_config_currency = meta_data.merchant_config_currency; @@ -208,6 +210,7 @@ impl DashboardRequestPayload { merchant_id, merchant_config_currency, apple_pay_combined, + endpoint_prefix, }) } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 693ea69ff4eb..2bc5e76fa15e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -232,6 +232,8 @@ api_key="Adyen API Key" key1="Adyen Account Id" [adyen.connector_webhook_details] merchant_secret="Source verification key" +[adyen.metadata] +endpoint_prefix="Live endpoint prefix" [adyen.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -657,6 +659,46 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" @@ -2506,6 +2548,8 @@ api_key="Api Key" payment_method_type = "sepa" [[adyen_payout.wallet]] payment_method_type = "paypal" +[adyen_payout.metadata] + endpoint_prefix="Live endpoint prefix" [adyen_payout.connector_auth.SignatureKey] api_key = "Adyen API Key (Payout creation)" api_secret = "Adyen Key (Payout submission)" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 78f6f8d2a768..41facc9732fb 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -124,6 +124,9 @@ key1="Adyen Account Id" [adyen.connector_webhook_details] merchant_secret="Source verification key" +[adyen.metadata] +endpoint_prefix="Live endpoint prefix" + [adyen.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -533,6 +536,46 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index dbb6284fbe9d..b76119a158e6 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -232,6 +232,8 @@ api_key="Adyen API Key" key1="Adyen Account Id" [adyen.connector_webhook_details] merchant_secret="Source verification key" +[adyen.metadata] +endpoint_prefix="Live endpoint prefix" [adyen.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -657,6 +659,46 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.AUD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.INR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.JPY.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.NZD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.ZAR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" @@ -2508,6 +2550,8 @@ api_key="Api Key" payment_method_type = "sepa" [[adyen_payout.wallet]] payment_method_type = "paypal" +[adyen_payout.metadata] + endpoint_prefix="Live endpoint prefix" [adyen_payout.connector_auth.SignatureKey] api_key = "Adyen API Key (Payout creation)" api_secret = "Adyen Key (Payout submission)" diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 3e187e25bc5e..f8af02857fe6 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -160,6 +160,7 @@ pub struct PaymentAttempt { pub unified_code: Option, pub unified_message: Option, pub mandate_data: Option, + pub payment_method_billing_address_id: Option, pub fingerprint_id: Option, } @@ -239,6 +240,7 @@ pub struct PaymentAttemptNew { pub unified_code: Option, pub unified_message: Option, pub mandate_data: Option, + pub payment_method_billing_address_id: Option, pub fingerprint_id: Option, } @@ -310,6 +312,7 @@ pub enum PaymentAttemptUpdate { surcharge_amount: Option, tax_amount: Option, merchant_connector_id: Option, + payment_method_billing_address_id: Option, fingerprint_id: Option, }, RejectUpdate { diff --git a/crates/diesel_models/src/api_keys.rs b/crates/diesel_models/src/api_keys.rs index 1875fc4dc51f..7676e20d224b 100644 --- a/crates/diesel_models/src/api_keys.rs +++ b/crates/diesel_models/src/api_keys.rs @@ -141,6 +141,8 @@ mod diesel_impl { pub struct ApiKeyExpiryTrackingData { pub key_id: String, pub merchant_id: String, + pub api_key_name: String, + pub prefix: String, pub api_key_expiry: Option, // Days on which email reminder about api_key expiry has to be sent, prior to it's expiry. pub expiry_reminder_days: Vec, diff --git a/crates/diesel_models/src/authentication.rs b/crates/diesel_models/src/authentication.rs new file mode 100644 index 000000000000..f9380cf39858 --- /dev/null +++ b/crates/diesel_models/src/authentication.rs @@ -0,0 +1,170 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{self, Deserialize, Serialize}; +use serde_json; + +use crate::schema::authentication; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = authentication, primary_key(authentication_id))] +pub struct Authentication { + pub authentication_id: String, + pub merchant_id: String, + pub authentication_connector: String, + pub connector_authentication_id: Option, + pub authentication_data: Option, + pub payment_method_id: String, + pub authentication_type: Option, + pub authentication_status: common_enums::AuthenticationStatus, + pub authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: time::PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: time::PrimitiveDateTime, + pub error_message: Option, + pub error_code: Option, + pub connector_metadata: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Insertable)] +#[diesel(table_name = authentication)] +pub struct AuthenticationNew { + pub authentication_id: String, + pub merchant_id: String, + pub authentication_connector: String, + pub connector_authentication_id: Option, + pub authentication_data: Option, + pub payment_method_id: String, + pub authentication_type: Option, + pub authentication_status: common_enums::AuthenticationStatus, + pub authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus, + pub error_message: Option, + pub error_code: Option, + pub connector_metadata: Option, +} + +#[derive(Debug)] +pub enum AuthenticationUpdate { + AuthenticationDataUpdate { + authentication_data: Option, + connector_authentication_id: Option, + payment_method_id: Option, + authentication_type: Option, + authentication_status: Option, + authentication_lifecycle_status: Option, + connector_metadata: Option, + }, + PostAuthorizationUpdate { + authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus, + }, + ErrorUpdate { + error_message: Option, + error_code: Option, + authentication_status: common_enums::AuthenticationStatus, + connector_authentication_id: Option, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq, AsChangeset, Serialize, Deserialize)] +#[diesel(table_name = authentication)] +pub struct AuthenticationUpdateInternal { + pub connector_authentication_id: Option, + pub authentication_data: Option, + pub payment_method_id: Option, + pub authentication_type: Option, + pub authentication_status: Option, + pub authentication_lifecycle_status: Option, + pub modified_at: time::PrimitiveDateTime, + pub error_message: Option, + pub error_code: Option, + pub connector_metadata: Option, +} + +impl AuthenticationUpdateInternal { + pub fn apply_changeset(self, source: Authentication) -> Authentication { + let Self { + connector_authentication_id, + authentication_data, + payment_method_id, + authentication_type, + authentication_status, + authentication_lifecycle_status, + modified_at: _, + error_code, + error_message, + connector_metadata, + } = self; + Authentication { + connector_authentication_id: connector_authentication_id + .or(source.connector_authentication_id), + authentication_data: authentication_data.or(source.authentication_data), + payment_method_id: payment_method_id.unwrap_or(source.payment_method_id), + authentication_type: authentication_type.or(source.authentication_type), + authentication_status: authentication_status.unwrap_or(source.authentication_status), + authentication_lifecycle_status: authentication_lifecycle_status + .unwrap_or(source.authentication_lifecycle_status), + modified_at: common_utils::date_time::now(), + error_code: error_code.or(source.error_code), + error_message: error_message.or(source.error_message), + connector_metadata: connector_metadata.or(source.connector_metadata), + ..source + } + } +} + +impl From for AuthenticationUpdateInternal { + fn from(auth_update: AuthenticationUpdate) -> Self { + match auth_update { + AuthenticationUpdate::AuthenticationDataUpdate { + authentication_data, + connector_authentication_id, + authentication_type, + authentication_status, + payment_method_id, + authentication_lifecycle_status, + connector_metadata, + } => Self { + authentication_data, + connector_authentication_id, + authentication_type, + authentication_status, + authentication_lifecycle_status, + modified_at: common_utils::date_time::now(), + payment_method_id, + error_message: None, + error_code: None, + connector_metadata, + }, + AuthenticationUpdate::ErrorUpdate { + error_message, + error_code, + authentication_status, + connector_authentication_id, + } => Self { + error_code, + error_message, + authentication_status: Some(authentication_status), + authentication_data: None, + connector_authentication_id, + authentication_type: None, + authentication_lifecycle_status: None, + modified_at: common_utils::date_time::now(), + payment_method_id: None, + connector_metadata: None, + }, + AuthenticationUpdate::PostAuthorizationUpdate { + authentication_lifecycle_status, + } => Self { + connector_authentication_id: None, + authentication_data: None, + payment_method_id: None, + authentication_type: None, + authentication_status: None, + authentication_lifecycle_status: Some(authentication_lifecycle_status), + modified_at: common_utils::date_time::now(), + error_message: None, + error_code: None, + connector_metadata: None, + }, + } + } +} diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index c0cebba77550..cefb0c240ec5 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -37,6 +37,7 @@ pub struct Customer { pub connector_customer: Option, pub modified_at: PrimitiveDateTime, pub address_id: Option, + pub default_payment_method_id: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -51,4 +52,5 @@ pub struct CustomerUpdateInternal { pub modified_at: Option, pub connector_customer: Option, pub address_id: Option, + pub default_payment_method_id: Option, } diff --git a/crates/diesel_models/src/dispute.rs b/crates/diesel_models/src/dispute.rs index 9bdac3322952..eae8281a99d3 100644 --- a/crates/diesel_models/src/dispute.rs +++ b/crates/diesel_models/src/dispute.rs @@ -29,6 +29,7 @@ pub struct DisputeNew { pub evidence: Option>, pub profile_id: Option, pub merchant_connector_id: Option, + pub dispute_amount: i64, } #[derive(Clone, Debug, PartialEq, Serialize, Identifiable, Queryable)] @@ -59,6 +60,7 @@ pub struct Dispute { pub evidence: Secret, pub profile_id: Option, pub merchant_connector_id: Option, + pub dispute_amount: i64, } #[derive(Debug)] diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index a616f302cfcb..0310944de7e4 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -372,6 +372,9 @@ pub enum BankNames { TsbBank, TescoBank, UlsterBank, + Yoursafe, + N26, + NationaleNederlanden, } #[derive( diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 927a05bfa566..e1e39560340a 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -66,6 +66,7 @@ pub struct PaymentAttempt { pub net_amount: Option, pub mandate_data: Option, pub fingerprint_id: Option, + pub payment_method_billing_address_id: Option, } impl PaymentAttempt { @@ -142,6 +143,7 @@ pub struct PaymentAttemptNew { pub net_amount: Option, pub mandate_data: Option, pub fingerprint_id: Option, + pub payment_method_billing_address_id: Option, } impl PaymentAttemptNew { @@ -218,6 +220,7 @@ pub enum PaymentAttemptUpdate { fingerprint_id: Option, updated_by: String, merchant_connector_id: Option, + payment_method_billing_address_id: Option, }, VoidUpdate { status: storage_enums::AttemptStatus, @@ -356,6 +359,7 @@ pub struct PaymentAttemptUpdateInternal { unified_code: Option>, unified_message: Option>, fingerprint_id: Option, + payment_method_billing_address_id: Option, } impl PaymentAttemptUpdateInternal { @@ -416,6 +420,7 @@ impl PaymentAttemptUpdate { encoded_data, unified_code, unified_message, + payment_method_billing_address_id, fingerprint_id, } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); PaymentAttempt { @@ -458,6 +463,8 @@ impl PaymentAttemptUpdate { encoded_data: encoded_data.or(source.encoded_data), unified_code: unified_code.unwrap_or(source.unified_code), unified_message: unified_message.unwrap_or(source.unified_message), + payment_method_billing_address_id: payment_method_billing_address_id + .or(source.payment_method_billing_address_id), fingerprint_id: fingerprint_id.or(source.fingerprint_id), ..source } @@ -536,6 +543,7 @@ impl From for PaymentAttemptUpdateInternal { merchant_connector_id, surcharge_amount, tax_amount, + payment_method_billing_address_id, fingerprint_id, } => Self { amount: Some(amount), @@ -559,6 +567,7 @@ impl From for PaymentAttemptUpdateInternal { merchant_connector_id: merchant_connector_id.map(Some), surcharge_amount, tax_amount, + payment_method_billing_address_id, fingerprint_id, ..Default::default() }, diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 09be739581ed..6191a768efe6 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -34,12 +34,13 @@ pub struct PaymentMethod { pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, + pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, } -#[derive(Clone, Debug, Eq, PartialEq, Insertable, Queryable, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_methods)] pub struct PaymentMethodNew { pub customer_id: String, @@ -64,6 +65,7 @@ pub struct PaymentMethodNew { pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, + pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, @@ -96,6 +98,7 @@ impl Default for PaymentMethodNew { last_modified: now, metadata: Option::default(), payment_method_data: Option::default(), + last_used_at: now, connector_mandate_details: Option::default(), customer_acceptance: Option::default(), status: storage_enums::PaymentMethodStatus::Active, @@ -117,6 +120,9 @@ pub enum PaymentMethodUpdate { PaymentMethodDataUpdate { payment_method_data: Option, }, + LastUsedUpdate { + last_used_at: PrimitiveDateTime, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -124,6 +130,7 @@ pub enum PaymentMethodUpdate { pub struct PaymentMethodUpdateInternal { metadata: Option, payment_method_data: Option, + last_used_at: Option, } impl PaymentMethodUpdateInternal { @@ -140,12 +147,19 @@ impl From for PaymentMethodUpdateInternal { PaymentMethodUpdate::MetadataUpdate { metadata } => Self { metadata, payment_method_data: None, + last_used_at: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, } => Self { metadata: None, payment_method_data, + last_used_at: None, + }, + PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { + metadata: None, + payment_method_data: None, + last_used_at: Some(last_used_at), }, } } diff --git a/crates/diesel_models/src/payouts.rs b/crates/diesel_models/src/payouts.rs index fd068b5a8d06..e8e783471464 100644 --- a/crates/diesel_models/src/payouts.rs +++ b/crates/diesel_models/src/payouts.rs @@ -29,6 +29,7 @@ pub struct Payouts { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, + pub attempt_count: i16, } impl Default for Payouts { @@ -53,6 +54,7 @@ impl Default for Payouts { metadata: Option::default(), created_at: now, last_modified_at: now, + attempt_count: i16::default(), } } } @@ -90,6 +92,7 @@ pub struct PayoutsNew { pub created_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, + pub attempt_count: i16, } #[derive(Debug)] @@ -114,6 +117,9 @@ pub enum PayoutsUpdate { recurring: bool, last_modified_at: Option, }, + AttemptCountUpdate { + attempt_count: i16, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -130,6 +136,7 @@ pub struct PayoutsUpdateInternal { pub metadata: Option, pub last_modified_at: Option, pub payout_method_id: Option, + pub attempt_count: Option, } impl From for PayoutsUpdateInternal { @@ -175,6 +182,10 @@ impl From for PayoutsUpdateInternal { recurring: Some(recurring), ..Default::default() }, + PayoutsUpdate::AttemptCountUpdate { attempt_count } => Self { + attempt_count: Some(attempt_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/process_tracker.rs b/crates/diesel_models/src/process_tracker.rs index 76e280d6bc08..74fa75a27512 100644 --- a/crates/diesel_models/src/process_tracker.rs +++ b/crates/diesel_models/src/process_tracker.rs @@ -229,6 +229,7 @@ pub enum ProcessTrackerRunner { RefundWorkflowRouter, DeleteTokenizeDataWorkflow, ApiKeyExpiryWorkflow, + OutgoingWebhookRetryWorkflow, } #[cfg(test)] diff --git a/crates/diesel_models/src/query/address.rs b/crates/diesel_models/src/query/address.rs index ada476229909..ffcd740f0760 100644 --- a/crates/diesel_models/src/query/address.rs +++ b/crates/diesel_models/src/query/address.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl AddressNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult
{ generics::generic_insert(conn, self).await } } impl Address { - #[instrument(skip(conn))] pub async fn find_by_address_id<'a>( conn: &PgPooledConn, address_id: &str, @@ -26,7 +23,6 @@ impl Address { .await } - #[instrument(skip(conn))] pub async fn update_by_address_id( conn: &PgPooledConn, address_id: String, @@ -56,7 +52,6 @@ impl Address { } } - #[instrument(skip(conn))] pub async fn update( self, conn: &PgPooledConn, @@ -82,7 +77,6 @@ impl Address { } } - #[instrument(skip(conn))] pub async fn delete_by_address_id( conn: &PgPooledConn, address_id: &str, @@ -110,7 +104,6 @@ impl Address { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_payment_id_address_id<'a>( conn: &PgPooledConn, merchant_id: &str, @@ -140,7 +133,6 @@ impl Address { } } - #[instrument(skip(conn))] pub async fn find_optional_by_address_id<'a>( conn: &PgPooledConn, address_id: &str, diff --git a/crates/diesel_models/src/query/api_keys.rs b/crates/diesel_models/src/query/api_keys.rs index b74142f72965..ad7ec7c068a8 100644 --- a/crates/diesel_models/src/query/api_keys.rs +++ b/crates/diesel_models/src/query/api_keys.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl ApiKeyNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl ApiKey { - #[instrument(skip(conn))] pub async fn update_by_merchant_id_key_id( conn: &PgPooledConn, merchant_id: String, @@ -57,7 +54,6 @@ impl ApiKey { } } - #[instrument(skip(conn))] pub async fn revoke_by_merchant_id_key_id( conn: &PgPooledConn, merchant_id: &str, @@ -72,7 +68,6 @@ impl ApiKey { .await } - #[instrument(skip(conn))] pub async fn find_optional_by_merchant_id_key_id( conn: &PgPooledConn, merchant_id: &str, @@ -87,7 +82,6 @@ impl ApiKey { .await } - #[instrument(skip(conn))] pub async fn find_optional_by_hashed_api_key( conn: &PgPooledConn, hashed_api_key: HashedApiKey, @@ -99,7 +93,6 @@ impl ApiKey { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/authentication.rs b/crates/diesel_models/src/query/authentication.rs new file mode 100644 index 000000000000..60e635ad1260 --- /dev/null +++ b/crates/diesel_models/src/query/authentication.rs @@ -0,0 +1,72 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; + +use super::generics; +use crate::{ + authentication::{ + Authentication, AuthenticationNew, AuthenticationUpdate, AuthenticationUpdateInternal, + }, + errors, + schema::authentication::dsl, + PgPooledConn, StorageResult, +}; + +impl AuthenticationNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Authentication { + pub async fn update_by_merchant_id_authentication_id( + conn: &PgPooledConn, + merchant_id: String, + authentication_id: String, + authorization_update: AuthenticationUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authentication_id.eq(authentication_id.to_owned())), + AuthenticationUpdateInternal::from(authorization_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NotFound => Err(error.attach_printable( + "Authentication with the given Authentication ID does not exist", + )), + errors::DatabaseError::NoFieldsToUpdate => { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authentication_id.eq(authentication_id.to_owned())), + ) + .await + } + _ => Err(error), + }, + result => result, + } + } + + pub async fn find_by_merchant_id_authentication_id( + conn: &PgPooledConn, + merchant_id: &str, + authentication_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authentication_id.eq(authentication_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/authorization.rs b/crates/diesel_models/src/query/authorization.rs index dc9515bda55e..59282c78f8b9 100644 --- a/crates/diesel_models/src/query/authorization.rs +++ b/crates/diesel_models/src/query/authorization.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -12,14 +11,12 @@ use crate::{ }; impl AuthorizationNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Authorization { - #[instrument(skip(conn))] pub async fn update_by_merchant_id_authorization_id( conn: &PgPooledConn, merchant_id: String, @@ -59,7 +56,6 @@ impl Authorization { } } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_payment_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/blocklist.rs b/crates/diesel_models/src/query/blocklist.rs index e1ba5fa923d6..2003a99700c1 100644 --- a/crates/diesel_models/src/query/blocklist.rs +++ b/crates/diesel_models/src/query/blocklist.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,12 @@ use crate::{ }; impl BlocklistNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Blocklist { - #[instrument(skip(conn))] pub async fn find_by_merchant_id_fingerprint_id( conn: &PgPooledConn, merchant_id: &str, @@ -31,7 +28,6 @@ impl Blocklist { .await } - #[instrument(skip(conn))] pub async fn list_by_merchant_id_data_kind( conn: &PgPooledConn, merchant_id: &str, @@ -51,7 +47,6 @@ impl Blocklist { .await } - #[instrument(skip(conn))] pub async fn list_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -66,7 +61,6 @@ impl Blocklist { .await } - #[instrument(skip(conn))] pub async fn delete_by_merchant_id_fingerprint_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/blocklist_fingerprint.rs b/crates/diesel_models/src/query/blocklist_fingerprint.rs index 4f3d77e63a81..370a06801c7b 100644 --- a/crates/diesel_models/src/query/blocklist_fingerprint.rs +++ b/crates/diesel_models/src/query/blocklist_fingerprint.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,12 @@ use crate::{ }; impl BlocklistFingerprintNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl BlocklistFingerprint { - #[instrument(skip(conn))] pub async fn find_by_merchant_id_fingerprint_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/blocklist_lookup.rs b/crates/diesel_models/src/query/blocklist_lookup.rs index ea28c94e4916..f5bc6eda5d7b 100644 --- a/crates/diesel_models/src/query/blocklist_lookup.rs +++ b/crates/diesel_models/src/query/blocklist_lookup.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,12 @@ use crate::{ }; impl BlocklistLookupNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl BlocklistLookup { - #[instrument(skip(conn))] pub async fn find_by_merchant_id_fingerprint( conn: &PgPooledConn, merchant_id: &str, @@ -31,7 +28,6 @@ impl BlocklistLookup { .await } - #[instrument(skip(conn))] pub async fn delete_by_merchant_id_fingerprint( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/business_profile.rs b/crates/diesel_models/src/query/business_profile.rs index fcdd97ca9dba..effe2219a69a 100644 --- a/crates/diesel_models/src/query/business_profile.rs +++ b/crates/diesel_models/src/query/business_profile.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl BusinessProfileNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl BusinessProfile { - #[instrument(skip(conn))] pub async fn update_by_profile_id( self, conn: &PgPooledConn, @@ -38,7 +35,6 @@ impl BusinessProfile { } } - #[instrument(skip(conn))] pub async fn find_by_profile_id(conn: &PgPooledConn, profile_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -47,7 +43,6 @@ impl BusinessProfile { .await } - #[instrument(skip(conn))] pub async fn find_by_profile_name_merchant_id( conn: &PgPooledConn, profile_name: &str, diff --git a/crates/diesel_models/src/query/capture.rs b/crates/diesel_models/src/query/capture.rs index c5a5725f1171..c39c4ec7f57d 100644 --- a/crates/diesel_models/src/query/capture.rs +++ b/crates/diesel_models/src/query/capture.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl CaptureNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Capture { - #[instrument(skip(conn))] pub async fn find_by_capture_id(conn: &PgPooledConn, capture_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -25,7 +22,7 @@ impl Capture { ) .await } - #[instrument(skip(conn))] + pub async fn update_with_capture_id( self, conn: &PgPooledConn, @@ -51,7 +48,6 @@ impl Capture { } } - #[instrument(skip(conn))] pub async fn find_all_by_merchant_id_payment_id_authorized_attempt_id( merchant_id: &str, payment_id: &str, diff --git a/crates/diesel_models/src/query/configs.rs b/crates/diesel_models/src/query/configs.rs index 306fb5e33538..b7745b3187c7 100644 --- a/crates/diesel_models/src/query/configs.rs +++ b/crates/diesel_models/src/query/configs.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,19 +9,16 @@ use crate::{ }; impl ConfigNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Config { - #[instrument(skip(conn))] pub async fn find_by_key(conn: &PgPooledConn, key: &str) -> StorageResult { generics::generic_find_by_id::<::Table, _, _>(conn, key.to_owned()).await } - #[instrument(skip(conn))] pub async fn update_by_key( conn: &PgPooledConn, key: &str, @@ -49,7 +45,6 @@ impl Config { } } - #[instrument(skip(conn))] pub async fn delete_by_key(conn: &PgPooledConn, key: &str) -> StorageResult { generics::generic_delete_one_with_result::<::Table, _, _>( conn, diff --git a/crates/diesel_models/src/query/customers.rs b/crates/diesel_models/src/query/customers.rs index 41dedbf8648f..e5ec96c8fc57 100644 --- a/crates/diesel_models/src/query/customers.rs +++ b/crates/diesel_models/src/query/customers.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl CustomerNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Customer { - #[instrument(skip(conn))] pub async fn update_by_customer_id_merchant_id( conn: &PgPooledConn, customer_id: String, @@ -45,7 +42,6 @@ impl Customer { } } - #[instrument(skip(conn))] pub async fn delete_by_customer_id_merchant_id( conn: &PgPooledConn, customer_id: &str, @@ -60,7 +56,6 @@ impl Customer { .await } - #[instrument(skip(conn))] pub async fn find_by_customer_id_merchant_id( conn: &PgPooledConn, customer_id: &str, @@ -73,7 +68,6 @@ impl Customer { .await } - #[instrument(skip(conn))] pub async fn list_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -88,7 +82,6 @@ impl Customer { .await } - #[instrument(skip(conn))] pub async fn find_optional_by_customer_id_merchant_id( conn: &PgPooledConn, customer_id: &str, diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index d91a40781285..6f88629d82a8 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::tracing::{self, instrument}; use crate::{ enums, @@ -13,7 +12,6 @@ use crate::{ }; impl DashboardMetadataNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/dispute.rs b/crates/diesel_models/src/query/dispute.rs index 78696d9e9c0a..974ed84e2365 100644 --- a/crates/diesel_models/src/query/dispute.rs +++ b/crates/diesel_models/src/query/dispute.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl DisputeNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Dispute { - #[instrument(skip(conn))] pub async fn find_by_merchant_id_payment_id_connector_dispute_id( conn: &PgPooledConn, merchant_id: &str, @@ -70,7 +67,6 @@ impl Dispute { .await } - #[instrument(skip(conn))] pub async fn update(self, conn: &PgPooledConn, dispute: DisputeUpdate) -> StorageResult { match generics::generic_update_with_unique_predicate_get_result::< ::Table, diff --git a/crates/diesel_models/src/query/events.rs b/crates/diesel_models/src/query/events.rs index c8711abf9e4a..9175823e5f9b 100644 --- a/crates/diesel_models/src/query/events.rs +++ b/crates/diesel_models/src/query/events.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,20 @@ use crate::{ }; impl EventNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Event { - #[instrument(skip(conn))] + pub async fn find_by_event_id(conn: &PgPooledConn, event_id: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::event_id.eq(event_id.to_owned()), + ) + .await + } + pub async fn update( conn: &PgPooledConn, event_id: &str, diff --git a/crates/diesel_models/src/query/file.rs b/crates/diesel_models/src/query/file.rs index 0314b04fb273..be81d1bf2613 100644 --- a/crates/diesel_models/src/query/file.rs +++ b/crates/diesel_models/src/query/file.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl FileMetadataNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl FileMetadata { - #[instrument(skip(conn))] pub async fn find_by_merchant_id_file_id( conn: &PgPooledConn, merchant_id: &str, @@ -32,7 +29,6 @@ impl FileMetadata { .await } - #[instrument(skip(conn))] pub async fn delete_by_merchant_id_file_id( conn: &PgPooledConn, merchant_id: &str, @@ -47,7 +43,6 @@ impl FileMetadata { .await } - #[instrument(skip(conn))] pub async fn update( self, conn: &PgPooledConn, diff --git a/crates/diesel_models/src/query/fraud_check.rs b/crates/diesel_models/src/query/fraud_check.rs index b63b48c193f4..18eee0c72810 100644 --- a/crates/diesel_models/src/query/fraud_check.rs +++ b/crates/diesel_models/src/query/fraud_check.rs @@ -1,19 +1,16 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::tracing::{self, instrument}; use crate::{ errors, fraud_check::*, query::generics, schema::fraud_check::dsl, PgPooledConn, StorageResult, }; impl FraudCheckNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl FraudCheck { - #[instrument(skip(conn))] pub async fn update_with_attempt_id( self, conn: &PgPooledConn, diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs index 667b2516bd9a..8f2e391df6ca 100644 --- a/crates/diesel_models/src/query/generics.rs +++ b/crates/diesel_models/src/query/generics.rs @@ -20,7 +20,7 @@ use diesel::{ Expression, Insertable, QueryDsl, QuerySource, Table, }; use error_stack::{report, IntoReport, ResultExt}; -use router_env::{instrument, logger, tracing}; +use router_env::logger; use crate::{ errors::{self}, @@ -71,7 +71,6 @@ pub mod db_metrics { use db_metrics::*; -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_insert(conn: &PgPooledConn, values: V) -> StorageResult where T: HasTable + Table + 'static + Debug, @@ -102,7 +101,6 @@ where .attach_printable_lazy(|| format!("Error while inserting {debug_values}")) } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_update( conn: &PgPooledConn, predicate: P, @@ -130,7 +128,6 @@ where .attach_printable_lazy(|| format!("Error while updating {debug_values}")) } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_update_with_results( conn: &PgPooledConn, predicate: P, @@ -178,7 +175,6 @@ where } } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_update_with_unique_predicate_get_result( conn: &PgPooledConn, predicate: P, @@ -216,7 +212,6 @@ where })? } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_update_by_id( conn: &PgPooledConn, id: Pk, @@ -267,7 +262,6 @@ where } } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_delete(conn: &PgPooledConn, predicate: P) -> StorageResult where T: FilterDsl

+ HasTable

+ Table + 'static, @@ -297,7 +291,6 @@ where }) } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_delete_one_with_result( conn: &PgPooledConn, predicate: P, @@ -330,7 +323,6 @@ where }) } -#[instrument(level = "DEBUG", skip_all)] async fn generic_find_by_id_core(conn: &PgPooledConn, id: Pk) -> StorageResult where T: FindDsl + HasTable
+ LimitDsl + Table + 'static, @@ -355,7 +347,6 @@ where .attach_printable_lazy(|| format!("Error finding record by primary key: {id:?}")) } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_find_by_id(conn: &PgPooledConn, id: Pk) -> StorageResult where T: FindDsl + HasTable
+ LimitDsl + Table + 'static, @@ -367,7 +358,6 @@ where generic_find_by_id_core::(conn, id).await } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_find_by_id_optional( conn: &PgPooledConn, id: Pk, @@ -383,7 +373,6 @@ where to_optional(generic_find_by_id_core::(conn, id).await) } -#[instrument(level = "DEBUG", skip_all)] async fn generic_find_one_core(conn: &PgPooledConn, predicate: P) -> StorageResult where T: FilterDsl

+ HasTable

+ Table + 'static, @@ -403,7 +392,6 @@ where .attach_printable_lazy(|| "Error finding record by predicate") } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_find_one(conn: &PgPooledConn, predicate: P) -> StorageResult where T: FilterDsl

+ HasTable

+ Table + 'static, @@ -413,7 +401,6 @@ where generic_find_one_core::(conn, predicate).await } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_find_one_optional( conn: &PgPooledConn, predicate: P, @@ -426,7 +413,6 @@ where to_optional(generic_find_one_core::(conn, predicate).await) } -#[instrument(level = "DEBUG", skip_all)] pub async fn generic_filter( conn: &PgPooledConn, predicate: P, diff --git a/crates/diesel_models/src/query/locker_mock_up.rs b/crates/diesel_models/src/query/locker_mock_up.rs index 381905d6bed2..4ce52cfc4078 100644 --- a/crates/diesel_models/src/query/locker_mock_up.rs +++ b/crates/diesel_models/src/query/locker_mock_up.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,12 @@ use crate::{ }; impl LockerMockUpNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl LockerMockUp { - #[instrument(skip(conn))] pub async fn find_by_card_id(conn: &PgPooledConn, card_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -25,7 +22,6 @@ impl LockerMockUp { .await } - #[instrument(skip(conn))] pub async fn delete_by_card_id(conn: &PgPooledConn, card_id: &str) -> StorageResult { generics::generic_delete_one_with_result::<::Table, _, _>( conn, diff --git a/crates/diesel_models/src/query/mandate.rs b/crates/diesel_models/src/query/mandate.rs index 00075528e57c..0baae87c5121 100644 --- a/crates/diesel_models/src/query/mandate.rs +++ b/crates/diesel_models/src/query/mandate.rs @@ -1,12 +1,10 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; use error_stack::report; -use router_env::{instrument, tracing}; use super::generics; use crate::{errors, mandate::*, schema::mandate::dsl, PgPooledConn, StorageResult}; impl MandateNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/merchant_account.rs b/crates/diesel_models/src/query/merchant_account.rs index ba20f2e36607..1d4eef752060 100644 --- a/crates/diesel_models/src/query/merchant_account.rs +++ b/crates/diesel_models/src/query/merchant_account.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl MerchantAccountNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl MerchantAccount { - #[instrument(skip(conn))] pub async fn update( self, conn: &PgPooledConn, @@ -67,7 +64,6 @@ impl MerchantAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -79,7 +75,6 @@ impl MerchantAccount { .await } - #[instrument(skip_all)] pub async fn find_by_publishable_key( conn: &PgPooledConn, publishable_key: &str, @@ -91,7 +86,6 @@ impl MerchantAccount { .await } - #[instrument(skip_all)] pub async fn list_by_organization_id( conn: &PgPooledConn, organization_id: &str, @@ -111,7 +105,6 @@ impl MerchantAccount { .await } - #[instrument(skip_all)] pub async fn list_multiple_merchant_accounts( conn: &PgPooledConn, merchant_ids: Vec, diff --git a/crates/diesel_models/src/query/merchant_connector_account.rs b/crates/diesel_models/src/query/merchant_connector_account.rs index e9ef4eabff0e..bd24d12ec22c 100644 --- a/crates/diesel_models/src/query/merchant_connector_account.rs +++ b/crates/diesel_models/src/query/merchant_connector_account.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -13,14 +12,12 @@ use crate::{ }; impl MerchantConnectorAccountNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl MerchantConnectorAccount { - #[instrument(skip(conn))] pub async fn update( self, conn: &PgPooledConn, @@ -55,7 +52,6 @@ impl MerchantConnectorAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector( conn: &PgPooledConn, merchant_id: &str, @@ -70,7 +66,6 @@ impl MerchantConnectorAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_profile_id_connector_name( conn: &PgPooledConn, profile_id: &str, @@ -85,7 +80,6 @@ impl MerchantConnectorAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_name( conn: &PgPooledConn, merchant_id: &str, @@ -108,7 +102,6 @@ impl MerchantConnectorAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_merchant_connector_id( conn: &PgPooledConn, merchant_id: &str, @@ -123,7 +116,6 @@ impl MerchantConnectorAccount { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/merchant_key_store.rs b/crates/diesel_models/src/query/merchant_key_store.rs index 0e2ec1ddadbd..1be82cb2c18d 100644 --- a/crates/diesel_models/src/query/merchant_key_store.rs +++ b/crates/diesel_models/src/query/merchant_key_store.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,14 +8,12 @@ use crate::{ }; impl MerchantKeyStoreNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl MerchantKeyStore { - #[instrument(skip(conn))] pub async fn find_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -28,7 +25,6 @@ impl MerchantKeyStore { .await } - #[instrument(skip(conn))] pub async fn delete_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -40,7 +36,6 @@ impl MerchantKeyStore { .await } - #[instrument(skip(conn))] pub async fn list_multiple_key_stores( conn: &PgPooledConn, merchant_ids: Vec, diff --git a/crates/diesel_models/src/query/organization.rs b/crates/diesel_models/src/query/organization.rs index 0bea1012c9a9..ea8698204d79 100644 --- a/crates/diesel_models/src/query/organization.rs +++ b/crates/diesel_models/src/query/organization.rs @@ -1,12 +1,10 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::tracing::{self, instrument}; use crate::{ organization::*, query::generics, schema::organization::dsl, PgPooledConn, StorageResult, }; impl OrganizationNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index ebd0dcd7351b..7e44059927b6 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -6,7 +6,6 @@ use diesel::{ QueryDsl, Table, }; use error_stack::{IntoReport, ResultExt}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -21,14 +20,12 @@ use crate::{ }; impl PaymentAttemptNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self.populate_derived_fields()).await } } impl PaymentAttempt { - #[instrument(skip(conn))] pub async fn update_with_attempt_id( self, conn: &PgPooledConn, @@ -56,7 +53,6 @@ impl PaymentAttempt { } } - #[instrument(skip(conn))] pub async fn find_optional_by_payment_id_merchant_id( conn: &PgPooledConn, payment_id: &str, @@ -71,7 +67,6 @@ impl PaymentAttempt { .await } - #[instrument(skip(conn))] pub async fn find_by_connector_transaction_id_payment_id_merchant_id( conn: &PgPooledConn, connector_transaction_id: &str, @@ -156,7 +151,6 @@ impl PaymentAttempt { ) } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_txn_id( conn: &PgPooledConn, merchant_id: &str, @@ -171,7 +165,6 @@ impl PaymentAttempt { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_attempt_id( conn: &PgPooledConn, merchant_id: &str, @@ -186,7 +179,6 @@ impl PaymentAttempt { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_preprocessing_id( conn: &PgPooledConn, merchant_id: &str, @@ -201,7 +193,6 @@ impl PaymentAttempt { .await } - #[instrument(skip(conn))] pub async fn find_by_payment_id_merchant_id_attempt_id( conn: &PgPooledConn, payment_id: &str, @@ -219,7 +210,6 @@ impl PaymentAttempt { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_payment_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/payment_intent.rs b/crates/diesel_models/src/query/payment_intent.rs index 7de8f4d9bdfc..82904e5a5691 100644 --- a/crates/diesel_models/src/query/payment_intent.rs +++ b/crates/diesel_models/src/query/payment_intent.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -12,14 +11,12 @@ use crate::{ }; impl PaymentIntentNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl PaymentIntent { - #[instrument(skip(conn))] pub async fn update( self, conn: &PgPooledConn, @@ -44,7 +41,6 @@ impl PaymentIntent { } } - #[instrument(skip(conn))] pub async fn find_by_payment_id_merchant_id( conn: &PgPooledConn, payment_id: &str, @@ -59,7 +55,6 @@ impl PaymentIntent { .await } - #[instrument(skip(conn))] pub async fn find_optional_by_payment_id_merchant_id( conn: &PgPooledConn, payment_id: &str, diff --git a/crates/diesel_models/src/query/payment_link.rs b/crates/diesel_models/src/query/payment_link.rs index 085257633c65..154e753699ac 100644 --- a/crates/diesel_models/src/query/payment_link.rs +++ b/crates/diesel_models/src/query/payment_link.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,7 +8,6 @@ use crate::{ }; impl PaymentLinkNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index aa2279627601..bed4d0790107 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl PaymentMethodNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl PaymentMethod { - #[instrument(skip(conn))] pub async fn delete_by_payment_method_id( conn: &PgPooledConn, payment_method_id: String, @@ -29,7 +26,6 @@ impl PaymentMethod { .await } - #[instrument(skip(conn))] pub async fn delete_by_merchant_id_payment_method_id( conn: &PgPooledConn, merchant_id: &str, @@ -44,7 +40,6 @@ impl PaymentMethod { .await } - #[instrument(skip(conn))] pub async fn find_by_locker_id(conn: &PgPooledConn, locker_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -53,7 +48,6 @@ impl PaymentMethod { .await } - #[instrument(skip(conn))] pub async fn find_by_payment_method_id( conn: &PgPooledConn, payment_method_id: &str, @@ -65,7 +59,6 @@ impl PaymentMethod { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -85,50 +78,40 @@ impl PaymentMethod { .await } - #[instrument(skip(conn))] pub async fn find_by_customer_id_merchant_id( conn: &PgPooledConn, customer_id: &str, merchant_id: &str, + limit: Option, ) -> StorageResult> { - generics::generic_filter::< - ::Table, - _, - <::Table as Table>::PrimaryKey, - _, - >( + generics::generic_filter::<::Table, _, _, _>( conn, dsl::customer_id .eq(customer_id.to_owned()) .and(dsl::merchant_id.eq(merchant_id.to_owned())), + limit, None, - None, - None, + Some(dsl::last_used_at.desc()), ) .await } - #[instrument(skip(conn))] pub async fn find_by_customer_id_merchant_id_status( conn: &PgPooledConn, customer_id: &str, merchant_id: &str, status: storage_enums::PaymentMethodStatus, + limit: Option, ) -> StorageResult> { - generics::generic_filter::< - ::Table, - _, - <::Table as Table>::PrimaryKey, - _, - >( + generics::generic_filter::<::Table, _, _, _>( conn, dsl::customer_id .eq(customer_id.to_owned()) .and(dsl::merchant_id.eq(merchant_id.to_owned())) .and(dsl::status.eq(status)), + limit, None, - None, - None, + Some(dsl::last_used_at.desc()), ) .await } diff --git a/crates/diesel_models/src/query/payout_attempt.rs b/crates/diesel_models/src/query/payout_attempt.rs index 804d49d9481b..a2e70a499711 100644 --- a/crates/diesel_models/src/query/payout_attempt.rs +++ b/crates/diesel_models/src/query/payout_attempt.rs @@ -1,6 +1,5 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; use error_stack::report; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -13,7 +12,6 @@ use crate::{ }; impl PayoutAttemptNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } @@ -34,6 +32,20 @@ impl PayoutAttempt { .await } + pub async fn find_by_merchant_id_payout_attempt_id( + conn: &PgPooledConn, + merchant_id: &str, + payout_attempt_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payout_attempt_id.eq(payout_attempt_id.to_owned())), + ) + .await + } + pub async fn update_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &str, @@ -54,4 +66,25 @@ impl PayoutAttempt { report!(errors::DatabaseError::NotFound).attach_printable("Error while updating payout") }) } + + pub async fn update_by_merchant_id_payout_attempt_id( + conn: &PgPooledConn, + merchant_id: &str, + payout_attempt_id: &str, + payout: PayoutAttemptUpdate, + ) -> StorageResult { + generics::generic_update_with_results::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payout_attempt_id.eq(payout_attempt_id.to_owned())), + PayoutAttemptUpdateInternal::from(payout), + ) + .await? + .first() + .cloned() + .ok_or_else(|| { + report!(errors::DatabaseError::NotFound).attach_printable("Error while updating payout") + }) + } } diff --git a/crates/diesel_models/src/query/refund.rs b/crates/diesel_models/src/query/refund.rs index a9980af7f179..0d5d1baf93b4 100644 --- a/crates/diesel_models/src/query/refund.rs +++ b/crates/diesel_models/src/query/refund.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -10,14 +9,12 @@ use crate::{ }; impl RefundNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Refund { - #[instrument(skip(conn))] pub async fn update(self, conn: &PgPooledConn, refund: RefundUpdate) -> StorageResult { match generics::generic_update_with_unique_predicate_get_result::< ::Table, @@ -42,7 +39,6 @@ impl Refund { } // This is required to be changed for KV. - #[instrument(skip(conn))] pub async fn find_by_merchant_id_refund_id( conn: &PgPooledConn, merchant_id: &str, @@ -57,7 +53,6 @@ impl Refund { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_refund_id_connector( conn: &PgPooledConn, merchant_id: &str, @@ -74,7 +69,6 @@ impl Refund { .await } - #[instrument(skip(conn))] pub async fn find_by_internal_reference_id_merchant_id( conn: &PgPooledConn, internal_reference_id: &str, @@ -89,7 +83,6 @@ impl Refund { .await } - #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_transaction_id( conn: &PgPooledConn, merchant_id: &str, @@ -112,7 +105,6 @@ impl Refund { .await } - #[instrument(skip(conn))] pub async fn find_by_payment_id_merchant_id( conn: &PgPooledConn, payment_id: &str, diff --git a/crates/diesel_models/src/query/reverse_lookup.rs b/crates/diesel_models/src/query/reverse_lookup.rs index 351277ca15f4..cfd4ab09a646 100644 --- a/crates/diesel_models/src/query/reverse_lookup.rs +++ b/crates/diesel_models/src/query/reverse_lookup.rs @@ -1,5 +1,4 @@ use diesel::{associations::HasTable, ExpressionMethods}; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -9,11 +8,10 @@ use crate::{ }; impl ReverseLookupNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } - #[instrument(skip(conn))] + pub async fn batch_insert( reverse_lookups: Vec, conn: &PgPooledConn, diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs index a54576cc91b7..73b596171db0 100644 --- a/crates/diesel_models/src/query/role.rs +++ b/crates/diesel_models/src/query/role.rs @@ -1,12 +1,10 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; -use router_env::tracing::{self, instrument}; use crate::{ enums::RoleScope, query::generics, role::*, schema::roles::dsl, PgPooledConn, StorageResult, }; impl RoleNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/routing_algorithm.rs b/crates/diesel_models/src/query/routing_algorithm.rs index b2a9687aa401..28a5e6816e39 100644 --- a/crates/diesel_models/src/query/routing_algorithm.rs +++ b/crates/diesel_models/src/query/routing_algorithm.rs @@ -1,7 +1,6 @@ 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::{ @@ -14,12 +13,10 @@ use crate::{ }; 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, @@ -34,7 +31,6 @@ impl RoutingAlgorithm { .await } - #[instrument(skip(conn))] pub async fn find_by_algorithm_id_profile_id( conn: &PgPooledConn, algorithm_id: &str, @@ -49,7 +45,6 @@ impl RoutingAlgorithm { .await } - #[instrument(skip(conn))] pub async fn find_metadata_by_algorithm_id_profile_id( conn: &PgPooledConn, algorithm_id: &str, @@ -114,7 +109,6 @@ impl RoutingAlgorithm { ) } - #[instrument(skip(conn))] pub async fn list_metadata_by_profile_id( conn: &PgPooledConn, profile_id: &str, @@ -171,7 +165,6 @@ impl RoutingAlgorithm { .collect()) } - #[instrument(skip(conn))] pub async fn list_metadata_by_merchant_id( conn: &PgPooledConn, merchant_id: &str, @@ -233,7 +226,6 @@ impl RoutingAlgorithm { .collect()) } - #[instrument(skip(conn))] pub async fn list_metadata_by_merchant_id_transaction_type( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index 6fb5b79ddc1e..9254a04de253 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -4,10 +4,7 @@ use diesel::{ JoinOnDsl, QueryDsl, }; use error_stack::IntoReport; -use router_env::{ - logger, - tracing::{self, instrument}, -}; +use router_env::logger; pub mod sample_data; use crate::{ @@ -23,7 +20,6 @@ use crate::{ }; impl UserNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index 5e759cf826bb..4cce0d3f8005 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -1,10 +1,8 @@ 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 } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index f3c6f23e0864..49ba814709ae 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -58,6 +58,38 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + authentication (authentication_id) { + #[max_length = 64] + authentication_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + authentication_connector -> Varchar, + #[max_length = 64] + connector_authentication_id -> Nullable, + authentication_data -> Nullable, + #[max_length = 64] + payment_method_id -> Varchar, + #[max_length = 64] + authentication_type -> Nullable, + #[max_length = 64] + authentication_status -> Varchar, + #[max_length = 64] + authentication_lifecycle_status -> Varchar, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + error_message -> Nullable, + #[max_length = 64] + error_code -> Nullable, + connector_metadata -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -227,6 +259,8 @@ diesel::table! { modified_at -> Timestamp, #[max_length = 64] address_id -> Nullable, + #[max_length = 64] + default_payment_method_id -> Nullable, } } @@ -293,6 +327,7 @@ diesel::table! { profile_id -> Nullable, #[max_length = 32] merchant_connector_id -> Nullable, + dispute_amount -> Int8, } } @@ -692,6 +727,8 @@ diesel::table! { mandate_data -> Nullable, #[max_length = 64] fingerprint_id -> Nullable, + #[max_length = 64] + payment_method_billing_address_id -> Nullable, } } @@ -831,6 +868,7 @@ diesel::table! { payment_method_data -> Nullable, #[max_length = 64] locker_id -> Nullable, + last_used_at -> Timestamp, connector_mandate_details -> Nullable, customer_acceptance -> Nullable, #[max_length = 64] @@ -907,6 +945,7 @@ diesel::table! { metadata -> Nullable, created_at -> Timestamp, last_modified_at -> Timestamp, + attempt_count -> Int2, } } @@ -1101,6 +1140,7 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + authentication, blocklist, blocklist_fingerprint, blocklist_lookup, diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs index 1eeb61d6135e..48028ba6b5f4 100644 --- a/crates/diesel_models/src/user/dashboard_metadata.rs +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -45,6 +45,7 @@ pub struct DashboardMetadataUpdateInternal { pub last_modified_at: PrimitiveDateTime, } +#[derive(Debug)] pub enum DashboardMetadataUpdate { UpdateData { data_key: enums::DashboardMetadata, diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index beac5b0733ea..67f6cceca13d 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -68,6 +68,7 @@ pub struct PaymentAttemptBatchNew { pub unified_message: Option, pub net_amount: Option, pub mandate_data: Option, + pub payment_method_billing_address_id: Option, pub fingerprint_id: Option, } @@ -123,6 +124,7 @@ impl PaymentAttemptBatchNew { unified_message: self.unified_message, net_amount: self.net_amount, mandate_data: self.mandate_data, + payment_method_billing_address_id: self.payment_method_billing_address_id, fingerprint_id: self.fingerprint_id, } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index ef573093be54..7c97fba35352 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -115,6 +115,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::customers::customers_update, routes::customers::customers_delete, routes::customers::customers_mandates_list, + routes::customers::default_payment_method_set_api, //Routes for payment methods routes::payment_method::create_payment_method_api, @@ -188,6 +189,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::PaymentMethodDeleteResponse, api_models::payment_methods::PaymentMethodUpdate, + api_models::payment_methods::CustomerDefaultPaymentMethodResponse, api_models::payment_methods::CardDetailFromLocker, api_models::payment_methods::CardDetail, api_models::payment_methods::RequestPaymentMethodTypes, @@ -293,6 +295,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::MandateData, api_models::payments::PhoneDetails, api_models::payments::PaymentMethodData, + api_models::payments::PaymentMethodDataRequest, api_models::payments::MandateType, api_models::payments::AcceptanceType, api_models::payments::MandateAmountData, @@ -374,6 +377,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::BrowserInformation, api_models::payments::PaymentCreatePaymentLinkConfig, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::DefaultPaymentMethod, api_models::payment_methods::MaskedBankDetails, api_models::payment_methods::SurchargeDetailsResponse, api_models::payment_methods::SurchargeResponse, diff --git a/crates/openapi/src/routes/customers.rs b/crates/openapi/src/routes/customers.rs index 19901cbbeb97..6011621fc13f 100644 --- a/crates/openapi/src/routes/customers.rs +++ b/crates/openapi/src/routes/customers.rs @@ -116,3 +116,23 @@ pub async fn customers_list() {} security(("api_key" = [])) )] pub async fn customers_mandates_list() {} + +/// Customers - Set Default Payment Method +/// +/// Set the Payment Method as Default for the Customer. +#[utoipa::path( + get, + path = "/{customer_id}/payment_methods/{payment_method_id}/default", + params ( + ("method_id" = String, Path, description = "Set the Payment Method as Default for the Customer"), + ), + responses( + (status = 200, description = "Payment Method has been set as default", body =CustomerDefaultPaymentMethodResponse ), + (status = 400, description = "Payment Method has already been set as default for that customer"), + (status = 404, description = "Payment Method not found for the customer") + ), + tag = "Customer Set Default Payment Method", + operation_id = "Set the Payment Method as Default", + security(("ephemeral_key" = [])) +)] +pub async fn default_payment_method_set_api() {} diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 32e850a073f3..85cfef3df54c 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true [dependencies] error-stack = "0.3.1" -fred = { version = "7.0.0", features = ["metrics", "partial-tracing", "subscriber-client"] } +fred = { version = "7.1.2", features = ["metrics", "partial-tracing", "subscriber-client", "check-unresponsive"] } futures = "0.3" serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" diff --git a/crates/redis_interface/src/lib.rs b/crates/redis_interface/src/lib.rs index 33d40ebe155d..b46e4aec1914 100644 --- a/crates/redis_interface/src/lib.rs +++ b/crates/redis_interface/src/lib.rs @@ -132,6 +132,11 @@ impl RedisConnectionPool { }, }; + let connection_config = fred::types::ConnectionConfig { + unresponsive_timeout: std::time::Duration::from_secs(conf.unresponsive_timeout), + ..fred::types::ConnectionConfig::default() + }; + if !conf.use_legacy_version { config.version = fred::types::RespVersion::RESP3; } @@ -151,7 +156,7 @@ impl RedisConnectionPool { let pool = fred::prelude::RedisPool::new( config, Some(perf), - None, + Some(connection_config), Some(reconnect_policy), conf.pool_size, ) @@ -201,6 +206,15 @@ impl RedisConnectionPool { } } } + + pub async fn on_unresponsive(&self) { + let _ = self.pool.clients().iter().map(|client| { + client.on_unresponsive(|server| { + logger::warn!(redis_server =?server.host, "Redis server is unresponsive"); + Ok(()) + }) + }); + } } struct RedisConfig { diff --git a/crates/redis_interface/src/types.rs b/crates/redis_interface/src/types.rs index 4ebb620c36a4..cb883fc1693c 100644 --- a/crates/redis_interface/src/types.rs +++ b/crates/redis_interface/src/types.rs @@ -57,6 +57,7 @@ pub struct RedisSettings { pub max_in_flight_commands: u64, pub default_command_timeout: u64, pub max_feed_count: u64, + pub unresponsive_timeout: u64, } impl RedisSettings { @@ -76,7 +77,17 @@ impl RedisSettings { "Redis `cluster_urls` must be specified if `cluster_enabled` is `true`".into(), )) .into_report() - }) + })?; + + when( + self.default_command_timeout < self.unresponsive_timeout, + || { + Err(errors::RedisError::InvalidConfiguration( + "Unresponsive timeout cannot be greater than the command timeout".into(), + )) + .into_report() + }, + ) } } @@ -97,8 +108,9 @@ impl Default for RedisSettings { auto_pipeline: true, disable_auto_backpressure: false, max_in_flight_commands: 5000, - default_command_timeout: 0, + default_command_timeout: 30, max_feed_count: 200, + unresponsive_timeout: 10, } } } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 5dbeb1bb199c..a748b6b273d0 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] email = ["external_services/email", "scheduler/email", "olap"] frm = [] stripe = ["dep:serde_qs"] @@ -27,6 +27,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = ["common_enums/payouts"] +payout_retry = ["payouts"] recon = ["email", "api_models/recon"] retry = [] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 51fb6ff822b4..f7bee88b8c33 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -9,8 +9,9 @@ pub mod routes { }; use api_models::analytics::{ GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, + GetDisputeMetricRequest, GetPaymentFiltersRequest, GetPaymentMetricRequest, + GetRefundFilterRequest, GetRefundMetricRequest, GetSdkEventFiltersRequest, + GetSdkEventMetricRequest, ReportRequest, }; use error_stack::ResultExt; use router_env::AnalyticsFlow; @@ -92,6 +93,10 @@ pub mod routes { web::resource("filters/disputes") .route(web::post().to(get_dispute_filters)), ) + .service( + web::resource("metrics/disputes") + .route(web::post().to(get_dispute_metrics)), + ) } route } @@ -612,4 +617,39 @@ pub mod routes { )) .await } + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetDisputeMetricRequest` element. + pub async fn get_dispute_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetDisputeMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetDisputeMetricRequest"); + let flow = AnalyticsFlow::GetDisputeMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::disputes::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } } diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index c2877535daa5..c586bfecdb70 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -255,6 +255,9 @@ impl ProcessTrackerWorkflows for WorkflowRunner { ) } } + storage::ProcessTrackerRunner::OutgoingWebhookRetryWorkflow => Ok(Box::new( + workflows::outgoing_webhook_retry::OutgoingWebhookRetryWorkflow, + )), } }; diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index d67166c0d04b..87560032ea4b 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -71,7 +71,7 @@ pub async fn payment_intents_create( )) .await } -#[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieveForceSync))] pub async fn payment_intents_retrieve( state: web::Data, req: HttpRequest, @@ -96,7 +96,7 @@ pub async fn payment_intents_retrieve( Err(err) => return api::log_and_return_error_response(report!(err)), }; - let flow = Flow::PaymentsRetrieve; + let flow = Flow::PaymentsRetrieveForceSync; let locking_action = payload.get_locking_input(flow.clone()); Box::pin(wrap::compatibility_api_wrap::< _, @@ -131,7 +131,7 @@ pub async fn payment_intents_retrieve( )) .await } -#[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip_all, fields(flow))] pub async fn payment_intents_retrieve_with_gateway_creds( state: web::Data, qs_config: web::Data, @@ -160,7 +160,13 @@ pub async fn payment_intents_retrieve_with_gateway_creds( Err(err) => return api::log_and_return_error_response(report!(err)), }; - let flow = Flow::PaymentsRetrieve; + let flow = match json_payload.force_sync { + Some(true) => Flow::PaymentsRetrieveForceSync, + _ => Flow::PaymentsRetrieve, + }; + + tracing::Span::current().record("flow", &flow.to_string()); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(wrap::compatibility_api_wrap::< _, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 22f03c92cc89..7d876b954b8e 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -336,7 +336,10 @@ impl TryFrom for payments::PaymentsRequest { payment_method_data: item.payment_method_data.as_ref().and_then(|pmd| { pmd.payment_method_details .as_ref() - .map(|spmd| payments::PaymentMethodData::from(spmd.to_owned())) + .map(|spmd| payments::PaymentMethodDataRequest { + payment_method_data: payments::PaymentMethodData::from(spmd.to_owned()), + billing: pmd.billing_details.clone().map(payments::Address::from), + }) }), payment_method: item .payment_method_data @@ -535,7 +538,7 @@ impl From for StripePaymentIntentResponse { capture_on: resp.capture_on, capture_method: resp.capture_method, payment_method: resp.payment_method, - payment_method_data: resp.payment_method_data.clone(), + payment_method_data: resp.payment_method_data.map(|pmd| pmd.payment_method_data), payment_token: resp.payment_token, shipping: resp.shipping, billing: resp.billing, diff --git a/crates/router/src/compatibility/stripe/refunds.rs b/crates/router/src/compatibility/stripe/refunds.rs index 77c3a61fa78c..80ebbc4f84d7 100644 --- a/crates/router/src/compatibility/stripe/refunds.rs +++ b/crates/router/src/compatibility/stripe/refunds.rs @@ -57,14 +57,14 @@ pub async fn refund_create( )) .await } -#[instrument(skip_all, fields(flow = ?Flow::RefundsRetrieve))] +#[instrument(skip_all, fields(flow))] pub async fn refund_retrieve_with_gateway_creds( state: web::Data, qs_config: web::Data, req: HttpRequest, form_payload: web::Bytes, ) -> HttpResponse { - let refund_request = match qs_config + let refund_request: refund_types::RefundsRetrieveRequest = match qs_config .deserialize_bytes(&form_payload) .map_err(|err| report!(errors::StripeErrorCode::from(err))) { @@ -72,7 +72,12 @@ pub async fn refund_retrieve_with_gateway_creds( Err(err) => return api::log_and_return_error_response(err), }; - let flow = Flow::RefundsRetrieve; + let flow = match refund_request.force_sync { + Some(true) => Flow::RefundsRetrieveForceSync, + _ => Flow::RefundsRetrieve, + }; + + tracing::Span::current().record("flow", &flow.to_string()); Box::pin(wrap::compatibility_api_wrap::< _, @@ -103,7 +108,7 @@ pub async fn refund_retrieve_with_gateway_creds( )) .await } -#[instrument(skip_all, fields(flow = ?Flow::RefundsRetrieve))] +#[instrument(skip_all, fields(flow = ?Flow::RefundsRetrieveForceSync))] pub async fn refund_retrieve( state: web::Data, req: HttpRequest, @@ -115,7 +120,7 @@ pub async fn refund_retrieve( merchant_connector_details: None, }; - let flow = Flow::RefundsRetrieve; + let flow = Flow::RefundsRetrieveForceSync; Box::pin(wrap::compatibility_api_wrap::< _, diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index 515e41ec91fa..6522dc4697c1 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -78,7 +78,7 @@ pub async fn setup_intents_create( )) .await } -#[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRetrieveForceSync))] pub async fn setup_intents_retrieve( state: web::Data, req: HttpRequest, @@ -103,7 +103,7 @@ pub async fn setup_intents_retrieve( Err(err) => return api::log_and_return_error_response(report!(err)), }; - let flow = Flow::PaymentsRetrieve; + let flow = Flow::PaymentsRetrieveForceSync; Box::pin(wrap::compatibility_api_wrap::< _, diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 4dcea1c4fefd..25bb5a35b89e 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -247,7 +247,10 @@ impl TryFrom for payments::PaymentsRequest { payment_method_data: item.payment_method_data.as_ref().and_then(|pmd| { pmd.payment_method_details .as_ref() - .map(|spmd| payments::PaymentMethodData::from(spmd.to_owned())) + .map(|spmd| payments::PaymentMethodDataRequest { + payment_method_data: payments::PaymentMethodData::from(spmd.to_owned()), + billing: pmd.billing_details.clone().map(payments::Address::from), + }) }), payment_method: item .payment_method_data diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index da14475f120c..da9a7f3d1634 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -135,7 +135,7 @@ where .map_into_boxed_body() } - Ok(api::ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { + Ok(api::ApplicationResponse::PaymentLinkForm(boxed_payment_link_data)) => { match *boxed_payment_link_data { api::PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { match api::build_payment_link_html(payment_link_data) { diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 771ad993ac91..93301364e62d 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -33,6 +33,8 @@ use crate::{ utils::{crypto, ByteSliceExt, BytesExt, OptionExt}, }; +const ADYEN_API_VERSION: &str = "v68"; + #[derive(Debug, Clone)] pub struct Adyen; @@ -268,6 +270,25 @@ impl // Not Implemented (R) } +fn build_env_specific_endpoint( + base_url: &str, + test_mode: Option, + connector_metadata: &Option, +) -> CustomResult { + if test_mode.unwrap_or(true) { + Ok(base_url.to_string()) + } else { + let adyen_connector_metadata_object = + transformers::AdyenConnectorMetadataObject::try_from(connector_metadata)?; + let endpoint_prefix = adyen_connector_metadata_object.endpoint_prefix.ok_or( + errors::ConnectorError::InvalidConnectorConfig { + config: "metadata.endpoint_prefix", + }, + )?; + Ok(base_url.replace("{{merchant_endpoint_prefix}}", &endpoint_prefix)) + } +} + impl services::ConnectorIntegration< api::SetupMandate, @@ -293,10 +314,15 @@ impl fn get_url( &self, - _req: &types::SetupMandateRouterData, + req: &types::SetupMandateRouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!("{}{}", self.base_url(connectors), "v68/payments")) + let endpoint = build_env_specific_endpoint( + self.base_url(connectors), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!("{}{}/payments", endpoint, ADYEN_API_VERSION)) } fn get_request_body( &self, @@ -418,11 +444,15 @@ impl connectors: &settings::Connectors, ) -> CustomResult { let id = req.request.connector_transaction_id.as_str(); - Ok(format!( - "{}{}/{}/captures", + + let endpoint = build_env_specific_endpoint( self.base_url(connectors), - "v68/payments", - id + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}{}/payments/{}/captures", + endpoint, ADYEN_API_VERSION, id )) } fn get_request_body( @@ -560,13 +590,17 @@ impl fn get_url( &self, - _req: &types::RouterData, + req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!( - "{}{}", + let endpoint = build_env_specific_endpoint( self.base_url(connectors), - "v68/payments/details" + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}{}/payments/details", + endpoint, ADYEN_API_VERSION )) } @@ -681,10 +715,15 @@ impl fn get_url( &self, - _req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!("{}{}", self.base_url(connectors), "v68/payments")) + let endpoint = build_env_specific_endpoint( + self.base_url(connectors), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!("{}{}/payments", endpoint, ADYEN_API_VERSION)) } fn get_request_body( @@ -785,12 +824,17 @@ impl fn get_url( &self, - _req: &types::PaymentsPreProcessingRouterData, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + self.base_url(connectors), + req.test_mode, + &req.connector_meta_data, + )?; Ok(format!( - "{}v69/paymentMethods/balance", - self.base_url(connectors) + "{}{}/paymentMethods/balance", + endpoint, ADYEN_API_VERSION )) } @@ -911,11 +955,16 @@ impl req: &types::PaymentsCancelRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let id = req.request.connector_transaction_id.as_str(); - Ok(format!( - "{}v68/payments/{}/cancels", + let id = req.request.connector_transaction_id.clone(); + + let endpoint = build_env_specific_endpoint( self.base_url(connectors), - id + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}{}/payments/{}/cancels", + endpoint, ADYEN_API_VERSION, id )) } @@ -991,12 +1040,17 @@ impl services::ConnectorIntegration, + req: &types::PayoutsRouterData, connectors: &settings::Connectors, ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.secondary_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; Ok(format!( - "{}pal/servlet/Payout/v68/declineThirdParty", - connectors.adyen.secondary_base_url + "{}pal/servlet/Payout/{}/declineThirdParty", + endpoint, ADYEN_API_VERSION )) } @@ -1083,12 +1137,17 @@ impl services::ConnectorIntegration, + req: &types::PayoutsRouterData, connectors: &settings::Connectors, ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.secondary_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; Ok(format!( - "{}pal/servlet/Payout/v68/storeDetailAndSubmitThirdParty", - connectors.adyen.secondary_base_url + "{}pal/servlet/Payout/{}/storeDetailAndSubmitThirdParty", + endpoint, ADYEN_API_VERSION )) } @@ -1180,10 +1239,15 @@ impl { fn get_url( &self, - _req: &types::PayoutsRouterData, + req: &types::PayoutsRouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!("{}v68/payments", self.base_url(connectors),)) + let endpoint = build_env_specific_endpoint( + self.base_url(connectors), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!("{}{}/payments", endpoint, ADYEN_API_VERSION)) } fn get_headers( @@ -1277,9 +1341,15 @@ impl services::ConnectorIntegration, connectors: &settings::Connectors, ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.secondary_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; Ok(format!( - "{}pal/servlet/Payout/v68/{}", - connectors.adyen.secondary_base_url, + "{}pal/servlet/Payout/{}/{}", + endpoint, + ADYEN_API_VERSION, match req.request.payout_type { storage_enums::PayoutType::Bank | storage_enums::PayoutType::Wallet => "confirmThirdParty".to_string(), @@ -1407,10 +1477,15 @@ impl services::ConnectorIntegration CustomResult { let connector_payment_id = req.request.connector_transaction_id.clone(); - Ok(format!( - "{}v68/payments/{}/refunds", + + let endpoint = build_env_specific_endpoint( self.base_url(connectors), - connector_payment_id + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}{}/payments/{}/refunds", + endpoint, ADYEN_API_VERSION, connector_payment_id )) } @@ -1591,7 +1666,7 @@ impl api::IncomingWebhook for Adyen { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; // for capture_event, original_reference field will have the authorized payment's PSP reference - if adyen::is_capture_event(¬if.event_code) { + if adyen::is_capture_or_cancel_event(¬if.event_code) { return Ok(api_models::webhooks::ObjectReferenceId::PaymentId( api_models::payments::PaymentIdType::ConnectorTransactionId( notif @@ -1630,6 +1705,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; Ok(IncomingWebhookEvent::foreign_from(( notif.event_code, + notif.success, notif.additional_data.dispute_status, ))) } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a2fc7cc7ba43..9fbf7d99812b 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2,7 +2,7 @@ use api_models::payouts::PayoutMethodData; use api_models::{enums, payments, webhooks}; use cards::CardNumber; -use common_utils::ext_traits::Encode; +use common_utils::{ext_traits::Encode, pii}; use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface}; use reqwest::Url; @@ -61,6 +61,21 @@ impl }) } } +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct AdyenConnectorMetadataObject { + pub endpoint_prefix: Option, +} + +impl TryFrom<&Option> for AdyenConnectorMetadataObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} // Adyen Types Definition // Payments Request and Response Types @@ -91,7 +106,8 @@ pub enum AuthType { #[serde(rename_all = "camelCase")] pub struct AdditionalData { authorisation_type: Option, - manual_capture: Option, + manual_capture: Option, + execute_three_d: Option, pub recurring_processing_model: Option, /// Enable recurring details in dashboard to receive this ID, https://docs.adyen.com/online-payments/tokenization/create-and-use-tokens#test-and-go-live #[serde(rename = "recurring.recurringDetailReference")] @@ -230,7 +246,7 @@ impl ForeignFrom<(bool, AdyenStatus, Option)> ) -> Self { match adyen_status { AdyenStatus::AuthenticationFinished => Self::AuthenticationSuccessful, - AdyenStatus::AuthenticationNotRequired => Self::Pending, + AdyenStatus::AuthenticationNotRequired | AdyenStatus::Received => Self::Pending, AdyenStatus::Authorised => match is_manual_capture { true => Self::Authorized, // In case of Automatic capture Authorized is the final status of the payment @@ -245,7 +261,6 @@ impl ForeignFrom<(bool, AdyenStatus, Option)> Some(common_enums::PaymentMethodType::Pix) => Self::AuthenticationPending, _ => Self::Pending, }, - AdyenStatus::Received => Self::Started, #[cfg(feature = "payouts")] AdyenStatus::PayoutConfirmReceived => Self::Started, #[cfg(feature = "payouts")] @@ -281,7 +296,6 @@ pub struct AdyenRefusal { #[derive(Debug, Clone, Serialize, serde::Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AdyenRedirection { - #[serde(rename = "redirectResult")] pub redirect_result: String, #[serde(rename = "type")] pub type_of_redirection_result: Option, @@ -432,6 +446,7 @@ pub struct Amount { #[derive(Debug, Clone, Serialize)] #[serde(tag = "type")] +#[serde(rename_all = "lowercase")] pub enum AdyenPaymentMethod<'a> { AdyenAffirm(Box), AdyenCard(Box), @@ -1016,6 +1031,9 @@ impl TryFrom<&api_enums::BankNames> for OpenBankingUKIssuer { | enums::BankNames::KrungsriBank | enums::BankNames::KrungThaiBank | enums::BankNames::TheSiamCommercialBank + | enums::BankNames::Yoursafe + | enums::BankNames::N26 + | enums::BankNames::NationaleNederlanden | enums::BankNames::KasikornBank => Err(errors::ConnectorError::NotSupported { message: String::from("BankRedirect"), connector: "Adyen", @@ -1079,8 +1097,9 @@ pub struct AdyenCancelRequest { #[derive(Default, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AdyenCancelResponse { - psp_reference: String, + payment_psp_reference: String, status: CancelStatus, + reference: String, } #[derive(Default, Debug, Deserialize, Serialize)] @@ -1365,16 +1384,17 @@ impl<'a> TryFrom<&api_enums::BankNames> for AdyenTestBankNames<'a> { api_models::enums::BankNames::AbnAmro => Self("1121"), api_models::enums::BankNames::AsnBank => Self("1151"), api_models::enums::BankNames::Bunq => Self("1152"), - api_models::enums::BankNames::Handelsbanken => Self("1153"), api_models::enums::BankNames::Ing => Self("1154"), api_models::enums::BankNames::Knab => Self("1155"), - api_models::enums::BankNames::Moneyou => Self("1156"), + api_models::enums::BankNames::N26 => Self("1156"), + api_models::enums::BankNames::NationaleNederlanden => Self("1157"), api_models::enums::BankNames::Rabobank => Self("1157"), api_models::enums::BankNames::Regiobank => Self("1158"), api_models::enums::BankNames::Revolut => Self("1159"), api_models::enums::BankNames::SnsBank => Self("1159"), api_models::enums::BankNames::TriodosBank => Self("1159"), api_models::enums::BankNames::VanLanschot => Self("1159"), + api_models::enums::BankNames::Yoursafe => Self("1159"), api_models::enums::BankNames::BankAustria => { Self("e6819e7a-f663-414b-92ec-cf7c82d2f4e5") } @@ -1419,6 +1439,34 @@ impl<'a> TryFrom<&api_enums::BankNames> for AdyenTestBankNames<'a> { } } +pub struct AdyenBankNames<'a>(&'a str); + +impl<'a> TryFrom<&api_enums::BankNames> for AdyenBankNames<'a> { + type Error = Error; + fn try_from(bank: &api_enums::BankNames) -> Result { + Ok(match bank { + api_models::enums::BankNames::AbnAmro => Self("0031"), + api_models::enums::BankNames::AsnBank => Self("0761"), + api_models::enums::BankNames::Bunq => Self("0802"), + api_models::enums::BankNames::Ing => Self("0721"), + api_models::enums::BankNames::Knab => Self("0801"), + api_models::enums::BankNames::N26 => Self("0807"), + api_models::enums::BankNames::NationaleNederlanden => Self("0808"), + api_models::enums::BankNames::Rabobank => Self("0021"), + api_models::enums::BankNames::Regiobank => Self("0771"), + api_models::enums::BankNames::Revolut => Self("0805"), + api_models::enums::BankNames::SnsBank => Self("0751"), + api_models::enums::BankNames::TriodosBank => Self("0511"), + api_models::enums::BankNames::VanLanschot => Self("0161"), + api_models::enums::BankNames::Yoursafe => Self("0806"), + _ => Err(errors::ConnectorError::NotSupported { + message: String::from("BankRedirect"), + connector: "Adyen", + })?, + }) + } +} + impl TryFrom<&types::ConnectorAuthType> for AdyenAuthType { type Error = Error; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { @@ -1594,19 +1642,28 @@ fn get_browser_info( } fn get_additional_data(item: &types::PaymentsAuthorizeRouterData) -> Option { - match item.request.capture_method { + let (authorisation_type, manual_capture) = match item.request.capture_method { Some(diesel_models::enums::CaptureMethod::Manual) - | Some(diesel_models::enums::CaptureMethod::ManualMultiple) => Some(AdditionalData { - authorisation_type: Some(AuthType::PreAuth), - manual_capture: Some(true), - network_tx_reference: None, - recurring_detail_reference: None, - recurring_shopper_reference: None, - recurring_processing_model: Some(AdyenRecurringModel::UnscheduledCardOnFile), - ..AdditionalData::default() - }), - _ => None, - } + | Some(diesel_models::enums::CaptureMethod::ManualMultiple) => { + (Some(AuthType::PreAuth), Some("true".to_string())) + } + _ => (None, None), + }; + let execute_three_d = if matches!(item.auth_type, enums::AuthenticationType::ThreeDs) { + Some("true".to_string()) + } else { + None + }; + Some(AdditionalData { + authorisation_type, + manual_capture, + execute_three_d, + network_tx_reference: None, + recurring_detail_reference: None, + recurring_shopper_reference: None, + recurring_processing_model: None, + ..AdditionalData::default() + }) } fn get_channel_type(pm_type: &Option) -> Option { @@ -2079,10 +2136,12 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> } } -impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod<'a> { +impl<'a> TryFrom<(&api_models::payments::BankRedirectData, Option)> + for AdyenPaymentMethod<'a> +{ type Error = Error; fn try_from( - bank_redirect_data: &api_models::payments::BankRedirectData, + (bank_redirect_data, test_mode): (&api_models::payments::BankRedirectData, Option), ) -> Result { match bank_redirect_data { api_models::payments::BankRedirectData::BancontactCard { @@ -2154,19 +2213,33 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod payment_type: PaymentType::Giropay, }))) } - api_models::payments::BankRedirectData::Ideal { bank_name, .. } => Ok( - AdyenPaymentMethod::Ideal(Box::new(BankRedirectionWithIssuer { - payment_type: PaymentType::Ideal, - issuer: Some( + api_models::payments::BankRedirectData::Ideal { bank_name, .. } => { + let issuer = if test_mode.unwrap_or(true) { + Some( AdyenTestBankNames::try_from(&bank_name.ok_or( errors::ConnectorError::MissingRequiredField { field_name: "ideal.bank_name", }, )?)? .0, - ), - })), - ), + ) + } else { + Some( + AdyenBankNames::try_from(&bank_name.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "ideal.bank_name", + }, + )?)? + .0, + ) + }; + Ok(AdyenPaymentMethod::Ideal(Box::new( + BankRedirectionWithIssuer { + payment_type: PaymentType::Ideal, + issuer, + }, + ))) + } api_models::payments::BankRedirectData::OnlineBankingCzechRepublic { issuer } => { Ok(AdyenPaymentMethod::OnlineBankingCzechRepublic(Box::new( OnlineBankingCzechRepublicData { @@ -2692,7 +2765,8 @@ impl<'a> let browser_info = get_browser_info(item.router_data)?; let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_return_url()?; - let payment_method = AdyenPaymentMethod::try_from(bank_redirect_data)?; + let payment_method = + AdyenPaymentMethod::try_from((bank_redirect_data, item.router_data.test_mode))?; let (shopper_locale, country) = get_redirect_extra_details(item.router_data)?; let line_items = Some(get_line_items(item)); @@ -2939,15 +3013,6 @@ impl TryFrom<&types::PaymentsCancelRouterData> for AdyenCancelRequest { } } -impl From for storage_enums::AttemptStatus { - fn from(status: CancelStatus) -> Self { - match status { - CancelStatus::Received => Self::Voided, - CancelStatus::Processing => Self::Pending, - } - } -} - impl TryFrom> for types::PaymentsCancelRouterData { @@ -2956,14 +3021,16 @@ impl TryFrom> item: types::PaymentsCancelResponseRouterData, ) -> Result { Ok(Self { - status: item.response.status.into(), + status: enums::AttemptStatus::Pending, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.psp_reference), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.payment_psp_reference, + ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.reference), incremental_authorization_allowed: None, }), ..item.data @@ -3683,11 +3750,7 @@ impl TryFrom> mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: item - .response - .merchant_reference - .clone() - .or(Some(item.response.psp_reference)), + connector_response_reference_id: Some(item.response.reference), incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount.value), @@ -3756,7 +3819,7 @@ impl TryFrom> ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.reference, + connector_refund_id: item.response.psp_reference, // From the docs, the only value returned is "received", outcome of refund is available // through refund notification webhook // For more info: https://docs.adyen.com/online-payments/refund @@ -3829,7 +3892,9 @@ pub enum WebhookEventCode { Authorisation, Refund, CancelOrRefund, + Cancellation, RefundFailed, + RefundReversed, NotificationOfChargeback, Chargeback, ChargebackReversed, @@ -3846,10 +3911,12 @@ pub fn is_transaction_event(event_code: &WebhookEventCode) -> bool { matches!(event_code, WebhookEventCode::Authorisation) } -pub fn is_capture_event(event_code: &WebhookEventCode) -> bool { +pub fn is_capture_or_cancel_event(event_code: &WebhookEventCode) -> bool { matches!( event_code, - WebhookEventCode::Capture | WebhookEventCode::CaptureFailed + WebhookEventCode::Capture + | WebhookEventCode::CaptureFailed + | WebhookEventCode::Cancellation ) } @@ -3859,6 +3926,7 @@ pub fn is_refund_event(event_code: &WebhookEventCode) -> bool { WebhookEventCode::Refund | WebhookEventCode::CancelOrRefund | WebhookEventCode::RefundFailed + | WebhookEventCode::RefundReversed ) } @@ -3874,31 +3942,66 @@ pub fn is_chargeback_event(event_code: &WebhookEventCode) -> bool { ) } -impl ForeignFrom<(WebhookEventCode, Option)> for webhooks::IncomingWebhookEvent { - fn foreign_from((code, status): (WebhookEventCode, Option)) -> Self { - match (code, status) { - (WebhookEventCode::Authorisation, _) => Self::PaymentIntentSuccess, - (WebhookEventCode::Refund, _) => Self::RefundSuccess, - (WebhookEventCode::CancelOrRefund, _) => Self::RefundSuccess, - (WebhookEventCode::RefundFailed, _) => Self::RefundFailure, - (WebhookEventCode::NotificationOfChargeback, _) => Self::DisputeOpened, - (WebhookEventCode::Chargeback, None) => Self::DisputeLost, - (WebhookEventCode::Chargeback, Some(DisputeStatus::Won)) => Self::DisputeWon, - (WebhookEventCode::Chargeback, Some(DisputeStatus::Lost)) => Self::DisputeLost, - (WebhookEventCode::Chargeback, Some(_)) => Self::DisputeOpened, - (WebhookEventCode::ChargebackReversed, Some(DisputeStatus::Pending)) => { - Self::DisputeChallenged +fn is_success_scenario(is_success: String) -> bool { + is_success.as_str() == "true" +} + +impl ForeignFrom<(WebhookEventCode, String, Option)> + for webhooks::IncomingWebhookEvent +{ + fn foreign_from( + (code, is_success, dispute_status): (WebhookEventCode, String, Option), + ) -> Self { + match code { + WebhookEventCode::Authorisation => { + if is_success_scenario(is_success) { + Self::PaymentIntentSuccess + } else { + Self::PaymentIntentFailure + } + } + WebhookEventCode::Refund | WebhookEventCode::CancelOrRefund => { + if is_success_scenario(is_success) { + Self::RefundSuccess + } else { + Self::RefundFailure + } + } + WebhookEventCode::Cancellation => { + if is_success_scenario(is_success) { + Self::PaymentIntentCancelled + } else { + Self::PaymentIntentCancelFailure + } } - (WebhookEventCode::ChargebackReversed, _) => Self::DisputeWon, - (WebhookEventCode::SecondChargeback, _) => Self::DisputeLost, - (WebhookEventCode::PrearbitrationWon, Some(DisputeStatus::Pending)) => { - Self::DisputeOpened + WebhookEventCode::RefundFailed | WebhookEventCode::RefundReversed => { + Self::RefundFailure } - (WebhookEventCode::PrearbitrationWon, _) => Self::DisputeWon, - (WebhookEventCode::PrearbitrationLost, _) => Self::DisputeLost, - (WebhookEventCode::Unknown, _) => Self::EventNotSupported, - (WebhookEventCode::Capture, _) => Self::PaymentIntentSuccess, - (WebhookEventCode::CaptureFailed, _) => Self::PaymentIntentFailure, + WebhookEventCode::NotificationOfChargeback => Self::DisputeOpened, + WebhookEventCode::Chargeback => match dispute_status { + Some(DisputeStatus::Won) => Self::DisputeWon, + Some(DisputeStatus::Lost) | None => Self::DisputeLost, + Some(_) => Self::DisputeOpened, + }, + WebhookEventCode::ChargebackReversed => match dispute_status { + Some(DisputeStatus::Pending) => Self::DisputeChallenged, + _ => Self::DisputeWon, + }, + WebhookEventCode::SecondChargeback => Self::DisputeLost, + WebhookEventCode::PrearbitrationWon => match dispute_status { + Some(DisputeStatus::Pending) => Self::DisputeOpened, + _ => Self::DisputeWon, + }, + WebhookEventCode::PrearbitrationLost => Self::DisputeLost, + WebhookEventCode::Capture => { + if is_success_scenario(is_success) { + Self::PaymentIntentCaptureSuccess + } else { + Self::PaymentIntentCaptureFailure + } + } + WebhookEventCode::CaptureFailed => Self::PaymentIntentCaptureFailure, + WebhookEventCode::Unknown => Self::EventNotSupported, } } } @@ -3949,7 +4052,13 @@ impl From for Response { psp_reference: notif.psp_reference, merchant_reference: notif.merchant_reference, result_code: match notif.success.as_str() { - "true" => AdyenStatus::Authorised, + "true" => { + if notif.event_code == WebhookEventCode::Cancellation { + AdyenStatus::Cancelled + } else { + AdyenStatus::Authorised + } + } _ => AdyenStatus::Refused, }, amount: Some(Amount { diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 0be25b078bc8..8e30e2b0475a 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -604,7 +604,7 @@ impl let processing_information = ProcessingInformation::from(( item, Some(PaymentSolution::ApplePay), - Some(apple_pay_wallet_data.payment_method.network), + Some(apple_pay_wallet_data.payment_method.network.clone()), )); let client_reference_information = ClientReferenceInformation::from(item); let expiration_month = apple_pay_data.get_expiry_month()?; @@ -622,14 +622,29 @@ impl item.router_data.request.metadata.clone().map(|metadata| { Vec::::foreign_from(metadata.peek().to_owned()) }); - + let ucaf_collection_indicator = match apple_pay_wallet_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; Ok(Self { processing_information, payment_information, order_information, client_reference_information, merchant_defined_information, - consumer_authentication_information: None, + consumer_authentication_information: Some(BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + }), }) } } @@ -703,7 +718,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> let processing_information = ProcessingInformation::from(( item, Some(PaymentSolution::ApplePay), - Some(apple_pay_data.payment_method.network), + Some(apple_pay_data.payment_method.network.clone()), )); let client_reference_information = ClientReferenceInformation::from(item); @@ -723,13 +738,31 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> metadata.peek().to_owned(), ) }); + let ucaf_collection_indicator = match apple_pay_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; Ok(Self { processing_information, payment_information, order_information, merchant_defined_information, client_reference_information, - consumer_authentication_information: None, + consumer_authentication_information: Some( + BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + }, + ), }) } } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 2a20999085da..7d2d4f7f72a4 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -188,7 +188,7 @@ pub struct CardSource { pub struct WalletSource { #[serde(rename = "type")] pub source_type: CheckoutSourceTypes, - pub token: String, + pub token: Secret, } #[derive(Debug, Serialize)] @@ -301,7 +301,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme Ok(PaymentSource::Wallets(WalletSource { source_type: CheckoutSourceTypes::Token, token: match item.router_data.get_payment_method_token()? { - types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::Token(token) => token.into(), types::PaymentMethodToken::ApplePayDecrypt(_) => { Err(errors::ConnectorError::InvalidWalletToken)? } @@ -314,7 +314,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme types::PaymentMethodToken::Token(apple_pay_payment_token) => { Ok(PaymentSource::Wallets(WalletSource { source_type: CheckoutSourceTypes::Token, - token: apple_pay_payment_token, + token: apple_pay_payment_token.into(), })) } types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 14f8b2a7f994..60ff508d39cd 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -850,7 +850,7 @@ impl let processing_information = ProcessingInformation::try_from(( item, Some(PaymentSolution::ApplePay), - Some(apple_pay_wallet_data.payment_method.network), + Some(apple_pay_wallet_data.payment_method.network.clone()), ))?; let client_reference_information = ClientReferenceInformation::from(item); let expiration_month = apple_pay_data.get_expiry_month()?; @@ -868,13 +868,28 @@ impl item.router_data.request.metadata.clone().map(|metadata| { Vec::::foreign_from(metadata.peek().to_owned()) }); - + let ucaf_collection_indicator = match apple_pay_wallet_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; Ok(Self { processing_information, payment_information, order_information, client_reference_information, - consumer_authentication_information: None, + consumer_authentication_information: Some(CybersourceConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + }), merchant_defined_information, }) } @@ -956,7 +971,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> ProcessingInformation::try_from(( item, Some(PaymentSolution::ApplePay), - Some(apple_pay_data.payment_method.network), + Some(apple_pay_data.payment_method.network.clone()), ))?; let client_reference_information = ClientReferenceInformation::from(item); @@ -976,14 +991,31 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> metadata.peek().to_owned(), ) }); - + let ucaf_collection_indicator = match apple_pay_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; Ok(Self { processing_information, payment_information, order_information, client_reference_information, merchant_defined_information, - consumer_authentication_information: None, + consumer_authentication_information: Some( + CybersourceConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + }, + ), }) } } diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index d3703bc6bf8a..21fc44fe84fe 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -547,7 +547,7 @@ pub struct GocardlessMandateResponse { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MandateResponse { - id: String, + id: Secret, } impl @@ -570,7 +570,7 @@ impl >, ) -> Result { let mandate_reference = Some(MandateReference { - connector_mandate_id: Some(item.response.mandates.id.clone()), + connector_mandate_id: Some(item.response.mandates.id.clone().expose()), payment_method_id: None, }); Ok(Self { diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index af1159cd2c38..be4691341b8c 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -64,7 +64,7 @@ pub struct MolliePaymentsRequest { payment_method_data: PaymentMethodData, metadata: Option, sequence_type: SequenceType, - mandate_id: Option, + mandate_id: Option>, } #[derive(Default, Debug, Serialize, Deserialize)] @@ -92,7 +92,7 @@ pub enum PaymentMethodData { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApplePayMethodData { - apple_pay_payment_token: String, + apple_pay_payment_token: Secret, } #[derive(Debug, Serialize)] @@ -333,7 +333,7 @@ fn get_payment_method_for_wallet( } api_models::payments::WalletData::ApplePay(applepay_wallet_data) => { Ok(PaymentMethodData::Applepay(Box::new(ApplePayMethodData { - apple_pay_payment_token: applepay_wallet_data.payment_data.to_owned(), + apple_pay_payment_token: Secret::new(applepay_wallet_data.payment_data.to_owned()), }))) } _ => Err(errors::ConnectorError::NotImplemented( @@ -451,16 +451,16 @@ pub struct Links { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardDetails { - pub card_number: String, - pub card_holder: String, - pub card_expiry_date: String, - pub card_cvv: String, + pub card_number: Secret, + pub card_holder: Secret, + pub card_expiry_date: Secret, + pub card_cvv: Secret, } #[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BankDetails { - billing_email: String, + billing_email: Email, } pub struct MollieAuthType { diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index e7b7e0e90227..527403736aa0 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -8,10 +8,13 @@ use error_stack::{IntoReport, ResultExt}; use regex::Regex; use transformers as nmi; -use super::utils as connector_utils; +use super::utils::{self as connector_utils}; use crate::{ configs::settings, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments, + }, events::connector_api_logs::ConnectorEvent, services::{self, request, ConnectorIntegration, ConnectorValidation}, types::{ @@ -980,3 +983,41 @@ impl api::IncomingWebhook for Nmi { } } } + +impl services::ConnectorRedirectResponse for Nmi { + fn get_flow_type( + &self, + _query_params: &str, + json_payload: Option, + action: services::PaymentAction, + ) -> CustomResult { + match action { + services::PaymentAction::CompleteAuthorize => { + let payload_data = + json_payload.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "connector_metadata", + })?; + + let redirect_res: nmi::NmiRedirectResponse = serde_json::from_value(payload_data) + .into_report() + .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "redirect_res", + })?; + + match redirect_res { + transformers::NmiRedirectResponse::NmiRedirectResponseData(_) => { + Ok(payments::CallConnectorAction::Trigger) + } + transformers::NmiRedirectResponse::NmiErrorResponseData(error_res) => { + Ok(payments::CallConnectorAction::StatusUpdate { + status: enums::AttemptStatus::Failure, + error_code: Some(error_res.code), + error_message: Some(error_res.message), + }) + } + } + } + services::PaymentAction::PSync => Ok(payments::CallConnectorAction::Trigger), + } + } +} diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index e83142f8efd8..2f8505522b22 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -268,6 +268,20 @@ pub struct NmiCompleteRequest { three_ds_version: Option, directory_server_id: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum NmiRedirectResponse { + NmiRedirectResponseData(NmiRedirectResponseData), + NmiErrorResponseData(NmiErrorResponseData), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NmiErrorResponseData { + pub code: String, + pub message: String, +} #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -279,7 +293,7 @@ pub struct NmiRedirectResponseData { three_ds_version: Option, order_id: Option, directory_server_id: Option, - customer_vault_id: Option, + customer_vault_id: String, } impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for NmiCompleteRequest { @@ -311,11 +325,7 @@ impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for Nm transaction_type, security_key: auth_type.api_key, orderid: three_ds_data.order_id, - customer_vault_id: three_ds_data.customer_vault_id.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "customer_vault_id", - }, - )?, + customer_vault_id: three_ds_data.customer_vault_id, email: item.router_data.request.email.clone(), cvv, cardholder_auth: three_ds_data.card_holder_auth, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index d5dd6e813ae2..eef33616cea8 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -20,7 +20,7 @@ use crate::{ consts, core::errors, services, - types::{self, api, storage::enums, transformers::ForeignTryFrom}, + types::{self, api, storage::enums, transformers::ForeignTryFrom, BrowserInformation}, utils::OptionExt, }; @@ -75,6 +75,7 @@ pub struct NuveiPaymentsRequest { pub transaction_type: TransactionType, pub is_rebilling: Option, pub payment_option: PaymentOption, + pub device_details: Option, pub checksum: String, pub billing_address: Option, pub related_transaction_id: Option, @@ -135,7 +136,6 @@ pub struct PaymentOption { pub card: Option, pub redirect_url: Option, pub user_payment_option_id: Option, - pub device_details: Option, pub alternative_payment_method: Option, pub billing_address: Option, } @@ -315,7 +315,7 @@ pub struct V2AdditionalParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeviceDetails { - pub ip_address: String, + pub ip_address: Secret, } impl From for TransactionType { @@ -615,6 +615,9 @@ impl TryFrom for NuveiBIC { | api_models::enums::BankNames::Starling | api_models::enums::BankNames::TsbBank | api_models::enums::BankNames::TescoBank + | api_models::enums::BankNames::Yoursafe + | api_models::enums::BankNames::N26 + | api_models::enums::BankNames::NationaleNederlanden | api_models::enums::BankNames::UlsterBank => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Nuvei"), @@ -746,6 +749,7 @@ impl let item = data.0; let request_data = match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(card) => get_card_info(item, &card), + api::PaymentMethodData::MandatePayment => Self::try_from(item), api::PaymentMethodData::Wallet(wallet) => match wallet { payments::WalletData::GooglePay(gpay_data) => Self::try_from(gpay_data), payments::WalletData::ApplePay(apple_pay_data) => Ok(Self::from(apple_pay_data)), @@ -845,7 +849,6 @@ impl payments::PaymentMethodData::BankDebit(_) | payments::PaymentMethodData::BankTransfer(_) | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) @@ -874,6 +877,7 @@ impl related_transaction_id: request_data.related_transaction_id, payment_option: request_data.payment_option, billing_address: request_data.billing_address, + device_details: request_data.device_details, url_details: Some(UrlDetails { success_url: return_url.clone(), failure_url: return_url.clone(), @@ -888,104 +892,110 @@ fn get_card_info( item: &types::RouterData, card_details: &payments::Card, ) -> Result> { - let browser_info = item.request.get_browser_info()?; + let browser_information = item.request.browser_info.clone(); let related_transaction_id = if item.is_three_ds() { item.request.related_transaction_id.clone() } else { None }; - let connector_mandate_id = &item.request.connector_mandate_id(); - if connector_mandate_id.is_some() { - Ok(NuveiPaymentsRequest { - related_transaction_id, - is_rebilling: Some("1".to_string()), // In case of second installment, rebilling should be 1 - user_token_id: Some(item.request.get_email()?), - payment_option: PaymentOption { - user_payment_option_id: connector_mandate_id.clone(), - ..Default::default() - }, - ..Default::default() - }) - } else { - let (is_rebilling, additional_params, user_token_id) = - match item.request.setup_mandate_details.clone() { - Some(mandate_data) => { - let details = match mandate_data - .mandate_type - .get_required_value("mandate_type") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "mandate_type", - })? { - MandateDataType::SingleUse(details) => details, - MandateDataType::MultiUse(details) => { - details.ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "mandate_data.mandate_type.multi_use", - })? - } - }; - let mandate_meta: NuveiMandateMeta = utils::to_connector_meta_from_secret( - Some(details.get_metadata().ok_or_else(utils::missing_field_err( - "mandate_data.mandate_type.{multi_use|single_use}.metadata", - ))?), - )?; - ( - Some("0".to_string()), // In case of first installment, rebilling should be 0 - Some(V2AdditionalParams { - rebill_expiry: Some( - details - .get_end_date(date_time::DateFormat::YYYYMMDD) - .change_context(errors::ConnectorError::DateFormattingFailed)? - .ok_or_else(utils::missing_field_err( - "mandate_data.mandate_type.{multi_use|single_use}.end_date", - ))?, - ), - rebill_frequency: Some(mandate_meta.frequency), - challenge_window_size: None, - }), - Some(item.request.get_email()?), - ) - } - _ => (None, None, None), - }; - let three_d = if item.is_three_ds() { - Some(ThreeD { - browser_details: Some(BrowserDetails { - accept_header: browser_info.get_accept_header()?, - ip: browser_info.get_ip_address()?, - java_enabled: browser_info.get_java_enabled()?.to_string().to_uppercase(), - java_script_enabled: browser_info - .get_java_script_enabled()? - .to_string() - .to_uppercase(), - language: browser_info.get_language()?, - screen_height: browser_info.get_screen_height()?, - screen_width: browser_info.get_screen_width()?, - color_depth: browser_info.get_color_depth()?, - user_agent: browser_info.get_user_agent()?, - time_zone: browser_info.get_time_zone()?, - }), - v2_additional_params: additional_params, - notification_url: item.request.complete_authorize_url.clone(), - merchant_url: item.return_url.clone(), - platform_type: Some(PlatformType::Browser), - method_completion_ind: Some(MethodCompletion::Unavailable), - ..Default::default() - }) - } else { - None - }; - Ok(NuveiPaymentsRequest { - related_transaction_id, - is_rebilling, - user_token_id, - payment_option: PaymentOption::from(NuveiCardDetails { - card: card_details.clone(), - three_d, + let address = item.get_billing_address_details_as_optional(); + + let billing_address = match address { + Some(address) => Some(BillingAddress { + first_name: Some(address.get_first_name()?.clone()), + last_name: Some(address.get_last_name()?.clone()), + email: item.request.get_email()?, + country: item.get_billing_country()?, + }), + None => None, + }; + let (is_rebilling, additional_params, user_token_id) = + match item.request.setup_mandate_details.clone() { + Some(mandate_data) => { + let details = match mandate_data + .mandate_type + .get_required_value("mandate_type") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "mandate_type", + })? { + MandateDataType::SingleUse(details) => details, + MandateDataType::MultiUse(details) => { + details.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "mandate_data.mandate_type.multi_use", + })? + } + }; + let mandate_meta: NuveiMandateMeta = utils::to_connector_meta_from_secret(Some( + details.get_metadata().ok_or_else(utils::missing_field_err( + "mandate_data.mandate_type.{multi_use|single_use}.metadata", + ))?, + ))?; + ( + Some("0".to_string()), // In case of first installment, rebilling should be 0 + Some(V2AdditionalParams { + rebill_expiry: Some( + details + .get_end_date(date_time::DateFormat::YYYYMMDD) + .change_context(errors::ConnectorError::DateFormattingFailed)? + .ok_or_else(utils::missing_field_err( + "mandate_data.mandate_type.{multi_use|single_use}.end_date", + ))?, + ), + rebill_frequency: Some(mandate_meta.frequency), + challenge_window_size: None, + }), + Some(item.request.get_email()?), + ) + } + _ => (None, None, None), + }; + let three_d = if item.is_three_ds() { + let browser_details = match &browser_information { + Some(browser_info) => Some(BrowserDetails { + accept_header: browser_info.get_accept_header()?, + ip: browser_info.get_ip_address()?, + java_enabled: browser_info.get_java_enabled()?.to_string().to_uppercase(), + java_script_enabled: browser_info + .get_java_script_enabled()? + .to_string() + .to_uppercase(), + language: browser_info.get_language()?, + screen_height: browser_info.get_screen_height()?, + screen_width: browser_info.get_screen_width()?, + color_depth: browser_info.get_color_depth()?, + user_agent: browser_info.get_user_agent()?, + time_zone: browser_info.get_time_zone()?, }), + None => None, + }; + Some(ThreeD { + browser_details, + v2_additional_params: additional_params, + notification_url: item.request.complete_authorize_url.clone(), + merchant_url: item.return_url.clone(), + platform_type: Some(PlatformType::Browser), + method_completion_ind: Some(MethodCompletion::Unavailable), ..Default::default() }) - } + } else { + None + }; + + Ok(NuveiPaymentsRequest { + related_transaction_id, + is_rebilling, + user_token_id, + device_details: Option::::foreign_try_from( + &item.request.browser_info.clone(), + )?, + payment_option: PaymentOption::from(NuveiCardDetails { + card: card_details.clone(), + three_d, + }), + billing_address, + ..Default::default() + }) } impl From for PaymentOption { fn from(card_details: NuveiCardDetails) -> Self { @@ -1532,6 +1542,51 @@ impl TryFrom } } +impl TryFrom<&types::RouterData> + for NuveiPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + data: &types::RouterData, + ) -> Result { + { + let item = data; + let connector_mandate_id = &item.request.connector_mandate_id(); + let related_transaction_id = if item.is_three_ds() { + item.request.related_transaction_id.clone() + } else { + None + }; + Ok(Self { + related_transaction_id, + device_details: Option::::foreign_try_from( + &item.request.browser_info.clone(), + )?, + is_rebilling: Some("1".to_string()), // In case of second installment, rebilling should be 1 + user_token_id: Some(item.request.get_email()?), + payment_option: PaymentOption { + user_payment_option_id: connector_mandate_id.clone(), + ..Default::default() + }, + ..Default::default() + }) + } + } +} + +impl ForeignTryFrom<&Option> for Option { + type Error = error_stack::Report; + fn foreign_try_from(browser_info: &Option) -> Result { + let device_details = match browser_info { + Some(browser_info) => Some(DeviceDetails { + ip_address: browser_info.get_ip_address()?, + }), + None => None, + }; + Ok(device_details) + } +} + fn get_refund_response( response: NuveiPaymentsResponse, http_code: u16, diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index 0c4543cd8a2f..5873609e3723 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -5,13 +5,13 @@ use std::fmt::Debug; use api_models::enums::AuthenticationType; use common_utils::{crypto, request::RequestContent}; use diesel_models::enums; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{IntoReport, Report, ResultExt}; use masking::ExposeInterface; use transformers as payme; use crate::{ configs::settings, - connector::{utils as connector_utils, utils::PaymentsPreProcessingData}, + connector::utils::{self as connector_utils, PaymentsPreProcessingData}, core::{ errors::{self, CustomResult}, payments, @@ -24,7 +24,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, ErrorResponse, Response, }, - utils::BytesExt, + utils::{handle_json_response_deserialization_failure, BytesExt}, }; #[derive(Debug, Clone)] @@ -83,29 +83,37 @@ impl ConnectorCommon for Payme { res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: payme::PaymeErrorResponse = - res.response - .parse_struct("PaymeErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - event_builder.map(|i| i.set_error_response_body(&response)); - router_env::logger::info!(connector_response=?response); - - let status_code = match res.status_code { - 500..=511 => 200, - _ => res.status_code, - }; - Ok(ErrorResponse { - status_code, - code: response.status_error_code.to_string(), - message: response.status_error_details.clone(), - reason: Some(format!( - "{}, additional info: {}", - response.status_error_details, response.status_additional_info - )), - attempt_status: None, - connector_transaction_id: None, - }) + let response: Result< + payme::PaymeErrorResponse, + Report, + > = res.response.parse_struct("PaymeErrorResponse"); + + match response { + Ok(response_data) => { + event_builder.map(|i| i.set_error_response_body(&response_data)); + router_env::logger::info!(connector_response=?response_data); + let status_code = match res.status_code { + 500..=511 => 200, + _ => res.status_code, + }; + Ok(ErrorResponse { + status_code, + code: response_data.status_error_code.to_string(), + message: response_data.status_error_details.clone(), + reason: Some(format!( + "{}, additional info: {}", + response_data.status_error_details, response_data.status_additional_info + )), + attempt_status: None, + connector_transaction_id: None, + }) + } + Err(error_msg) => { + event_builder.map(|event| event.set_error(serde_json::json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); + router_env::logger::error!(deserialization_error =? error_msg); + handle_json_response_deserialization_failure(res, "payme".to_owned()) + } + } } } diff --git a/crates/router/src/connector/placetopay.rs b/crates/router/src/connector/placetopay.rs index 81255f450e5d..c28fed1fedef 100644 --- a/crates/router/src/connector/placetopay.rs +++ b/crates/router/src/connector/placetopay.rs @@ -119,8 +119,8 @@ impl ConnectorValidation for Placetopay { ) -> CustomResult<(), errors::ConnectorError> { let capture_method = capture_method.unwrap_or_default(); match capture_method { - enums::CaptureMethod::Manual => Ok(()), - enums::CaptureMethod::Automatic + enums::CaptureMethod::Automatic => Ok(()), + enums::CaptureMethod::Manual | enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err(utils::construct_not_supported_error_report( capture_method, @@ -615,7 +615,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) diff --git a/crates/router/src/connector/placetopay/transformers.rs b/crates/router/src/connector/placetopay/transformers.rs index f1c054b960b6..e5dedf6ded0f 100644 --- a/crates/router/src/connector/placetopay/transformers.rs +++ b/crates/router/src/connector/placetopay/transformers.rs @@ -206,30 +206,43 @@ impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuthType { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum PlacetopayStatus { +pub enum PlacetopayTransactionStatus { Ok, Failed, Approved, + // ApprovedPartial, + // PartialExpired, Rejected, Pending, PendingValidation, PendingProcess, + // Refunded, + // Reversed, + Error, + // Unknown, + // Manual, + // Dispute, + //The statuses that are commented out are awaiting clarification on the connector. } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PlacetopayStatusResponse { - status: PlacetopayStatus, + status: PlacetopayTransactionStatus, } -impl From for enums::AttemptStatus { - fn from(item: PlacetopayStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(item: PlacetopayTransactionStatus) -> Self { match item { - PlacetopayStatus::Approved | PlacetopayStatus::Ok => Self::Authorized, - PlacetopayStatus::Failed | PlacetopayStatus::Rejected => Self::Failure, - PlacetopayStatus::Pending - | PlacetopayStatus::PendingValidation - | PlacetopayStatus::PendingProcess => Self::Authorizing, + PlacetopayTransactionStatus::Approved | PlacetopayTransactionStatus::Ok => { + Self::Charged + } + PlacetopayTransactionStatus::Failed + | PlacetopayTransactionStatus::Rejected + | PlacetopayTransactionStatus::Error => Self::Failure, + PlacetopayTransactionStatus::Pending + | PlacetopayTransactionStatus::PendingValidation + | PlacetopayTransactionStatus::PendingProcess => Self::Pending, } } } @@ -239,6 +252,7 @@ impl From for enums::AttemptStatus { pub struct PlacetopayPaymentsResponse { status: PlacetopayStatusResponse, internal_reference: u64, + authorization: Option, } impl @@ -263,7 +277,11 @@ impl ), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata: item + .response + .authorization + .clone() + .map(|authorization| serde_json::json!(authorization)), network_txn_id: None, connector_response_reference_id: None, incremental_authorization_allowed: None, @@ -281,55 +299,90 @@ pub struct PlacetopayRefundRequest { auth: PlacetopayAuth, internal_reference: u64, action: PlacetopayNextAction, + authorization: Option, } impl TryFrom<&types::RefundsRouterData> for PlacetopayRefundRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { - let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; - let internal_reference = item - .request - .connector_transaction_id - .parse::() - .into_report() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let action = PlacetopayNextAction::Refund; - - Ok(Self { - auth, - internal_reference, - action, - }) + if item.request.refund_amount == item.request.payment_amount { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Reverse; + let authorization = match item.request.connector_metadata.clone() { + Some(metadata) => metadata.as_str().map(|auth| auth.to_string()), + None => None, + }; + Ok(Self { + auth, + internal_reference, + action, + authorization, + }) + } else { + Err(errors::ConnectorError::NotSupported { + message: "Partial Refund".to_string(), + connector: "placetopay", + } + .into()) + } } } impl From for enums::RefundStatus { fn from(item: PlacetopayRefundStatus) -> Self { match item { - PlacetopayRefundStatus::Refunded => Self::Success, - PlacetopayRefundStatus::Failed | PlacetopayRefundStatus::Rejected => Self::Failure, - PlacetopayRefundStatus::Pending | PlacetopayRefundStatus::PendingProcess => { - Self::Pending - } + PlacetopayRefundStatus::Ok + | PlacetopayRefundStatus::Approved + | PlacetopayRefundStatus::Refunded => Self::Success, + PlacetopayRefundStatus::Failed + | PlacetopayRefundStatus::Rejected + | PlacetopayRefundStatus::Error => Self::Failure, + PlacetopayRefundStatus::Pending + | PlacetopayRefundStatus::PendingProcess + | PlacetopayRefundStatus::PendingValidation => Self::Pending, } } } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PlacetopayRefundResponse { - status: PlacetopayRefundStatus, - internal_reference: u64, -} - #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PlacetopayRefundStatus { - Refunded, - Rejected, + Ok, Failed, + Approved, + // ApprovedPartial, + // PartialExpired, + Rejected, Pending, + PendingValidation, PendingProcess, + Refunded, + // Reversed, + Error, + // Unknown, + // Manual, + // Dispute, + //The statuses that are commented out are awaiting clarification on the connector. +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRefundStatusResponse { + status: PlacetopayRefundStatus, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRefundResponse { + status: PlacetopayRefundStatusResponse, + internal_reference: u64, } impl TryFrom> @@ -342,7 +395,7 @@ impl TryFrom Result; #[cfg(feature = "payouts")] fn get_quote_id(&self) -> Result; + fn get_billing_address_details_as_optional(&self) -> Option; } pub trait PaymentResponseRouterData { @@ -182,6 +183,14 @@ impl RouterData for types::RouterData Option { + self.address + .billing + .as_ref() + .and_then(|a| a.address.as_ref()) + .cloned() + } + fn get_billing_address_with_phone_number(&self) -> Result<&api::Address, Error> { self.address .billing diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index ccb482d5b7c5..b7c9a6826ce3 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -240,10 +240,8 @@ impl TryFrom<&types::ConnectorAuthType> for VoltAuthType { impl From for enums::AttemptStatus { fn from(item: VoltPaymentStatus) -> Self { match item { - VoltPaymentStatus::Completed - | VoltPaymentStatus::Received - | VoltPaymentStatus::Settled => Self::Charged, - VoltPaymentStatus::DelayedAtBank => Self::Pending, + VoltPaymentStatus::Received | VoltPaymentStatus::Settled => Self::Charged, + VoltPaymentStatus::Completed | VoltPaymentStatus::DelayedAtBank => Self::Pending, VoltPaymentStatus::NewPayment | VoltPaymentStatus::BankRedirect | VoltPaymentStatus::AwaitingCheckoutAuthorisation => Self::AuthenticationPending, @@ -421,13 +419,13 @@ impl impl From for enums::AttemptStatus { fn from(status: VoltWebhookPaymentStatus) -> Self { match status { - VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { - Self::Charged - } + VoltWebhookPaymentStatus::Received => Self::Charged, VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { Self::Failure } - VoltWebhookPaymentStatus::Pending => Self::Pending, + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Pending => { + Self::Pending + } } } } @@ -577,13 +575,13 @@ impl From for api::IncomingWebhookEvent { fn from(status: VoltWebhookBodyEventType) -> Self { match status { VoltWebhookBodyEventType::Payment(payment_data) => match payment_data.status { - VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { - Self::PaymentIntentSuccess - } + VoltWebhookPaymentStatus::Received => Self::PaymentIntentSuccess, VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { Self::PaymentIntentFailure } - VoltWebhookPaymentStatus::Pending => Self::PaymentIntentProcessing, + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Pending => { + Self::PaymentIntentProcessing + } }, VoltWebhookBodyEventType::Refund(refund_data) => match refund_data.status { VoltWebhookRefundsStatus::RefundConfirmed => Self::RefundSuccess, diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index 82ae59205b1f..88bc46bfc847 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -55,7 +55,6 @@ pub struct SubError { pub message: String, pub path: Option, pub field: Option, - pub arguments: Option>, } // Payouts diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 87886c2c861f..2b87d64c6ac8 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1695,6 +1695,7 @@ pub(crate) fn validate_auth_and_metadata_type( } api_enums::Connector::Adyen => { adyen::transformers::AdyenAuthType::try_from(val)?; + adyen::transformers::AdyenConnectorMetadataObject::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Airwallex => { diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 7d2069618d53..a2fcacb3b3d4 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -207,6 +207,8 @@ pub async fn add_api_key_expiry_task( let api_key_expiry_tracker = storage::ApiKeyExpiryTrackingData { key_id: api_key.key_id.clone(), merchant_id: api_key.merchant_id.clone(), + api_key_name: api_key.name.clone(), + prefix: api_key.prefix.clone(), // We need API key expiry too, because we need to decide on the schedule_time in // execute_workflow() where we won't be having access to the Api key object. api_key_expiry: api_key.expires_at, @@ -369,6 +371,8 @@ pub async fn update_api_key_expiry_task( let updated_tracking_data = &storage::ApiKeyExpiryTrackingData { key_id: api_key.key_id.clone(), merchant_id: api_key.merchant_id.clone(), + api_key_name: api_key.name.clone(), + prefix: api_key.prefix.clone(), api_key_expiry: api_key.expires_at, expiry_reminder_days, }; diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 521e3a22166b..02127efe6deb 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -108,6 +108,7 @@ pub async fn create_customer( address_id: address.clone().map(|addr| addr.address_id), created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), + default_payment_method_id: None, }) } .await @@ -208,6 +209,7 @@ pub async fn delete_customer( .find_payment_method_by_customer_id_merchant_id_list( &req.customer_id, &merchant_account.merchant_id, + None, ) .await { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index cec563b9631b..968181061a89 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -292,6 +292,10 @@ pub enum WebhooksFlowError { OutgoingWebhookEncodingFailed, #[error("Missing required field: {field_name}")] MissingRequiredField { field_name: &'static str }, + #[error("Failed to update outgoing webhook process tracker task")] + OutgoingWebhookProcessTrackerTaskUpdateFailed, + #[error("Failed to schedule retry attempt for outgoing webhook")] + OutgoingWebhookRetrySchedulingFailed, } #[derive(Debug, thiserror::Error)] diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index 1347bb8ffd0b..c5ddb1ed6bb2 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -43,7 +43,11 @@ pub async fn rust_locker_migration( for customer in domain_customers { let result = db - .find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id) + .find_payment_method_by_customer_id_merchant_id_list( + &customer.customer_id, + merchant_id, + None, + ) .change_context(errors::ApiErrorResponse::InternalServerError) .and_then(|pm| { call_to_locker( diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 5af948bcf3e0..096d5c88743a 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -195,7 +195,7 @@ pub async fn intiate_payment_link_flow( js_script, css_script, }; - return Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + return Ok(services::ApplicationResponse::PaymentLinkForm(Box::new( services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data), ))); }; @@ -225,7 +225,7 @@ pub async fn intiate_payment_link_flow( sdk_url: state.conf.payment_link.sdk_url.clone(), css_script, }; - Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + Ok(services::ApplicationResponse::PaymentLinkForm(Box::new( services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data), ))) } @@ -574,7 +574,7 @@ pub async fn get_payment_link_status( js_script, css_script, }; - Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + Ok(services::ApplicationResponse::PaymentLinkForm(Box::new( services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_status_data), ))) } diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html index be4369be1d97..35fd540b30d2 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html @@ -29,10 +29,32 @@ - + + + - + + + diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js index 068247bad8e0..2831e7710058 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js @@ -1,4 +1,4 @@ -// @ts-check +// @ts-nocheck /** * UTIL FUNCTIONS @@ -162,6 +162,7 @@ function invert(color, bw) { * UTIL FUNCTIONS END HERE */ +// @ts-ignore {{ payment_details_js_script }} // @ts-ignore @@ -190,6 +191,21 @@ var hyper = null; function boot() { // @ts-ignore var paymentDetails = window.__PAYMENT_DETAILS; + var orderDetails = paymentDetails.order_details; + if (orderDetails!==null) { + var charges = 0; + + for (var i = 0; i < orderDetails.length; i++) { + charges += parseFloat(orderDetails[i].amount * orderDetails[i].quantity); + } + orderDetails.push({ + "amount": (paymentDetails.amount - charges).toFixed(2), + "product_img_link": "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png", + "product_name": "Miscellaneous charges\n" + + "(includes taxes, shipping, discounts, offers etc.)", + "quantity": null + }); + } if (paymentDetails.merchant_name) { document.title = "Payment requested by " + paymentDetails.merchant_name; @@ -283,6 +299,7 @@ function initializeEventListeners(paymentDetails) { } } + // @ts-ignore window.addEventListener("resize", function (event) { var currentHeight = window.innerHeight; var currentWidth = window.innerWidth; @@ -420,6 +437,7 @@ function mountUnifiedCheckout(id) { * - Handle errors and redirect to status page * @param {Event} e */ +// @ts-ignore function handleSubmit(e) { // @ts-ignore var paymentDetails = window.__PAYMENT_DETAILS; @@ -526,6 +544,7 @@ function formatDate(date) { var hours = date.getHours(); var minutes = date.getMinutes(); + // @ts-ignore minutes = minutes < 10 ? "0" + minutes : minutes; var suffix = hours > 11 ? "PM" : "AM"; hours = hours % 12; @@ -651,6 +670,7 @@ function renderCart(paymentDetails) { item, paymentDetails, index !== 0 && index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE, + // @ts-ignore cartItemsNode ); }); @@ -714,7 +734,7 @@ function renderCartItem( item, paymentDetails, shouldAddDividerNode, - cartItemsNode + cartItemsNode, ) { // Wrappers var itemWrapperNode = document.createElement("div"); @@ -730,20 +750,29 @@ function renderCartItem( productNameNode.className = "hyper-checkout-card-item-name"; productNameNode.innerText = item.product_name; // Product quantity - var quantityNode = document.createElement("div"); - quantityNode.className = "hyper-checkout-card-item-quantity"; - quantityNode.innerText = "Qty: " + item.quantity; + if (item.quantity !== null) { + var quantityNode = document.createElement("div"); + quantityNode.className = "hyper-checkout-card-item-quantity"; + quantityNode.innerText = "Qty: " + item.quantity; + } // Product price var priceNode = document.createElement("div"); priceNode.className = "hyper-checkout-card-item-price"; priceNode.innerText = paymentDetails.currency + " " + item.amount; // Append items - nameAndQuantityWrapperNode.append(productNameNode, quantityNode); + + nameAndQuantityWrapperNode.append(productNameNode); + if (item.quantity !== null) { + // @ts-ignore + nameAndQuantityWrapperNode.append(quantityNode); + } + itemWrapperNode.append( productImageNode, nameAndQuantityWrapperNode, priceNode ); + if (shouldAddDividerNode) { var dividerNode = document.createElement("div"); dividerNode.className = "hyper-checkout-cart-item-divider"; @@ -792,13 +821,16 @@ function handleCartView(paymentDetails) { ); }); } - if (cartItemsNode instanceof HTMLDivElement) { + if (cartItemsNode instanceof HTMLDivElement){ cartItemsNode.style.maxHeight = cartItemsNode.scrollHeight + "px"; + cartItemsNode.style.height = cartItemsNode.scrollHeight + "px"; } - if (cartButtonTextNode instanceof HTMLButtonElement) { + + if (cartButtonTextNode instanceof HTMLSpanElement) { cartButtonTextNode.innerText = "Show Less"; } + var arrowUpImage = document.getElementById("arrow-up"); if ( cartButtonImageNode instanceof Object && @@ -833,7 +865,7 @@ function handleCartView(paymentDetails) { setTimeout(function () { var hiddenItemsCount = orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; - if (cartButtonTextNode instanceof HTMLButtonElement) { + if (cartButtonTextNode instanceof HTMLSpanElement) { cartButtonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; } var arrowDownImage = document.getElementById("arrow-down"); diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index bdbd9b02d9ef..df1fb1e358d4 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -156,6 +156,10 @@ impl PaymentMethodRetrieve for Oss { helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), + card_token + .payment_method_id + .as_ref() + .unwrap_or(&card_token.token), payment_intent, card_token_data, ) @@ -167,6 +171,10 @@ impl PaymentMethodRetrieve for Oss { helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), + card_token + .payment_method_id + .as_ref() + .unwrap_or(&card_token.token), payment_intent, card_token_data, ) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index a92215936a09..ce7f48405e28 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,8 +7,9 @@ use api_models::{ admin::{self, PaymentMethodsEnabled}, enums::{self as api_enums}, payment_methods::{ - BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails, - PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, + BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, + CustomerDefaultPaymentMethodResponse, MaskedBankDetails, PaymentExperienceTypes, + PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, }, @@ -25,6 +26,7 @@ use diesel_models::{ business_profile::BusinessProfile, encryption::Encryption, enums as storage_enums, payment_method, }; +use domain::CustomerUpdate; use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; @@ -128,11 +130,12 @@ pub fn store_default_payment_method( created: Some(common_utils::date_time::now()), recurring_enabled: false, //[#219] installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), }; + (payment_method_response, None) } - #[instrument(skip_all)] pub async fn get_or_insert_payment_method( db: &dyn db::StorageInterface, @@ -348,7 +351,13 @@ pub async fn add_payment_method( None => { let pm_metadata = resp.metadata.as_ref().map(|data| data.peek()); - let locker_id = Some(resp.payment_method_id); + let locker_id = if resp.payment_method == api_enums::PaymentMethod::Card + || resp.payment_method == api_enums::PaymentMethod::BankTransfer + { + Some(resp.payment_method_id) + } else { + None + }; resp.payment_method_id = generate_id(consts::ID_LENGTH, "pm"); insert_payment_method( db, @@ -1174,8 +1183,8 @@ pub async fn list_payment_methods( db, pi.shipping_address_id.clone(), &key_store, - pi.payment_id.clone(), - merchant_account.merchant_id.clone(), + &pi.payment_id, + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await @@ -1191,8 +1200,8 @@ pub async fn list_payment_methods( db, pi.billing_address_id.clone(), &key_store, - pi.payment_id.clone(), - merchant_account.merchant_id.clone(), + &pi.payment_id, + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await @@ -2584,6 +2593,8 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( customer_id: Option<&str>, ) -> errors::RouterResponse { let db = state.store.as_ref(); + let limit = req.clone().and_then(|pml_req| pml_req.limit); + if let Some(customer_id) = customer_id { Box::pin(list_customer_payment_method( &state, @@ -2591,6 +2602,7 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( key_store, None, customer_id, + limit, )) .await } else { @@ -2614,6 +2626,7 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( key_store, payment_intent, &customer_id, + limit, )) .await } @@ -2634,6 +2647,7 @@ pub async fn list_customer_payment_method( key_store: domain::MerchantKeyStore, payment_intent: Option, customer_id: &str, + limit: Option, ) -> errors::RouterResponse { let db = &*state.store; @@ -2645,13 +2659,14 @@ pub async fn list_customer_payment_method( } }; - db.find_customer_by_customer_id_merchant_id( - customer_id, - &merchant_account.merchant_id, - &key_store, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + let customer = db + .find_customer_by_customer_id_merchant_id( + customer_id, + &merchant_account.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let key = key_store.key.get_inner().peek(); @@ -2671,6 +2686,7 @@ pub async fn list_customer_payment_method( customer_id, &merchant_account.merchant_id, common_enums::PaymentMethodStatus::Active, + limit, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; @@ -2758,6 +2774,7 @@ pub async fn list_customer_payment_method( let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), + payment_method_id: pm.payment_method_id.clone(), customer_id: pm.customer_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, @@ -2773,6 +2790,9 @@ pub async fn list_customer_payment_method( bank: bank_details, surcharge_details: None, requires_cvv, + last_used_at: Some(pm.last_used_at), + default_payment_method_set: customer.default_payment_method_id.is_some() + && customer.default_payment_method_id == Some(pm.payment_method_id), }; customer_pms.push(pma.to_owned()); @@ -3059,7 +3079,96 @@ async fn get_bank_account_connector_details( None => Ok(None), } } +pub async fn set_default_payment_method( + state: routes::AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + customer_id: &str, + payment_method_id: String, +) -> errors::RouterResponse { + let db = &*state.store; + //check for the customer + let customer = db + .find_customer_by_customer_id_merchant_id( + customer_id, + &merchant_account.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + // check for the presence of payment_method + let payment_method = db + .find_payment_method(&payment_method_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + utils::when( + payment_method.customer_id != customer_id + && payment_method.merchant_id != merchant_account.merchant_id, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "The payment_method_id is not valid".to_string(), + }) + .into_report() + }, + )?; + + utils::when( + Some(payment_method_id.clone()) == customer.default_payment_method_id, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Payment Method is already set as default".to_string(), + }) + .into_report() + }, + )?; + + let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id: Some(payment_method_id.clone()), + }; + + // update the db with the default payment method id + let updated_customer_details = db + .update_customer_by_customer_id_merchant_id( + customer_id.to_owned(), + merchant_account.merchant_id, + customer_update, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the default payment method id for the customer")?; + let resp = CustomerDefaultPaymentMethodResponse { + default_payment_method_id: updated_customer_details.default_payment_method_id, + customer_id: customer.customer_id, + payment_method_type: payment_method.payment_method_type, + payment_method: payment_method.payment_method, + }; + + Ok(services::ApplicationResponse::Json(resp)) +} + +pub async fn update_last_used_at( + pm_id: &str, + state: &routes::AppState, +) -> errors::RouterResult<()> { + let update_last_used = storage::PaymentMethodUpdate::LastUsedUpdate { + last_used_at: common_utils::date_time::now(), + }; + let payment_method = state + .store + .find_payment_method(pm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + state + .store + .update_payment_method(payment_method, update_last_used) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the last_used_at in db")?; + Ok(()) +} #[cfg(feature = "payouts")] pub async fn get_bank_from_hs_locker( state: &routes::AppState, @@ -3243,9 +3352,10 @@ pub async fn retrieve_payment_method( card, metadata: pm.metadata, created: Some(pm.created_at), - recurring_enabled: false, //[#219] - installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219], + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(pm.last_used_at), }, )) } diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 19ecf733dfb2..35491d747ef7 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -327,7 +327,8 @@ pub fn mk_add_bank_response_hs( created: Some(common_utils::date_time::now()), recurring_enabled: false, // [#256] installment_payment_enabled: false, // #[#256] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), } } @@ -370,7 +371,8 @@ pub fn mk_add_card_response_hs( created: Some(common_utils::date_time::now()), recurring_enabled: false, // [#256] installment_payment_enabled: false, // #[#256] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), // [#256] } } diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 622fed0738ad..af9f94a0bdf1 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1084,8 +1084,8 @@ pub async fn get_delete_tokenize_schedule_time( .await; let mapping = match redis_mapping { Ok(x) => x, - Err(err) => { - logger::info!("Redis Mapping Error: {}", err); + Err(error) => { + logger::info!(?error, "Redis Mapping Error"); process_data::PaymentMethodsPTMapping::default() } }; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index b94b8627ef98..7e8fa2ae44d8 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1070,7 +1070,6 @@ where customer, ) .await?; - *payment_data = pd; // Validating the blocklist guard and generate the fingerprint @@ -1141,7 +1140,6 @@ where let pm_token = router_data .add_payment_method_token(state, &connector, &tokenization_action) .await?; - if let Some(payment_method_token) = pm_token.clone() { router_data.payment_method_token = Some(router_types::PaymentMethodToken::Token( payment_method_token, @@ -1846,7 +1844,7 @@ async fn decide_payment_method_tokenize_action( } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum TokenizationAction { TokenizeInRouter, TokenizeInConnector, @@ -2028,6 +2026,7 @@ pub enum CallConnectorAction { pub struct PaymentAddress { pub shipping: Option, pub billing: Option, + pub payment_method_billing: Option, } #[derive(Clone)] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index ebc0cf3664af..d69f668bf9e7 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -393,7 +393,6 @@ default_imp_for_connector_redirect_response!( connector::Klarna, connector::Multisafepay, connector::Nexinets, - connector::Nmi, connector::Opayo, connector::Opennode, connector::Payeezy, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0baba7035cea..d711bca7ab41 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -387,16 +387,16 @@ pub async fn get_address_by_id( db: &dyn StorageInterface, address_id: Option, merchant_key_store: &domain::MerchantKeyStore, - payment_id: String, - merchant_id: String, + payment_id: &str, + merchant_id: &str, storage_scheme: storage_enums::MerchantStorageScheme, ) -> CustomResult, errors::ApiErrorResponse> { match address_id { None => Ok(None), Some(address_id) => Ok(db .find_address_by_merchant_id_payment_id_address_id( - &merchant_id, - &payment_id, + merchant_id, + payment_id, &address_id, merchant_key_store, storage_scheme, @@ -994,7 +994,7 @@ pub fn verify_mandate_details( #[instrument(skip_all)] pub fn payment_attempt_status_fsm( - payment_method_data: &Option, + payment_method_data: &Option, confirm: Option, ) -> storage_enums::AttemptStatus { match payment_method_data { @@ -1007,7 +1007,7 @@ pub fn payment_attempt_status_fsm( } pub fn payment_intent_status_fsm( - payment_method_data: &Option, + payment_method_data: &Option, confirm: Option, ) -> storage_enums::IntentStatus { match payment_method_data { @@ -1131,6 +1131,7 @@ pub(crate) async fn get_payment_method_create_request( customer_id: Some(customer.customer_id.to_owned()), card_network: None, }; + Ok(payment_method_request) } }, @@ -1402,6 +1403,7 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( modified_at: common_utils::date_time::now(), connector_customer: None, address_id: None, + default_payment_method_id: None, }) } .await @@ -1545,7 +1547,8 @@ pub async fn retrieve_payment_method_with_temporary_token( pub async fn retrieve_card_with_permanent_token( state: &AppState, - token: &str, + locker_id: &str, + payment_method_id: &str, payment_intent: &PaymentIntent, card_token_data: Option<&CardToken>, ) -> RouterResult { @@ -1556,11 +1559,11 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::UnprocessableEntity { message: "no customer id provided for the payment".to_string(), })?; - - let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card information from the permanent locker")?; + let card = + cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, locker_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked // from payment_method_data.card_token object @@ -1593,7 +1596,7 @@ pub async fn retrieve_card_with_permanent_token( card_issuing_country: None, bank_code: None, }; - + cards::update_last_used_at(payment_method_id, state).await?; Ok(api::PaymentMethodData::Card(api_card)) } @@ -1945,7 +1948,7 @@ pub(crate) fn validate_payment_method_fields_present( .map_or(Ok(()), |req_payment_method_data| { req.payment_method.map_or(Ok(()), |req_payment_method| { validate_payment_method_and_payment_method_data( - req_payment_method_data, + req_payment_method_data.payment_method_data, req_payment_method, ) }) @@ -3137,7 +3140,7 @@ impl AttemptType { // In case if fields are not overridden by the request then they contain the same data that was in the previous attempt provided it is populated in this function. #[inline(always)] fn make_new_payment_attempt( - payment_method_data: &Option, + payment_method_data: &Option, old_payment_attempt: PaymentAttempt, new_attempt_count: i16, storage_scheme: enums::MerchantStorageScheme, @@ -3209,6 +3212,8 @@ impl AttemptType { unified_message: None, net_amount: old_payment_attempt.amount, mandate_data: old_payment_attempt.mandate_data, + // New payment method billing address can be passed for a retry + payment_method_billing_address_id: None, fingerprint_id: None, } } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index ce3998506c9e..caca29d8e2aa 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -96,25 +96,32 @@ impl currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.get_total_amount().into(); - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -144,6 +151,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: None, payment_method_data: None, diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index fecf3971019a..097b7ab40e17 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -78,26 +78,32 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -153,6 +159,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: None, payment_method_data: None, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index acf2ce431195..e9d520e4abcb 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -127,26 +127,32 @@ impl amount = payment_attempt.get_total_amount().into(); - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -198,6 +204,9 @@ impl address: payments::PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: None, payment_method_data: None, 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 6bbe1c89a471..f7e7598cf73c 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -120,7 +120,10 @@ impl if should_validate_pm_or_token_given { helpers::validate_pm_or_token_given( &request.payment_method, - &request.payment_method_data, + &request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), &request.payment_method_type, &mandate_type, &token, @@ -146,25 +149,32 @@ impl .or_else(|| request.customer_id.clone()), )?; - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - request.shipping.as_ref(), - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + + let billing_address = helpers::get_address_by_id( db, - request.billing.as_ref(), - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -233,9 +243,15 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 2d2ee0e8728b..5c47c3f323ea 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -96,6 +96,7 @@ impl .in_current_span(), ); + // Parallel calls - level 0 let (mut payment_intent, mandate_details) = tokio::try_join!( utils::flatten_join_error(payment_intent_fut), utils::flatten_join_error(mandate_details_fut) @@ -271,6 +272,7 @@ impl | api_models::enums::IntentStatus::RequiresPaymentMethod | api_models::enums::IntentStatus::RequiresConfirmation => { // Normal payment + // Parallel calls - level 1 let (payment_attempt, shipping_address, billing_address, business_profile, _) = tokio::try_join!( utils::flatten_join_error(payment_attempt_fut), @@ -360,13 +362,21 @@ impl field_name: "browser_info", })?; - helpers::validate_card_data(request.payment_method_data.clone())?; + helpers::validate_card_data( + request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), + )?; let token = token.or_else(|| payment_attempt.payment_token.clone()); helpers::validate_pm_or_token_given( &request.payment_method, - &request.payment_method_data, + &request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), &request.payment_method_type, &mandate_type, &token, @@ -399,8 +409,9 @@ impl .as_ref() .map(|mcd| mcd.creds_identifier.to_owned()); - payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); - payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); + payment_intent.shipping_address_id = + shipping_address.as_ref().map(|i| i.address_id.clone()); + payment_intent.billing_address_id = billing_address.as_ref().map(|i| i.address_id.clone()); payment_intent.return_url = request .return_url .as_ref() @@ -450,21 +461,74 @@ impl sm }); - let additional_pm_data = request + let n_request_payment_method_data = request .payment_method_data .as_ref() - .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, &*state.store).await - }) - .await; + .map(|pmd| pmd.payment_method_data.clone()); + + let store = state.clone().store; + + let additional_pm_data_fut = tokio::spawn(async move { + Ok(n_request_payment_method_data + .async_map(|payment_method_data| async move { + helpers::get_additional_payment_data(&payment_method_data, store.as_ref()).await + }) + .await) + }); + + let store = state.clone().store; + + let n_payment_method_billing_address_id = + payment_attempt.payment_method_billing_address_id.clone(); + let n_request_payment_method_billing_address = request + .payment_method_data + .as_ref() + .and_then(|pmd| pmd.billing.clone()); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let m_key_store = key_store.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_merchant_id = merchant_id.clone(); + + let payment_method_billing_future = tokio::spawn( + async move { + helpers::create_or_update_address_for_payment_by_request( + store.as_ref(), + n_request_payment_method_billing_address.as_ref(), + n_payment_method_billing_address_id.as_deref(), + m_merchant_id.as_str(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, + ) + .await + } + .in_current_span(), + ); + + // Parallel calls - level 2 + let (additional_pm_data, payment_method_billing) = tokio::try_join!( + utils::flatten_join_error(additional_pm_data_fut), + utils::flatten_join_error(payment_method_billing_future), + )?; + let payment_method_data_after_card_bin_call = request .payment_method_data .as_ref() .zip(additional_pm_data) .map(|(payment_method_data, additional_payment_data)| { - payment_method_data.apply_additional_payment_data(additional_payment_data) + payment_method_data + .payment_method_data + .apply_additional_payment_data(additional_payment_data) }); + payment_attempt.payment_method_billing_address_id = payment_method_billing + .as_ref() + .map(|payment_method_billing| payment_method_billing.address_id.clone()); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -479,6 +543,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: request.confirm, payment_method_data: payment_method_data_after_card_bin_call, @@ -705,9 +772,13 @@ impl let business_sub_label = payment_data.payment_attempt.business_sub_label.clone(); let authentication_type = payment_data.payment_attempt.authentication_type; - let (shipping_address, billing_address) = ( + let (shipping_address_id, billing_address_id, payment_method_billing_address_id) = ( payment_data.payment_intent.shipping_address_id.clone(), payment_data.payment_intent.billing_address_id.clone(), + payment_data + .payment_attempt + .payment_method_billing_address_id + .clone(), ); let customer_id = customer.clone().map(|c| c.customer_id); @@ -775,6 +846,7 @@ impl merchant_connector_id, surcharge_amount, tax_amount, + payment_method_billing_address_id, fingerprint_id: m_fingerprint_id, }, storage_scheme, @@ -787,8 +859,8 @@ impl let m_payment_data_payment_intent = payment_data.payment_intent.clone(); let m_customer_id = customer_id.clone(); - let m_shipping_address_id = shipping_address.clone(); - let m_billing_address_id = billing_address.clone(); + let m_shipping_address_id = shipping_address_id.clone(); + let m_billing_address_id = billing_address_id.clone(); let m_return_url = return_url.clone(); let m_business_label = business_label.clone(); let m_description = description.clone(); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2eaa11f44ee8..11ab21b891d3 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -8,7 +8,7 @@ use data_models::{ payments::payment_attempt::PaymentAttempt, }; use diesel_models::ephemeral_key; -use error_stack::{self, report, ResultExt}; +use error_stack::{self, ResultExt}; use masking::PeekInterface; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -146,6 +146,22 @@ impl ) .await?; + let payment_method_billing_address = + helpers::create_or_find_address_for_payment_by_request( + db, + request + .payment_method_data + .as_ref() + .and_then(|pmd| pmd.billing.as_ref()), + None, + merchant_id, + customer_details.customer_id.as_ref(), + merchant_key_store, + &payment_id, + merchant_account.storage_scheme, + ) + .await?; + let browser_info = request .browser_info .clone() @@ -216,9 +232,13 @@ impl merchant_account, money, request, - shipping_address.clone().map(|x| x.address_id), + shipping_address + .as_ref() + .map(|address| address.address_id.clone()), payment_link_data.clone(), - billing_address.clone().map(|x| x.address_id), + billing_address + .as_ref() + .map(|address| address.address_id.clone()), attempt_id, profile_id, session_expiry, @@ -234,6 +254,9 @@ impl request, browser_info, state, + payment_method_billing_address + .as_ref() + .map(|address| address.address_id.clone()), ) .await?; @@ -341,7 +364,9 @@ impl .as_ref() .zip(additional_payment_data) .map(|(payment_method_data, additional_payment_data)| { - payment_method_data.apply_additional_payment_data(additional_payment_data) + payment_method_data + .payment_method_data + .apply_additional_payment_data(additional_payment_data) }); let amount = payment_attempt.get_total_amount().into(); let payment_data = PaymentData { @@ -358,6 +383,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing_address + .as_ref() + .map(|address| address.into()), }, confirm: request.confirm, payment_method_data: payment_method_data_after_card_bin_call, @@ -624,28 +652,25 @@ impl ValidateRequest, state: &AppState, + payment_method_billing_address_id: Option, ) -> RouterResult<( storage::PaymentAttemptNew, Option, @@ -702,7 +728,11 @@ impl PaymentCreate { .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, &*state.store).await + helpers::get_additional_payment_data( + &payment_method_data.payment_method_data, + &*state.store, + ) + .await }) .await; let additional_pm_data_value = additional_pm_data @@ -776,6 +806,7 @@ impl PaymentCreate { .as_ref() .and_then(|inner| inner.mandate_type.clone().map(Into::into)), mandate_data, + payment_method_billing_address_id, ..storage::PaymentAttemptNew::default() }, additional_pm_data, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 02d2e953c19c..281ffdd06f7e 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -77,26 +77,32 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -141,6 +147,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: None, payment_method_data: None, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 099eae71800b..33e8ff26edc0 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -483,6 +483,13 @@ async fn payment_response_update_tracker( 200..=299 => storage::enums::AttemptStatus::Failure, _ => router_data.status, } + } else if flow_name == "Capture" { + match err.status_code { + 500..=511 => storage::enums::AttemptStatus::Pending, + // don't update the status for 429 error status + 429 => router_data.status, + _ => storage::enums::AttemptStatus::Failure, + } } else { match err.status_code { 500..=511 => storage::enums::AttemptStatus::Pending, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 7db174313d92..966600a5498f 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -85,26 +85,32 @@ impl let amount = payment_attempt.get_total_amount().into(); - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), + payment_intent.shipping_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -166,6 +172,9 @@ impl address: payments::PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: None, payment_method_data: None, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 79130b211a78..2ca413b73aa2 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -40,7 +40,7 @@ impl _request: &api::PaymentsStartRequest, _mandate_type: Option, merchant_account: &domain::MerchantAccount, - mechant_key_store: &domain::MerchantKeyStore, + key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _payment_confirm_source: Option, ) -> RouterResult> { @@ -84,25 +84,32 @@ impl currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.get_total_amount().into(); - let shipping_address = helpers::create_or_find_address_for_payment_by_request( + let shipping_address = helpers::get_address_by_id( db, - None, - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent.customer_id.as_ref(), - mechant_key_store, + payment_intent.shipping_address_id.clone(), + key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; - let billing_address = helpers::create_or_find_address_for_payment_by_request( + + let billing_address = helpers::get_address_by_id( db, - None, - payment_intent.billing_address_id.as_deref(), + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, merchant_id, - payment_intent.customer_id.as_ref(), - mechant_key_store, + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), + key_store, &payment_intent.payment_id, + merchant_id, merchant_account.storage_scheme, ) .await?; @@ -142,6 +149,9 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: Some(payment_attempt.confirm), payment_attempt, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 81dfb7127db3..aa622b3558b3 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -238,7 +238,7 @@ async fn get_tracker_for_sync< operation: Op, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { - 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, @@ -259,8 +259,8 @@ async fn get_tracker_for_sync< db, payment_intent.shipping_address_id.clone(), mechant_key_store, - payment_intent.payment_id.clone(), - merchant_account.merchant_id.clone(), + &payment_intent.payment_id.clone(), + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await?; @@ -268,12 +268,24 @@ async fn get_tracker_for_sync< db, payment_intent.billing_address_id.clone(), mechant_key_store, - payment_intent.payment_id.clone(), - merchant_account.merchant_id.clone(), + &payment_intent.payment_id.clone(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await?; + + let payment_method_billing = helpers::get_address_by_id( + db, + payment_attempt.payment_method_billing_address_id.clone(), + mechant_key_store, + &payment_intent.payment_id.clone(), + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await?; + payment_attempt.encoded_data = request.param.clone(); + let attempts = match request.expand_attempts { Some(true) => { Some(db @@ -406,6 +418,9 @@ async fn get_tracker_for_sync< address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing + .as_ref() + .map(|address| address.into()), }, confirm: Some(request.force_sync), payment_method_data: None, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 071d919eb191..cd73df26ff35 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -75,7 +75,12 @@ impl helpers::validate_customer_access(&payment_intent, auth_flow, request)?; - helpers::validate_card_data(request.payment_method_data.clone())?; + helpers::validate_card_data( + request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), + )?; helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, @@ -193,6 +198,24 @@ impl ) .await?; + let payment_method_billing = helpers::create_or_update_address_for_payment_by_request( + db, + request + .payment_method_data + .as_ref() + .and_then(|pmd| pmd.billing.as_ref()), + payment_attempt.payment_method_billing_address_id.as_deref(), + merchant_id, + payment_intent + .customer_id + .as_ref() + .or(customer_details.customer_id.as_ref()), + key_store, + &payment_intent.payment_id, + merchant_account.storage_scheme, + ) + .await?; + 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); @@ -221,7 +244,10 @@ impl if request.confirm.unwrap_or(false) { helpers::validate_pm_or_token_given( &request.payment_method, - &request.payment_method_data, + &request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), &request.payment_method_type, &mandate_type, &token, @@ -356,9 +382,13 @@ impl address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), + payment_method_billing: payment_method_billing.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: request + .payment_method_data + .as_ref() + .map(|pmd| pmd.payment_method_data.clone()), force_sync: None, refunds: vec![], disputes: vec![], @@ -683,17 +713,6 @@ impl ValidateRequest address: PaymentAddress { billing: None, shipping: None, + payment_method_billing: None, }, confirm: None, payment_method_data: None, diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index d08f0208500c..1f793e2e1855 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1,4 +1,5 @@ use api_models::payment_methods::PaymentMethodsData; +use common_enums::PaymentMethod; use common_utils::{ext_traits::ValueExt, pii}; use error_stack::{report, ResultExt}; use masking::ExposeInterface; @@ -342,7 +343,11 @@ where None => { let pm_metadata = create_payment_method_metadata(None, connector_token)?; - locker_id = Some(resp.payment_method_id); + locker_id = if resp.payment_method == PaymentMethod::Card { + Some(resp.payment_method_id) + } else { + None + }; resp.payment_method_id = generate_id(consts::ID_LENGTH, "pm"); payment_methods::cards::create_payment_method( db, @@ -426,6 +431,7 @@ async fn skip_saving_card_in_locker( metadata: None, created: Some(common_utils::date_time::now()), bank_transfer: None, + last_used_at: Some(common_utils::date_time::now()), }; Ok((pm_resp, None)) @@ -445,6 +451,7 @@ async fn skip_saving_card_in_locker( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), bank_transfer: None, + last_used_at: Some(common_utils::date_time::now()), }; Ok((payment_method_response, None)) } @@ -491,6 +498,7 @@ pub async fn save_in_locker( recurring_enabled: false, //[#219] installment_payment_enabled: false, //[#219] payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + last_used_at: Some(common_utils::date_time::now()), }; Ok((payment_method_response, None)) } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6cf547f668ff..7c7bac80cf02 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -452,9 +452,16 @@ where let merchant_decision = payment_intent.merchant_decision.to_owned(); let frm_message = payment_data.frm_message.map(FrmMessage::foreign_from); - let payment_method_data_response = + let payment_method_data = additional_payment_method_data.map(api::PaymentMethodDataResponse::from); + let payment_method_data_response = payment_method_data.map(|payment_method_data| { + api_models::payments::PaymentMethodDataResponseWithBilling { + payment_method_data, + billing: payment_data.address.payment_method_billing, + } + }); + let mut headers = connector_http_status_code .map(|status_code| { vec![( diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index e7425e364566..b2736b6356ff 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -1,6 +1,10 @@ pub mod helpers; +#[cfg(feature = "payout_retry")] +pub mod retry; pub mod validator; +use std::vec::IntoIter; + use api_models::enums as api_enums; use common_utils::{crypto::Encryptable, ext_traits::ValueExt, pii}; use diesel_models::enums as storage_enums; @@ -42,8 +46,18 @@ pub struct PayoutData { } // ********************************************** CORE FLOWS ********************************************** +pub fn get_next_connector( + connectors: &mut IntoIter, +) -> RouterResult { + connectors + .next() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Connector not found in connectors iterator") +} + #[cfg(feature = "payouts")] -pub async fn get_connector_data( +pub async fn get_connector_choice( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, @@ -51,7 +65,7 @@ pub async fn get_connector_data( routing_algorithm: Option, payout_data: &mut PayoutData, eligible_connectors: Option>, -) -> RouterResult { +) -> RouterResult { let eligible_routable_connectors = eligible_connectors.map(|connectors| { connectors .into_iter() @@ -59,7 +73,7 @@ pub async fn get_connector_data( .collect() }); let connector_choice = helpers::get_default_payout_connector(state, routing_algorithm).await?; - let connector_details = match connector_choice { + match connector_choice { api::ConnectorChoice::SessionMultiple(_) => { Err(errors::ApiErrorResponse::InternalServerError) .into_report() @@ -94,7 +108,7 @@ pub async fn get_connector_data( payout_data, eligible_routable_connectors, ) - .await? + .await } api::ConnectorChoice::Decide => { @@ -119,39 +133,75 @@ pub async fn get_connector_data( payout_data, eligible_routable_connectors, ) - .await? - } - }; - let connector_data = match connector_details { - api::ConnectorCallType::SessionMultiple(_) => { - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("Invalid connector details - SessionMultiple")? + .await } - api::ConnectorCallType::PreDetermined(connector) => connector, + } +} +#[cfg(feature = "payouts")] +#[instrument(skip_all)] +pub async fn make_connector_decision( + state: &AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + req: &payouts::PayoutCreateRequest, + connector_call_type: api::ConnectorCallType, + mut payout_data: PayoutData, +) -> RouterResult { + match connector_call_type { + api::ConnectorCallType::PreDetermined(connector_data) => { + call_connector_payout( + state, + merchant_account, + key_store, + req, + &connector_data, + &mut payout_data, + ) + .await + } api::ConnectorCallType::Retryable(connectors) => { let mut connectors = connectors.into_iter(); - payments::get_connector_data(&mut connectors)? - } - }; - // Update connector in DB - payout_data.payout_attempt.connector = Some(connector_data.connector_name.to_string()); - let updated_payout_attempt = storage::PayoutAttemptUpdate::UpdateRouting { - connector: connector_data.connector_name.to_string(), - routing_info: payout_data.payout_attempt.routing_info.clone(), - }; - let db = &*state.store; - db.update_payout_attempt_by_merchant_id_payout_id( - &payout_data.payout_attempt.merchant_id, - &payout_data.payout_attempt.payout_id, - updated_payout_attempt, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error updating routing info in payout_attempt")?; - Ok(connector_data) + let connector_data = get_next_connector(&mut connectors)?; + + payout_data = call_connector_payout( + state, + merchant_account, + key_store, + req, + &connector_data, + &mut payout_data, + ) + .await?; + + #[cfg(feature = "payout_retry")] + { + use crate::core::payouts::retry::{self, GsmValidation}; + let config_bool = retry::config_should_call_gsm_payout( + &*state.store, + &merchant_account.merchant_id, + ) + .await; + + if config_bool && payout_data.should_call_gsm() { + payout_data = retry::do_gsm_actions( + state, + connectors, + connector_data, + payout_data, + merchant_account, + key_store, + req, + ) + .await?; + } + } + + Ok(payout_data) + } + _ => Err(errors::ApiErrorResponse::InternalServerError)?, + } } #[cfg(feature = "payouts")] @@ -178,8 +228,7 @@ pub async fn payouts_create_core( ) .await?; - // Form connector data - let connector_data = get_connector_data( + let connector_call_type = get_connector_choice( &state, &merchant_account, &key_store, @@ -190,13 +239,21 @@ pub async fn payouts_create_core( ) .await?; - call_connector_payout( + payout_data = make_connector_decision( &state, &merchant_account, &key_store, &req, - &connector_data, - &mut payout_data, + connector_call_type, + payout_data, + ) + .await?; + + response_handler( + &state, + &merchant_account, + &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), + &payout_data, ) .await } @@ -246,6 +303,8 @@ pub async fn payouts_update_core( let db = &*state.store; let payout_id = req.payout_id.clone().get_required_value("payout_id")?; + let payout_attempt_id = + utils::get_payment_attempt_id(payout_id.to_owned(), payouts.attempt_count); let merchant_id = &merchant_account.merchant_id; payout_data.payouts = db .update_payout_by_merchant_id_payout_id(merchant_id, &payout_id, updated_payouts) @@ -279,9 +338,9 @@ pub async fn payouts_update_core( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - &payout_id, + &payout_attempt_id, update_payout_attempt, ) .await @@ -290,10 +349,53 @@ pub async fn payouts_update_core( } } - let connector_data = match &payout_attempt.connector { - // Evaluate and fetch connector data - None => { - get_connector_data( + payout_data = match ( + req.connector.clone(), + payout_data.payout_attempt.connector.clone(), + ) { + // if the connector is not updated but was provided during payout create + (None, Some(connector)) => { + let connector_data = api::ConnectorData::get_payout_connector_by_name( + &state.conf.connectors, + connector.as_str(), + api::GetToken::Connector, + payout_attempt.merchant_connector_id.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get the connector data")?; + + call_connector_payout( + &state, + &merchant_account, + &key_store, + &req, + &connector_data, + &mut payout_data, + ) + .await? + } + // if the connector is updated or not present both in create and update call + _ => { + payout_data.payout_attempt.connector = None; + payout_data.payout_attempt.routing_info = None; + + //fetch payout_method_data + payout_data.payout_method_data = Some( + helpers::make_payout_method_data( + &state, + req.payout_method_data.as_ref(), + payout_data.payout_attempt.payout_token.as_deref(), + &payout_data.payout_attempt.customer_id, + &payout_data.payout_attempt.merchant_id, + &payout_data.payout_attempt.payout_id, + Some(&payouts.payout_type), + &key_store, + ) + .await? + .get_required_value("payout_method_data")?, + ); + + let connector_call_type = get_connector_choice( &state, &merchant_account, &key_store, @@ -302,27 +404,25 @@ pub async fn payouts_update_core( &mut payout_data, req.connector.clone(), ) + .await?; + + make_connector_decision( + &state, + &merchant_account, + &key_store, + &req, + connector_call_type, + payout_data, + ) .await? } - - // Use existing connector - Some(connector) => api::ConnectorData::get_payout_connector_by_name( - &state.conf.connectors, - connector, - api::GetToken::Connector, - payout_attempt.merchant_connector_id.clone(), - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get the connector data")?, }; - call_connector_payout( + response_handler( &state, &merchant_account, - &key_store, - &req, - &connector_data, - &mut payout_data, + &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), + &payout_data, ) .await } @@ -548,9 +648,30 @@ pub async fn call_connector_payout( req: &payouts::PayoutCreateRequest, connector_data: &api::ConnectorData, payout_data: &mut PayoutData, -) -> RouterResponse { +) -> RouterResult { let payout_attempt = &payout_data.payout_attempt.to_owned(); let payouts: &diesel_models::payouts::Payouts = &payout_data.payouts.to_owned(); + + // update connector_name + if payout_data.payout_attempt.connector.is_none() + || payout_data.payout_attempt.connector != Some(connector_data.connector_name.to_string()) + { + payout_data.payout_attempt.connector = Some(connector_data.connector_name.to_string()); + let updated_payout_attempt = storage::PayoutAttemptUpdate::UpdateRouting { + connector: connector_data.connector_name.to_string(), + routing_info: payout_data.payout_attempt.routing_info.clone(), + }; + let db = &*state.store; + db.update_payout_attempt_by_merchant_id_payout_attempt_id( + &payout_data.payout_attempt.merchant_id, + &payout_data.payout_attempt.payout_attempt_id, + updated_payout_attempt, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating routing info in payout_attempt")?; + }; + // Fetch / store payout_method_data if payout_data.payout_method_data.is_none() || payout_attempt.payout_token.is_none() { payout_data.payout_method_data = Some( @@ -568,6 +689,7 @@ pub async fn call_connector_payout( .get_required_value("payout_method_data")?, ); } + if let Some(true) = req.confirm { // Eligibility flow if payouts.payout_type == storage_enums::PayoutType::Card @@ -659,13 +781,7 @@ pub async fn call_connector_payout( .attach_printable("Payout fulfillment failed for given Payout request")?; } - response_handler( - state, - merchant_account, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), - payout_data, - ) - .await + Ok(payout_data.to_owned()) } #[cfg(feature = "payouts")] @@ -796,6 +912,9 @@ pub async fn check_payout_eligibility( let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let payout_id = &payout_data.payouts.payout_id; + let payout_attempt_id = + &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); + match router_data_resp.response { Ok(payout_response_data) => { let payout_attempt = &payout_data.payout_attempt; @@ -812,9 +931,9 @@ pub async fn check_payout_eligibility( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payout_attempt, ) .await @@ -839,9 +958,9 @@ pub async fn check_payout_eligibility( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payout_attempt, ) .await @@ -902,6 +1021,9 @@ pub async fn create_payout( let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let payout_id = &payout_data.payouts.payout_id; + let payout_attempt_id = + &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); + match router_data_resp.response { Ok(payout_response_data) => { let payout_attempt = &payout_data.payout_attempt; @@ -918,9 +1040,9 @@ pub async fn create_payout( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payout_attempt, ) .await @@ -945,9 +1067,9 @@ pub async fn create_payout( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payout_attempt, ) .await @@ -1095,6 +1217,9 @@ pub async fn fulfill_payout( let merchant_id = &merchant_account.merchant_id; let payout_attempt = &payout_data.payout_attempt; let payout_id = &payout_attempt.payout_id; + let payout_attempt_id = + &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); + match router_data_resp.response { Ok(payout_response_data) => { if payout_data.payouts.recurring && payout_data.payouts.payout_method_id.is_none() { @@ -1122,9 +1247,9 @@ pub async fn fulfill_payout( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payouts, ) .await @@ -1148,9 +1273,9 @@ pub async fn fulfill_payout( last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( + .update_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - payout_id, + payout_attempt_id, updated_payouts, ) .await @@ -1324,6 +1449,7 @@ pub async fn payout_create_db_entries( .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) .set_payout_method_id(payout_method_id) + .set_attempt_count(1) .to_owned(); let payouts = db .insert_payout(payouts_req) @@ -1407,8 +1533,10 @@ pub async fn make_payout_data( .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; + let payout_attempt_id = utils::get_payment_attempt_id(payout_id, payouts.attempt_count); + let payout_attempt = db - .find_payout_attempt_by_merchant_id_payout_id(merchant_id, &payout_id) + .find_payout_attempt_by_merchant_id_payout_attempt_id(merchant_id, &payout_attempt_id) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 7b5ec096248d..fcd2569ea995 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -6,6 +6,7 @@ use common_utils::{ use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; +use router_env::logger; use super::PayoutData; use crate::{ @@ -24,7 +25,7 @@ use crate::{ utils as core_utils, }, db::StorageInterface, - routes::AppState, + routes::{metrics, AppState}, services, types::{ api::{self, enums as api_enums}, @@ -432,6 +433,7 @@ pub async fn get_or_create_customer_details( created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), address_id: None, + default_payment_method_id: None, }; Ok(Some( @@ -642,6 +644,49 @@ pub fn should_call_payout_connector_create_customer<'a>( } } +pub async fn get_gsm_record( + state: &AppState, + error_code: Option, + error_message: Option, + connector_name: Option, + flow: String, +) -> Option { + let get_gsm = || async { + state.store.find_gsm_rule( + connector_name.clone().unwrap_or_default(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name.unwrap_or_default(), + flow, + error_code, + error_message + ); + metrics::AUTO_PAYOUT_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_PAYOUT_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} + pub fn is_payout_initiated(status: api_enums::PayoutStatus) -> bool { matches!( status, diff --git a/crates/router/src/core/payouts/retry.rs b/crates/router/src/core/payouts/retry.rs new file mode 100644 index 000000000000..8965ac9c2658 --- /dev/null +++ b/crates/router/src/core/payouts/retry.rs @@ -0,0 +1,273 @@ +use std::{str::FromStr, vec::IntoIter}; + +use api_models::payouts::PayoutCreateRequest; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use super::{call_connector_payout, PayoutData}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payouts, + }, + db::StorageInterface, + routes::{self, app, metrics}, + types::{api, domain, storage}, + utils, +}; + +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn do_gsm_actions( + state: &app::AppState, + mut connectors: IntoIter, + original_connector_data: api::ConnectorData, + mut payout_data: PayoutData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + req: &PayoutCreateRequest, +) -> RouterResult { + let mut retries = None; + + metrics::AUTO_PAYOUT_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); + + let mut connector = original_connector_data; + + loop { + let gsm = get_gsm(state, &connector, &payout_data).await?; + + match get_gsm_decision(gsm) { + api_models::gsm::GsmDecision::Retry => { + retries = get_retries(state, retries, &merchant_account.merchant_id).await; + + if retries.is_none() || retries == Some(0) { + metrics::AUTO_PAYOUT_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + logger::info!("retries exhausted for auto_retry payout"); + break; + } + + if connectors.len() == 0 { + logger::info!("connectors exhausted for auto_retry payout"); + metrics::AUTO_PAYOUT_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + break; + } + + connector = super::get_next_connector(&mut connectors)?; + + payout_data = do_retry( + &state.clone(), + connector.to_owned(), + merchant_account, + key_store, + payout_data, + req, + ) + .await?; + + retries = retries.map(|i| i - 1); + } + api_models::gsm::GsmDecision::Requeue => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + "Requeue not implemented".to_string(), + ), + }) + .into_report()? + } + api_models::gsm::GsmDecision::DoDefault => break, + } + } + Ok(payout_data) +} + +#[instrument(skip_all)] +pub async fn get_retries( + state: &app::AppState, + retries: Option, + merchant_id: &str, +) -> Option { + match retries { + Some(retries) => Some(retries), + None => { + let key = format!("max_auto_payout_retries_enabled_{merchant_id}"); + let db = &*state.store; + db.find_config_by_key(key.as_str()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|retries_config| { + retries_config + .config + .parse::() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Retries config parsing failed") + }) + .map_err(|err| { + logger::error!(retries_error=?err); + None:: + }) + .ok() + } + } +} + +#[instrument(skip_all)] +pub async fn get_gsm( + state: &app::AppState, + original_connector_data: &api::ConnectorData, + payout_data: &PayoutData, +) -> RouterResult> { + let error_code = payout_data.payout_attempt.error_code.to_owned(); + let error_message = payout_data.payout_attempt.error_message.to_owned(); + let connector_name = Some(original_connector_data.connector_name.to_string()); + let flow = "payout_flow".to_string(); + + Ok( + payouts::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) + .await, + ) +} + +#[instrument(skip_all)] +pub fn get_gsm_decision( + option_gsm: Option, +) -> api_models::gsm::GsmDecision { + let option_gsm_decision = option_gsm + .and_then(|gsm| { + api_models::gsm::GsmDecision::from_str(gsm.decision.as_str()) + .into_report() + .map_err(|err| { + let api_error = err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("gsm decision parsing failed"); + logger::warn!(get_gsm_decision_parse_error=?api_error, "error fetching gsm decision"); + api_error + }) + .ok() + }); + + if option_gsm_decision.is_some() { + metrics::AUTO_PAYOUT_RETRY_GSM_MATCH_COUNT.add(&metrics::CONTEXT, 1, &[]); + } + option_gsm_decision.unwrap_or_default() +} + +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn do_retry( + state: &routes::AppState, + connector: api::ConnectorData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + mut payout_data: PayoutData, + req: &PayoutCreateRequest, +) -> RouterResult { + metrics::AUTO_RETRY_PAYOUT_COUNT.add(&metrics::CONTEXT, 1, &[]); + + modify_trackers(state, &connector, merchant_account, &mut payout_data).await?; + + call_connector_payout( + state, + merchant_account, + key_store, + req, + &connector, + &mut payout_data, + ) + .await +} + +#[instrument(skip_all)] +pub async fn modify_trackers( + state: &routes::AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + payout_data: &mut PayoutData, +) -> RouterResult<()> { + let new_attempt_count = payout_data.payouts.attempt_count + 1; + + let db = &*state.store; + + // update payout table's attempt count + let payouts = payout_data.payouts.to_owned(); + let updated_payouts = storage::PayoutsUpdate::AttemptCountUpdate { + attempt_count: new_attempt_count, + }; + + let payout_id = payouts.payout_id.clone(); + let merchant_id = &merchant_account.merchant_id; + payout_data.payouts = db + .update_payout_by_merchant_id_payout_id(merchant_id, &payout_id.to_owned(), updated_payouts) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payouts")?; + + let payout_attempt_id = + utils::get_payment_attempt_id(payout_id.to_owned(), payout_data.payouts.attempt_count); + + let new_payout_attempt_req = storage::PayoutAttemptNew::default() + .set_payout_attempt_id(payout_attempt_id.to_string()) + .set_payout_id(payout_id.to_owned()) + .set_customer_id(payout_data.payout_attempt.customer_id.to_owned()) + .set_connector(Some(connector.connector_name.to_string())) + .set_merchant_id(payout_data.payout_attempt.merchant_id.to_owned()) + .set_address_id(payout_data.payout_attempt.address_id.to_owned()) + .set_business_country(payout_data.payout_attempt.business_country.to_owned()) + .set_business_label(payout_data.payout_attempt.business_label.to_owned()) + .set_payout_token(payout_data.payout_attempt.payout_token.to_owned()) + .set_created_at(Some(common_utils::date_time::now())) + .set_last_modified_at(Some(common_utils::date_time::now())) + .set_profile_id(Some(payout_data.payout_attempt.profile_id.to_string())) + .to_owned(); + payout_data.payout_attempt = db + .insert_payout_attempt(new_payout_attempt_req) + .await + .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { payout_id }) + .attach_printable("Error inserting payouts in db")?; + + payout_data.merchant_connector_account = None; + + Ok(()) +} + +pub async fn config_should_call_gsm_payout( + db: &dyn StorageInterface, + merchant_id: &String, +) -> bool { + let config = db + .find_config_by_key_unwrap_or( + format!("should_call_gsm_payout_{}", merchant_id).as_str(), + Some("false".to_string()), + ) + .await; + match config { + Ok(conf) => conf.config == "true", + Err(err) => { + logger::error!("{err}"); + false + } + } +} + +pub trait GsmValidation { + // TODO : move this function to appropriate place later. + fn should_call_gsm(&self) -> bool; +} + +impl GsmValidation for PayoutData { + #[inline(always)] + fn should_call_gsm(&self) -> bool { + match self.payout_attempt.status { + common_enums::PayoutStatus::Success + | common_enums::PayoutStatus::Cancelled + | common_enums::PayoutStatus::Pending + | common_enums::PayoutStatus::Ineligible + | common_enums::PayoutStatus::RequiresCreation + | common_enums::PayoutStatus::RequiresPayoutMethodData + | common_enums::PayoutStatus::RequiresFulfillment => false, + common_enums::PayoutStatus::Failed => true, + } + } +} diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 982ed9cae94f..21ba27eac179 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -112,8 +112,8 @@ pub async fn create_link_token( &*state.store, pi.billing_address_id.clone(), &key_store, - pi.payment_id.clone(), - merchant_account.merchant_id.clone(), + &pi.payment_id, + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await @@ -297,6 +297,7 @@ async fn store_bank_details_in_payment_methods( .find_payment_method_by_customer_id_merchant_id_list( &customer_id, &merchant_account.merchant_id, + None, ) .await .change_context(ApiErrorResponse::InternalServerError)?; @@ -677,8 +678,8 @@ pub async fn retrieve_payment_method_from_auth_service( &*state.store, payment_intent.billing_address_id.clone(), key_store, - payment_intent.payment_id.clone(), - merchant_account.merchant_id.clone(), + &payment_intent.payment_id, + &merchant_account.merchant_id, merchant_account.storage_scheme, ) .await?; diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 06030262fbc5..80a0c285e41c 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -1167,34 +1167,3 @@ pub async fn get_refund_sync_process_schedule_time( Ok(process_tracker_utils::get_time_from_delta(time_delta)) } - -pub async fn retry_refund_sync_task( - db: &dyn db::StorageInterface, - connector: String, - merchant_id: String, - pt: storage::ProcessTracker, -) -> Result<(), errors::ProcessTrackerError> { - let schedule_time = - get_refund_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?; - - match schedule_time { - Some(s_time) => { - let retry_schedule = db - .as_scheduler() - .retry_process(pt, s_time) - .await - .map_err(Into::into); - metrics::TASKS_RESET_COUNT.add( - &metrics::CONTEXT, - 1, - &[metrics::request::add_attributes("flow", "Refund")], - ); - retry_schedule - } - None => db - .as_scheduler() - .finish_process_with_business_status(pt, "RETRIES_EXCEEDED".to_string()) - .await - .map_err(Into::into), - } -} diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index cc31798bd8b5..6aa226cd933c 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -1,7 +1,4 @@ -use api_models::user_role::{ - role::{self as role_api}, - Permission, -}; +use api_models::user_role::role::{self as role_api}; use common_enums::RoleScope; use common_utils::generate_id_with_default_len; use diesel_models::role::{RoleNew, RoleUpdate}; @@ -20,10 +17,10 @@ use crate::{ utils, }; -pub async fn get_role_from_token( +pub async fn get_role_from_token_with_permissions( state: AppState, user_from_token: UserFromToken, -) -> UserResponse> { +) -> UserResponse { let role_info = user_from_token .get_role_info_from_db(&state) .await @@ -35,21 +32,43 @@ pub async fn get_role_from_token( .map(Into::into) .collect(); - Ok(ApplicationResponse::Json(permissions)) + Ok(ApplicationResponse::Json( + role_api::GetRoleFromTokenResponse::Permissions(permissions), + )) +} + +pub async fn get_role_from_token_with_groups( + state: AppState, + user_from_token: UserFromToken, +) -> UserResponse { + let role_info = user_from_token + .get_role_info_from_db(&state) + .await + .attach_printable("Invalid role_id in JWT")?; + + let permissions = role_info.get_permission_groups().to_vec(); + + Ok(ApplicationResponse::Json( + role_api::GetRoleFromTokenResponse::Groups(permissions), + )) } pub async fn create_role( state: AppState, user_from_token: UserFromToken, req: role_api::CreateRoleRequest, -) -> UserResponse<()> { +) -> UserResponse { let now = common_utils::date_time::now(); - let role_name = RoleName::new(req.role_name)?.get_role_name(); + let role_name = RoleName::new(req.role_name)?; - if req.groups.is_empty() { - return Err(UserErrors::InvalidRoleOperation.into()) - .attach_printable("Role groups cannot be empty"); - } + utils::user_role::validate_role_groups(&req.groups)?; + utils::user_role::validate_role_name( + &state, + &role_name, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await?; if matches!(req.role_scope, RoleScope::Organization) && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN @@ -58,19 +77,11 @@ pub async fn create_role( .attach_printable("Non org admin user creating org level role"); } - utils::user_role::is_role_name_already_present_for_merchant( - &state, - &role_name, - &user_from_token.merchant_id, - &user_from_token.org_id, - ) - .await?; - - state + let role = state .store .insert_role(RoleNew { role_id: generate_id_with_default_len("role"), - role_name, + role_name: role_name.get_role_name(), merchant_id: user_from_token.merchant_id, org_id: user_from_token.org_id, groups: req.groups, @@ -83,7 +94,14 @@ pub async fn create_role( .await .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; - Ok(ApplicationResponse::StatusOk) + Ok(ApplicationResponse::Json( + role_api::RoleInfoWithGroupsResponse { + groups: role.groups, + role_id: role.role_id, + role_name: role.role_name, + role_scope: role.scope, + }, + )) } // TODO: To be deprecated once groups are stable @@ -245,15 +263,11 @@ pub async fn update_role( user_from_token: UserFromToken, req: role_api::UpdateRoleRequest, role_id: &str, -) -> UserResponse<()> { - let role_name = req - .role_name - .map(RoleName::new) - .transpose()? - .map(RoleName::get_role_name); +) -> UserResponse { + let role_name = req.role_name.map(RoleName::new).transpose()?; if let Some(ref role_name) = role_name { - utils::user_role::is_role_name_already_present_for_merchant( + utils::user_role::validate_role_name( &state, role_name, &user_from_token.merchant_id, @@ -262,6 +276,10 @@ pub async fn update_role( .await?; } + if let Some(ref groups) = req.groups { + utils::user_role::validate_role_groups(groups)?; + } + let role_info = roles::RoleInfo::from_role_id( &state, role_id, @@ -275,23 +293,16 @@ pub async fn update_role( && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN { return Err(UserErrors::InvalidRoleOperation.into()) - .attach_printable("Non org admin user creating org level role"); + .attach_printable("Non org admin user changing org level role"); } - if let Some(ref groups) = req.groups { - if groups.is_empty() { - return Err(UserErrors::InvalidRoleOperation.into()) - .attach_printable("role groups cannot be empty"); - } - } - - state + let updated_role = state .store .update_role_by_role_id( role_id, RoleUpdate::UpdateDetails { groups: req.groups, - role_name, + role_name: role_name.map(RoleName::get_role_name), last_modified_at: common_utils::date_time::now(), last_modified_by: user_from_token.user_id, }, @@ -301,5 +312,12 @@ pub async fn update_role( blacklist::insert_role_in_blacklist(&state, role_id).await?; - Ok(ApplicationResponse::StatusOk) + Ok(ApplicationResponse::Json( + role_api::RoleInfoWithGroupsResponse { + groups: updated_role.groups, + role_id: updated_role.role_id, + role_name: updated_role.role_name, + role_scope: updated_role.scope, + }, + )) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index d749163aa289..91aa36957af0 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -111,6 +111,7 @@ pub async fn construct_payout_router_data<'a, F>( email: a.email.to_owned().map(Email::from), } }), + payment_method_billing: None, }; let test_mode: Option = merchant_connector_account.is_test_mode_on(); diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index ef313b622284..b3e04e72f33b 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -32,6 +32,7 @@ use crate::{ events::{ api_logs::ApiEvent, outgoing_webhook_logs::{OutgoingWebhookEvent, OutgoingWebhookEventMetric}, + RawEvent, }, logger, routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, @@ -43,15 +44,13 @@ use crate::{ transformers::{ForeignInto, ForeignTryInto}, }, utils::{self as helper_utils, generate_id, OptionExt, ValueExt}, + workflows::outgoing_webhook_retry, }; const OUTGOING_WEBHOOK_TIMEOUT_SECS: u64 = 5; const MERCHANT_ID: &str = "merchant_id"; -pub async fn payments_incoming_webhook_flow< - W: types::OutgoingWebhookType, - Ctx: PaymentMethodRetrieve, ->( +pub async fn payments_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -169,7 +168,7 @@ pub async fn payments_incoming_webhook_flow< // If event is NOT an UnsupportedEvent, trigger Outgoing Webhook if let Some(outgoing_event_type) = event_type { - create_event_and_trigger_outgoing_webhook::( + create_event_and_trigger_outgoing_webhook( state, merchant_account, business_profile, @@ -196,7 +195,7 @@ pub async fn payments_incoming_webhook_flow< #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] -pub async fn refunds_incoming_webhook_flow( +pub async fn refunds_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -275,7 +274,7 @@ pub async fn refunds_incoming_webhook_flow( if let Some(outgoing_event_type) = event_type { let refund_response: api_models::refunds::RefundResponse = updated_refund.clone().foreign_into(); - create_event_and_trigger_outgoing_webhook::( + create_event_and_trigger_outgoing_webhook( state, merchant_account, business_profile, @@ -352,7 +351,7 @@ pub async fn get_or_update_dispute_object( let dispute_id = generate_id(consts::ID_LENGTH, "dp"); let new_dispute = diesel_models::dispute::DisputeNew { dispute_id, - amount: dispute_details.amount, + amount: dispute_details.amount.clone(), currency: dispute_details.currency, dispute_stage: dispute_details.dispute_stage, dispute_status: event_type @@ -374,6 +373,7 @@ pub async fn get_or_update_dispute_object( profile_id: Some(business_profile.profile_id.clone()), evidence: None, merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + dispute_amount: dispute_details.amount.parse::().unwrap_or(0), }; state .store @@ -413,7 +413,7 @@ pub async fn get_or_update_dispute_object( } } -pub async fn mandates_incoming_webhook_flow( +pub async fn mandates_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -470,7 +470,7 @@ pub async fn mandates_incoming_webhook_flow( ); let event_type: Option = updated_mandate.mandate_status.foreign_into(); if let Some(outgoing_event_type) = event_type { - create_event_and_trigger_outgoing_webhook::( + create_event_and_trigger_outgoing_webhook( state, merchant_account, business_profile, @@ -495,7 +495,7 @@ pub async fn mandates_incoming_webhook_flow( #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] -pub async fn disputes_incoming_webhook_flow( +pub async fn disputes_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -537,7 +537,7 @@ pub async fn disputes_incoming_webhook_flow( let disputes_response = Box::new(dispute_object.clone().foreign_into()); let event_type: enums::EventType = dispute_object.dispute_status.foreign_into(); - create_event_and_trigger_outgoing_webhook::( + create_event_and_trigger_outgoing_webhook( state, merchant_account, business_profile, @@ -561,7 +561,7 @@ pub async fn disputes_incoming_webhook_flow( } } -async fn bank_transfer_webhook_flow( +async fn bank_transfer_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -623,7 +623,7 @@ async fn bank_transfer_webhook_flow( + create_event_and_trigger_outgoing_webhook( state, merchant_account, business_profile, @@ -648,53 +648,7 @@ async fn bank_transfer_webhook_flow, - primary_object_id: String, - primary_object_type: enums::EventObjectType, - content: api::OutgoingWebhookContent, -) -> CustomResult<(), errors::ApiErrorResponse> { - match merchant_account.get_compatible_connector() { - #[cfg(feature = "stripe")] - Some(api_models::enums::Connector::Stripe) => { - create_event_and_trigger_outgoing_webhook::( - state.clone(), - merchant_account, - business_profile, - event_type, - event_class, - intent_reference_id, - primary_object_id, - primary_object_type, - content, - ) - .await - } - _ => { - create_event_and_trigger_outgoing_webhook::( - state.clone(), - merchant_account, - business_profile, - event_type, - event_class, - intent_reference_id, - primary_object_id, - primary_object_type, - content, - ) - .await - } - } -} - -#[allow(clippy::too_many_arguments)] -#[instrument(skip_all)] -pub async fn create_event_and_trigger_outgoing_webhook( +pub(crate) async fn create_event_and_trigger_outgoing_webhook( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -705,6 +659,7 @@ pub async fn create_event_and_trigger_outgoing_webhook CustomResult<(), errors::ApiErrorResponse> { + let merchant_id = business_profile.merchant_id.clone(); let event_id = format!("{primary_object_id}_{event_type}"); let new_event = storage::EventNew { event_id: event_id.clone(), @@ -722,7 +677,7 @@ pub async fn create_event_and_trigger_outgoing_webhook Ok(event), Err(error) => { if error.current_context().is_db_unique_violation() { - logger::info!("Merchant already notified about the event {event_id}"); + logger::debug!("Event `{event_id}` already exists in the database"); return Ok(()); } else { logger::error!(event_insertion_failure=?error); @@ -735,57 +690,132 @@ pub async fn create_event_and_trigger_outgoing_webhook(business_profile, outgoing_webhook, state).await; - - if let Err(e) = result { - error.replace( - serde_json::to_value(e.current_context()) - .into_report() - .attach_printable("Failed to serialize json error response") - .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) - .ok() - .into(), - ); - logger::error!(?e); - } - let outgoing_webhook_event_type = content.get_outgoing_webhook_event_type(); - let webhook_event = OutgoingWebhookEvent::new( - merchant_account.merchant_id.clone(), - event.event_id.clone(), - event_type, - outgoing_webhook_event_type, - error, - ); - match webhook_event.clone().try_into() { - Ok(event) => { - state_clone.event_handler().log_event(event); - } - Err(err) => { - logger::error!(error=?err, event=?webhook_event, "Error Logging Outgoing Webhook Event"); - } + tokio::spawn( + async move { + trigger_appropriate_webhook_and_raise_event( + state, + merchant_account, + business_profile, + outgoing_webhook, + types::WebhookDeliveryAttempt::InitialAttempt, + content, + event.event_id, + event_type, + process_tracker, + ) + .await; } - }.in_current_span()); + .in_current_span(), + ); } Ok(()) } -pub async fn trigger_webhook_to_merchant( +#[allow(clippy::too_many_arguments)] +pub(crate) async fn trigger_appropriate_webhook_and_raise_event( + state: AppState, + merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, - webhook: api::OutgoingWebhook, + outgoing_webhook: api::OutgoingWebhook, + delivery_attempt: types::WebhookDeliveryAttempt, + content: api::OutgoingWebhookContent, + event_id: String, + event_type: enums::EventType, + process_tracker: Option, +) { + match merchant_account.get_compatible_connector() { + #[cfg(feature = "stripe")] + Some(api_models::enums::Connector::Stripe) => { + trigger_webhook_and_raise_event::( + state, + business_profile, + outgoing_webhook, + delivery_attempt, + content, + event_id, + event_type, + process_tracker, + ) + .await + } + _ => { + trigger_webhook_and_raise_event::( + state, + business_profile, + outgoing_webhook, + delivery_attempt, + content, + event_id, + event_type, + process_tracker, + ) + .await + } + } +} + +#[allow(clippy::too_many_arguments)] +async fn trigger_webhook_and_raise_event( + state: AppState, + business_profile: diesel_models::business_profile::BusinessProfile, + outgoing_webhook: api::OutgoingWebhook, + delivery_attempt: types::WebhookDeliveryAttempt, + content: api::OutgoingWebhookContent, + event_id: String, + event_type: enums::EventType, + process_tracker: Option, +) { + let merchant_id = business_profile.merchant_id.clone(); + let trigger_webhook_result = trigger_webhook_to_merchant::( + state.clone(), + business_profile, + outgoing_webhook, + delivery_attempt, + process_tracker, + ) + .await; + + raise_webhooks_analytics_event( + state, + trigger_webhook_result, + content, + &merchant_id, + &event_id, + event_type, + ); +} + +async fn trigger_webhook_to_merchant( state: AppState, + business_profile: diesel_models::business_profile::BusinessProfile, + webhook: api::OutgoingWebhook, + delivery_attempt: types::WebhookDeliveryAttempt, + process_tracker: Option, ) -> CustomResult<(), errors::WebhooksFlowError> { let webhook_details_json = business_profile .webhook_details @@ -842,40 +872,135 @@ pub async fn trigger_webhook_to_merchant( ); logger::debug!(outgoing_webhook_response=?response); - match response { - Err(e) => { - // [#217]: Schedule webhook for retry. - Err(e).change_context(errors::WebhooksFlowError::CallToMerchantFailed)?; - } - Ok(res) => { - if res.status().is_success() { - metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add( - &metrics::CONTEXT, - 1, - &[metrics::KeyValue::new( - MERCHANT_ID, - business_profile.merchant_id.clone(), - )], - ); - let update_event = storage::EventUpdate::UpdateWebhookNotified { - is_webhook_notified: Some(true), - }; - state + let api_client_error_handler = + |client_error: error_stack::Report, + delivery_attempt: types::WebhookDeliveryAttempt| { + let error = + client_error.change_context(errors::WebhooksFlowError::CallToMerchantFailed); + logger::error!( + ?error, + ?delivery_attempt, + "An error occurred when sending webhook to merchant" + ); + }; + let success_response_handler = + |state: AppState, + merchant_id: String, + outgoing_webhook_event_id: String, + process_tracker: Option, + business_status: &'static str| async move { + metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue::new(MERCHANT_ID, merchant_id)], + ); + + let update_event = storage::EventUpdate::UpdateWebhookNotified { + is_webhook_notified: Some(true), + }; + state + .store + .update_event(outgoing_webhook_event_id, update_event) + .await + .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)?; + + match process_tracker { + Some(process_tracker) => state .store - .update_event(outgoing_webhook_event_id, update_event) + .as_scheduler() + .finish_process_with_business_status(process_tracker, business_status.into()) .await - .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)?; - } else { - metrics::WEBHOOK_OUTGOING_NOT_RECEIVED_COUNT.add( - &metrics::CONTEXT, - 1, - &[metrics::KeyValue::new( - MERCHANT_ID, - business_profile.merchant_id.clone(), - )], - ); - // [#217]: Schedule webhook for retry. - Err(errors::WebhooksFlowError::NotReceivedByMerchant).into_report()?; + .change_context( + errors::WebhooksFlowError::OutgoingWebhookProcessTrackerTaskUpdateFailed, + ), + None => Ok(()), + } + }; + let error_response_handler = |merchant_id: String, + delivery_attempt: types::WebhookDeliveryAttempt, + status_code: u16, + log_message: &'static str| { + metrics::WEBHOOK_OUTGOING_NOT_RECEIVED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue::new(MERCHANT_ID, merchant_id)], + ); + + let error = report!(errors::WebhooksFlowError::NotReceivedByMerchant); + logger::warn!(?error, ?delivery_attempt, ?status_code, %log_message); + }; + + match delivery_attempt { + types::WebhookDeliveryAttempt::InitialAttempt => match response { + Err(client_error) => api_client_error_handler(client_error, delivery_attempt), + Ok(response) => { + if response.status().is_success() { + success_response_handler( + state.clone(), + business_profile.merchant_id, + outgoing_webhook_event_id, + process_tracker, + "INITIAL_DELIVERY_ATTEMPT_SUCCESSFUL", + ) + .await?; + } else { + error_response_handler( + business_profile.merchant_id, + delivery_attempt, + response.status().as_u16(), + "Ignoring error when sending webhook to merchant", + ); + } + } + }, + types::WebhookDeliveryAttempt::AutomaticRetry => { + let process_tracker = process_tracker + .get_required_value("process_tracker") + .change_context(errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed) + .attach_printable("`process_tracker` is unavailable in automatic retry flow")?; + match response { + Err(client_error) => { + api_client_error_handler(client_error, delivery_attempt); + // Schedule a retry attempt for webhook delivery + outgoing_webhook_retry::retry_webhook_delivery_task( + &*state.store, + &business_profile.merchant_id, + process_tracker, + ) + .await + .change_context( + errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed, + )?; + } + Ok(response) => { + if response.status().is_success() { + success_response_handler( + state.clone(), + business_profile.merchant_id, + outgoing_webhook_event_id, + Some(process_tracker), + "COMPLETED_BY_PT", + ) + .await?; + } else { + error_response_handler( + business_profile.merchant_id.clone(), + delivery_attempt, + response.status().as_u16(), + "An error occurred when sending webhook to merchant", + ); + // Schedule a retry attempt for webhook delivery + outgoing_webhook_retry::retry_webhook_delivery_task( + &*state.store, + &business_profile.merchant_id, + process_tracker, + ) + .await + .change_context( + errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed, + )?; + } + } } } } @@ -883,6 +1008,48 @@ pub async fn trigger_webhook_to_merchant( Ok(()) } +fn raise_webhooks_analytics_event( + state: AppState, + trigger_webhook_result: CustomResult<(), errors::WebhooksFlowError>, + content: api::OutgoingWebhookContent, + merchant_id: &str, + event_id: &str, + event_type: enums::EventType, +) { + let error = if let Err(error) = trigger_webhook_result { + logger::error!(?error, "Failed to send webhook to merchant"); + + serde_json::to_value(error.current_context()) + .into_report() + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .map_err(|error| { + logger::error!(?error, "Failed to serialize outgoing webhook error as JSON"); + error + }) + .ok() + } else { + None + }; + + let outgoing_webhook_event_content = content.get_outgoing_webhook_event_content(); + let webhook_event = OutgoingWebhookEvent::new( + merchant_id.to_owned(), + event_id.to_owned(), + event_type, + outgoing_webhook_event_content, + error, + ); + + match RawEvent::try_from(webhook_event.clone()) { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(error) => { + logger::error!(?error, event=?webhook_event, "Error logging outgoing webhook event"); + } + } +} + pub async fn webhooks_wrapper( flow: &impl router_env::types::FlowMetric, state: AppState, @@ -1195,7 +1362,7 @@ pub async fn webhooks_core Box::pin(payments_incoming_webhook_flow::( + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, @@ -1206,7 +1373,7 @@ pub async fn webhooks_core Box::pin(refunds_incoming_webhook_flow::( + api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow( state.clone(), merchant_account, business_profile, @@ -1219,7 +1386,7 @@ pub async fn webhooks_core disputes_incoming_webhook_flow::( + api::WebhookFlow::Dispute => disputes_incoming_webhook_flow( state.clone(), merchant_account, business_profile, @@ -1232,7 +1399,7 @@ pub async fn webhooks_core Box::pin(bank_transfer_webhook_flow::( + api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow::( state.clone(), merchant_account, business_profile, @@ -1245,7 +1412,7 @@ pub async fn webhooks_core WebhookResponseTracker::NoEffect, - api::WebhookFlow::Mandate => mandates_incoming_webhook_flow::( + api::WebhookFlow::Mandate => mandates_incoming_webhook_flow( state.clone(), merchant_account, business_profile, @@ -1379,3 +1546,68 @@ async fn fetch_optional_mca_and_connector( Ok((None, connector)) } } + +pub async fn add_outgoing_webhook_retry_task_to_process_tracker( + db: &dyn StorageInterface, + business_profile: &diesel_models::business_profile::BusinessProfile, + event: &storage::Event, +) -> CustomResult { + let schedule_time = outgoing_webhook_retry::get_webhook_delivery_retry_schedule_time( + db, + &business_profile.merchant_id, + 0, + ) + .await + .ok_or(errors::StorageError::ValueNotFound( + "Process tracker schedule time".into(), // Can raise a better error here + )) + .into_report() + .attach_printable("Failed to obtain initial process tracker schedule time")?; + + let tracking_data = types::OutgoingWebhookTrackingData { + merchant_id: business_profile.merchant_id.clone(), + business_profile_id: business_profile.profile_id.clone(), + event_type: event.event_type, + event_class: event.event_class, + primary_object_id: event.primary_object_id.clone(), + primary_object_type: event.primary_object_type, + }; + + let runner = storage::ProcessTrackerRunner::OutgoingWebhookRetryWorkflow; + let task = "OUTGOING_WEBHOOK_RETRY"; + let tag = ["OUTGOING_WEBHOOKS"]; + let process_tracker_id = scheduler::utils::get_process_tracker_id( + runner, + task, + &event.primary_object_id, + &business_profile.merchant_id, + ); + let process_tracker_entry = storage::ProcessTrackerNew::new( + process_tracker_id, + task, + runner, + tag, + tracking_data, + schedule_time, + ) + .map_err(errors::StorageError::from)?; + + match db.insert_process(process_tracker_entry).await { + Ok(process_tracker) => { + crate::routes::metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[add_attributes("flow", "OutgoingWebhookRetry")], + ); + Ok(process_tracker) + } + Err(error) => { + crate::routes::metrics::TASK_ADDITION_FAILURES_COUNT.add( + &metrics::CONTEXT, + 1, + &[add_attributes("flow", "OutgoingWebhookRetry")], + ); + Err(error) + } + } +} diff --git a/crates/router/src/core/webhooks/types.rs b/crates/router/src/core/webhooks/types.rs index a0e999971c55..a6bc7fbc23e1 100644 --- a/crates/router/src/core/webhooks/types.rs +++ b/crates/router/src/core/webhooks/types.rs @@ -3,7 +3,7 @@ use common_utils::{crypto::SignMessage, ext_traits::Encode}; use error_stack::ResultExt; use serde::Serialize; -use crate::{core::errors, headers, services::request::Maskable}; +use crate::{core::errors, headers, services::request::Maskable, types::storage::enums}; pub trait OutgoingWebhookType: Serialize + From + Sync + Send + std::fmt::Debug + 'static @@ -43,3 +43,19 @@ impl OutgoingWebhookType for webhooks::OutgoingWebhook { header.push((headers::X_WEBHOOK_SIGNATURE.to_string(), signature.into())) } } + +#[derive(Debug, Clone, Copy)] +pub(crate) enum WebhookDeliveryAttempt { + InitialAttempt, + AutomaticRetry, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct OutgoingWebhookTrackingData { + pub(crate) merchant_id: String, + pub(crate) business_profile_id: String, + pub(crate) event_type: enums::EventType, + pub(crate) event_class: enums::EventClass, + pub(crate) primary_object_id: String, + pub(crate) primary_object_type: enums::EventObjectType, +} diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 311fdc127d1b..4e4f933be4ed 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -1,6 +1,5 @@ use diesel_models::{address::AddressUpdateInternal, enums::MerchantStorageScheme}; use error_stack::ResultExt; -use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -95,6 +94,7 @@ mod storage { }; #[async_trait::async_trait] impl AddressInterface for Store { + #[instrument(skip_all)] async fn find_address_by_address_id( &self, address_id: &str, @@ -114,6 +114,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn find_address_by_merchant_id_payment_id_address_id( &self, merchant_id: &str, @@ -162,6 +163,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn update_address_for_payments( &self, this: domain::Address, @@ -188,6 +190,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn insert_address_for_payments( &self, _payment_id: &str, @@ -213,6 +216,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn insert_address_for_customers( &self, address: domain::Address, @@ -236,6 +240,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn update_address_by_merchant_id_customer_id( &self, customer_id: &str, @@ -295,6 +300,7 @@ mod storage { }; #[async_trait::async_trait] impl AddressInterface for Store { + #[instrument(skip_all)] async fn find_address_by_address_id( &self, address_id: &str, @@ -314,6 +320,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn find_address_by_merchant_id_payment_id_address_id( &self, merchant_id: &str, @@ -381,6 +388,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn update_address_for_payments( &self, this: domain::Address, @@ -449,6 +457,7 @@ mod storage { } } + #[instrument(skip_all)] async fn insert_address_for_payments( &self, payment_id: &str, @@ -538,6 +547,7 @@ mod storage { } } + #[instrument(skip_all)] async fn insert_address_for_customers( &self, address: domain::Address, @@ -561,6 +571,7 @@ mod storage { .await } + #[instrument(skip_all)] async fn update_address_by_merchant_id_customer_id( &self, customer_id: &str, @@ -650,7 +661,6 @@ impl AddressInterface for MockDb { } } - #[instrument(skip_all)] async fn update_address( &self, address_id: String, diff --git a/crates/router/src/db/api_keys.rs b/crates/router/src/db/api_keys.rs index 94edac969026..c0d9ea364102 100644 --- a/crates/router/src/db/api_keys.rs +++ b/crates/router/src/db/api_keys.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; #[cfg(feature = "accounts_cache")] use storage_impl::redis::cache::CacheKind; #[cfg(feature = "accounts_cache")] @@ -52,6 +53,7 @@ pub trait ApiKeyInterface { #[async_trait::async_trait] impl ApiKeyInterface for Store { + #[instrument(skip_all)] async fn insert_api_key( &self, api_key: storage::ApiKeyNew, @@ -64,6 +66,7 @@ impl ApiKeyInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_api_key( &self, merchant_id: String, @@ -113,6 +116,7 @@ impl ApiKeyInterface for Store { } } + #[instrument(skip_all)] async fn revoke_api_key( &self, merchant_id: &str, @@ -156,6 +160,7 @@ impl ApiKeyInterface for Store { } } + #[instrument(skip_all)] async fn find_api_key_by_merchant_id_key_id_optional( &self, merchant_id: &str, @@ -168,6 +173,7 @@ impl ApiKeyInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_api_key_by_hash_optional( &self, hashed_api_key: storage::HashedApiKey, @@ -198,6 +204,7 @@ impl ApiKeyInterface for Store { } } + #[instrument(skip_all)] async fn list_api_keys_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/authentication.rs b/crates/router/src/db/authentication.rs new file mode 100644 index 000000000000..1916a326c78e --- /dev/null +++ b/crates/router/src/db/authentication.rs @@ -0,0 +1,159 @@ +use diesel_models::authentication::AuthenticationUpdateInternal; +use error_stack::IntoReport; +use router_env::{instrument, tracing}; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait AuthenticationInterface { + async fn insert_authentication( + &self, + authentication: storage::AuthenticationNew, + ) -> CustomResult; + + async fn find_authentication_by_merchant_id_authentication_id( + &self, + merchant_id: String, + authentication_id: String, + ) -> CustomResult; + + async fn update_authentication_by_merchant_id_authentication_id( + &self, + previous_state: storage::Authentication, + authentication_update: storage::AuthenticationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl AuthenticationInterface for Store { + #[instrument(skip_all)] + async fn insert_authentication( + &self, + authentication: storage::AuthenticationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + authentication + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + #[instrument(skip_all)] + async fn find_authentication_by_merchant_id_authentication_id( + &self, + merchant_id: String, + authentication_id: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::Authentication::find_by_merchant_id_authentication_id( + &conn, + &merchant_id, + &authentication_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + #[instrument(skip_all)] + async fn update_authentication_by_merchant_id_authentication_id( + &self, + previous_state: storage::Authentication, + authentication_update: storage::AuthenticationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Authentication::update_by_merchant_id_authentication_id( + &conn, + previous_state.merchant_id, + previous_state.authentication_id, + authentication_update, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl AuthenticationInterface for MockDb { + async fn insert_authentication( + &self, + authentication: storage::AuthenticationNew, + ) -> CustomResult { + let mut authentications = self.authentications.lock().await; + if authentications.iter().any(|authentication_inner| { + authentication_inner.authentication_id == authentication.authentication_id + }) { + Err(errors::StorageError::DuplicateValue { + entity: "authentication_id", + key: Some(authentication.authentication_id.clone()), + })? + } + let authentication = storage::Authentication { + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + authentication_id: authentication.authentication_id, + merchant_id: authentication.merchant_id, + authentication_status: authentication.authentication_status, + authentication_connector: authentication.authentication_connector, + connector_authentication_id: authentication.connector_authentication_id, + authentication_data: authentication.authentication_data, + payment_method_id: authentication.payment_method_id, + authentication_type: authentication.authentication_type, + authentication_lifecycle_status: authentication.authentication_lifecycle_status, + error_code: authentication.error_code, + error_message: authentication.error_message, + connector_metadata: authentication.connector_metadata, + }; + authentications.push(authentication.clone()); + Ok(authentication) + } + + async fn find_authentication_by_merchant_id_authentication_id( + &self, + merchant_id: String, + authentication_id: String, + ) -> CustomResult { + let authentications = self.authentications.lock().await; + authentications + .iter() + .find(|a| a.merchant_id == merchant_id && a.authentication_id == authentication_id) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "cannot find authentication for authentication_id = {authentication_id} and merchant_id = {merchant_id}" + )).into(), + ).cloned() + } + + async fn update_authentication_by_merchant_id_authentication_id( + &self, + previous_state: storage::Authentication, + authentication_update: storage::AuthenticationUpdate, + ) -> CustomResult { + let mut authentications = self.authentications.lock().await; + let authentication_id = previous_state.authentication_id.clone(); + let merchant_id = previous_state.merchant_id.clone(); + authentications + .iter_mut() + .find(|authentication| authentication.authentication_id == authentication_id && authentication.merchant_id == merchant_id) + .map(|authentication| { + let authentication_update_internal = + AuthenticationUpdateInternal::from(authentication_update); + let updated_authentication = authentication_update_internal.apply_changeset(previous_state); + *authentication = updated_authentication.clone(); + updated_authentication + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "cannot find authentication for authentication_id = {authentication_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } +} diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs index d167d1775375..322be5fc6db6 100644 --- a/crates/router/src/db/authorization.rs +++ b/crates/router/src/db/authorization.rs @@ -1,5 +1,6 @@ use diesel_models::authorization::AuthorizationUpdateInternal; use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -31,6 +32,7 @@ pub trait AuthorizationInterface { #[async_trait::async_trait] impl AuthorizationInterface for Store { + #[instrument(skip_all)] async fn insert_authorization( &self, authorization: storage::AuthorizationNew, @@ -43,6 +45,7 @@ impl AuthorizationInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_all_authorizations_by_merchant_id_payment_id( &self, merchant_id: &str, @@ -55,6 +58,7 @@ impl AuthorizationInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_authorization_by_merchant_id_authorization_id( &self, merchant_id: String, diff --git a/crates/router/src/db/blocklist.rs b/crates/router/src/db/blocklist.rs index 93361552de70..083c988b8d5b 100644 --- a/crates/router/src/db/blocklist.rs +++ b/crates/router/src/db/blocklist.rs @@ -58,6 +58,7 @@ impl BlocklistInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_blocklist_entry_by_merchant_id_fingerprint_id( &self, merchant_id: &str, @@ -70,6 +71,7 @@ impl BlocklistInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_blocklist_entries_by_merchant_id( &self, merchant_id: &str, @@ -81,6 +83,7 @@ impl BlocklistInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_blocklist_entries_by_merchant_id_data_kind( &self, merchant_id: &str, @@ -101,6 +104,7 @@ impl BlocklistInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( &self, merchant_id: &str, @@ -168,6 +172,7 @@ impl BlocklistInterface for KafkaStore { self.diesel_store.insert_blocklist_entry(pm_blocklist).await } + #[instrument(skip_all)] async fn find_blocklist_entry_by_merchant_id_fingerprint_id( &self, merchant_id: &str, @@ -178,6 +183,7 @@ impl BlocklistInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( &self, merchant_id: &str, @@ -188,6 +194,7 @@ impl BlocklistInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn list_blocklist_entries_by_merchant_id_data_kind( &self, merchant_id: &str, @@ -200,6 +207,7 @@ impl BlocklistInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn list_blocklist_entries_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/blocklist_fingerprint.rs b/crates/router/src/db/blocklist_fingerprint.rs index d9107d3d1c13..1be6f60e8770 100644 --- a/crates/router/src/db/blocklist_fingerprint.rs +++ b/crates/router/src/db/blocklist_fingerprint.rs @@ -39,6 +39,7 @@ impl BlocklistFingerprintInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( &self, merchant_id: &str, @@ -58,7 +59,6 @@ impl BlocklistFingerprintInterface for Store { #[async_trait::async_trait] impl BlocklistFingerprintInterface for MockDb { - #[instrument(skip_all)] async fn insert_blocklist_fingerprint_entry( &self, _pm_fingerprint_new: storage::BlocklistFingerprintNew, @@ -87,6 +87,7 @@ impl BlocklistFingerprintInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs index 5060fac7f6d3..ad8315b757d5 100644 --- a/crates/router/src/db/blocklist_lookup.rs +++ b/crates/router/src/db/blocklist_lookup.rs @@ -45,6 +45,7 @@ impl BlocklistLookupInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, merchant_id: &str, @@ -57,6 +58,7 @@ impl BlocklistLookupInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, merchant_id: &str, @@ -109,6 +111,7 @@ impl BlocklistLookupInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, merchant_id: &str, @@ -119,6 +122,7 @@ impl BlocklistLookupInterface for KafkaStore { .await } + #[instrument(skip_all)] async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, merchant_id: &str, diff --git a/crates/router/src/db/business_profile.rs b/crates/router/src/db/business_profile.rs index f29e0e9cecce..e0e004645410 100644 --- a/crates/router/src/db/business_profile.rs +++ b/crates/router/src/db/business_profile.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::Store; use crate::{ @@ -46,6 +47,7 @@ pub trait BusinessProfileInterface { #[async_trait::async_trait] impl BusinessProfileInterface for Store { + #[instrument(skip_all)] async fn insert_business_profile( &self, business_profile: business_profile::BusinessProfileNew, @@ -58,6 +60,7 @@ impl BusinessProfileInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_business_profile_by_profile_id( &self, profile_id: &str, @@ -69,6 +72,7 @@ impl BusinessProfileInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_business_profile_by_profile_name_merchant_id( &self, profile_name: &str, @@ -85,6 +89,7 @@ impl BusinessProfileInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_business_profile_by_profile_id( &self, current_state: business_profile::BusinessProfile, @@ -101,6 +106,7 @@ impl BusinessProfileInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_business_profile_by_profile_id_merchant_id( &self, profile_id: &str, @@ -117,6 +123,7 @@ impl BusinessProfileInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_business_profile_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/cache.rs b/crates/router/src/db/cache.rs index 4bda08e22c03..7f2462b3fc82 100644 --- a/crates/router/src/db/cache.rs +++ b/crates/router/src/db/cache.rs @@ -1,6 +1,7 @@ use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use storage_impl::redis::{ cache::{Cache, CacheKind, Cacheable}, pub_sub::PubSubInterface, @@ -12,6 +13,7 @@ use crate::{ core::errors::{self, CustomResult}, }; +#[instrument(skip_all)] pub async fn get_or_populate_redis( store: &dyn StorageInterface, key: impl AsRef, @@ -52,6 +54,7 @@ where } } +#[instrument(skip_all)] pub async fn get_or_populate_in_memory( store: &dyn StorageInterface, key: &str, @@ -73,6 +76,7 @@ where } } +#[instrument(skip_all)] pub async fn redact_cache( store: &dyn StorageInterface, key: &str, @@ -100,6 +104,7 @@ where Ok(data) } +#[instrument(skip_all)] pub async fn publish_into_redact_channel<'a, K: IntoIterator> + Send>( store: &dyn StorageInterface, keys: K, @@ -125,6 +130,7 @@ pub async fn publish_into_redact_channel<'a, K: IntoIterator()) } +#[instrument(skip_all)] pub async fn publish_and_redact<'a, T, F, Fut>( store: &dyn StorageInterface, key: CacheKind<'a>, @@ -139,6 +145,7 @@ where Ok(data) } +#[instrument(skip_all)] pub async fn publish_and_redact_multiple<'a, T, F, Fut, K>( store: &dyn StorageInterface, keys: K, diff --git a/crates/router/src/db/capture.rs b/crates/router/src/db/capture.rs index f9f60233a3aa..1373b45c2cd7 100644 --- a/crates/router/src/db/capture.rs +++ b/crates/router/src/db/capture.rs @@ -33,6 +33,7 @@ pub trait CaptureInterface { #[cfg(feature = "kv_store")] mod storage { use error_stack::IntoReport; + use router_env::{instrument, tracing}; use super::CaptureInterface; use crate::{ @@ -44,6 +45,7 @@ mod storage { #[async_trait::async_trait] impl CaptureInterface for Store { + #[instrument(skip_all)] async fn insert_capture( &self, capture: CaptureNew, @@ -60,6 +62,7 @@ mod storage { db_call().await } + #[instrument(skip_all)] async fn update_capture_with_capture_id( &self, this: Capture, @@ -76,6 +79,7 @@ mod storage { db_call().await } + #[instrument(skip_all)] async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( &self, merchant_id: &str, @@ -103,6 +107,7 @@ mod storage { #[cfg(not(feature = "kv_store"))] mod storage { use error_stack::IntoReport; + use router_env::{instrument, tracing}; use super::CaptureInterface; use crate::{ @@ -114,6 +119,7 @@ mod storage { #[async_trait::async_trait] impl CaptureInterface for Store { + #[instrument(skip_all)] async fn insert_capture( &self, capture: CaptureNew, @@ -130,6 +136,7 @@ mod storage { db_call().await } + #[instrument(skip_all)] async fn update_capture_with_capture_id( &self, this: Capture, @@ -146,6 +153,7 @@ mod storage { db_call().await } + #[instrument(skip_all)] async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/configs.rs b/crates/router/src/db/configs.rs index 1d472c1fb660..3f49254ae446 100644 --- a/crates/router/src/db/configs.rs +++ b/crates/router/src/db/configs.rs @@ -68,6 +68,7 @@ impl ConfigInterface for Store { config.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn update_config_in_database( &self, key: &str, @@ -81,6 +82,7 @@ impl ConfigInterface for Store { } //update in DB and remove in redis and cache + #[instrument(skip_all)] async fn update_config_by_key( &self, key: &str, @@ -92,6 +94,7 @@ impl ConfigInterface for Store { .await } + #[instrument(skip_all)] async fn find_config_by_key_from_db( &self, key: &str, @@ -104,6 +107,7 @@ impl ConfigInterface for Store { } //check in cache, then redis then finally DB, and on the way back populate redis and cache + #[instrument(skip_all)] async fn find_config_by_key( &self, key: &str, @@ -118,6 +122,7 @@ impl ConfigInterface for Store { cache::get_or_populate_in_memory(self, key, find_config_by_key_from_db, &CONFIG_CACHE).await } + #[instrument(skip_all)] async fn find_config_by_key_unwrap_or( &self, key: &str, @@ -157,6 +162,7 @@ impl ConfigInterface for Store { cache::get_or_populate_in_memory(self, key, find_else_unwrap_or, &CONFIG_CACHE).await } + #[instrument(skip_all)] async fn delete_config_by_key( &self, key: &str, diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index 68b449412084..9385e237e695 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -68,6 +68,7 @@ where #[async_trait::async_trait] impl CustomerInterface for Store { + #[instrument(skip_all)] async fn find_customer_optional_by_customer_id_merchant_id( &self, customer_id: &str, @@ -129,6 +130,7 @@ impl CustomerInterface for Store { .await } + #[instrument(skip_all)] async fn find_customer_by_customer_id_merchant_id( &self, customer_id: &str, @@ -155,6 +157,7 @@ impl CustomerInterface for Store { } } + #[instrument(skip_all)] async fn list_customers_by_merchant_id( &self, merchant_id: &str, @@ -180,6 +183,7 @@ impl CustomerInterface for Store { Ok(customers) } + #[instrument(skip_all)] async fn insert_customer( &self, customer_data: domain::Customer, @@ -202,6 +206,7 @@ impl CustomerInterface for Store { .await } + #[instrument(skip_all)] async fn delete_customer_by_customer_id_merchant_id( &self, customer_id: &str, diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs index 49dd43313c4c..fee09870d597 100644 --- a/crates/router/src/db/dashboard_metadata.rs +++ b/crates/router/src/db/dashboard_metadata.rs @@ -1,5 +1,6 @@ use diesel_models::{enums, user::dashboard_metadata as storage}; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use storage_impl::MockDb; use crate::{ @@ -55,6 +56,7 @@ pub trait DashboardMetadataInterface { #[async_trait::async_trait] impl DashboardMetadataInterface for Store { + #[instrument(skip_all)] async fn insert_metadata( &self, metadata: storage::DashboardMetadataNew, @@ -67,6 +69,7 @@ impl DashboardMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_metadata( &self, user_id: Option, @@ -89,6 +92,7 @@ impl DashboardMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, @@ -109,6 +113,7 @@ impl DashboardMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_merchant_scoped_dashboard_metadata( &self, merchant_id: &str, @@ -126,6 +131,8 @@ impl DashboardMetadataInterface for Store { .map_err(Into::into) .into_report() } + + #[instrument(skip_all)] async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id( &self, user_id: &str, @@ -142,6 +149,7 @@ impl DashboardMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key( &self, user_id: &str, diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index 4529b121fa24..b0d0594c0135 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -1,4 +1,5 @@ use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -48,6 +49,7 @@ pub trait DisputeInterface { #[async_trait::async_trait] impl DisputeInterface for Store { + #[instrument(skip_all)] async fn insert_dispute( &self, dispute: storage::DisputeNew, @@ -60,6 +62,7 @@ impl DisputeInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_by_merchant_id_payment_id_connector_dispute_id( &self, merchant_id: &str, @@ -78,6 +81,7 @@ impl DisputeInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_dispute_by_merchant_id_dispute_id( &self, merchant_id: &str, @@ -90,6 +94,7 @@ impl DisputeInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_disputes_by_merchant_id( &self, merchant_id: &str, @@ -102,6 +107,7 @@ impl DisputeInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_disputes_by_merchant_id_payment_id( &self, merchant_id: &str, @@ -114,6 +120,7 @@ impl DisputeInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_dispute( &self, this: storage::Dispute, @@ -173,6 +180,7 @@ impl DisputeInterface for MockDb { profile_id: dispute.profile_id, evidence, merchant_connector_id: dispute.merchant_connector_id, + dispute_amount: dispute.dispute_amount, }; locked_disputes.push(new_dispute.clone()); @@ -407,6 +415,7 @@ mod tests { evidence: Some(Secret::from(Value::String("evidence".into()))), profile_id: None, merchant_connector_id: None, + dispute_amount: 1040, } } diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index 9b7936ef2afc..a1d43e83b871 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -27,6 +27,7 @@ mod storage { use common_utils::date_time; use error_stack::ResultExt; use redis_interface::HsetnxReply; + use router_env::{instrument, tracing}; use storage_impl::redis::kv_store::RedisConnInterface; use time::ext::NumericalDuration; @@ -39,6 +40,7 @@ mod storage { #[async_trait::async_trait] impl EphemeralKeyInterface for Store { + #[instrument(skip_all)] async fn create_ephemeral_key( &self, new: EphemeralKeyNew, @@ -92,6 +94,7 @@ mod storage { Err(er) => Err(er).change_context(errors::StorageError::KVError), } } + #[instrument(skip_all)] async fn get_ephemeral_key( &self, key: &str, diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index 3d87fc70ea13..28fe4ef32d6e 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -1,4 +1,5 @@ use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -13,6 +14,12 @@ pub trait EventInterface { &self, event: storage::EventNew, ) -> CustomResult; + + async fn find_event_by_event_id( + &self, + event_id: &str, + ) -> CustomResult; + async fn update_event( &self, event_id: String, @@ -22,6 +29,7 @@ pub trait EventInterface { #[async_trait::async_trait] impl EventInterface for Store { + #[instrument(skip_all)] async fn insert_event( &self, event: storage::EventNew, @@ -29,6 +37,20 @@ impl EventInterface for Store { let conn = connection::pg_connection_write(self).await?; event.insert(&conn).await.map_err(Into::into).into_report() } + + #[instrument(skip_all)] + async fn find_event_by_event_id( + &self, + event_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::Event::find_by_event_id(&conn, event_id) + .await + .map_err(Into::into) + .into_report() + } + + #[instrument(skip_all)] async fn update_event( &self, event_id: String, @@ -71,6 +93,24 @@ impl EventInterface for MockDb { Ok(stored_event) } + + async fn find_event_by_event_id( + &self, + event_id: &str, + ) -> CustomResult { + let locked_events = self.events.lock().await; + locked_events + .iter() + .find(|event| event.event_id == event_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No event available with event_id = {event_id}" + )) + .into(), + ) + } + async fn update_event( &self, event_id: String, diff --git a/crates/router/src/db/file.rs b/crates/router/src/db/file.rs index 5ed8f22a2f39..ffd18336efa2 100644 --- a/crates/router/src/db/file.rs +++ b/crates/router/src/db/file.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -35,6 +36,7 @@ pub trait FileMetadataInterface { #[async_trait::async_trait] impl FileMetadataInterface for Store { + #[instrument(skip_all)] async fn insert_file_metadata( &self, file: storage::FileMetadataNew, @@ -43,6 +45,7 @@ impl FileMetadataInterface for Store { file.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn find_file_metadata_by_merchant_id_file_id( &self, merchant_id: &str, @@ -55,6 +58,7 @@ impl FileMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_file_metadata_by_merchant_id_file_id( &self, merchant_id: &str, @@ -67,6 +71,7 @@ impl FileMetadataInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_file_metadata( &self, this: storage::FileMetadata, diff --git a/crates/router/src/db/fraud_check.rs b/crates/router/src/db/fraud_check.rs index 2d107766c7b8..656cac0adb54 100644 --- a/crates/router/src/db/fraud_check.rs +++ b/crates/router/src/db/fraud_check.rs @@ -1,5 +1,6 @@ use diesel_models::fraud_check::{self as storage, FraudCheck, FraudCheckUpdate}; use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -36,6 +37,7 @@ pub trait FraudCheckInterface { #[async_trait::async_trait] impl FraudCheckInterface for Store { + #[instrument(skip_all)] async fn insert_fraud_check_response( &self, new: storage::FraudCheckNew, @@ -43,6 +45,8 @@ impl FraudCheckInterface for Store { let conn = connection::pg_connection_write(self).await?; new.insert(&conn).await.map_err(Into::into).into_report() } + + #[instrument(skip_all)] async fn update_fraud_check_response_with_attempt_id( &self, this: FraudCheck, @@ -54,6 +58,8 @@ impl FraudCheckInterface for Store { .map_err(Into::into) .into_report() } + + #[instrument(skip_all)] async fn find_fraud_check_by_payment_id( &self, payment_id: String, @@ -66,6 +72,7 @@ impl FraudCheckInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_fraud_check_by_payment_id_if_present( &self, payment_id: String, @@ -114,12 +121,15 @@ impl FraudCheckInterface for MockDb { #[cfg(feature = "kafka_events")] #[async_trait::async_trait] impl FraudCheckInterface for super::KafkaStore { + #[instrument(skip_all)] async fn insert_fraud_check_response( &self, _new: storage::FraudCheckNew, ) -> CustomResult { Err(errors::StorageError::MockDbError)? } + + #[instrument(skip_all)] async fn update_fraud_check_response_with_attempt_id( &self, _this: FraudCheck, @@ -128,6 +138,7 @@ impl FraudCheckInterface for super::KafkaStore { Err(errors::StorageError::MockDbError)? } + #[instrument(skip_all)] async fn find_fraud_check_by_payment_id( &self, _payment_id: String, diff --git a/crates/router/src/db/gsm.rs b/crates/router/src/db/gsm.rs index b623bdc2bcf5..4a9cf651d2b4 100644 --- a/crates/router/src/db/gsm.rs +++ b/crates/router/src/db/gsm.rs @@ -1,5 +1,6 @@ use diesel_models::gsm as storage; use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -52,6 +53,7 @@ pub trait GsmInterface { #[async_trait::async_trait] impl GsmInterface for Store { + #[instrument(skip_all)] async fn add_gsm_rule( &self, rule: storage::GatewayStatusMappingNew, @@ -60,6 +62,7 @@ impl GsmInterface for Store { rule.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn find_gsm_decision( &self, connector: String, @@ -77,6 +80,7 @@ impl GsmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_gsm_rule( &self, connector: String, @@ -92,6 +96,7 @@ impl GsmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_gsm_rule( &self, connector: String, @@ -108,6 +113,7 @@ impl GsmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_gsm_rule( &self, connector: String, diff --git a/crates/router/src/db/health_check.rs b/crates/router/src/db/health_check.rs index 6ebc9dfff5ad..5423fef9557d 100644 --- a/crates/router/src/db/health_check.rs +++ b/crates/router/src/db/health_check.rs @@ -1,7 +1,7 @@ use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; use diesel_models::ConfigNew; use error_stack::ResultExt; -use router_env::logger; +use router_env::{instrument, logger, tracing}; use super::{MockDb, Store}; use crate::{ @@ -17,6 +17,7 @@ pub trait HealthCheckDbInterface { #[async_trait::async_trait] impl HealthCheckDbInterface for Store { + #[instrument(skip_all)] async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { let conn = connection::pg_connection_write(self) .await diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 1c1ec00ce793..8ba977befcc8 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -487,6 +487,13 @@ impl EventInterface for KafkaStore { self.diesel_store.insert_event(event).await } + async fn find_event_by_event_id( + &self, + event_id: &str, + ) -> CustomResult { + self.diesel_store.find_event_by_event_id(event_id).await + } + async fn update_event( &self, event_id: String, @@ -1267,9 +1274,10 @@ impl PaymentMethodInterface for KafkaStore { &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id) + .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id, limit) .await } @@ -1278,9 +1286,15 @@ impl PaymentMethodInterface for KafkaStore { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_payment_method_by_customer_id_merchant_id_status(customer_id, merchant_id, status) + .find_payment_method_by_customer_id_merchant_id_status( + customer_id, + merchant_id, + status, + limit, + ) .await } @@ -1333,6 +1347,16 @@ impl PayoutAttemptInterface for KafkaStore { .await } + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_attempt_by_merchant_id_payout_attempt_id(merchant_id, payout_attempt_id) + .await + } + async fn update_payout_attempt_by_merchant_id_payout_id( &self, merchant_id: &str, @@ -1344,6 +1368,21 @@ impl PayoutAttemptInterface for KafkaStore { .await } + async fn update_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_attempt_by_merchant_id_payout_attempt_id( + merchant_id, + payout_attempt_id, + payout, + ) + .await + } + async fn insert_payout_attempt( &self, payout: storage::PayoutAttemptNew, diff --git a/crates/router/src/db/locker_mock_up.rs b/crates/router/src/db/locker_mock_up.rs index ca3028486bbc..d70c6baa815c 100644 --- a/crates/router/src/db/locker_mock_up.rs +++ b/crates/router/src/db/locker_mock_up.rs @@ -1,4 +1,5 @@ use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -27,6 +28,7 @@ pub trait LockerMockUpInterface { #[async_trait::async_trait] impl LockerMockUpInterface for Store { + #[instrument(skip_all)] async fn find_locker_by_card_id( &self, card_id: &str, @@ -38,6 +40,7 @@ impl LockerMockUpInterface for Store { .into_report() } + #[instrument(skip_all)] async fn insert_locker_mock_up( &self, new: storage::LockerMockUpNew, @@ -46,6 +49,7 @@ impl LockerMockUpInterface for Store { new.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn delete_locker_mock_up( &self, card_id: &str, diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index 0cf5cabf2e42..eb2859e57e1f 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -1,4 +1,5 @@ use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -48,6 +49,7 @@ pub trait MandateInterface { #[async_trait::async_trait] impl MandateInterface for Store { + #[instrument(skip_all)] async fn find_mandate_by_merchant_id_mandate_id( &self, merchant_id: &str, @@ -60,6 +62,7 @@ impl MandateInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_mandate_by_merchant_id_connector_mandate_id( &self, merchant_id: &str, @@ -76,6 +79,7 @@ impl MandateInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_mandate_by_merchant_id_customer_id( &self, merchant_id: &str, @@ -88,6 +92,7 @@ impl MandateInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_mandate_by_merchant_id_mandate_id( &self, merchant_id: &str, @@ -101,6 +106,7 @@ impl MandateInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_mandates_by_merchant_id( &self, merchant_id: &str, @@ -113,6 +119,7 @@ impl MandateInterface for Store { .into_report() } + #[instrument(skip_all)] async fn insert_mandate( &self, mandate: storage::MandateNew, diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 70d417c0366d..3ecf897148e7 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use common_utils::ext_traits::AsyncExt; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; #[cfg(feature = "accounts_cache")] use storage_impl::redis::cache::{CacheKind, ACCOUNTS_CACHE}; @@ -78,6 +79,7 @@ where #[async_trait::async_trait] impl MerchantAccountInterface for Store { + #[instrument(skip_all)] async fn insert_merchant( &self, merchant_account: domain::MerchantAccount, @@ -97,6 +99,7 @@ impl MerchantAccountInterface for Store { .change_context(errors::StorageError::DecryptionError) } + #[instrument(skip_all)] async fn find_merchant_account_by_merchant_id( &self, merchant_id: &str, @@ -129,6 +132,7 @@ impl MerchantAccountInterface for Store { } } + #[instrument(skip_all)] async fn update_merchant( &self, this: domain::MerchantAccount, @@ -155,6 +159,7 @@ impl MerchantAccountInterface for Store { .change_context(errors::StorageError::DecryptionError) } + #[instrument(skip_all)] async fn update_specific_fields_in_merchant( &self, merchant_id: &str, @@ -181,6 +186,7 @@ impl MerchantAccountInterface for Store { .change_context(errors::StorageError::DecryptionError) } + #[instrument(skip_all)] async fn find_merchant_account_by_publishable_key( &self, publishable_key: &str, @@ -228,6 +234,7 @@ impl MerchantAccountInterface for Store { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn list_merchant_accounts_by_organization_id( &self, organization_id: &str, @@ -268,6 +275,7 @@ impl MerchantAccountInterface for Store { Ok(merchant_accounts) } + #[instrument(skip_all)] async fn delete_merchant_account_by_merchant_id( &self, merchant_id: &str, @@ -305,6 +313,7 @@ impl MerchantAccountInterface for Store { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn list_multiple_merchant_accounts( &self, merchant_ids: Vec, diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index d01c4f3784d9..e4cad30ec29e 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -1,5 +1,6 @@ use common_utils::ext_traits::{AsyncExt, ByteSliceExt, Encode}; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; #[cfg(feature = "accounts_cache")] use storage_impl::redis::cache; use storage_impl::redis::kv_store::RedisConnInterface; @@ -36,6 +37,7 @@ pub trait ConnectorAccessToken { #[async_trait::async_trait] impl ConnectorAccessToken for Store { + #[instrument(skip_all)] async fn get_access_token( &self, merchant_id: &str, @@ -62,6 +64,7 @@ impl ConnectorAccessToken for Store { Ok(access_token) } + #[instrument(skip_all)] async fn set_access_token( &self, merchant_id: &str, @@ -165,6 +168,7 @@ where #[async_trait::async_trait] impl MerchantConnectorAccountInterface for Store { + #[instrument(skip_all)] async fn find_merchant_connector_account_by_merchant_id_connector_label( &self, merchant_id: &str, @@ -210,6 +214,7 @@ impl MerchantConnectorAccountInterface for Store { } } + #[instrument(skip_all)] async fn find_merchant_connector_account_by_profile_id_connector_name( &self, profile_id: &str, @@ -255,6 +260,7 @@ impl MerchantConnectorAccountInterface for Store { } } + #[instrument(skip_all)] async fn find_merchant_connector_account_by_merchant_id_connector_name( &self, merchant_id: &str, @@ -284,6 +290,7 @@ impl MerchantConnectorAccountInterface for Store { .await } + #[instrument(skip_all)] async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( &self, merchant_id: &str, @@ -326,6 +333,7 @@ impl MerchantConnectorAccountInterface for Store { } } + #[instrument(skip_all)] async fn insert_merchant_connector_account( &self, t: domain::MerchantConnectorAccount, @@ -347,6 +355,7 @@ impl MerchantConnectorAccountInterface for Store { .await } + #[instrument(skip_all)] async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( &self, merchant_id: &str, @@ -372,6 +381,7 @@ impl MerchantConnectorAccountInterface for Store { .await } + #[instrument(skip_all)] async fn update_merchant_connector_account( &self, this: domain::MerchantConnectorAccount, @@ -430,6 +440,7 @@ impl MerchantConnectorAccountInterface for Store { } } + #[instrument(skip_all)] async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index 630b833afdde..e3d81302cb95 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -1,5 +1,6 @@ use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use router_env::{instrument, tracing}; #[cfg(feature = "accounts_cache")] use storage_impl::redis::cache::{CacheKind, ACCOUNTS_CACHE}; @@ -43,6 +44,7 @@ pub trait MerchantKeyStoreInterface { #[async_trait::async_trait] impl MerchantKeyStoreInterface for Store { + #[instrument(skip_all)] async fn insert_merchant_key_store( &self, merchant_key_store: domain::MerchantKeyStore, @@ -62,6 +64,7 @@ impl MerchantKeyStoreInterface for Store { .change_context(errors::StorageError::DecryptionError) } + #[instrument(skip_all)] async fn get_merchant_key_store_by_merchant_id( &self, merchant_id: &str, @@ -104,6 +107,7 @@ impl MerchantKeyStoreInterface for Store { } } + #[instrument(skip_all)] async fn delete_merchant_key_store_by_merchant_id( &self, merchant_id: &str, @@ -137,6 +141,7 @@ impl MerchantKeyStoreInterface for Store { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn list_multiple_key_stores( &self, merchant_ids: Vec, diff --git a/crates/router/src/db/organization.rs b/crates/router/src/db/organization.rs index ddb8d9f9d907..63e4a0e8cd37 100644 --- a/crates/router/src/db/organization.rs +++ b/crates/router/src/db/organization.rs @@ -1,6 +1,7 @@ use common_utils::errors::CustomResult; use diesel_models::organization as storage; use error_stack::IntoReport; +use router_env::{instrument, tracing}; use crate::{connection, core::errors, services::Store}; @@ -25,6 +26,7 @@ pub trait OrganizationInterface { #[async_trait::async_trait] impl OrganizationInterface for Store { + #[instrument(skip_all)] async fn insert_organization( &self, organization: storage::OrganizationNew, @@ -37,6 +39,7 @@ impl OrganizationInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_organization_by_org_id( &self, org_id: &str, @@ -48,6 +51,7 @@ impl OrganizationInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_organization_by_org_id( &self, org_id: &str, diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs index a56f9dcce2ef..1a85de9e8826 100644 --- a/crates/router/src/db/payment_link.rs +++ b/crates/router/src/db/payment_link.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; use crate::{ connection, @@ -29,6 +30,7 @@ pub trait PaymentLinkInterface { #[async_trait::async_trait] impl PaymentLinkInterface for Store { + #[instrument(skip_all)] async fn find_payment_link_by_payment_link_id( &self, payment_link_id: &str, @@ -40,6 +42,7 @@ impl PaymentLinkInterface for Store { .into_report() } + #[instrument(skip_all)] async fn insert_payment_link( &self, payment_link_config: storage::PaymentLinkNew, @@ -52,6 +55,7 @@ impl PaymentLinkInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_payment_link_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index f15ceecd1c44..ef471fbd2464 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -1,5 +1,6 @@ use diesel_models::payment_method::PaymentMethodUpdateInternal; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -24,6 +25,7 @@ pub trait PaymentMethodInterface { &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError>; async fn find_payment_method_by_customer_id_merchant_id_status( @@ -31,6 +33,7 @@ pub trait PaymentMethodInterface { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError>; async fn insert_payment_method( @@ -53,6 +56,7 @@ pub trait PaymentMethodInterface { #[async_trait::async_trait] impl PaymentMethodInterface for Store { + #[instrument(skip_all)] async fn find_payment_method( &self, payment_method_id: &str, @@ -64,6 +68,7 @@ impl PaymentMethodInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_payment_method_by_locker_id( &self, locker_id: &str, @@ -75,6 +80,7 @@ impl PaymentMethodInterface for Store { .into_report() } + #[instrument(skip_all)] async fn insert_payment_method( &self, payment_method_new: storage::PaymentMethodNew, @@ -87,6 +93,7 @@ impl PaymentMethodInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_payment_method( &self, payment_method: storage::PaymentMethod, @@ -100,23 +107,32 @@ impl PaymentMethodInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_list( &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage::PaymentMethod::find_by_customer_id_merchant_id(&conn, customer_id, merchant_id) - .await - .map_err(Into::into) - .into_report() + storage::PaymentMethod::find_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + limit, + ) + .await + .map_err(Into::into) + .into_report() } + #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; storage::PaymentMethod::find_by_customer_id_merchant_id_status( @@ -124,6 +140,7 @@ impl PaymentMethodInterface for Store { customer_id, merchant_id, status, + limit, ) .await .map_err(Into::into) @@ -221,6 +238,7 @@ impl PaymentMethodInterface for MockDb { payment_method_issuer_code: payment_method_new.payment_method_issuer_code, metadata: payment_method_new.metadata, payment_method_data: payment_method_new.payment_method_data, + last_used_at: payment_method_new.last_used_at, connector_mandate_details: payment_method_new.connector_mandate_details, customer_acceptance: payment_method_new.customer_acceptance, status: payment_method_new.status, @@ -233,6 +251,7 @@ impl PaymentMethodInterface for MockDb { &self, customer_id: &str, merchant_id: &str, + _limit: Option, ) -> CustomResult, errors::StorageError> { let payment_methods = self.payment_methods.lock().await; let payment_methods_found: Vec = payment_methods @@ -256,6 +275,7 @@ impl PaymentMethodInterface for MockDb { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + _limit: Option, ) -> CustomResult, errors::StorageError> { let payment_methods = self.payment_methods.lock().await; let payment_methods_found: Vec = payment_methods diff --git a/crates/router/src/db/payout_attempt.rs b/crates/router/src/db/payout_attempt.rs index d95037ace90a..0e327eaa14da 100644 --- a/crates/router/src/db/payout_attempt.rs +++ b/crates/router/src/db/payout_attempt.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -15,6 +16,12 @@ pub trait PayoutAttemptInterface { _payout_id: &str, ) -> CustomResult; + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + ) -> CustomResult; + async fn update_payout_attempt_by_merchant_id_payout_id( &self, _merchant_id: &str, @@ -22,6 +29,13 @@ pub trait PayoutAttemptInterface { _payout: storage::PayoutAttemptUpdate, ) -> CustomResult; + async fn update_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + _payout: storage::PayoutAttemptUpdate, + ) -> CustomResult; + async fn insert_payout_attempt( &self, _payout: storage::PayoutAttemptNew, @@ -30,6 +44,7 @@ pub trait PayoutAttemptInterface { #[async_trait::async_trait] impl PayoutAttemptInterface for Store { + #[instrument(skip_all)] async fn find_payout_attempt_by_merchant_id_payout_id( &self, merchant_id: &str, @@ -42,6 +57,24 @@ impl PayoutAttemptInterface for Store { .into_report() } + #[instrument(skip_all)] + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::PayoutAttempt::find_by_merchant_id_payout_attempt_id( + &conn, + merchant_id, + payout_attempt_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + #[instrument(skip_all)] async fn update_payout_attempt_by_merchant_id_payout_id( &self, merchant_id: &str, @@ -60,6 +93,26 @@ impl PayoutAttemptInterface for Store { .into_report() } + #[instrument(skip_all)] + async fn update_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::PayoutAttempt::update_by_merchant_id_payout_attempt_id( + &conn, + merchant_id, + payout_attempt_id, + payout, + ) + .await + .map_err(Into::into) + .into_report() + } + + #[instrument(skip_all)] async fn insert_payout_attempt( &self, payout: storage::PayoutAttemptNew, @@ -80,6 +133,15 @@ impl PayoutAttemptInterface for MockDb { Err(errors::StorageError::MockDbError)? } + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn update_payout_attempt_by_merchant_id_payout_id( &self, _merchant_id: &str, @@ -90,6 +152,16 @@ impl PayoutAttemptInterface for MockDb { Err(errors::StorageError::MockDbError)? } + async fn update_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + _payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn insert_payout_attempt( &self, _payout: storage::PayoutAttemptNew, diff --git a/crates/router/src/db/payouts.rs b/crates/router/src/db/payouts.rs index cd9c50217b21..b20441de0f11 100644 --- a/crates/router/src/db/payouts.rs +++ b/crates/router/src/db/payouts.rs @@ -1,4 +1,5 @@ use error_stack::IntoReport; +use router_env::{instrument, tracing}; use super::{MockDb, Store}; use crate::{ @@ -30,6 +31,7 @@ pub trait PayoutsInterface { #[async_trait::async_trait] impl PayoutsInterface for Store { + #[instrument(skip_all)] async fn find_payout_by_merchant_id_payout_id( &self, merchant_id: &str, @@ -42,6 +44,7 @@ impl PayoutsInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_payout_by_merchant_id_payout_id( &self, merchant_id: &str, @@ -55,6 +58,7 @@ impl PayoutsInterface for Store { .into_report() } + #[instrument(skip_all)] async fn insert_payout( &self, payout: storage::PayoutsNew, diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 9664c29eedce..cc2d7e8b7b98 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -94,6 +94,7 @@ pub trait RefundInterface { #[cfg(not(feature = "kv_store"))] mod storage { use error_stack::IntoReport; + use router_env::{instrument, tracing}; use super::RefundInterface; use crate::{ @@ -105,6 +106,7 @@ mod storage { #[async_trait::async_trait] impl RefundInterface for Store { + #[instrument(skip_all)] async fn find_refund_by_internal_reference_id_merchant_id( &self, internal_reference_id: &str, @@ -122,6 +124,7 @@ mod storage { .into_report() } + #[instrument(skip_all)] async fn insert_refund( &self, new: storage_types::RefundNew, @@ -131,6 +134,7 @@ mod storage { new.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_connector_transaction_id( &self, merchant_id: &str, @@ -148,6 +152,7 @@ mod storage { .into_report() } + #[instrument(skip_all)] async fn update_refund( &self, this: storage_types::Refund, @@ -161,6 +166,7 @@ mod storage { .into_report() } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_refund_id( &self, merchant_id: &str, @@ -174,6 +180,7 @@ mod storage { .into_report() } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_connector_refund_id_connector( &self, merchant_id: &str, @@ -193,6 +200,7 @@ mod storage { .into_report() } + #[instrument(skip_all)] async fn find_refund_by_payment_id_merchant_id( &self, payment_id: &str, @@ -207,6 +215,7 @@ mod storage { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_refund_by_constraints( &self, merchant_id: &str, @@ -229,6 +238,7 @@ mod storage { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, @@ -246,6 +256,7 @@ mod storage { .into_report() } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn get_total_count_of_refunds( &self, merchant_id: &str, @@ -270,6 +281,7 @@ mod storage { use common_utils::{date_time, ext_traits::Encode, fallback_reverse_lookup_not_found}; 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::RefundInterface; @@ -283,6 +295,7 @@ mod storage { }; #[async_trait::async_trait] impl RefundInterface for Store { + #[instrument(skip_all)] async fn find_refund_by_internal_reference_id_merchant_id( &self, internal_reference_id: &str, @@ -328,6 +341,7 @@ mod storage { } } + #[instrument(skip_all)] async fn insert_refund( &self, new: storage_types::RefundNew, @@ -452,6 +466,7 @@ mod storage { } } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_connector_transaction_id( &self, merchant_id: &str, @@ -501,6 +516,7 @@ mod storage { } } + #[instrument(skip_all)] async fn update_refund( &self, this: storage_types::Refund, @@ -551,6 +567,7 @@ mod storage { } } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_refund_id( &self, merchant_id: &str, @@ -592,6 +609,7 @@ mod storage { } } + #[instrument(skip_all)] async fn find_refund_by_merchant_id_connector_refund_id_connector( &self, merchant_id: &str, @@ -640,6 +658,7 @@ mod storage { } } + #[instrument(skip_all)] async fn find_refund_by_payment_id_merchant_id( &self, payment_id: &str, @@ -679,6 +698,7 @@ mod storage { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_refund_by_constraints( &self, merchant_id: &str, @@ -701,6 +721,7 @@ mod storage { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_refund_by_meta_constraints( &self, merchant_id: &str, @@ -715,6 +736,7 @@ mod storage { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn get_total_count_of_refunds( &self, merchant_id: &str, diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 344077f3ec0f..ccb77571cfd8 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -24,6 +24,7 @@ pub trait ReverseLookupInterface { #[cfg(not(feature = "kv_store"))] mod storage { use error_stack::IntoReport; + use router_env::{instrument, tracing}; use super::{ReverseLookupInterface, Store}; use crate::{ @@ -37,6 +38,7 @@ mod storage { #[async_trait::async_trait] impl ReverseLookupInterface for Store { + #[instrument(skip_all)] async fn insert_reverse_lookup( &self, new: ReverseLookupNew, @@ -46,6 +48,7 @@ mod storage { new.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn get_lookup_by_lookup_id( &self, id: &str, @@ -64,6 +67,7 @@ mod storage { mod storage { use error_stack::{IntoReport, ResultExt}; use redis_interface::SetnxReply; + use router_env::{instrument, tracing}; use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; use super::{ReverseLookupInterface, Store}; @@ -80,6 +84,7 @@ mod storage { #[async_trait::async_trait] impl ReverseLookupInterface for Store { + #[instrument(skip_all)] async fn insert_reverse_lookup( &self, new: ReverseLookupNew, @@ -125,6 +130,7 @@ mod storage { } } + #[instrument(skip_all)] async fn get_lookup_by_lookup_id( &self, id: &str, diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 111b531e2f4d..c8777e3112cc 100644 --- a/crates/router/src/db/role.rs +++ b/crates/router/src/db/role.rs @@ -1,6 +1,7 @@ use common_enums::enums; use diesel_models::role as storage; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -48,6 +49,7 @@ pub trait RoleInterface { #[async_trait::async_trait] impl RoleInterface for Store { + #[instrument(skip_all)] async fn insert_role( &self, role: storage::RoleNew, @@ -56,6 +58,7 @@ impl RoleInterface for Store { role.insert(&conn).await.map_err(Into::into).into_report() } + #[instrument(skip_all)] async fn find_role_by_role_id( &self, role_id: &str, @@ -67,6 +70,7 @@ impl RoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_role_by_role_id_in_merchant_scope( &self, role_id: &str, @@ -80,6 +84,7 @@ impl RoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_role_by_role_id( &self, role_id: &str, @@ -92,6 +97,7 @@ impl RoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_role_by_role_id( &self, role_id: &str, @@ -103,6 +109,7 @@ impl RoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_all_roles( &self, merchant_id: &str, diff --git a/crates/router/src/db/routing_algorithm.rs b/crates/router/src/db/routing_algorithm.rs index 9970d1d9c9e5..e0f74358b97a 100644 --- a/crates/router/src/db/routing_algorithm.rs +++ b/crates/router/src/db/routing_algorithm.rs @@ -1,5 +1,6 @@ use diesel_models::routing_algorithm as routing_storage; use error_stack::IntoReport; +use router_env::{instrument, tracing}; use storage_impl::mock_db::MockDb; use crate::{ @@ -60,6 +61,7 @@ pub trait RoutingAlgorithmInterface { #[async_trait::async_trait] impl RoutingAlgorithmInterface for Store { + #[instrument(skip_all)] async fn insert_routing_algorithm( &self, routing_algorithm: routing_storage::RoutingAlgorithm, @@ -72,6 +74,7 @@ impl RoutingAlgorithmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_routing_algorithm_by_profile_id_algorithm_id( &self, profile_id: &str, @@ -88,6 +91,7 @@ impl RoutingAlgorithmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_routing_algorithm_by_algorithm_id_merchant_id( &self, algorithm_id: &str, @@ -104,6 +108,7 @@ impl RoutingAlgorithmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( &self, algorithm_id: &str, @@ -120,6 +125,7 @@ impl RoutingAlgorithmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_routing_algorithm_metadata_by_profile_id( &self, profile_id: &str, @@ -135,6 +141,7 @@ impl RoutingAlgorithmInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_routing_algorithm_metadata_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index c7c005a0b52b..6148e67b2940 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -1,6 +1,7 @@ use diesel_models::{user as storage, user_role::UserRole}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -52,6 +53,7 @@ pub trait UserInterface { #[async_trait::async_trait] impl UserInterface for Store { + #[instrument(skip_all)] async fn insert_user( &self, user_data: storage::UserNew, @@ -64,6 +66,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_user_by_email( &self, user_email: &str, @@ -75,6 +78,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_user_by_id( &self, user_id: &str, @@ -86,6 +90,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_user_by_user_id( &self, user_id: &str, @@ -98,6 +103,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_user_by_email( &self, user_email: &str, @@ -110,6 +116,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_user_by_user_id( &self, user_id: &str, @@ -121,6 +128,7 @@ impl UserInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_users_and_roles_by_merchant_id( &self, merchant_id: &str, diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 12816fa006b7..ae2c85db0417 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -3,6 +3,7 @@ use std::{collections::HashSet, ops::Not}; use async_bb8_diesel::AsyncConnection; use diesel_models::{enums, user_role as storage}; use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; use super::MockDb; use crate::{ @@ -64,6 +65,7 @@ pub trait UserRoleInterface { #[async_trait::async_trait] impl UserRoleInterface for Store { + #[instrument(skip_all)] async fn insert_user_role( &self, user_role: storage::UserRoleNew, @@ -76,6 +78,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_user_role_by_user_id( &self, user_id: &str, @@ -87,6 +90,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn find_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -103,6 +107,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -121,6 +126,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn update_user_roles_by_user_id_org_id( &self, user_id: &str, @@ -139,6 +145,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn delete_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -155,6 +162,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn list_user_roles_by_user_id( &self, user_id: &str, @@ -166,6 +174,7 @@ impl UserRoleInterface for Store { .into_report() } + #[instrument(skip_all)] async fn transfer_org_ownership_between_users( &self, from_user_id: &str, diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 0091de588f13..0f309ce5de00 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -33,6 +33,7 @@ pub enum EventType { ApiLogs, ConnectorApiLogs, OutgoingWebhookLogs, + Dispute, } #[derive(Debug, Default, Deserialize, Clone)] diff --git a/crates/router/src/events/outgoing_webhook_logs.rs b/crates/router/src/events/outgoing_webhook_logs.rs index c2b2366fac9f..bc5907b0cc40 100644 --- a/crates/router/src/events/outgoing_webhook_logs.rs +++ b/crates/router/src/events/outgoing_webhook_logs.rs @@ -43,10 +43,10 @@ pub enum OutgoingWebhookEventContent { }, } pub trait OutgoingWebhookEventMetric { - fn get_outgoing_webhook_event_type(&self) -> Option; + fn get_outgoing_webhook_event_content(&self) -> Option; } impl OutgoingWebhookEventMetric for OutgoingWebhookContent { - fn get_outgoing_webhook_event_type(&self) -> Option { + fn get_outgoing_webhook_event_content(&self) -> Option { match self { Self::PaymentDetails(payment_payload) => Some(OutgoingWebhookEventContent::Payment { payment_id: payment_payload.payment_id.clone(), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 73558c78d7bc..1d82bc7539f5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -632,6 +632,10 @@ impl Customers { web::resource("/{customer_id}/payment_methods") .route(web::get().to(list_customer_payment_method_api)), ) + .service( + web::resource("/{customer_id}/payment_methods/{payment_method_id}/default") + .route(web::post().to(default_payment_method_set_api)), + ) .service( web::resource("/{customer_id}") .route(web::get().to(customers_retrieve)) diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index 60f6dd741866..28b3e0db2119 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -224,7 +224,8 @@ pub async fn attach_dispute_evidence( )) .await } -/// Diputes - Retrieve Dispute + +/// Disputes - Retrieve Dispute #[utoipa::path( get, path = "/disputes/evidence/{dispute_id}", diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 9471289a0c8c..959f387a315d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -96,12 +96,14 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsRetrieve | Flow::PaymentMethodsUpdate | Flow::PaymentMethodsDelete - | Flow::ValidatePaymentMethod => Self::PaymentMethods, + | Flow::ValidatePaymentMethod + | Flow::DefaultPaymentMethodsSet => Self::PaymentMethods, Flow::PmAuthLinkTokenCreate | Flow::PmAuthExchangeToken => Self::PaymentMethodAuth, Flow::PaymentsCreate | Flow::PaymentsRetrieve + | Flow::PaymentsRetrieveForceSync | Flow::PaymentsUpdate | Flow::PaymentsConfirm | Flow::PaymentsCapture @@ -123,6 +125,7 @@ impl From for ApiIdentifier { Flow::RefundsCreate | Flow::RefundsRetrieve + | Flow::RefundsRetrieveForceSync | Flow::RefundsUpdate | Flow::RefundsList => Self::Refunds, @@ -173,7 +176,7 @@ impl From for ApiIdentifier { | Flow::Signout | Flow::ChangePassword | Flow::SetDashboardMetadata - | Flow::GetMutltipleDashboardMetadata + | Flow::GetMultipleDashboardMetadata | Flow::VerifyPaymentConnector | Flow::InternalUserSignup | Flow::SwitchMerchant diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index f667479ceaa6..780dd51bbde1 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -101,7 +101,7 @@ counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_SUCCESSFUL_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT, GLOBAL_METER); -// Metrics for Auto Retries +// Metrics for Payment Auto Retries counter_metric!(AUTO_RETRY_CONNECTION_CLOSED, GLOBAL_METER); counter_metric!(AUTO_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_GSM_MISS_COUNT, GLOBAL_METER); @@ -110,7 +110,17 @@ counter_metric!(AUTO_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_PAYMENT_COUNT, GLOBAL_METER); +// Metrics for Payout Auto Retries +counter_metric!(AUTO_PAYOUT_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER); +counter_metric!(AUTO_PAYOUT_RETRY_GSM_MISS_COUNT, GLOBAL_METER); +counter_metric!(AUTO_PAYOUT_RETRY_GSM_FETCH_FAILURE_COUNT, GLOBAL_METER); +counter_metric!(AUTO_PAYOUT_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); +counter_metric!(AUTO_PAYOUT_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_PAYOUT_COUNT, GLOBAL_METER); + +// Scheduler / Process Tracker related metrics counter_metric!(TASKS_ADDED_COUNT, GLOBAL_METER); // Tasks added to process tracker +counter_metric!(TASK_ADDITION_FAILURES_COUNT, GLOBAL_METER); // Failures in task addition to process tracker counter_metric!(TASKS_RESET_COUNT, GLOBAL_METER); // Tasks reset in process tracker for requeue flow pub mod request; diff --git a/crates/router/src/routes/metrics/request.rs b/crates/router/src/routes/metrics/request.rs index 30db586b7f9c..029c1c61f247 100644 --- a/crates/router/src/routes/metrics/request.rs +++ b/crates/router/src/routes/metrics/request.rs @@ -60,7 +60,7 @@ pub fn track_response_status_code(response: &ApplicationResponse) -> i64 { | ApplicationResponse::StatusOk | ApplicationResponse::TextPlain(_) | ApplicationResponse::Form(_) - | ApplicationResponse::PaymenkLinkForm(_) + | ApplicationResponse::PaymentLinkForm(_) | ApplicationResponse::FileData(_) | ApplicationResponse::JsonWithHeaders(_) => 200, ApplicationResponse::JsonForRedirection(_) => 302, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 89ca36c8c154..5469f9816599 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -102,6 +102,12 @@ pub async fn list_customer_payment_method_api( let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); let customer_id = customer_id.into_inner().0; + + let ephemeral_auth = + match auth::is_ephemeral_auth(req.headers(), &*state.store, &customer_id).await { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(err), + }; Box::pin(api::server_wrap( flow, state, @@ -116,7 +122,7 @@ pub async fn list_customer_payment_method_api( Some(&customer_id), ) }, - &auth::ApiKeyAuth, + &*ephemeral_auth, api_locking::LockAction::NotApplicable, )) .await @@ -157,6 +163,7 @@ pub async fn list_customer_payment_method_api_client( Ok((auth, _auth_flow)) => (auth, _auth_flow), Err(e) => return api::log_and_return_error_response(e), }; + Box::pin(api::server_wrap( flow, state, @@ -252,6 +259,42 @@ pub async fn payment_method_delete_api( )) .await } + +#[instrument(skip_all, fields(flow = ?Flow::DefaultPaymentMethodsSet))] +pub async fn default_payment_method_set_api( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::DefaultPaymentMethodsSet; + let payload = path.into_inner(); + let customer_id = payload.clone().customer_id; + + let ephemeral_auth = + match auth::is_ephemeral_auth(req.headers(), &*state.store, &customer_id).await { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(err), + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, default_payment_method| { + cards::set_default_payment_method( + state, + auth.merchant_account, + auth.key_store, + &customer_id, + default_payment_method.payment_method_id, + ) + }, + &*ephemeral_auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index b822e6d4e686..24849d828e23 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -229,7 +229,7 @@ pub async fn payments_start( operation_id = "Retrieve a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] +#[instrument(skip(state, req), fields(flow, payment_id))] // #[get("/{payment_id}")] pub async fn payments_retrieve( state: web::Data, @@ -237,7 +237,10 @@ pub async fn payments_retrieve( path: web::Path, json_payload: web::Query, ) -> impl Responder { - let flow = Flow::PaymentsRetrieve; + let flow = match json_payload.force_sync { + Some(true) => Flow::PaymentsRetrieveForceSync, + _ => Flow::PaymentsRetrieve, + }; let payload = payment_types::PaymentsRetrieveRequest { resource_id: payment_types::PaymentIdType::PaymentIntentId(path.to_string()), merchant_id: json_payload.merchant_id.clone(), @@ -249,6 +252,7 @@ pub async fn payments_retrieve( }; tracing::Span::current().record("payment_id", &path.to_string()); + tracing::Span::current().record("flow", &flow.to_string()); let (auth_type, auth_flow) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { @@ -300,7 +304,7 @@ pub async fn payments_retrieve( operation_id = "Retrieve a Payment", security(("api_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] +#[instrument(skip(state, req), fields(flow, payment_id))] // #[post("/sync")] pub async fn payments_retrieve_with_gateway_creds( state: web::Data, @@ -320,9 +324,13 @@ pub async fn payments_retrieve_with_gateway_creds( merchant_connector_details: json_payload.merchant_connector_details.clone(), ..Default::default() }; - let flow = Flow::PaymentsRetrieve; + let flow = match json_payload.force_sync { + Some(true) => Flow::PaymentsRetrieveForceSync, + _ => Flow::PaymentsRetrieve, + }; tracing::Span::current().record("payment_id", &json_payload.payment_id); + tracing::Span::current().record("flow", &flow.to_string()); let locking_action = payload.get_locking_input(flow.clone()); diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 47e9f2bf42a8..ef9ffb411240 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -63,7 +63,7 @@ pub async fn refunds_create( operation_id = "Retrieve a Refund", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::RefundsRetrieve))] +#[instrument(skip_all, fields(flow))] // #[get("/{id}")] pub async fn refunds_retrieve( state: web::Data, @@ -76,7 +76,12 @@ pub async fn refunds_retrieve( force_sync: query_params.force_sync, merchant_connector_details: None, }; - let flow = Flow::RefundsRetrieve; + let flow = match query_params.force_sync { + Some(true) => Flow::RefundsRetrieveForceSync, + _ => Flow::RefundsRetrieve, + }; + + tracing::Span::current().record("flow", &flow.to_string()); Box::pin(api::server_wrap( flow, @@ -115,14 +120,20 @@ pub async fn refunds_retrieve( operation_id = "Retrieve a Refund", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::RefundsRetrieve))] +#[instrument(skip_all, fields(flow))] // #[post("/sync")] pub async fn refunds_retrieve_with_body( state: web::Data, req: HttpRequest, json_payload: web::Json, ) -> HttpResponse { - let flow = Flow::RefundsRetrieve; + let flow = match json_payload.force_sync { + Some(true) => Flow::RefundsRetrieveForceSync, + _ => Flow::RefundsRetrieve, + }; + + tracing::Span::current().record("flow", &flow.to_string()); + Box::pin(api::server_wrap( flow, state, diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0568b31338b1..6603eb44b81e 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -179,7 +179,7 @@ pub async fn get_multiple_dashboard_metadata( req: HttpRequest, query: web::Query, ) -> HttpResponse { - let flow = Flow::GetMutltipleDashboardMetadata; + let flow = Flow::GetMultipleDashboardMetadata; let payload = match ReportSwitchExt::<_, ApiErrorResponse>::switch(parse_string_to_enums( query.into_inner().keys, )) { diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 75de9e7e6f7d..65e2aa4cdb87 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -41,14 +41,27 @@ pub async fn get_authorization_info( .await } -pub async fn get_role_from_token(state: web::Data, req: HttpRequest) -> HttpResponse { +pub async fn get_role_from_token( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { let flow = Flow::GetRoleFromToken; + let respond_with_groups = query.into_inner().groups.unwrap_or(false); + Box::pin(api::server_wrap( flow, state.clone(), &req, (), - |state, user, _| role_core::get_role_from_token(state, user), + |state, user, _| async move { + // TODO: Permissions to be deprecated once groups are stable + if respond_with_groups { + role_core::get_role_from_token_with_groups(state, user).await + } else { + role_core::get_role_from_token_with_permissions(state, user).await + } + }, &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index bfba31c34467..f01b0ffc346e 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -845,7 +845,7 @@ pub enum ApplicationResponse { TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(Box), - PaymenkLinkForm(Box), + PaymentLinkForm(Box), FileData((Vec, mime::Mime)), JsonWithHeaders((R, Vec<(String, String)>)), } @@ -1186,7 +1186,7 @@ where .map_into_boxed_body() } - Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { + Ok(ApplicationResponse::PaymentLinkForm(boxed_payment_link_data)) => { match *boxed_payment_link_data { PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { match build_payment_link_html(payment_link_data) { @@ -1821,18 +1821,16 @@ pub fn build_redirection_form( amount: '{amount}' }}; - var responseForm = document.createElement('form'); - responseForm.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/nmi\"); - responseForm.method='POST'; - const threeDSsecureInterface = threeDS.createUI(options); threeDSsecureInterface.on('challenge', function(e) {{ - console.log('Challenged'); document.getElementById('loader-wrapper').style.display = 'none'; }}); threeDSsecureInterface.on('complete', function(e) {{ + var responseForm = document.createElement('form'); + responseForm.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/nmi\"); + responseForm.method='POST'; var item1=document.createElement('input'); item1.type='hidden'; @@ -1887,6 +1885,23 @@ pub fn build_redirection_form( }}); threeDSsecureInterface.on('failure', function(e) {{ + var responseForm = document.createElement('form'); + responseForm.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/nmi\"); + responseForm.method='POST'; + + var error_code=document.createElement('input'); + error_code.type='hidden'; + error_code.name='code'; + error_code.value= e.code; + responseForm.appendChild(error_code); + + var error_message=document.createElement('input'); + error_message.type='hidden'; + error_message.name='message'; + error_message.value= e.message; + responseForm.appendChild(error_message); + + document.body.appendChild(responseForm); responseForm.submit(); }}); diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 9accf094ea47..568af6e19fd3 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -26,7 +26,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::OrganizationManage, ], role_id: consts::user_role::ROLE_ID_INTERNAL_ADMIN.to_string(), - role_name: "Internal Admin".to_string(), + role_name: "internal_admin".to_string(), scope: RoleScope::Organization, is_invitable: false, is_deletable: false, @@ -46,7 +46,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, ], role_id: consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), - role_name: "Internal View Only".to_string(), + role_name: "internal_view_only".to_string(), scope: RoleScope::Organization, is_invitable: false, is_deletable: false, @@ -73,7 +73,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::OrganizationManage, ], role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), - role_name: "Organization Admin".to_string(), + role_name: "organization_admin".to_string(), scope: RoleScope::Organization, is_invitable: false, is_deletable: false, @@ -100,7 +100,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, ], role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(), - role_name: "Admin".to_string(), + role_name: "admin".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, @@ -120,7 +120,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(), - role_name: "View Only".to_string(), + role_name: "view_only".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, @@ -139,7 +139,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN.to_string(), - role_name: "IAM".to_string(), + role_name: "iam".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, @@ -159,7 +159,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, ], role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(), - role_name: "Developer".to_string(), + role_name: "developer".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, @@ -180,7 +180,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(), - role_name: "Operator".to_string(), + role_name: "operator".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, @@ -198,7 +198,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(), - role_name: "Customer Support".to_string(), + role_name: "customer_support".to_string(), scope: RoleScope::Organization, is_invitable: true, is_deletable: true, diff --git a/crates/router/src/services/email/assets/api_key_expiry_reminder.html b/crates/router/src/services/email/assets/api_key_expiry_reminder.html index 1865ae38141b..772602d71e7c 100644 --- a/crates/router/src/services/email/assets/api_key_expiry_reminder.html +++ b/crates/router/src/services/email/assets/api_key_expiry_reminder.html @@ -43,20 +43,7 @@ cellpadding="0" cellspacing="0" > - - - + - - - - - - - - - - +

Dear Merchant,

- It has come to our attention that your API key will expire in {expires_in} days. To ensure uninterrupted + It has come to our attention that your API key, {api_key_name} ({prefix}*****) will expire in {expires_in} days. To ensure uninterrupted access to our platform and continued smooth operation of your services, we kindly request that you take the necessary actions as soon as possible. @@ -118,39 +105,11 @@ margin: 0 auto; padding: 0; " - width="100%" - >
@@ -184,20 +143,7 @@ width="100%" >
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index fa1eecbaab83..2fbe7d606e58 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -50,6 +50,8 @@ pub enum EmailBody { }, ApiKeyExpiryReminder { expires_in: u8, + api_key_name: String, + prefix: String, }, } @@ -128,8 +130,14 @@ Email : {user_email} (note: This is an auto generated email. Use merchant email for any further communications)", ), - EmailBody::ApiKeyExpiryReminder { expires_in } => format!( + EmailBody::ApiKeyExpiryReminder { + expires_in, + api_key_name, + prefix, + } => format!( include_str!("assets/api_key_expiry_reminder.html"), + api_key_name = api_key_name, + prefix = prefix, expires_in = expires_in, ), } @@ -441,6 +449,8 @@ pub struct ApiKeyExpiryReminder { pub recipient_email: domain::UserEmail, pub subject: &'static str, pub expires_in: u8, + pub api_key_name: String, + pub prefix: String, } #[async_trait::async_trait] @@ -450,6 +460,8 @@ impl EmailData for ApiKeyExpiryReminder { let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder { expires_in: self.expires_in, + api_key_name: self.api_key_name.clone(), + prefix: self.prefix.clone(), }); Ok(EmailContents { diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs index ef9352e55b48..ab4a8948a6b4 100644 --- a/crates/router/src/services/kafka.rs +++ b/crates/router/src/services/kafka.rs @@ -347,6 +347,7 @@ impl KafkaProducer { EventType::Refund => &self.refund_analytics_topic, EventType::ConnectorApiLogs => &self.connector_logs_topic, EventType::OutgoingWebhookLogs => &self.outgoing_webhook_logs_topic, + EventType::Dispute => &self.dispute_analytics_topic, } } } diff --git a/crates/router/src/services/kafka/dispute.rs b/crates/router/src/services/kafka/dispute.rs index 3ccdbe53cb1c..2925bb5221db 100644 --- a/crates/router/src/services/kafka/dispute.rs +++ b/crates/router/src/services/kafka/dispute.rs @@ -7,7 +7,7 @@ use crate::types::storage::dispute::Dispute; #[derive(serde::Serialize, Debug)] pub struct KafkaDispute<'a> { pub dispute_id: &'a String, - pub amount: &'a String, + pub dispute_amount: i64, pub currency: &'a String, pub dispute_stage: &'a storage_enums::DisputeStage, pub dispute_status: &'a storage_enums::DisputeStatus, @@ -38,7 +38,7 @@ impl<'a> KafkaDispute<'a> { pub fn from_storage(dispute: &'a Dispute) -> Self { Self { dispute_id: &dispute.dispute_id, - amount: &dispute.amount, + dispute_amount: dispute.amount.parse::().unwrap_or_default(), currency: &dispute.currency, dispute_stage: &dispute.dispute_stage, dispute_status: &dispute.dispute_status, diff --git a/crates/router/src/services/kafka/payment_attempt.rs b/crates/router/src/services/kafka/payment_attempt.rs index ea0721f418e5..98f88fb69f5f 100644 --- a/crates/router/src/services/kafka/payment_attempt.rs +++ b/crates/router/src/services/kafka/payment_attempt.rs @@ -1,4 +1,5 @@ -use data_models::payments::payment_attempt::PaymentAttempt; +// use diesel_models::enums::MandateDetails; +use data_models::{mandates::MandateDetails, payments::payment_attempt::PaymentAttempt}; use diesel_models::enums as storage_enums; use time::OffsetDateTime; @@ -39,6 +40,15 @@ pub struct KafkaPaymentAttempt<'a> { // TODO: These types should implement copy ideally pub payment_experience: Option<&'a storage_enums::PaymentExperience>, pub payment_method_type: Option<&'a storage_enums::PaymentMethodType>, + pub payment_method_data: Option, + pub error_reason: Option<&'a String>, + pub multiple_capture_count: Option, + pub amount_capturable: i64, + pub merchant_connector_id: Option<&'a String>, + pub net_amount: i64, + pub unified_code: Option<&'a String>, + pub unified_message: Option<&'a String>, + pub mandate_data: Option<&'a MandateDetails>, } impl<'a> KafkaPaymentAttempt<'a> { @@ -74,6 +84,15 @@ impl<'a> KafkaPaymentAttempt<'a> { connector_metadata: attempt.connector_metadata.as_ref().map(|v| v.to_string()), payment_experience: attempt.payment_experience.as_ref(), payment_method_type: attempt.payment_method_type.as_ref(), + payment_method_data: attempt.payment_method_data.as_ref().map(|v| v.to_string()), + error_reason: attempt.error_reason.as_ref(), + multiple_capture_count: attempt.multiple_capture_count, + amount_capturable: attempt.amount_capturable, + merchant_connector_id: attempt.merchant_connector_id.as_ref(), + net_amount: attempt.net_amount, + unified_code: attempt.unified_code.as_ref(), + unified_message: attempt.unified_message.as_ref(), + mandate_data: attempt.mandate_data.as_ref(), } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 894b3d830dd8..009033d53413 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -204,7 +204,7 @@ type BoxedConnector = Box<&'static (dyn Connector + Sync)>; // Normal flow will call the connector and follow the flow specific operations (capture, authorize) // SessionTokenFromMetadata will avoid calling the connector instead create the session token ( for sdk ) -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Debug)] pub enum GetToken { GpayMetadata, ApplePayMetadata, @@ -214,7 +214,7 @@ pub enum GetToken { /// Routing algorithm will output merchant connector identifier instead of connector name /// In order to support backwards compatibility for older routing algorithms and merchant accounts /// the support for connector name is retained -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ConnectorData { pub connector: BoxedConnector, pub connector_name: types::Connector, diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 32430c0918a2..6f08a7fd7e47 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -32,6 +32,7 @@ impl From<(domain::Customer, Option)> for CustomerResp created_at: cust.created_at, metadata: cust.metadata, address, + default_payment_method_id: cust.default_payment_method_id, } .into() } diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index ca852f832ee8..642e94ad69e7 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,10 +1,11 @@ pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, - GetTokenizePayloadResponse, PaymentMethodCreate, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodList, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, - PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, - TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, + CustomerPaymentMethodsListResponse, DefaultPaymentMethod, 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/payments.rs b/crates/router/src/types/api/payments.rs index 9159abf4bd1c..1204ba79acf2 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -4,13 +4,13 @@ pub use api_models::payments::{ MandateTransactionType, MandateType, MandateValidationFields, NextActionType, OnlineMandate, PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, - PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, - PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, - PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, - PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, - VerifyRequest, VerifyResponse, WalletData, + PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, + PaymentRetrieveBodyWithCredentials, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, + PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, + PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, + PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, + TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; @@ -259,7 +259,10 @@ mod payments_test { fn payments_request() -> PaymentsRequest { PaymentsRequest { amount: Some(Amount::from(200)), - payment_method_data: Some(PaymentMethodData::Card(card())), + payment_method_data: Some(PaymentMethodDataRequest { + payment_method_data: PaymentMethodData::Card(card()), + billing: None, + }), ..PaymentsRequest::default() } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index e4c347187cbc..221d16a8b462 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -90,6 +90,7 @@ impl VerifyConnectorData { address: types::PaymentAddress { shipping: None, billing: None, + payment_method_billing: None, }, payment_id: common_utils::generate_id_with_default_len( consts::VERIFY_CONNECTOR_ID_PREFIX, diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index fe575851dc49..5437d06a2e65 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -22,6 +22,7 @@ pub struct Customer { pub modified_at: PrimitiveDateTime, pub connector_customer: Option, pub address_id: Option, + pub default_payment_method_id: Option, } #[async_trait::async_trait] @@ -45,6 +46,7 @@ impl super::behaviour::Conversion for Customer { modified_at: self.modified_at, connector_customer: self.connector_customer, address_id: self.address_id, + default_payment_method_id: self.default_payment_method_id, }) } @@ -72,6 +74,7 @@ impl super::behaviour::Conversion for Customer { modified_at: item.modified_at, connector_customer: item.connector_customer, address_id: item.address_id, + default_payment_method_id: item.default_payment_method_id, }) } .await @@ -114,6 +117,9 @@ pub enum CustomerUpdate { ConnectorCustomer { connector_customer: Option, }, + UpdateDefaultPaymentMethod { + default_payment_method_id: Option, + }, } impl From for CustomerUpdateInternal { @@ -138,12 +144,20 @@ impl From for CustomerUpdateInternal { connector_customer, modified_at: Some(date_time::now()), address_id, + ..Default::default() }, CustomerUpdate::ConnectorCustomer { connector_customer } => Self { connector_customer, modified_at: Some(common_utils::date_time::now()), ..Default::default() }, + CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id, + } => Self { + default_payment_method_id, + modified_at: Some(common_utils::date_time::now()), + ..Default::default() + }, } } } diff --git a/crates/router/src/types/storage/authentication.rs b/crates/router/src/types/storage/authentication.rs new file mode 100644 index 000000000000..a2dc97536f92 --- /dev/null +++ b/crates/router/src/types/storage/authentication.rs @@ -0,0 +1 @@ +pub use diesel_models::authentication::{Authentication, AuthenticationNew, AuthenticationUpdate}; diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index f218751204bb..473afd1bf656 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -802,21 +802,19 @@ where if let Some(event_type) = event_type { tokio::spawn( async move { - Box::pin( - webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - m_state, - merchant_account, - business_profile, - event_type, - diesel_models::enums::EventClass::Payments, - None, - payment_id, - diesel_models::enums::EventObjectType::PaymentDetails, - webhooks::OutgoingWebhookContent::PaymentDetails( - payments_response_json, - ), + Box::pin(webhooks_core::create_event_and_trigger_outgoing_webhook( + m_state, + merchant_account, + business_profile, + event_type, + diesel_models::enums::EventClass::Payments, + None, + payment_id, + diesel_models::enums::EventObjectType::PaymentDetails, + webhooks::OutgoingWebhookContent::PaymentDetails( + payments_response_json, ), - ) + )) .await } .in_current_span(), diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 8ae65ce86e00..bb83b82f2da6 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,10 +1,12 @@ use api_models::user_role as user_role_api; +use common_enums::PermissionGroup; use error_stack::ResultExt; use crate::{ core::errors::{UserErrors, UserResult}, routes::AppState, - services::authorization::permissions::Permission, + services::authorization::{permissions::Permission, roles}, + types::domain, }; impl From for user_role_api::Permission { @@ -40,23 +42,44 @@ impl From for user_role_api::Permission { } } -pub async fn is_role_name_already_present_for_merchant( +pub fn validate_role_groups(groups: &[PermissionGroup]) -> UserResult<()> { + if groups.is_empty() { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Role groups cannot be empty"); + } + + if groups.contains(&PermissionGroup::OrganizationManage) { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Organization manage group cannot be added to role"); + } + + Ok(()) +} + +pub async fn validate_role_name( state: &AppState, - role_name: &str, + role_name: &domain::RoleName, merchant_id: &str, org_id: &str, ) -> UserResult<()> { - let role_name_list: Vec = state + let role_name_str = role_name.clone().get_role_name(); + + let is_present_in_predefined_roles = roles::predefined_roles::PREDEFINED_ROLES + .iter() + .any(|(_, role_info)| role_info.get_role_name() == role_name_str); + + // TODO: Create and use find_by_role_name to make this efficient + let is_present_in_custom_roles = state .store .list_all_roles(merchant_id, org_id) .await .change_context(UserErrors::InternalServerError)? .iter() - .map(|role| role.role_name.to_owned()) - .collect(); + .any(|role| role.role_name == role_name_str); - if role_name_list.contains(&role_name.to_string()) { + if is_present_in_predefined_roles || is_present_in_custom_roles { return Err(UserErrors::RoleNameAlreadyExists.into()); } + Ok(()) } diff --git a/crates/router/src/workflows.rs b/crates/router/src/workflows.rs index deb5bf785faa..6158031079c1 100644 --- a/crates/router/src/workflows.rs +++ b/crates/router/src/workflows.rs @@ -1,5 +1,6 @@ #[cfg(feature = "email")] pub mod api_key_expiry; +pub mod outgoing_webhook_retry; pub mod payment_sync; pub mod refund_router; pub mod tokenized_data; diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index e9e1f323708c..c5914810108c 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -54,6 +54,10 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { let retry_count = process.retry_count; + let api_key_name = tracking_data.api_key_name.clone(); + + let prefix = tracking_data.prefix.clone(); + let expires_in = tracking_data .expiry_reminder_days .get( @@ -69,6 +73,9 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { })?, subject: "API Key Expiry Notice", expires_in: *expires_in, + api_key_name, + prefix, + }; state diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs new file mode 100644 index 000000000000..6ce7e2454f8b --- /dev/null +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -0,0 +1,374 @@ +use api_models::{ + enums::EventType, + webhooks::{OutgoingWebhook, OutgoingWebhookContent}, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use error_stack::ResultExt; +use router_env::tracing::{self, instrument}; +use scheduler::{ + consumer::{self, workflows::ProcessTrackerWorkflow}, + types::process_data, + utils as scheduler_utils, +}; + +use crate::{ + core::webhooks::{self as webhooks_core, types::OutgoingWebhookTrackingData}, + db::StorageInterface, + errors, logger, + routes::AppState, + types::{domain, storage}, +}; + +pub struct OutgoingWebhookRetryWorkflow; + +#[async_trait::async_trait] +impl ProcessTrackerWorkflow for OutgoingWebhookRetryWorkflow { + #[instrument(skip_all)] + async fn execute_workflow<'a>( + &'a self, + state: &'a AppState, + process: storage::ProcessTracker, + ) -> Result<(), errors::ProcessTrackerError> { + let tracking_data: OutgoingWebhookTrackingData = process + .tracking_data + .clone() + .parse_value("OutgoingWebhookTrackingData")?; + + let db = &*state.store; + let key_store = db + .get_merchant_key_store_by_merchant_id( + &tracking_data.merchant_id, + &db.get_master_key().to_vec().into(), + ) + .await?; + let merchant_account = db + .find_merchant_account_by_merchant_id(&tracking_data.merchant_id, &key_store) + .await?; + let business_profile = db + .find_business_profile_by_profile_id(&tracking_data.business_profile_id) + .await?; + + let event_id = format!( + "{}_{}", + tracking_data.primary_object_id, tracking_data.event_type + ); + let event = db.find_event_by_event_id(&event_id).await?; + + let (content, event_type) = get_outgoing_webhook_content_and_event_type( + state.clone(), + merchant_account.clone(), + key_store, + &tracking_data, + ) + .await?; + + match event_type { + // Resource status is same as the event type of the current event + Some(event_type) if event_type == tracking_data.event_type => { + let outgoing_webhook = OutgoingWebhook { + merchant_id: tracking_data.merchant_id.clone(), + event_id: event_id.clone(), + event_type, + content: content.clone(), + timestamp: event.created_at, + }; + + webhooks_core::trigger_appropriate_webhook_and_raise_event( + state.clone(), + merchant_account, + business_profile, + outgoing_webhook, + webhooks_core::types::WebhookDeliveryAttempt::AutomaticRetry, + content, + event_id, + event_type, + Some(process), + ) + .await; + } + // Resource status has changed since the event was created, finish task + _ => { + logger::warn!( + %event_id, + "The current status of the resource `{}` (event type: {:?}) and the status of \ + the resource when the event was created (event type: {:?}) differ, finishing task", + tracking_data.primary_object_id, + event_type, + tracking_data.event_type + ); + db.as_scheduler() + .finish_process_with_business_status( + process.clone(), + "RESOURCE_STATUS_MISMATCH".to_string(), + ) + .await?; + } + } + + Ok(()) + } + + #[instrument(skip_all)] + async fn error_handler<'a>( + &'a self, + state: &'a AppState, + process: storage::ProcessTracker, + error: errors::ProcessTrackerError, + ) -> errors::CustomResult<(), errors::ProcessTrackerError> { + consumer::consumer_error_handler(state.store.as_scheduler(), process, error).await + } +} + +/// Get the schedule time for the specified retry count. +/// +/// The schedule time can be configured in configs with this key: `pt_mapping_outgoing_webhooks`. +/// +/// ```json +/// { +/// "default_mapping": { +/// "start_after": 60, +/// "frequency": [300], +/// "count": [5] +/// }, +/// "custom_merchant_mapping": { +/// "merchant_id1": { +/// "start_after": 30, +/// "frequency": [300], +/// "count": [2] +/// } +/// } +/// } +/// ``` +/// +/// This configuration value represents: +/// - `default_mapping.start_after`: The first retry attempt should happen after 60 seconds by +/// default. +/// - `default_mapping.frequency` and `count`: The next 5 retries should have an interval of 300 +/// seconds between them by default. +/// - `custom_merchant_mapping.merchant_id1`: Merchant-specific retry configuration for merchant +/// with merchant ID `merchant_id1`. +#[instrument(skip_all)] +pub(crate) async fn get_webhook_delivery_retry_schedule_time( + db: &dyn StorageInterface, + merchant_id: &str, + retry_count: i32, +) -> Option { + let key = "pt_mapping_outgoing_webhooks"; + + let result = db + .find_config_by_key(key) + .await + .map(|value| value.config) + .and_then(|config| { + config + .parse_struct("OutgoingWebhookRetryProcessTrackerMapping") + .change_context(errors::StorageError::DeserializationFailed) + }); + let mapping = result.map_or_else( + |error| { + if error.current_context().is_db_not_found() { + logger::debug!("Outgoing webhooks retry config `{key}` not found, ignoring"); + } else { + logger::error!( + ?error, + "Failed to read outgoing webhooks retry config `{key}`" + ); + } + process_data::OutgoingWebhookRetryProcessTrackerMapping::default() + }, + |mapping| { + logger::debug!(?mapping, "Using custom outgoing webhooks retry config"); + mapping + }, + ); + + let time_delta = scheduler_utils::get_outgoing_webhook_retry_schedule_time( + mapping, + merchant_id, + retry_count, + ); + + scheduler_utils::get_time_from_delta(time_delta) +} + +/// Schedule the webhook delivery task for retry +#[instrument(skip_all)] +pub(crate) async fn retry_webhook_delivery_task( + db: &dyn StorageInterface, + merchant_id: &str, + process: storage::ProcessTracker, +) -> errors::CustomResult<(), errors::StorageError> { + let schedule_time = + get_webhook_delivery_retry_schedule_time(db, merchant_id, process.retry_count + 1).await; + + match schedule_time { + Some(schedule_time) => { + db.as_scheduler() + .retry_process(process, schedule_time) + .await + } + None => { + db.as_scheduler() + .finish_process_with_business_status(process, "RETRIES_EXCEEDED".to_string()) + .await + } + } +} + +#[instrument(skip_all)] +async fn get_outgoing_webhook_content_and_event_type( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + tracking_data: &OutgoingWebhookTrackingData, +) -> Result<(OutgoingWebhookContent, Option), errors::ProcessTrackerError> { + use api_models::{ + mandates::MandateId, + payments::{HeaderPayload, PaymentIdType, PaymentsResponse, PaymentsRetrieveRequest}, + refunds::{RefundResponse, RefundsRetrieveRequest}, + }; + + use crate::{ + core::{ + disputes::retrieve_dispute, + mandate::get_mandate, + payment_methods::Oss, + payments::{payments_core, CallConnectorAction, PaymentStatus}, + refunds::refund_retrieve_core, + }, + services::{ApplicationResponse, AuthFlow}, + types::{ + api::{DisputeId, PSync}, + transformers::ForeignFrom, + }, + }; + + match tracking_data.event_class { + diesel_models::enums::EventClass::Payments => { + let payment_id = tracking_data.primary_object_id.clone(); + let request = PaymentsRetrieveRequest { + resource_id: PaymentIdType::PaymentIntentId(payment_id), + merchant_id: Some(tracking_data.merchant_id.clone()), + force_sync: false, + ..Default::default() + }; + + let payments_response = match payments_core::( + state, + merchant_account, + key_store, + PaymentStatus, + request, + AuthFlow::Client, + CallConnectorAction::Avoid, + None, + HeaderPayload::default(), + ) + .await? + { + ApplicationResponse::Json(payments_response) + | ApplicationResponse::JsonWithHeaders((payments_response, _)) => { + Ok(payments_response) + } + ApplicationResponse::StatusOk + | ApplicationResponse::TextPlain(_) + | ApplicationResponse::JsonForRedirection(_) + | ApplicationResponse::Form(_) + | ApplicationResponse::PaymentLinkForm(_) + | ApplicationResponse::FileData(_) => { + Err(errors::ProcessTrackerError::ResourceFetchingFailed { + resource_name: tracking_data.primary_object_id.clone(), + }) + } + }?; + let event_type = Option::::foreign_from(payments_response.status); + logger::debug!(current_resource_status=%payments_response.status); + + Ok(( + OutgoingWebhookContent::PaymentDetails(payments_response), + event_type, + )) + } + + diesel_models::enums::EventClass::Refunds => { + let refund_id = tracking_data.primary_object_id.clone(); + let request = RefundsRetrieveRequest { + refund_id, + force_sync: Some(false), + merchant_connector_details: None, + }; + + let refund = refund_retrieve_core(state, merchant_account, key_store, request).await?; + let event_type = Option::::foreign_from(refund.refund_status); + logger::debug!(current_resource_status=%refund.refund_status); + let refund_response = RefundResponse::foreign_from(refund); + + Ok(( + OutgoingWebhookContent::RefundDetails(refund_response), + event_type, + )) + } + + diesel_models::enums::EventClass::Disputes => { + let dispute_id = tracking_data.primary_object_id.clone(); + let request = DisputeId { dispute_id }; + + let dispute_response = + match retrieve_dispute(state, merchant_account, request).await? { + ApplicationResponse::Json(dispute_response) + | ApplicationResponse::JsonWithHeaders((dispute_response, _)) => { + Ok(dispute_response) + } + ApplicationResponse::StatusOk + | ApplicationResponse::TextPlain(_) + | ApplicationResponse::JsonForRedirection(_) + | ApplicationResponse::Form(_) + | ApplicationResponse::PaymentLinkForm(_) + | ApplicationResponse::FileData(_) => { + Err(errors::ProcessTrackerError::ResourceFetchingFailed { + resource_name: tracking_data.primary_object_id.clone(), + }) + } + } + .map(Box::new)?; + let event_type = Some(EventType::foreign_from(dispute_response.dispute_status)); + logger::debug!(current_resource_status=%dispute_response.dispute_status); + + Ok(( + OutgoingWebhookContent::DisputeDetails(dispute_response), + event_type, + )) + } + + diesel_models::enums::EventClass::Mandates => { + let mandate_id = tracking_data.primary_object_id.clone(); + let request = MandateId { mandate_id }; + + let mandate_response = + match get_mandate(state, merchant_account, key_store, request).await? { + ApplicationResponse::Json(mandate_response) + | ApplicationResponse::JsonWithHeaders((mandate_response, _)) => { + Ok(mandate_response) + } + ApplicationResponse::StatusOk + | ApplicationResponse::TextPlain(_) + | ApplicationResponse::JsonForRedirection(_) + | ApplicationResponse::Form(_) + | ApplicationResponse::PaymentLinkForm(_) + | ApplicationResponse::FileData(_) => { + Err(errors::ProcessTrackerError::ResourceFetchingFailed { + resource_name: tracking_data.primary_object_id.clone(), + }) + } + } + .map(Box::new)?; + let event_type = Option::::foreign_from(mandate_response.status); + logger::debug!(current_resource_status=%mandate_response.status); + + Ok(( + OutgoingWebhookContent::MandateDetails(mandate_response), + event_type, + )) + } + } +} diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 92057b08899c..c328a67a0ffd 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -247,12 +247,12 @@ pub async fn get_sync_process_schedule_time( }); let mapping = match mapping { Ok(x) => x, - Err(err) => { - logger::info!("Redis Mapping Error: {}", err); + Err(error) => { + logger::info!(?error, "Redis Mapping Error"); process_data::ConnectorPTMapping::default() } }; - let time_delta = scheduler_utils::get_schedule_time(mapping, merchant_id, retry_count + 1); + let time_delta = scheduler_utils::get_schedule_time(mapping, merchant_id, retry_count); Ok(scheduler_utils::get_time_from_delta(time_delta)) } @@ -267,7 +267,7 @@ pub async fn retry_sync_task( pt: storage::ProcessTracker, ) -> Result { let schedule_time = - get_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?; + get_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count + 1).await?; match schedule_time { Some(s_time) => { diff --git a/crates/router/tests/connectors/dlocal.rs b/crates/router/tests/connectors/dlocal.rs index 6b3a2954b67b..992b1ae6ce72 100644 --- a/crates/router/tests/connectors/dlocal.rs +++ b/crates/router/tests/connectors/dlocal.rs @@ -437,6 +437,7 @@ pub fn get_payment_info() -> PaymentInfo { }), email: None, }), + payment_method_billing: None, }), auth_type: None, access_token: None, diff --git a/crates/router/tests/connectors/multisafepay.rs b/crates/router/tests/connectors/multisafepay.rs index ff56239e972d..393074ee63ed 100644 --- a/crates/router/tests/connectors/multisafepay.rs +++ b/crates/router/tests/connectors/multisafepay.rs @@ -55,6 +55,7 @@ fn get_default_payment_info() -> Option { phone: None, email: None, }), + payment_method_billing: None, }); Some(PaymentInfo { address, diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 0ec07f4f7242..26f846932104 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -59,6 +59,7 @@ fn get_default_payment_info() -> Option { phone: None, email: None, }), + payment_method_billing: None, }), auth_type: None, access_token: None, diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 8ab2779d97df..c1578c2d44e9 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -316,19 +316,22 @@ async fn payments_create_core() { return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OnSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), - payment_method_data: Some(api::PaymentMethodData::Card(api::Card { - card_number: "4242424242424242".to_string().try_into().unwrap(), - card_exp_month: "10".to_string().into(), - card_exp_year: "35".to_string().into(), - card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), - card_cvc: "123".to_string().into(), - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: Some(masking::Secret::new("nick_name".into())), - })), + payment_method_data: Some(api::PaymentMethodDataRequest { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_number: "4242424242424242".to_string().try_into().unwrap(), + card_exp_month: "10".to_string().into(), + card_exp_year: "35".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), + card_cvc: "123".to_string().into(), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: Some(masking::Secret::new("nick_name".into())), + }), + billing: None, + }), payment_method: Some(api_enums::PaymentMethod::Card), shipping: Some(api::Address { address: None, @@ -494,19 +497,22 @@ async fn payments_create_core_adyen_no_redirect() { return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OnSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), - payment_method_data: Some(api::PaymentMethodData::Card(api::Card { - card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), - card_exp_month: "03".to_string().into(), - card_exp_year: "2030".to_string().into(), - card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), - card_cvc: "737".to_string().into(), - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: Some(masking::Secret::new("nick_name".into())), - })), + payment_method_data: Some(api::PaymentMethodDataRequest { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), + card_exp_month: "03".to_string().into(), + card_exp_year: "2030".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), + card_cvc: "737".to_string().into(), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: Some(masking::Secret::new("nick_name".into())), + }), + billing: None, + }), payment_method: Some(api_enums::PaymentMethod::Card), shipping: Some(api::Address { address: None, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index d901da342f37..dadc3c9477eb 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -76,19 +76,22 @@ async fn payments_create_core() { return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: None, authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), - payment_method_data: Some(api::PaymentMethodData::Card(api::Card { - card_number: "4242424242424242".to_string().try_into().unwrap(), - card_exp_month: "10".to_string().into(), - card_exp_year: "35".to_string().into(), - card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), - card_cvc: "123".to_string().into(), - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: Some(masking::Secret::new("nick_name".into())), - })), + payment_method_data: Some(api::PaymentMethodDataRequest { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_number: "4242424242424242".to_string().try_into().unwrap(), + card_exp_month: "10".to_string().into(), + card_exp_year: "35".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), + card_cvc: "123".to_string().into(), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: Some(masking::Secret::new("nick_name".into())), + }), + billing: None, + }), payment_method: Some(api_enums::PaymentMethod::Card), shipping: Some(api::Address { address: None, @@ -261,19 +264,23 @@ async fn payments_create_core_adyen_no_redirect() { return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OffSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), - payment_method_data: Some(api::PaymentMethodData::Card(api::Card { - card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), - card_exp_month: "03".to_string().into(), - card_exp_year: "2030".to_string().into(), - card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), - card_cvc: "737".to_string().into(), - bank_code: None, - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - nick_name: Some(masking::Secret::new("nick_name".into())), - })), + payment_method_data: Some(api::PaymentMethodDataRequest { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), + card_exp_month: "03".to_string().into(), + card_exp_year: "2030".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), + card_cvc: "737".to_string().into(), + bank_code: None, + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + nick_name: Some(masking::Secret::new("nick_name".into())), + }), + billing: None, + }), + payment_method: Some(api_enums::PaymentMethod::Card), shipping: Some(api::Address { address: None, diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index be6aa257d746..b68cdcc6fcc1 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -55,6 +55,7 @@ pub enum AnalyticsFlow { GetConnectorEvents, GetOutgoingWebhookEvents, GetDisputeFilters, + GetDisputeMetrics, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 62c09c42455e..0de256a6aca0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -123,10 +123,14 @@ pub enum Flow { PaymentMethodsUpdate, /// Payment methods delete flow. PaymentMethodsDelete, + /// Default Payment method flow. + DefaultPaymentMethodsSet, /// Payments create flow. PaymentsCreate, /// Payments Retrieve flow. PaymentsRetrieve, + /// Payments Retrieve force sync flow. + PaymentsRetrieveForceSync, /// Payments update flow. PaymentsUpdate, /// Payments confirm flow. @@ -168,6 +172,8 @@ pub enum Flow { RefundsCreate, /// Refunds retrieve flow. RefundsRetrieve, + /// Refunds retrieve force sync flow. + RefundsRetrieveForceSync, /// Refunds update flow. RefundsUpdate, /// Refunds list flow. @@ -303,7 +309,7 @@ pub enum Flow { /// Set Dashboard Metadata flow SetDashboardMetadata, /// Get Multiple Dashboard Metadata flow - GetMutltipleDashboardMetadata, + GetMultipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, /// Internal user signup diff --git a/crates/scheduler/src/consumer/types/process_data.rs b/crates/scheduler/src/consumer/types/process_data.rs index d5299493b1fc..267305c37f99 100644 --- a/crates/scheduler/src/consumer/types/process_data.rs +++ b/crates/scheduler/src/consumer/types/process_data.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use diesel_models::enums; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct RetryMapping { pub start_after: i32, pub frequency: Vec, @@ -11,7 +11,6 @@ pub struct RetryMapping { } #[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct ConnectorPTMapping { pub default_mapping: RetryMapping, pub custom_merchant_mapping: HashMap, @@ -33,7 +32,6 @@ impl Default for ConnectorPTMapping { } #[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct PaymentMethodsPTMapping { pub default_mapping: RetryMapping, pub custom_pm_mapping: HashMap, @@ -53,3 +51,43 @@ impl Default for PaymentMethodsPTMapping { } } } + +/// Configuration for outgoing webhook retries. +#[derive(Debug, Serialize, Deserialize)] +pub struct OutgoingWebhookRetryProcessTrackerMapping { + /// Default (fallback) retry configuration used when no merchant-specific retry configuration + /// exists. + pub default_mapping: RetryMapping, + + /// Merchant-specific retry configuration. + pub custom_merchant_mapping: HashMap, +} + +impl Default for OutgoingWebhookRetryProcessTrackerMapping { + fn default() -> Self { + Self { + default_mapping: RetryMapping { + // 1st attempt happens after 1 minute + start_after: 60, + + frequency: vec![ + // 2nd and 3rd attempts happen at intervals of 5 minutes each + 60 * 5, + // 4th, 5th, 6th, 7th and 8th attempts happen at intervals of 10 minutes each + 60 * 10, + // 9th, 10th, 11th, 12th and 13th attempts happen at intervals of 1 hour each + 60 * 60, + // 14th, 15th and 16th attempts happen at intervals of 6 hours each + 60 * 60 * 6, + ], + count: vec![ + 2, // 2nd and 3rd attempts + 5, // 4th, 5th, 6th, 7th and 8th attempts + 5, // 9th, 10th, 11th, 12th and 13th attempts + 3, // 14th, 15th and 16th attempts + ], + }, + custom_merchant_mapping: HashMap::new(), + } + } +} diff --git a/crates/scheduler/src/db/queue.rs b/crates/scheduler/src/db/queue.rs index 2c02b405d81e..a463111ab3be 100644 --- a/crates/scheduler/src/db/queue.rs +++ b/crates/scheduler/src/db/queue.rs @@ -144,7 +144,7 @@ impl QueueInterface for MockDb { ) -> CustomResult, ProcessTrackerError> { // [#172]: Implement function for `MockDb` Err(ProcessTrackerError::ResourceFetchingFailed { - resource_name: "consumer_tasks", + resource_name: "consumer_tasks".to_string(), })? } diff --git a/crates/scheduler/src/errors.rs b/crates/scheduler/src/errors.rs index 78a0bdee6242..6a14bd2611e0 100644 --- a/crates/scheduler/src/errors.rs +++ b/crates/scheduler/src/errors.rs @@ -34,7 +34,7 @@ pub enum ProcessTrackerError { #[error("Failed to fetch processes from database")] ProcessFetchingFailed, #[error("Failed while fetching: {resource_name}")] - ResourceFetchingFailed { resource_name: &'static str }, + ResourceFetchingFailed { resource_name: String }, #[error("Failed while executing: {flow}")] FlowExecutionError { flow: &'static str }, #[error("Not Implemented")] diff --git a/crates/scheduler/src/utils.rs b/crates/scheduler/src/utils.rs index 174efd637e9d..48d02f7fcedc 100644 --- a/crates/scheduler/src/utils.rs +++ b/crates/scheduler/src/utils.rs @@ -342,22 +342,46 @@ pub fn get_pm_schedule_time( } } -/// Get the delay based on the retry count -fn get_delay<'a>( +pub fn get_outgoing_webhook_retry_schedule_time( + mapping: process_data::OutgoingWebhookRetryProcessTrackerMapping, + merchant_name: &str, retry_count: i32, - mut array: impl Iterator, ) -> Option { - match array.next() { - Some(ele) => { - let v = retry_count - ele.0; - if v <= 0 { - Some(*ele.1) - } else { - get_delay(v, array) - } + let retry_mapping = match mapping.custom_merchant_mapping.get(merchant_name) { + Some(map) => map.clone(), + None => mapping.default_mapping, + }; + + // For first try, get the `start_after` time + if retry_count == 0 { + Some(retry_mapping.start_after) + } else { + get_delay( + retry_count, + retry_mapping + .count + .iter() + .zip(retry_mapping.frequency.iter()), + ) + } +} + +/// Get the delay based on the retry count +fn get_delay<'a>(retry_count: i32, array: impl Iterator) -> Option { + // Preferably, fix this by using unsigned ints + if retry_count <= 0 { + return None; + } + + let mut cumulative_count = 0; + for (&count, &frequency) in array { + cumulative_count += count; + if cumulative_count >= retry_count { + return Some(frequency); } - None => None, } + + None } pub(crate) async fn lock_acquire_release( @@ -392,3 +416,38 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_delay() { + let count = [10, 5, 3, 2]; + let frequency = [300, 600, 1800, 3600]; + + let retry_counts_and_expected_delays = [ + (-4, None), + (-2, None), + (0, None), + (4, Some(300)), + (7, Some(300)), + (10, Some(300)), + (12, Some(600)), + (16, Some(1800)), + (18, Some(1800)), + (20, Some(3600)), + (24, None), + (30, None), + ]; + + for (retry_count, expected_delay) in retry_counts_and_expected_delays { + let delay = get_delay(retry_count, count.iter().zip(frequency.iter())); + + assert_eq!( + delay, expected_delay, + "Delay and expected delay differ for `retry_count` = {retry_count}" + ); + } + } +} diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index 5add5036f082..7ae41f79c6b8 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -148,6 +148,7 @@ impl PaymentAttemptInterface for MockDb { unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, mandate_data: payment_attempt.mandate_data, + payment_method_billing_address_id: payment_attempt.payment_method_billing_address_id, fingerprint_id: payment_attempt.fingerprint_id, }; payment_attempts.push(payment_attempt.clone()); diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index a51f95e1e749..e7c6ad01efc3 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -74,6 +74,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, connector_transaction_id: &str, @@ -96,6 +97,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( &self, payment_id: &str, @@ -137,6 +139,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -181,6 +184,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn get_filters_for_payments( &self, pi: &[PaymentIntent], @@ -218,6 +222,7 @@ impl PaymentAttemptInterface for RouterStore { ) } + #[instrument(skip_all)] async fn find_payment_attempt_by_preprocessing_id_merchant_id( &self, preprocessing_id: &str, @@ -239,6 +244,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn find_attempts_by_merchant_id_payment_id( &self, merchant_id: &str, @@ -259,6 +265,7 @@ impl PaymentAttemptInterface for RouterStore { }) } + #[instrument(skip_all)] async fn find_payment_attempt_by_attempt_id_merchant_id( &self, attempt_id: &str, @@ -276,6 +283,7 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] async fn get_total_count_of_filtered_payment_attempts( &self, merchant_id: &str, @@ -391,6 +399,9 @@ impl PaymentAttemptInterface for KVRouterStore { unified_code: payment_attempt.unified_code.clone(), unified_message: payment_attempt.unified_message.clone(), mandate_data: payment_attempt.mandate_data.clone(), + payment_method_billing_address_id: payment_attempt + .payment_method_billing_address_id + .clone(), fingerprint_id: payment_attempt.fingerprint_id.clone(), }; @@ -557,6 +568,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, connector_transaction_id: &str, @@ -604,6 +616,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( &self, payment_id: &str, @@ -652,6 +665,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( &self, payment_id: &str, @@ -703,6 +717,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -803,6 +818,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_by_attempt_id_merchant_id( &self, attempt_id: &str, @@ -859,6 +875,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_payment_attempt_by_preprocessing_id_merchant_id( &self, preprocessing_id: &str, @@ -915,6 +932,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn find_attempts_by_merchant_id_payment_id( &self, merchant_id: &str, @@ -954,6 +972,7 @@ impl PaymentAttemptInterface for KVRouterStore { } } + #[instrument(skip_all)] async fn get_filters_for_payments( &self, pi: &[PaymentIntent], @@ -965,6 +984,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await } + #[instrument(skip_all)] async fn get_total_count_of_filtered_payment_attempts( &self, merchant_id: &str, @@ -1106,6 +1126,7 @@ impl DataModelExt for PaymentAttempt { unified_code: self.unified_code, unified_message: self.unified_message, mandate_data: self.mandate_data.map(|d| d.to_storage_model()), + payment_method_billing_address_id: self.payment_method_billing_address_id, fingerprint_id: self.fingerprint_id, } } @@ -1165,6 +1186,7 @@ impl DataModelExt for PaymentAttempt { mandate_data: storage_model .mandate_data .map(MandateDetails::from_storage_model), + payment_method_billing_address_id: storage_model.payment_method_billing_address_id, fingerprint_id: storage_model.fingerprint_id, } } @@ -1222,6 +1244,7 @@ impl DataModelExt for PaymentAttemptNew { unified_code: self.unified_code, unified_message: self.unified_message, mandate_data: self.mandate_data.map(|d| d.to_storage_model()), + payment_method_billing_address_id: self.payment_method_billing_address_id, fingerprint_id: self.fingerprint_id, } } @@ -1279,6 +1302,7 @@ impl DataModelExt for PaymentAttemptNew { mandate_data: storage_model .mandate_data .map(MandateDetails::from_storage_model), + payment_method_billing_address_id: storage_model.payment_method_billing_address_id, fingerprint_id: storage_model.fingerprint_id, } } @@ -1383,6 +1407,7 @@ impl DataModelExt for PaymentAttemptUpdate { fingerprint_id, updated_by, merchant_connector_id: connector_id, + payment_method_billing_address_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { amount, currency, @@ -1405,6 +1430,7 @@ impl DataModelExt for PaymentAttemptUpdate { fingerprint_id, updated_by, merchant_connector_id: connector_id, + payment_method_billing_address_id, }, Self::VoidUpdate { status, @@ -1655,6 +1681,7 @@ impl DataModelExt for PaymentAttemptUpdate { fingerprint_id, updated_by, merchant_connector_id: connector_id, + payment_method_billing_address_id, } => Self::ConfirmUpdate { amount, currency, @@ -1677,6 +1704,7 @@ impl DataModelExt for PaymentAttemptUpdate { fingerprint_id, updated_by, merchant_connector_id: connector_id, + payment_method_billing_address_id, }, DieselPaymentAttemptUpdate::VoidUpdate { status, @@ -1855,6 +1883,7 @@ impl DataModelExt for PaymentAttemptUpdate { } #[inline] +#[instrument(skip_all)] async fn add_connector_txn_id_to_reverse_lookup( store: &KVRouterStore, key: &str, @@ -1877,6 +1906,7 @@ async fn add_connector_txn_id_to_reverse_lookup( } #[inline] +#[instrument(skip_all)] async fn add_preprocessing_id_to_reverse_lookup( store: &KVRouterStore, key: &str, diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index d201d2a053a1..1dc0b1f90f30 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -316,6 +316,7 @@ impl PaymentIntentInterface for KVRouterStore { #[async_trait::async_trait] impl PaymentIntentInterface for crate::RouterStore { + #[instrument(skip_all)] async fn insert_payment_intent( &self, new: PaymentIntentNew, @@ -332,6 +333,7 @@ impl PaymentIntentInterface for crate::RouterStore { .map(PaymentIntent::from_storage_model) } + #[instrument(skip_all)] async fn update_payment_intent( &self, this: PaymentIntent, @@ -366,6 +368,7 @@ impl PaymentIntentInterface for crate::RouterStore { }) } + #[instrument(skip_all)] async fn get_active_payment_attempt( &self, payment: &mut PaymentIntent, @@ -394,6 +397,7 @@ impl PaymentIntentInterface for crate::RouterStore { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_payment_intent_by_constraints( &self, merchant_id: &str, @@ -507,6 +511,7 @@ impl PaymentIntentInterface for crate::RouterStore { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn filter_payment_intents_by_time_range_constraints( &self, merchant_id: &str, @@ -520,6 +525,7 @@ impl PaymentIntentInterface for crate::RouterStore { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn get_filtered_payment_intents_attempt( &self, merchant_id: &str, @@ -660,6 +666,7 @@ impl PaymentIntentInterface for crate::RouterStore { } #[cfg(feature = "olap")] + #[instrument(skip_all)] async fn get_filtered_active_attempt_ids_for_total_count( &self, merchant_id: &str, diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index ceb188b74db6..59ff36e6cc5c 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -17,6 +17,7 @@ async-trait = "0.1.68" base64 = "0.21.2" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } rand = "0.8.5" +regex = "1.10.3" reqwest = { version = "0.11.18", features = ["native-tls"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" diff --git a/crates/test_utils/src/main.rs b/crates/test_utils/src/main.rs index 22c91e063d8f..006075895b53 100644 --- a/crates/test_utils/src/main.rs +++ b/crates/test_utils/src/main.rs @@ -16,28 +16,20 @@ 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(); + // Filter out None values leaving behind Some(Path) + let paths: Vec = runner.modified_file_paths.into_iter().flatten().collect(); + let git_status = Command::new("git").arg("restore").args(&paths).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}"); + match git_status { + Ok(output) => { + if !output.status.success() { + 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 { diff --git a/crates/test_utils/src/newman_runner.rs b/crates/test_utils/src/newman_runner.rs index 961853548a27..e70c630cffea 100644 --- a/crates/test_utils/src/newman_runner.rs +++ b/crates/test_utils/src/newman_runner.rs @@ -1,7 +1,14 @@ -use std::{env, io::Write, path::Path, process::Command}; +use std::{ + env, + fs::{self, OpenOptions}, + io::{self, Write}, + path::Path, + process::{exit, Command}, +}; use clap::{arg, command, Parser}; use masking::PeekInterface; +use regex::Regex; use crate::connector_auth::{ConnectorAuthType, ConnectorAuthenticationMap}; #[derive(Parser)] @@ -33,14 +40,24 @@ struct Args { pub struct ReturnArgs { pub newman_command: Command, - pub file_modified_flag: bool, + pub modified_file_paths: Vec>, pub collection_path: String, } -// Just by the name of the connector, this function generates the name of the collection dir +// Generates the name of the collection JSON file for the specified connector. +// Example: CONNECTOR_NAME="stripe" -> OUTPUT: postman/collection-json/stripe.postman_collection.json +#[inline] +fn get_collection_path(name: impl AsRef) -> String { + format!( + "postman/collection-json/{}.postman_collection.json", + name.as_ref() + ) +} + +// Generates the name of the collection directory for the specified connector. // Example: CONNECTOR_NAME="stripe" -> OUTPUT: postman/collection-dir/stripe #[inline] -fn get_path(name: impl AsRef) -> String { +fn get_dir_path(name: impl AsRef) -> String { format!("postman/collection-dir/{}", name.as_ref()) } @@ -72,22 +89,34 @@ pub fn generate_newman_command() -> ReturnArgs { let base_url = args.base_url; let admin_api_key = args.admin_api_key; - let collection_path = get_path(&connector_name); + let collection_path = get_collection_path(&connector_name); + let collection_dir_path = get_dir_path(&connector_name); let auth_map = ConnectorAuthenticationMap::new(); let inner_map = auth_map.inner(); - // Newman runner - // Depending on the conditions satisfied, variables are added. Since certificates of stripe have already - // been added to the postman collection, those conditions are set to true and collections that have - // variables set up for certificate, will consider those variables and will fail. + /* + Newman runner + Certificate keys are added through secrets in CI, so there's no need to explicitly pass it as arguments. + It can be overridden by explicitly passing certificates as arguments. + + If the collection requires certificates (Stripe collection for example) during the merchant connector account create step, + then Stripe's certificates will be passed implicitly (for now). + If any other connector requires certificates to be passed, that has to be passed explicitly for now. + */ let mut newman_command = Command::new("newman"); - newman_command.args(["dir-run", &collection_path]); + newman_command.args(["run", &collection_path]); newman_command.args(["--env-var", &format!("admin_api_key={admin_api_key}")]); newman_command.args(["--env-var", &format!("baseUrl={base_url}")]); - if let Some(auth_type) = inner_map.get(&connector_name) { + let custom_header_exist = check_for_custom_headers(args.custom_headers, &collection_dir_path); + + // validation of connector is needed here as a work around to the limitation of the fork of newman that Hyperswitch uses + let (connector_name, modified_collection_file_paths) = + check_connector_for_dynamic_amount(&connector_name); + + if let Some(auth_type) = inner_map.get(connector_name) { match auth_type { ConnectorAuthType::HeaderKey { api_key } => { newman_command.args([ @@ -187,24 +216,126 @@ pub fn generate_newman_command() -> ReturnArgs { newman_command.arg("--verbose"); } - let mut modified = false; - if let Some(headers) = &args.custom_headers { + ReturnArgs { + newman_command, + modified_file_paths: vec![modified_collection_file_paths, custom_header_exist], + collection_path, + } +} + +pub fn check_for_custom_headers(headers: Option>, path: &str) -> Option { + if let Some(headers) = &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; + + if let Err(err) = insert_content(path, &content_to_insert) { + eprintln!("An error occurred while inserting the custom header: {err}"); } } else { eprintln!("Invalid header format: {}", header); } } + + return Some(format!("{}/event.prerequest.js", path)); } + None +} - ReturnArgs { - newman_command, - file_modified_flag: modified, - collection_path, +// If the connector name exists in dynamic_amount_connectors, +// the corresponding collection is modified at runtime to remove double quotes +pub fn check_connector_for_dynamic_amount(connector_name: &str) -> (&str, Option) { + let collection_dir_path = get_dir_path(connector_name); + + let dynamic_amount_connectors = ["nmi", "powertranz"]; + + if dynamic_amount_connectors.contains(&connector_name) { + return remove_quotes_for_integer_values(connector_name).unwrap_or((connector_name, None)); + } + /* + If connector name does not exist in dynamic_amount_connectors but we want to run it with custom headers, + since we're running from collections directly, we'll have to export the collection again and it is much simpler. + We could directly inject the custom-headers using regex, but it is not encouraged as it is hard + to determine the place of edit. + */ + export_collection(connector_name, collection_dir_path); + + (connector_name, None) +} + +/* +Existing issue with the fork of newman is that, it requires you to pass variables like `{{value}}` within +double quotes without which it fails to execute. +For integer values like `amount`, this is a bummer as it flags the value stating it is of type +string and not integer. +Refactoring is done in 2 steps: +- Export the collection to json (although the json will be up-to-date, we export it again for safety) +- Use regex to replace the values which removes double quotes from integer values + Ex: \"{{amount}}\" -> {{amount}} +*/ + +pub fn remove_quotes_for_integer_values( + connector_name: &str, +) -> Result<(&str, Option), io::Error> { + let collection_path = get_collection_path(connector_name); + let collection_dir_path = get_dir_path(connector_name); + + let values_to_replace = [ + "amount", + "another_random_number", + "capture_amount", + "random_number", + "refund_amount", + ]; + + export_collection(connector_name, collection_dir_path); + + let mut contents = fs::read_to_string(&collection_path)?; + for value_to_replace in values_to_replace { + if let Ok(re) = Regex::new(&format!( + r#"\\"(?P\{{\{{{}\}}\}})\\""#, + value_to_replace + )) { + contents = re.replace_all(&contents, "$field").to_string(); + } else { + eprintln!("Regex validation failed."); + } + + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(&collection_path)?; + + file.write_all(contents.as_bytes())?; + } + + Ok((connector_name, Some(collection_path))) +} + +pub fn export_collection(connector_name: &str, collection_dir_path: String) { + let collection_path = get_collection_path(connector_name); + + let mut newman_command = Command::new("newman"); + newman_command.args([ + "dir-import".to_owned(), + collection_dir_path, + "-o".to_owned(), + collection_path.clone(), + ]); + + match newman_command.spawn().and_then(|mut child| child.wait()) { + Ok(exit_status) => { + if exit_status.success() { + println!("Conversion of collection from directory structure to json successful!"); + } else { + eprintln!("Conversion of collection from directory structure to json failed!"); + exit(exit_status.code().unwrap_or(1)); + } + } + Err(err) => { + eprintln!("Failed to execute dir-import: {:?}", err); + exit(1); + } } } diff --git a/crates/test_utils/tests/connectors/selenium.rs b/crates/test_utils/tests/connectors/selenium.rs index 303d4cd7ccbf..5030b984861b 100644 --- a/crates/test_utils/tests/connectors/selenium.rs +++ b/crates/test_utils/tests/connectors/selenium.rs @@ -9,7 +9,7 @@ use std::{ collections::{HashMap, HashSet}, env, io::Read, - path::MAIN_SEPARATOR, + path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}, time::Duration, }; @@ -862,7 +862,7 @@ fn get_chrome_profile_path() -> Result { .map(|str| { let mut fp = str.split(MAIN_SEPARATOR).collect::>(); fp.truncate(3); - fp.join(&MAIN_SEPARATOR.to_string()) + fp.join(MAIN_SEPARATOR_STR) }) .unwrap(); if env::consts::OS == "macos" { @@ -880,7 +880,7 @@ fn get_firefox_profile_path() -> Result { .map(|str| { let mut fp = str.split(MAIN_SEPARATOR).collect::>(); fp.truncate(3); - fp.join(&MAIN_SEPARATOR.to_string()) + fp.join(MAIN_SEPARATOR_STR) }) .unwrap(); if env::consts::OS == "macos" { diff --git a/loadtest/Dockerfile b/loadtest/Dockerfile index 45c4c7540a19..ebea439eb816 100644 --- a/loadtest/Dockerfile +++ b/loadtest/Dockerfile @@ -1,10 +1,10 @@ -FROM rust:1.65 AS builder +FROM rust:latest AS builder WORKDIR /app COPY . . RUN cargo install diesel_cli && cargo build --bin router --release -FROM rust:1.65 AS runtime +FROM rust:latest AS runtime WORKDIR /app COPY --from=builder /app/migrations migrations COPY --from=builder /app/target/release/router router diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2f35958ae6d0..aa0d75d609fc 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -191,6 +191,10 @@ open_banking_uk = {country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT [pm_filters.adyen] boleto = { country = "BR", currency = "BRL" } +sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD"} +ideal = { country = "NL", currency = "EUR" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } diff --git a/migrations/2024-01-22-091431_create_authentication_table/down.sql b/migrations/2024-01-22-091431_create_authentication_table/down.sql new file mode 100644 index 000000000000..4e6e99bb6be7 --- /dev/null +++ b/migrations/2024-01-22-091431_create_authentication_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS authentication; diff --git a/migrations/2024-01-22-091431_create_authentication_table/up.sql b/migrations/2024-01-22-091431_create_authentication_table/up.sql new file mode 100644 index 000000000000..553b8e5b0e1c --- /dev/null +++ b/migrations/2024-01-22-091431_create_authentication_table/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS authentication ( + authentication_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + authentication_connector VARCHAR(64) NOT NULL, + connector_authentication_id VARCHAR(64), + authentication_data JSONB, + payment_method_id VARCHAR(64) NOT NULL, + authentication_type VARCHAR(64), + authentication_status VARCHAR(64) NOT NULL, + authentication_lifecycle_status VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + error_message VARCHAR(64), + error_code VARCHAR(64), + PRIMARY KEY (authentication_id) +); diff --git a/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/down.sql b/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/down.sql new file mode 100644 index 000000000000..66a0e41f25b4 --- /dev/null +++ b/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payouts DROP COLUMN attempt_count; diff --git a/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/up.sql b/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/up.sql new file mode 100644 index 000000000000..7819ca130a75 --- /dev/null +++ b/migrations/2024-02-05-123412_add_attempt_count_column_to_payouts/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +ALTER TABLE payouts +ADD COLUMN attempt_count SMALLINT NOT NULL DEFAULT 1; + + +UPDATE payouts +SET attempt_count = payout_id_count.count +FROM (SELECT payout_id, count(payout_id) FROM payout_attempt GROUP BY payout_id) as payout_id_count +WHERE payouts.payout_id = payout_id_count.payout_id; diff --git a/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/down.sql b/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/down.sql new file mode 100644 index 000000000000..0b9bf9a0741f --- /dev/null +++ b/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE authentication DROP COLUMN IF EXISTS connector_metadata; diff --git a/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/up.sql b/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/up.sql new file mode 100644 index 000000000000..919b65e7e543 --- /dev/null +++ b/migrations/2024-02-20-180952_add_connector_metadata_in_authentication/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE authentication ADD COLUMN IF NOT EXISTS connector_metadata JSONB DEFAULT NULL; diff --git a/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/down.sql b/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/down.sql new file mode 100644 index 000000000000..3ca40852fe41 --- /dev/null +++ b/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS payment_method_billing_address_id; diff --git a/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/up.sql b/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/up.sql new file mode 100644 index 000000000000..7cedd57f374c --- /dev/null +++ b/migrations/2024-02-21-101951_add_payment_method_billing_to_payment_attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt +ADD COLUMN IF NOT EXISTS payment_method_billing_address_id VARCHAR(64); diff --git a/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql new file mode 100644 index 000000000000..fc9fc6350edd --- /dev/null +++ b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_methods DROP COLUMN IF EXISTS last_used_at; \ No newline at end of file diff --git a/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql new file mode 100644 index 000000000000..f1c0aab4def1 --- /dev/null +++ b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP; \ No newline at end of file diff --git a/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql new file mode 100644 index 000000000000..948123ce7e22 --- /dev/null +++ b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE customers DROP COLUMN IF EXISTS default_payment_method; diff --git a/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql new file mode 100644 index 000000000000..abaeb1df7180 --- /dev/null +++ b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE customers +ADD COLUMN IF NOT EXISTS default_payment_method_id VARCHAR(64); \ No newline at end of file diff --git a/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/down.sql b/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/down.sql new file mode 100644 index 000000000000..e89bb9a356b8 --- /dev/null +++ b/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/down.sql @@ -0,0 +1,17 @@ +-- This file should undo anything in `up.sql` +-- Drop the new column +ALTER TABLE dispute +DROP COLUMN IF EXISTS dispute_amount; + +-- Optionally, if you want to revert the UPDATE statement as well (assuming you have a backup) +-- You can restore the original data from the backup or use the old values to update the table + +-- For example, if you have a backup and want to revert the changes made by the UPDATE statement +-- You can replace the data with the backup data or the original values +-- For demonstration purposes, we're assuming you have a backup table named dispute_backup + +-- Restore the original values from the backup or any other source +-- UPDATE dispute +-- SET dispute_amount = backup.dispute_amount +-- FROM dispute_backup AS backup +-- WHERE dispute.id = backup.id; diff --git a/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/up.sql b/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/up.sql new file mode 100644 index 000000000000..b56ffc4fe9c1 --- /dev/null +++ b/migrations/2024-02-28-103308_add_dispute_amount_to_dispute/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +-- Add the new column with a default value +ALTER TABLE dispute +ADD COLUMN dispute_amount BIGINT NOT NULL DEFAULT 0; + +-- Update existing rows to set the default value based on the integer equivalent of the amount column +UPDATE dispute +SET dispute_amount = CAST(amount AS BIGINT); diff --git a/monitoring/docker-compose-ckh.yaml b/monitoring/docker-compose-ckh.yaml index 2bbf5b2ec4ac..88fc88d133a4 100644 --- a/monitoring/docker-compose-ckh.yaml +++ b/monitoring/docker-compose-ckh.yaml @@ -156,7 +156,7 @@ services: hard: 262144 hyperswitch-server: - image: rust:1.65 + image: rust:latest command: cargo run -- -f ./config/docker_compose.toml working_dir: /app ports: diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 30e75c028d5e..293bfc85a7c8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4080,6 +4080,50 @@ } ] } + }, + "/{customer_id}/payment_methods/{payment_method_id}/default": { + "get": { + "tags": [ + "Customer Set Default Payment Method" + ], + "summary": "Customers - Set Default Payment Method", + "description": "Customers - Set Default Payment Method\n\nSet the Payment Method as Default for the Customer.", + "operationId": "Set the Payment Method as Default", + "parameters": [ + { + "name": "method_id", + "in": "path", + "description": "Set the Payment Method as Default for the Customer", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payment Method has been set as default", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerDefaultPaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Payment Method has already been set as default for that customer" + }, + "404": { + "description": "Payment Method not found for the customer" + } + }, + "security": [ + { + "ephemeral_key": [] + } + ] + } } }, "components": { @@ -5092,7 +5136,10 @@ "starling", "tsb_bank", "tesco_bank", - "ulster_bank" + "ulster_bank", + "yoursafe", + "n26", + "nationale_nederlanden" ] }, "BankRedirectBilling": { @@ -7308,6 +7355,37 @@ } } }, + "CustomerDefaultPaymentMethodResponse": { + "type": "object", + "required": [ + "customer_id", + "payment_method" + ], + "properties": { + "default_payment_method_id": { + "type": "string", + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy", + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw" + }, + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + } + } + }, "CustomerDeleteResponse": { "type": "object", "required": [ @@ -7384,11 +7462,13 @@ "type": "object", "required": [ "payment_token", + "payment_method_id", "customer_id", "payment_method", "recurring_enabled", "installment_payment_enabled", - "requires_cvv" + "requires_cvv", + "default_payment_method_set" ], "properties": { "payment_token": { @@ -7396,6 +7476,11 @@ "description": "Token for payment method in temporary card locker which gets refreshed often", "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" }, + "payment_method_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "pm_iouuy468iyuowqs" + }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -7495,6 +7580,18 @@ "type": "boolean", "description": "Whether this payment method requires CVV to be collected", "example": true + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was last used", + "example": "2024-02-24T11:04:09.922Z", + "nullable": true + }, + "default_payment_method_set": { + "type": "boolean", + "description": "Indicates if the payment method has been set to default or not", + "example": true } } }, @@ -7644,6 +7741,28 @@ "type": "object", "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", "nullable": true + }, + "default_payment_method_id": { + "type": "string", + "description": "The identifier for the default payment method.", + "example": "pm_djh2837dwduh890123", + "nullable": true, + "maxLength": 64 + } + } + }, + "DefaultPaymentMethod": { + "type": "object", + "required": [ + "customer_id", + "payment_method_id" + ], + "properties": { + "customer_id": { + "type": "string" + }, + "payment_method_id": { + "type": "string" } } }, @@ -11691,6 +11810,26 @@ } ] }, + "PaymentMethodDataRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodData" + }, + { + "type": "object", + "properties": { + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + } + } + } + ] + }, "PaymentMethodDeleteResponse": { "type": "object", "required": [ @@ -11888,6 +12027,12 @@ } ], "nullable": true + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "example": "2024-02-24T11:04:09.922Z", + "nullable": true } } }, @@ -12314,7 +12459,7 @@ "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/PaymentMethodDataRequest" } ], "nullable": true @@ -12671,7 +12816,7 @@ "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/PaymentMethodDataRequest" } ], "nullable": true @@ -13071,7 +13216,7 @@ "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/PaymentMethodDataRequest" } ], "nullable": true @@ -14068,7 +14213,7 @@ "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/PaymentMethodDataRequest" } ], "nullable": true diff --git a/postman/collection-dir/aci/Health check/New Request/request.json b/postman/collection-dir/aci/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/aci/Health check/New Request/request.json +++ b/postman/collection-dir/aci/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js index dcf3f1916430..cfbd9695bd1e 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Cancel/event.test.js @@ -22,7 +22,7 @@ pm.test("[POST]::/payments/:id/cancel - Response has JSON Body", function () { let jsonData = {}; try { jsonData = pm.response.json(); -} catch (e) {} +} catch (e) { } // pm.collectionVariables - Set payment_id as variable for jsonData.payment_id if (jsonData?.payment_id) { @@ -53,9 +53,9 @@ if (jsonData?.client_secret) { // Response body should have value "cancelled" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'", + "[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("cancelled"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js index 5e52e13a59e1..ec02aad86268 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario5-Void the payment/Payments - Retrieve/event.test.js @@ -19,7 +19,7 @@ pm.test("[GET]::/payments/:id - Response has JSON Body", function () { let jsonData = {}; try { jsonData = pm.response.json(); -} catch (e) {} +} catch (e) { } // pm.collectionVariables - Set payment_id as variable for jsonData.payment_id if (jsonData?.payment_id) { @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "cancelled" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'", + "[POST]::/payments/:id - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("cancelled"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index b5002aeca8e7..401201dff648 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -46,7 +46,7 @@ "key1": "{{connector_key1}}", "api_secret": "{{connector_api_secret}}" }, - "test_mode": false, + "test_mode": true, "disabled": false, "business_country": "US", "business_label": "default", @@ -56,12 +56,17 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["AmericanExpress", - "Discover", - "Interac", - "JCB", - "Mastercard", - "Visa", "DinersClub","UnionPay","RuPay"], + "card_networks": [ + "AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", + "DinersClub", + "UnionPay", + "RuPay" + ], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -69,12 +74,17 @@ }, { "payment_method_type": "debit", - "card_networks": ["AmericanExpress", - "Discover", - "Interac", - "JCB", - "Mastercard", - "Visa", "DinersClub","UnionPay","RuPay"], + "card_networks": [ + "AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", + "DinersClub", + "UnionPay", + "RuPay" + ], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -203,15 +213,15 @@ { "payment_method": "gift_card", "payment_method_types": [ - { - "payment_method_type": "givex", - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true - } + { + "payment_method_type": "givex", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } ] - }, + }, { "payment_method": "bank_redirect", "payment_method_types": [ @@ -303,7 +313,10 @@ { "type": "CARD", "parameters": { - "allowed_auth_methods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowed_auth_methods": [ + "PAN_ONLY", + "CRYPTOGRAM_3DS" + ], "allowed_card_networks": [ "AMEX", "DISCOVER", @@ -327,8 +340,14 @@ }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": ["{{baseUrl}}"], - "path": ["account", ":account_id", "connectors"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], "variable": [ { "key": "account_id", @@ -338,4 +357,4 @@ ] }, "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." -} +} \ No newline at end of file diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json index 0ba1b1689c38..4d3b41b7c0a0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -271,6 +271,11 @@ "payment_method_type": "bacs", "recurring_enabled": true, "installment_payment_enabled": true + }, + { + "payment_method_type": "sepa", + "recurring_enabled": true, + "installment_payment_enabled": true } ] } diff --git a/postman/collection-dir/adyen_uk/Health check/New Request/request.json b/postman/collection-dir/adyen_uk/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/adyen_uk/Health check/New Request/request.json +++ b/postman/collection-dir/adyen_uk/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/airwallex/Health check/New Request/request.json b/postman/collection-dir/airwallex/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/airwallex/Health check/New Request/request.json +++ b/postman/collection-dir/airwallex/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/authorizedotnet/Health check/New Request/request.json b/postman/collection-dir/authorizedotnet/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/authorizedotnet/Health check/New Request/request.json +++ b/postman/collection-dir/authorizedotnet/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/bambora/Health check/New Request/request.json b/postman/collection-dir/bambora/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/bambora/Health check/New Request/request.json +++ b/postman/collection-dir/bambora/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/bambora_3ds/Health check/New Request/request.json b/postman/collection-dir/bambora_3ds/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/bambora_3ds/Health check/New Request/request.json +++ b/postman/collection-dir/bambora_3ds/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/bankofamerica/Health check/New Request/request.json b/postman/collection-dir/bankofamerica/Health check/New Request/request.json index 4cc8d4b1a966..ce92926c776c 100644 --- a/postman/collection-dir/bankofamerica/Health check/New Request/request.json +++ b/postman/collection-dir/bankofamerica/Health check/New Request/request.json @@ -1,20 +1,9 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] + "host": ["{{baseUrl}}"], + "path": ["health"] } } diff --git a/postman/collection-dir/bluesnap/Health check/New Request/request.json b/postman/collection-dir/bluesnap/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/bluesnap/Health check/New Request/request.json +++ b/postman/collection-dir/bluesnap/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/braintree/Health check/New Request/request.json b/postman/collection-dir/braintree/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/braintree/Health check/New Request/request.json +++ b/postman/collection-dir/braintree/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/checkout/Health check/New Request/request.json b/postman/collection-dir/checkout/Health check/New Request/request.json index 4cc8d4b1a966..ce92926c776c 100644 --- a/postman/collection-dir/checkout/Health check/New Request/request.json +++ b/postman/collection-dir/checkout/Health check/New Request/request.json @@ -1,20 +1,9 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] + "host": ["{{baseUrl}}"], + "path": ["health"] } } diff --git a/postman/collection-dir/forte/Health check/New Request/request.json b/postman/collection-dir/forte/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/forte/Health check/New Request/request.json +++ b/postman/collection-dir/forte/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/globalpay/Health check/New Request/request.json b/postman/collection-dir/globalpay/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/globalpay/Health check/New Request/request.json +++ b/postman/collection-dir/globalpay/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/hyperswitch/Health check/New Request/request.json b/postman/collection-dir/hyperswitch/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/hyperswitch/Health check/New Request/request.json +++ b/postman/collection-dir/hyperswitch/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/mollie/Health check/New Request/request.json b/postman/collection-dir/mollie/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/mollie/Health check/New Request/request.json +++ b/postman/collection-dir/mollie/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/multisafepay/Health check/New Request/request.json b/postman/collection-dir/multisafepay/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/multisafepay/Health check/New Request/request.json +++ b/postman/collection-dir/multisafepay/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/nexinets/Health check/New Request/request.json b/postman/collection-dir/nexinets/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/nexinets/Health check/New Request/request.json +++ b/postman/collection-dir/nexinets/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Confirm/request.json b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Confirm/request.json index 6c99817eb047..7b72495e5c4c 100644 --- a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Confirm/request.json +++ b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Confirm/request.json @@ -29,12 +29,7 @@ "key": "Accept", "value": "application/json" }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, + , { "key": "publishable_key", "value": "", @@ -79,14 +74,8 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], "variable": [ { "key": "id", diff --git a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Update/request.json b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Update/request.json index 2d931088d180..bad528598247 100644 --- a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Update/request.json +++ b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9-Update amount with automatic capture/Payments - Update/request.json @@ -18,18 +18,14 @@ } }, "raw_json_formatted": { - "amount": "{{another_random_number}}" + "amount": "{{another_random_number}}", + "amount_to_capture": "{{another_random_number}}" } }, "url": { "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], "variable": [ { "key": "id", diff --git a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Confirm/request.json b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Confirm/request.json index 6c99817eb047..7b72495e5c4c 100644 --- a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Confirm/request.json +++ b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Confirm/request.json @@ -29,12 +29,7 @@ "key": "Accept", "value": "application/json" }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, + , { "key": "publishable_key", "value": "", @@ -79,14 +74,8 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], "variable": [ { "key": "id", diff --git a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Update/request.json b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Update/request.json index 2d931088d180..bad528598247 100644 --- a/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Update/request.json +++ b/postman/collection-dir/nmi/Flow Testcases/Happy Cases/Scenario9a-Update amount with manual capture/Payments - Update/request.json @@ -18,18 +18,14 @@ } }, "raw_json_formatted": { - "amount": "{{another_random_number}}" + "amount": "{{another_random_number}}", + "amount_to_capture": "{{another_random_number}}" } }, "url": { "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], "variable": [ { "key": "id", diff --git a/postman/collection-dir/nmi/Health check/New Request/request.json b/postman/collection-dir/nmi/Health check/New Request/request.json index 4cc8d4b1a966..ce92926c776c 100644 --- a/postman/collection-dir/nmi/Health check/New Request/request.json +++ b/postman/collection-dir/nmi/Health check/New Request/request.json @@ -1,20 +1,9 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] + "host": ["{{baseUrl}}"], + "path": ["health"] } } diff --git a/postman/collection-dir/payme/Health check/New Request/request.json b/postman/collection-dir/payme/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/payme/Health check/New Request/request.json +++ b/postman/collection-dir/payme/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/paypal/Health check/New Request/request.json b/postman/collection-dir/paypal/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/paypal/Health check/New Request/request.json +++ b/postman/collection-dir/paypal/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/powertranz/Health check/New Request/request.json b/postman/collection-dir/powertranz/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/powertranz/Health check/New Request/request.json +++ b/postman/collection-dir/powertranz/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/rapyd/Health check/New Request/request.json b/postman/collection-dir/rapyd/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/rapyd/Health check/New Request/request.json +++ b/postman/collection-dir/rapyd/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/shift4/Health check/New Request/request.json b/postman/collection-dir/shift4/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/shift4/Health check/New Request/request.json +++ b/postman/collection-dir/shift4/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/event.prerequest.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Confirm/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/event.prerequest.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create a payment and Confirm/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json index fed600e09cd9..dfe421f512fb 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json @@ -24,23 +24,12 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true } ], "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - "payment_methods" - ], + "host": ["{{baseUrl}}"], + "path": ["account", "payment_methods"], "query": [ { "key": "client_secret", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json index fed600e09cd9..dfe421f512fb 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json @@ -24,23 +24,12 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true } ], "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - "payment_methods" - ], + "host": ["{{baseUrl}}"], + "path": ["account", "payment_methods"], "query": [ { "key": "client_secret", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario23- Update Amount/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario23- Update Amount/Payments - Confirm/request.json index 5b0c090f2be1..5a51941cf640 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario23- Update Amount/Payments - Confirm/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario23- Update Amount/Payments - Confirm/request.json @@ -29,12 +29,7 @@ "key": "Accept", "value": "application/json" }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, + , { "key": "publishable_key", "value": "", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/event.test.js new file mode 100644 index 000000000000..d18cc474f875 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/event.test.js @@ -0,0 +1,89 @@ +// 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; + }, +); + +// Response body should have "payment_method_data.billing" +pm.test( + "[POST]::/payments - Content check if 'payment_method_data.billing' exists", + function () { + pm.expect(typeof jsonData.payment_method_data.billing !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/request.json new file mode 100644 index 000000000000..7a755ed4e6eb --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/request.json @@ -0,0 +1,108 @@ +{ + "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": 6540, + "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": "26", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + }, + "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/Scenario29-Create payment with payment method billing/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..808bc500b895 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/event.test.js @@ -0,0 +1,89 @@ +// 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; + }, +); + +// Response body should have "payment_method_data.billing" +pm.test( + "[POST]::/payments - Content check if 'payment_method_data.billing' exists", + function () { + pm.expect(typeof jsonData.payment_method_data.billing !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "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/Scenario29-Create payment with payment method billing/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario29-Create payment with payment method billing/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/.meta.json new file mode 100644 index 000000000000..66671828f4b6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Update", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/event.test.js new file mode 100644 index 000000000000..87c1a31e472f --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/event.test.js @@ -0,0 +1,58 @@ +// 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 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 "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/request.json new file mode 100644 index 000000000000..95c2541e0bca --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/request.json @@ -0,0 +1,70 @@ +{ + "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": false, + "business_country": "US", + "business_label": "default", + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "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", + "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/Scenario30-Update payment with payment method billing/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..1f8005b2b7d0 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/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 "requires_confirmation" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'", + function () { + pm.expect(jsonData.status).to.eql("requires_confirmation"); + }, + ); +} + +// Response body should have "payment_method_data.billing" +pm.test( + "[POST]::/payments - Content check if 'payment_method_data.billing' exists", + function () { + pm.expect(typeof jsonData.payment_method_data.billing !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "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/Scenario30-Update payment with payment method billing/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/event.test.js new file mode 100644 index 000000000000..5bb946ab51a7 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/event.test.js @@ -0,0 +1,41 @@ +// 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) { } + +// Response body should have value "requires_confirmation" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_confirmation"); + }, + ); +} + +// Response body should have "payment_method_data.billing" +pm.test( + "[POST]::/payments - Content check if 'payment_method_data.billing' exists", + function () { + pm.expect(typeof jsonData.payment_method_data.billing !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/request.json new file mode 100644 index 000000000000..a8fc9965c7dd --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/request.json @@ -0,0 +1,59 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "01", + "card_exp_year": "26", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "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/Scenario30-Update payment with payment method billing/Payments - Update/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario30-Update payment with payment method billing/Payments - Update/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..f92fba3d0440 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - 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/:id/confirm - 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/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/request.json new file mode 100644 index 000000000000..b92a83b5adc6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/request.json @@ -0,0 +1,93 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_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_json_formatted": { + "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": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// 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 "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "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": false, + "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", + "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", + "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/Scenario31-Pass payment method billing in Confirm/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..00a1aa1ce9a0 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/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 "payment_method_data.billing" +pm.test( + "[POST]::/payments - Content check if 'payment_method_data.billing' exists", + function () { + pm.expect(typeof jsonData.payment_method_data.billing !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/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/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario31-Pass payment method billing in Confirm/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Health check/New Request/request.json b/postman/collection-dir/stripe/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/stripe/Health check/New Request/request.json +++ b/postman/collection-dir/stripe/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/trustpay/Health check/New Request/request.json b/postman/collection-dir/trustpay/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/trustpay/Health check/New Request/request.json +++ b/postman/collection-dir/trustpay/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/volt/Health check/New Request/request.json b/postman/collection-dir/volt/Health check/New Request/request.json index 4cc8d4b1a966..ce92926c776c 100644 --- a/postman/collection-dir/volt/Health check/New Request/request.json +++ b/postman/collection-dir/volt/Health check/New Request/request.json @@ -1,20 +1,9 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] + "host": ["{{baseUrl}}"], + "path": ["health"] } } diff --git a/postman/collection-dir/wise/Health check/Health/request.json b/postman/collection-dir/wise/Health check/Health/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/wise/Health check/Health/request.json +++ b/postman/collection-dir/wise/Health check/Health/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/worldline/Health check/New Request/request.json b/postman/collection-dir/worldline/Health check/New Request/request.json index e40e93961785..ce92926c776c 100644 --- a/postman/collection-dir/worldline/Health check/New Request/request.json +++ b/postman/collection-dir/worldline/Health check/New Request/request.json @@ -1,13 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-dir/zen/Health check/New Request/request.json b/postman/collection-dir/zen/Health check/New Request/request.json index 9b836ff05cb9..24ea5a4157a0 100644 --- a/postman/collection-dir/zen/Health check/New Request/request.json +++ b/postman/collection-dir/zen/Health check/New Request/request.json @@ -3,14 +3,7 @@ "type": "noauth" }, "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], diff --git a/postman/collection-json/aci.postman_collection.json b/postman/collection-json/aci.postman_collection.json index 9d58d212f973..6bf1ad2b1d69 100644 --- a/postman/collection-json/aci.postman_collection.json +++ b/postman/collection-json/aci.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index be116b75557b..1092f8bb71e3 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -90,14 +90,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ @@ -472,7 +465,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"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\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"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},{\"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_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":true,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"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\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"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},{\"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_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -609,7 +602,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"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\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"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},{\"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_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"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\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"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},{\"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_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -3020,7 +3013,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", @@ -3051,9 +3044,9 @@ "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -3134,7 +3127,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", @@ -3178,9 +3171,9 @@ "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", diff --git a/postman/collection-json/airwallex.postman_collection.json b/postman/collection-json/airwallex.postman_collection.json index 7a0d9164e922..f2e804f2b3ec 100644 --- a/postman/collection-json/airwallex.postman_collection.json +++ b/postman/collection-json/airwallex.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/authorizedotnet.postman_collection.json b/postman/collection-json/authorizedotnet.postman_collection.json index 8130f1fd9a10..4008c4f9ceee 100644 --- a/postman/collection-json/authorizedotnet.postman_collection.json +++ b/postman/collection-json/authorizedotnet.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/bambora.postman_collection.json b/postman/collection-json/bambora.postman_collection.json index d9161f7929c6..b6e689790ee6 100644 --- a/postman/collection-json/bambora.postman_collection.json +++ b/postman/collection-json/bambora.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/bambora_3ds.postman_collection.json b/postman/collection-json/bambora_3ds.postman_collection.json index ca4999c9dfdb..0d908b26bf83 100644 --- a/postman/collection-json/bambora_3ds.postman_collection.json +++ b/postman/collection-json/bambora_3ds.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json index 752b77dcd150..1850cee8b609 100644 --- a/postman/collection-json/bankofamerica.postman_collection.json +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index fa6c9258b8d0..adb18e9970f5 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/braintree.postman_collection.json b/postman/collection-json/braintree.postman_collection.json index dce605f85c78..96879450ee91 100644 --- a/postman/collection-json/braintree.postman_collection.json +++ b/postman/collection-json/braintree.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index d510b1c2a17f..aedd67ecfa8b 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/forte.postman_collection.json b/postman/collection-json/forte.postman_collection.json index 8297b4778f4a..96a64d519e70 100644 --- a/postman/collection-json/forte.postman_collection.json +++ b/postman/collection-json/forte.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/globalpay.postman_collection.json b/postman/collection-json/globalpay.postman_collection.json index 2a2d27a87ca4..205275b052a2 100644 --- a/postman/collection-json/globalpay.postman_collection.json +++ b/postman/collection-json/globalpay.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/hyperswitch.postman_collection.json b/postman/collection-json/hyperswitch.postman_collection.json index 0acf2ee2b3fc..57186715422b 100644 --- a/postman/collection-json/hyperswitch.postman_collection.json +++ b/postman/collection-json/hyperswitch.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/mollie.postman_collection.json b/postman/collection-json/mollie.postman_collection.json index f956058b5ca8..e2649a261fa3 100644 --- a/postman/collection-json/mollie.postman_collection.json +++ b/postman/collection-json/mollie.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/multisafepay.postman_collection.json b/postman/collection-json/multisafepay.postman_collection.json index 50a2655c5b05..290a321fcfcc 100644 --- a/postman/collection-json/multisafepay.postman_collection.json +++ b/postman/collection-json/multisafepay.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/nexinets.postman_collection.json b/postman/collection-json/nexinets.postman_collection.json index cb74918d194f..aa0c435f498c 100644 --- a/postman/collection-json/nexinets.postman_collection.json +++ b/postman/collection-json/nexinets.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/payme.postman_collection.json b/postman/collection-json/payme.postman_collection.json index 280a131386e5..5451f143cb39 100644 --- a/postman/collection-json/payme.postman_collection.json +++ b/postman/collection-json/payme.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index 83f27e69dc56..a4a64bde1533 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -81,14 +81,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/powertranz.postman_collection.json b/postman/collection-json/powertranz.postman_collection.json index ad81a38c1f0b..b158827bbb75 100644 --- a/postman/collection-json/powertranz.postman_collection.json +++ b/postman/collection-json/powertranz.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/rapyd.postman_collection.json b/postman/collection-json/rapyd.postman_collection.json index b3d6a9416f84..39ea3aab24cb 100644 --- a/postman/collection-json/rapyd.postman_collection.json +++ b/postman/collection-json/rapyd.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/shift4.postman_collection.json b/postman/collection-json/shift4.postman_collection.json index 13b4f25c3c65..196dcbcb30b4 100644 --- a/postman/collection-json/shift4.postman_collection.json +++ b/postman/collection-json/shift4.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 7df7ba5158db..78a1777864a7 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -6312,10 +6312,10 @@ "name": "Happy Cases", "item": [ { - "name": "Scenario28-Confirm a payment with requires_customer_action status", + "name": "Scenario2-Create a payment and Confirm", "item": [ { - "name": "Payments - Create with confirm true", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -6383,104 +6383,24 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"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,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"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\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"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\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_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 - Confirm", - "event": [ { - "listen": "test", + "listen": "prerequest", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 400\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// 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) { }", - "", - "", - "// Response body should have appropriatae error message", - "if (jsonData?.message) {", - " pm.test(", - " \"Content check if appropriate error message is present\",", - " function () {", - " pm.expect(jsonData.message).to.eql(\"You cannot confirm this payment because it has status requires_customer_action\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -6488,26 +6408,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -6526,56 +6426,45 @@ "language": "json" } }, - "raw": "{\"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\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\",\"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/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "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 confirm true", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - 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\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - 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.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6627,21 +6516,21 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - 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" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -6649,6 +6538,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -6667,18 +6576,27 @@ "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\":6540,\"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\":\"26\",\"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\"}}" + "raw": "{\"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\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "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" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -6751,24 +6669,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[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" @@ -6813,10 +6722,10 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario28-Confirm a payment with requires_customer_action status", "item": [ { - "name": "Payments - Create", + "name": "Payments - Create with confirm true", "event": [ { "listen": "test", @@ -6884,24 +6793,24 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6927,7 +6836,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"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\",\"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\",\"business_country\":\"US\",\"business_label\":\"default\",\"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\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"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\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_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", @@ -6950,22 +6859,1030 @@ "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", + "pm.test(\"[GET]::/payments/:id - Status code is 400\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// 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) { }", + "", + "", + "// Response body should have appropriatae error message", + "if (jsonData?.message) {", + " pm.test(", + " \"Content check if appropriate error message is present\",", + " function () {", + " pm.expect(jsonData.message).to.eql(\"You cannot confirm this payment because it has status requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_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": "{\"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\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario29-Create payment with payment method billing", + "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;", + " },", + ");", + "", + "// Response body should have \"payment_method_data.billing\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data.billing' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data.billing !== \"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\":6540,\"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\":\"26\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}},\"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;", + " },", + ");", + "", + "// Response body should have \"payment_method_data.billing\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data.billing' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data.billing !== \"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": "Scenario30-Update payment with payment method billing", + "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 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 \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "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\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"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\",\"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 - Update", + "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) { }", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have \"payment_method_data.billing\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data.billing' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data.billing !== \"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": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"26\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "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 \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have \"payment_method_data.billing\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data.billing' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data.billing !== \"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": "Scenario31-Pass payment method billing in Confirm", + "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 \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "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\":false,\"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\",\"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", + "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 - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - 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/:id/confirm - 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/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_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": "{\"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\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "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(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "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(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6973,7 +7890,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", @@ -7017,21 +7934,154 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"payment_method_data.billing\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data.billing' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data.billing !== \"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": [ + { + "name": "Payments - Create", + "event": [ { - "listen": "prerequest", + "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" @@ -7039,26 +8089,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -7077,27 +8107,18 @@ "language": "json" } }, - "raw": "{\"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\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "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\":6540,\"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\":\"26\",\"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/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "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": [] }, @@ -7170,15 +8191,24 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[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" diff --git a/postman/collection-json/trustpay.postman_collection.json b/postman/collection-json/trustpay.postman_collection.json index cacc015851d8..1203cd1793e5 100644 --- a/postman/collection-json/trustpay.postman_collection.json +++ b/postman/collection-json/trustpay.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/volt.postman_collection.json b/postman/collection-json/volt.postman_collection.json index 78d4e9d6ea41..011173f9ff82 100644 --- a/postman/collection-json/volt.postman_collection.json +++ b/postman/collection-json/volt.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json index 410f066ff6fb..f7d47298085f 100644 --- a/postman/collection-json/wise.postman_collection.json +++ b/postman/collection-json/wise.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/worldline.postman_collection.json b/postman/collection-json/worldline.postman_collection.json index 7b1a9d027ba4..c8e7969b7665 100644 --- a/postman/collection-json/worldline.postman_collection.json +++ b/postman/collection-json/worldline.postman_collection.json @@ -55,14 +55,7 @@ ], "request": { "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [ diff --git a/postman/collection-json/zen.postman_collection.json b/postman/collection-json/zen.postman_collection.json index 067cd0840a37..54ce9e9e98fe 100644 --- a/postman/collection-json/zen.postman_collection.json +++ b/postman/collection-json/zen.postman_collection.json @@ -58,14 +58,7 @@ "type": "noauth" }, "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], + "header": [], "url": { "raw": "{{baseUrl}}/health", "host": [