diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 98a33d5d0213..638d5540d3d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,17 +1,16 @@ -* @juspay/hyperswitch-maintainers @jarnura @ashokkjag +* @juspay/hyperswitch-maintainers docs/ @juspay/hyperswitch-maintainers -openapi/ @juspay/hyperswitch-maintainers -postman/ @juspay/hyperswitch-maintainers scripts/ @juspay/hyperswitch-maintainers *.md @juspay/hyperswitch-maintainers *.sh @juspay/hyperswitch-maintainers LICENSE @juspay/hyperswitch-maintainers NOTICE @juspay/hyperswitch-maintainers -.github/ @juspay/hyperswitch-maintainers .gitignore @juspay/hyperswitch-maintainers Makefile @juspay/hyperswitch-maintainers +.github/ @juspay/hyperswitch-devops + config/ @juspay/hyperswitch-framework crates/ @juspay/hyperswitch-framework crates/router/src/types/ @juspay/hyperswitch-framework @@ -20,6 +19,9 @@ crates/router/src/db/ @juspay/hyperswitch-framework crates/router/src/routes/ @juspay/hyperswitch-framework migrations/ @juspay/hyperswitch-framework openapi/ @juspay/hyperswitch-framework +postman/ @juspay/hyperswitch-framework +Cargo.toml @juspay/hyperswitch-framework +Cargo.lock @juspay/hyperswitch-framework connector-template/ @juspay/hyperswitch-connector crates/router/src/connector/ @juspay/hyperswitch-connector diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml new file mode 100644 index 000000000000..59e0bbee3cb4 --- /dev/null +++ b/.github/workflows/hotfix-pr-check.yml @@ -0,0 +1,167 @@ +name: Hotfix-PR-Check + +on: + pull_request: + types: + - opened + - edited + - synchronize + branches: + - "hotfix-*" + +jobs: + hotfix_pr_check: + name: Verify Hotfix PR + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get hotfix pull request body + shell: bash + run: | + echo '${{ github.event.pull_request.body }}' > hotfix_pr_body.txt + + - name: Get a list of all original PR numbers + shell: bash + run: | + + # Extract a list of lines with the format 'juspay/hyperswitch/pull/1200' or '#1200' using 'sed'. + # If empty, then error out and exit. + # else, use 'grep' to extract out 'juspay/hyperswitch/pull/1200' or '#1200' patterns from each line. + # Use 'sed' to remove the part of the matched strings that precedes the last "/" character (in cases like, juspay/hyperswitch/pull/1200 - 1200) + # and sed again to remove any "#" characters from the extracted numeric part (in cases like #1200 - 1200), ultimately getting PR/issue number. + # Finally, remove (if any) duplicates from the list + + SED_OUTPUT=$(sed -E '/\/juspay\/hyperswitch\/pull\/[0-9]+|#[0-9]+/!d' hotfix_pr_body.txt) + + if [ -z "$SED_OUTPUT" ]; then + echo "::error::No original PRs found" + exit 1 + else + PR_NUMBERS=($(echo "$SED_OUTPUT" | grep -oE 'juspay/hyperswitch/pull/[0-9]+|#([0-9]+)' | sed 's/.*\///' | sed 's/#//' | sort -u)) + echo "PR_NUMBERS=${PR_NUMBERS[@]}" >> $GITHUB_ENV + echo "Original PR's found: ("${PR_NUMBERS[*]/#/#}")" + fi + + - name: Verify Original PRs + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + PR_NUMBERS="${PR_NUMBERS[*]}" + all_checks_failed=1 + + PR_AUTHORS=() + PR_TITLES=() + PR_BASE_REFS=() + PR_STATES=() + + for pr_number in ${PR_NUMBERS}; do + is_pull_request="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "/repos/juspay/hyperswitch/issues/${pr_number}" | jq '.pull_request')" + + if [[ "$is_pull_request" == null ]]; then + continue + else + pr_info=$(gh pr view "${pr_number}" --json number,title,baseRefName,state,author) + pr_author=$(echo "${pr_info}" | jq -r '.author.login') + pr_title=$(echo "${pr_info}" | jq -r '.title') + pr_base_ref=$(echo "${pr_info}" | jq -r '.baseRefName') + pr_state=$(echo "${pr_info}" | jq -r '.state') + + if [[ "${pr_author}" == "${{ github.event.pull_request.user.login }}" && \ + "${pr_title}" == "${{ github.event.pull_request.title }}" && \ + "${pr_base_ref}" == "main" && \ + "${pr_state}" == "MERGED" ]]; then + + all_checks_failed=0 + break + fi + + PR_AUTHORS+=("$pr_author") + PR_TITLES+=("$pr_title") + PR_BASE_REFS+=("$pr_base_ref") + PR_STATES+=("$pr_state") + + fi + done + + if [[ $all_checks_failed -eq 1 ]]; then + + # Set a flag to track if a author match is found + author_match_found=0 + + for ((i = 0; i < ${#PR_AUTHORS[@]}; i++)); do + if [[ "${{github.event.pull_request.user.login}}" == "${PR_AUTHORS[i]}" ]]; then + # If a match is found, set the flag to 1 and break out of the loop + author_match_found=1 + break + fi + done + + if [[ $author_match_found -eq 0 ]]; then + echo "::error::Hotfix PR author does not match any of the Original PR authors. Hotfix PR author: '${{ github.event.pull_request.user.login }}'" + fi + + + # Set a flag to track if a title match is found + title_match_found=0 + + for ((i = 0; i < ${#PR_TITLES[@]}; i++)); do + if [[ "${{github.event.pull_request.title}}" == "${PR_TITLES[i]}" ]]; then + # If a match is found, set the flag to 1 and break out of the loop + title_match_found=1 + break + fi + done + + if [[ $title_match_found -eq 0 ]]; then + echo "::error::Hotfix PR title does not match any of the Original PR titles. Hotfix PR title: '${{ github.event.pull_request.title }}'" + fi + + + # Set a flag to track if any of the original PRs point to the 'main' + original_pr_points_to_main=0 + + for ((i = 0; i < ${#PR_BASE_REFS[@]}; i++)); do + if [[ "${PR_BASE_REFS[i]}" == "main" ]]; then + # If a match is found, set the flag to 1 and break out of the loop + original_pr_points_to_main=1 + break + fi + done + + if [[ $original_pr_points_to_main -eq 0 ]]; then + echo "::error::None of the Original PR's baseRef is 'main'" + fi + + + # Set a flag to track if any of the original PR's state is 'MERGED' + original_pr_merged=0 + + for ((i = 0; i < ${#PR_STATES[@]}; i++)); do + if [[ "${PR_STATES[i]}" == "MERGED" ]]; then + # If a match is found, set the flag to 1 and break out of the loop + original_pr_merged=1 + break + fi + done + + if [[ $original_pr_merged -eq 0 ]]; then + echo "::error::None of the Original PR is merged" + fi + + # Print all Original PR's (number), (pr_title), (pr_author), (pr_base_ref) and (pr_state) + i=0 + echo "Original PR info:" + for pr_number in ${PR_NUMBERS}; do + echo "#${pr_number} - pr_title: '${PR_TITLES[i]}' - pr_author: '${PR_AUTHORS[i]}' - pr_base_ref: '${PR_BASE_REFS[i]}' - pr_state: '${PR_STATES[i]}'" + i+=1 + done + + exit 1 + + else + echo "::notice::Hotfix PR satisfies all the required conditions" + + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index abaa4ed4f500..01e0ce2b1d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,113 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.63.0 (2023-10-20) + +### Features + +- Add support for updating surcharge_applicable field intent ([#2647](https://github.com/juspay/hyperswitch/pull/2647)) ([`949937e`](https://github.com/juspay/hyperswitch/commit/949937e3644346f8b2b952944efb884f270645a8)) + +### Bug Fixes + +- Kms decryption of redis_temp_locker_encryption_key ([#2650](https://github.com/juspay/hyperswitch/pull/2650)) ([`5a6601f`](https://github.com/juspay/hyperswitch/commit/5a6601fad4d11cd7d2f1322a6453504494d20c6f)) + +### Refactors + +- **router:** [Nexi nets] Remove Default Case Handling ([#2639](https://github.com/juspay/hyperswitch/pull/2639)) ([`4b64c56`](https://github.com/juspay/hyperswitch/commit/4b64c563558d7c0a02b248c23921ed47ff294980)) + +**Full Changelog:** [`v1.62.0...v1.63.0`](https://github.com/juspay/hyperswitch/compare/v1.62.0...v1.63.0) + +- - - + + +## 1.62.0 (2023-10-19) + +### Features + +- **connector:** + - [Worldpay] Use connector_request_reference_id as reference to the connector ([#2553](https://github.com/juspay/hyperswitch/pull/2553)) ([`9ea5830`](https://github.com/juspay/hyperswitch/commit/9ea5830befe333270f8f424753e1b46a439e79bb)) + - [ProphetPay] Template generation ([#2610](https://github.com/juspay/hyperswitch/pull/2610)) ([`7e6207e`](https://github.com/juspay/hyperswitch/commit/7e6207e6ca98fe2af9a61e272735e9d2292d6a92)) + - [Bambora] Use connector_response_reference_id as reference to the connector ([#2635](https://github.com/juspay/hyperswitch/pull/2635)) ([`a9b5dc9`](https://github.com/juspay/hyperswitch/commit/a9b5dc9ab767eb54a95bcebc4fd5a7b00dbf65f6)) + - [Klarna] Add order id as the reference id to merchant ([#2614](https://github.com/juspay/hyperswitch/pull/2614)) ([`b7d5573`](https://github.com/juspay/hyperswitch/commit/b7d557367a3a5aca478ffd2087af8077bc4e7e2b)) + +### Bug Fixes + +- Payment_method_data and description null during payment confirm ([#2618](https://github.com/juspay/hyperswitch/pull/2618)) ([`6765a1c`](https://github.com/juspay/hyperswitch/commit/6765a1c695493499d1907c56d05bdcd80a2fea93)) + +### Refactors + +- **connector:** + - [Dlocal] Currency Unit Conversion ([#2615](https://github.com/juspay/hyperswitch/pull/2615)) ([`1f2fe51`](https://github.com/juspay/hyperswitch/commit/1f2fe5170ae318a8b1613f6f02538a36f30f0b3d)) + - [Iatapay] remove default case handling ([#2587](https://github.com/juspay/hyperswitch/pull/2587)) ([`6494e8a`](https://github.com/juspay/hyperswitch/commit/6494e8a6e4a195ecc9ca5b2f6ac0a636f06b03f7)) + - [noon] remove cancellation_reason ([#2627](https://github.com/juspay/hyperswitch/pull/2627)) ([`41b7742`](https://github.com/juspay/hyperswitch/commit/41b7742b5498bfa9ef32b9408ab2d9a7a43b01dc)) + - [Forte] Remove Default Case Handling ([#2625](https://github.com/juspay/hyperswitch/pull/2625)) ([`418715b`](https://github.com/juspay/hyperswitch/commit/418715b816337bcaeee1aceeb911e6d329add2ad)) + - [Dlocal] remove default case handling ([#2624](https://github.com/juspay/hyperswitch/pull/2624)) ([`1584313`](https://github.com/juspay/hyperswitch/commit/158431391d560be4a79160ccea7bf5feaa4b52db)) +- Remove code related to temp locker ([#2640](https://github.com/juspay/hyperswitch/pull/2640)) ([`cc0b422`](https://github.com/juspay/hyperswitch/commit/cc0b42263257b6cf6c7f94350442a74d3c02750b)) +- Add surcharge_applicable to payment_intent and remove surcharge_metadata from payment_attempt ([#2642](https://github.com/juspay/hyperswitch/pull/2642)) ([`e5fbaae`](https://github.com/juspay/hyperswitch/commit/e5fbaae0d4278681e5f589aa46c867e7904c4646)) + +### Testing + +- **postman:** Update postman collection files ([`2593dd1`](https://github.com/juspay/hyperswitch/commit/2593dd17c30d7f327b54f3c386a9fd42ae8146ca)) + +### Miscellaneous Tasks + +- **deps:** Bump rustix from 0.37.24 to 0.37.25 ([#2637](https://github.com/juspay/hyperswitch/pull/2637)) ([`67d0062`](https://github.com/juspay/hyperswitch/commit/67d006272158372a4b9ec65cbbe7b2ae8f35eb69)) + +### Build System / Dependencies + +- **deps:** Use `async-bb8-diesel` from `crates.io` instead of git repository ([#2619](https://github.com/juspay/hyperswitch/pull/2619)) ([`14c0821`](https://github.com/juspay/hyperswitch/commit/14c0821b8085279072db3484a3b1bcdde0f7893b)) + +**Full Changelog:** [`v1.61.0...v1.62.0`](https://github.com/juspay/hyperswitch/compare/v1.61.0...v1.62.0) + +- - - + + +## 1.61.0 (2023-10-18) + +### Features + +- **Connector:** [Paypal] add support for dispute webhooks for paypal connector ([#2353](https://github.com/juspay/hyperswitch/pull/2353)) ([`6cf8f05`](https://github.com/juspay/hyperswitch/commit/6cf8f0582cfa4f6a58c67a868cb67846970b3835)) +- **apple_pay:** Add support for decrypted apple pay token for checkout ([#2628](https://github.com/juspay/hyperswitch/pull/2628)) ([`794dbc6`](https://github.com/juspay/hyperswitch/commit/794dbc6a766d12ff3cdf0b782abb4c48b8fa77d0)) +- **connector:** + - [Aci] Update connector_response_reference_id with merchant reference ([#2551](https://github.com/juspay/hyperswitch/pull/2551)) ([`9e450b8`](https://github.com/juspay/hyperswitch/commit/9e450b81ca8bc4b1ddbbe2c1d732dbc58c61934e)) + - [Bambora] use connector_request_reference_id ([#2518](https://github.com/juspay/hyperswitch/pull/2518)) ([`73e9391`](https://github.com/juspay/hyperswitch/commit/73e93910cd3bd668d721b15edb86240adc18f46b)) + - [Tsys] Use connector_request_reference_id as reference to the connector ([#2631](https://github.com/juspay/hyperswitch/pull/2631)) ([`b145463`](https://github.com/juspay/hyperswitch/commit/b1454634259144d896716e5cef37d9b8491f55b9)) +- **core:** Replace temp locker with redis ([#2594](https://github.com/juspay/hyperswitch/pull/2594)) ([`2edbd61`](https://github.com/juspay/hyperswitch/commit/2edbd6123512a6f2f4d51d5c2d1ed8b6ee502813)) +- **events:** Add events for incoming API requests ([#2621](https://github.com/juspay/hyperswitch/pull/2621)) ([`7a76d6c`](https://github.com/juspay/hyperswitch/commit/7a76d6c01a0c6087c6429e58cc9dd6b4ea7fc0aa)) +- **merchant_account:** Add merchant account list endpoint ([#2560](https://github.com/juspay/hyperswitch/pull/2560)) ([`a1472c6`](https://github.com/juspay/hyperswitch/commit/a1472c6b78afa819cbe026a7db1e0c2b9016715e)) +- Update surcharge_amount and tax_amount in update_trackers of payment_confirm ([#2603](https://github.com/juspay/hyperswitch/pull/2603)) ([`2f9a355`](https://github.com/juspay/hyperswitch/commit/2f9a3557f63150bcd27e27c6510a799669706718)) + +### Bug Fixes + +- **connector:** + - [Authorizedotnet]fix error deserialization incase of authentication failure ([#2600](https://github.com/juspay/hyperswitch/pull/2600)) ([`4859b7d`](https://github.com/juspay/hyperswitch/commit/4859b7da73125c2da72f4754863ff4485bebce29)) + - [Paypal]fix error deserelization for source verification call ([#2611](https://github.com/juspay/hyperswitch/pull/2611)) ([`da77d13`](https://github.com/juspay/hyperswitch/commit/da77d1393b8f6ab658dd7f3c202dd6c7d15c0ebd)) +- **payments:** Fix payment update enum being inserted into kv ([#2612](https://github.com/juspay/hyperswitch/pull/2612)) ([`9aa1c75`](https://github.com/juspay/hyperswitch/commit/9aa1c75eca24caa14af5f4801173cd59f76d7e57)) + +### Refactors + +- **events:** Allow box dyn for event handler ([#2629](https://github.com/juspay/hyperswitch/pull/2629)) ([`01410bb`](https://github.com/juspay/hyperswitch/commit/01410bb9f233637e98f27ebe509e859c7dad2cf4)) +- **payment_connector:** Allow connector label to be updated ([#2622](https://github.com/juspay/hyperswitch/pull/2622)) ([`c86ac9b`](https://github.com/juspay/hyperswitch/commit/c86ac9b1fe5388666463aa16c899427a2bf442fb)) +- **router:** Remove unnecessary function from Refunds Validate Flow ([#2609](https://github.com/juspay/hyperswitch/pull/2609)) ([`3399328`](https://github.com/juspay/hyperswitch/commit/3399328ae7f525fb72e0751182cf32d0b2470594)) +- Refactor connector auth type failure to 4xx ([#2616](https://github.com/juspay/hyperswitch/pull/2616)) ([`1dad745`](https://github.com/juspay/hyperswitch/commit/1dad7455c4ae8d26d52c44d90f5b8d815d85d205)) + +### Testing + +- **postman:** Update postman collection files ([`d899025`](https://github.com/juspay/hyperswitch/commit/d89902507486b8b97011fb63ed0343f727255ca2)) + +### Documentation + +- **postman:** Rewrite postman documentation to help devs develop tests for their features ([#2613](https://github.com/juspay/hyperswitch/pull/2613)) ([`1548ee6`](https://github.com/juspay/hyperswitch/commit/1548ee62b661200fcb9d439d16c072a66dbfa718)) + +### Miscellaneous Tasks + +- **scripts:** Add connector script changes ([#2620](https://github.com/juspay/hyperswitch/pull/2620)) ([`373a10b`](https://github.com/juspay/hyperswitch/commit/373a10beffc7cddef6ff76f5c8fff91ca3618581)) + +**Full Changelog:** [`v1.60.0...v1.61.0`](https://github.com/juspay/hyperswitch/compare/v1.60.0...v1.61.0) + +- - - + + ## 1.60.0 (2023-10-17) ### Features diff --git a/Cargo.lock b/Cargo.lock index 7a8bbe6fe7bb..66bace95e7a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,7 +487,8 @@ dependencies = [ [[package]] name = "async-bb8-diesel" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/async-bb8-diesel?rev=be3d9bce50051d8c0e0c06078e8066cc27db3001#be3d9bce50051d8c0e0c06078e8066cc27db3001" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779f1fa3defe66bf147fe5c811b23a02cfcaa528a25293e0b20d1911eac1fb05" dependencies = [ "async-trait", "bb8", @@ -534,7 +535,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.24", + "rustix 0.37.25", "slab", "socket2 0.4.9", "waker-fn", @@ -4424,9 +4425,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.24" +version = "0.37.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" dependencies = [ "bitflags 1.3.2", "errno", diff --git a/config/config.example.toml b/config/config.example.toml index e97b73d87c6b..db562ffe7e66 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -115,6 +115,7 @@ host = "" # Locker host mock_locker = true # Emulate a locker locally using Postgres basilisk_host = "" # Basilisk host locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +redis_temp_locker_encryption_key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" # encryption key for redis temp locker [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response @@ -199,6 +200,7 @@ payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/config/development.toml b/config/development.toml index a29cc9ec5be2..701805ffe0ad 100644 --- a/config/development.toml +++ b/config/development.toml @@ -50,6 +50,7 @@ applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT" host = "" mock_locker = true basilisk_host = "" +redis_temp_locker_encryption_key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" [jwekey] locker_key_identifier1 = "" @@ -101,6 +102,7 @@ cards = [ "paypal", "payu", "powertranz", + "prophetpay", "shift4", "square", "stax", @@ -171,6 +173,7 @@ payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 8504ec130e62..de984597d055 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -46,6 +46,7 @@ recon_admin_api_key = "recon_test_admin" host = "" mock_locker = true basilisk_host = "" +redis_temp_locker_encryption_key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" [jwekey] locker_key_identifier1 = "" @@ -114,6 +115,7 @@ payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" @@ -174,6 +176,7 @@ cards = [ "paypal", "payu", "powertranz", + "prophetpay", "shift4", "square", "stax", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 9f9793d7d76c..05f527d24662 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -27,7 +27,7 @@ pub struct {{project-name | downcase | pascal_case}}; impl api::Payment for {{project-name | downcase | pascal_case}} {} impl api::PaymentSession for {{project-name | downcase | pascal_case}} {} impl api::ConnectorAccessToken for {{project-name | downcase | pascal_case}} {} -impl api::PreVerify for {{project-name | downcase | pascal_case}} {} +impl api::MandateSetup for {{project-name | downcase | pascal_case}} {} impl api::PaymentAuthorize for {{project-name | downcase | pascal_case}} {} impl api::PaymentSync for {{project-name | downcase | pascal_case}} {} impl api::PaymentCapture for {{project-name | downcase | pascal_case}} {} diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index acffbfe8129c..af6a8aa446fc 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -14,6 +14,11 @@ use crate::{ payment_methods, }; +#[derive(Clone, Debug, Deserialize, ToSchema)] +pub struct MerchantAccountListRequest { + pub organization_id: String, +} + #[derive(Clone, Debug, Deserialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct MerchantAccountCreate { @@ -582,10 +587,9 @@ pub struct MerchantConnectorCreate { /// Name of the Connector #[schema(value_type = Connector, example = "stripe")] pub connector_name: api_enums::Connector, - // /// Connector label for specific country and Business - #[serde(skip_deserializing)] + /// Connector label for a connector, this can serve as a field to identify the connector as per business details #[schema(example = "stripe_US_travel")] - pub connector_label: String, + pub connector_label: Option, /// Unique ID of the connector #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] @@ -678,8 +682,8 @@ pub struct MerchantConnectorResponse { /// Name of the Connector #[schema(example = "stripe")] pub connector_name: String, - // /// Connector label for specific country and Business - #[serde(skip_deserializing)] + + /// Connector label for a connector, this can serve as a field to identify the connector as per business details #[schema(example = "stripe_US_travel")] pub connector_label: Option, @@ -772,6 +776,9 @@ pub struct MerchantConnectorUpdate { #[schema(value_type = ConnectorType, example = "payment_processor")] pub connector_type: api_enums::ConnectorType, + /// Connector label for a connector, this can serve as a field to identify the connector as per business details + pub connector_label: Option, + /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: Option, diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 7a02d3dee65f..65f6dea1b5ed 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -107,6 +107,7 @@ pub enum Connector { Paypal, Payu, Powertranz, + // Prophetpay, added as a template code for future usage Rapyd, Shift4, Square, @@ -225,6 +226,7 @@ pub enum RoutableConnectors { Paypal, Payu, Powertranz, + // Prophetpay, added as a template code for future usage Rapyd, Shift4, Square, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 9d3983e73a1f..cdb8ad6ad595 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -345,6 +345,23 @@ pub struct SurchargeMetadata { pub surcharge_results: HashMap, } +impl SurchargeMetadata { + pub fn get_key_for_surcharge_details_hash_map( + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> String { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } +} + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case", tag = "type", content = "value")] pub enum Surcharge { @@ -754,13 +771,6 @@ pub struct DeleteTokenizeByTokenRequest { pub service_name: String, } -#[derive(Debug, serde::Serialize)] // Blocked: Yet to be implemented by `basilisk` -pub struct DeleteTokenizeByDateRequest { - pub buffer_minutes: i32, - pub service_name: String, - pub max_rows: i32, -} - #[derive(Debug, serde::Deserialize)] pub struct GetTokenizePayloadResponse { pub lookup_key: String, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index e763a3e26774..f61f6f9a44ac 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -311,7 +311,9 @@ pub struct PaymentsRequest { pub payment_type: Option, } -#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema)] +#[derive( + Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema, PartialEq, +)] pub struct RequestSurchargeDetails { pub surcharge_amount: i64, pub tax_amount: Option, @@ -2096,6 +2098,9 @@ pub struct PaymentsResponse { /// The business profile that is associated with this payment pub profile_id: Option, + /// details of surcharge applied on this payment + pub surcharge_details: Option, + /// total number of attempts associated with this payment pub attempt_count: i16, diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs index 6541633a56ee..7f3430657058 100644 --- a/crates/common_utils/src/errors.rs +++ b/crates/common_utils/src/errors.rs @@ -77,11 +77,20 @@ pub enum QrCodeError { } /// Api Models construction error -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] -pub enum ApiModelsError { +#[derive(Debug, Clone, thiserror::Error, PartialEq)] +pub enum PercentageError { /// Percentage Value provided was invalid #[error("Invalid Percentage value")] InvalidPercentageValue, + + /// Error occurred while calculating percentage + #[error("Failed apply percentage of {percentage} on {amount}")] + UnableToApplyPercentage { + /// percentage value + percentage: f32, + /// amount value + amount: i64, + }, } /// Allows [error_stack::Report] to change between error contexts diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index d745334a21ea..b28bffe0dc90 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -2,7 +2,7 @@ use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::errors::{ApiModelsError, CustomResult}; +use crate::errors::{CustomResult, PercentageError}; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] @@ -21,16 +21,16 @@ fn get_invalid_percentage_error_message(precision: u8) -> String { impl Percentage { /// construct percentage using a string representation of float value - pub fn from_string(value: String) -> CustomResult { + pub fn from_string(value: String) -> CustomResult { if Self::is_valid_string_value(&value)? { Ok(Self { percentage: value .parse() .into_report() - .change_context(ApiModelsError::InvalidPercentageValue)?, + .change_context(PercentageError::InvalidPercentageValue)?, }) } else { - Err(ApiModelsError::InvalidPercentageValue.into()) + Err(PercentageError::InvalidPercentageValue.into()) .attach_printable(get_invalid_percentage_error_message(PRECISION)) } } @@ -38,15 +38,37 @@ impl Percentage { pub fn get_percentage(&self) -> f32 { self.percentage } - fn is_valid_string_value(value: &str) -> CustomResult { + + /// apply the percentage to amount and ceil the result + #[allow(clippy::as_conversions)] + pub fn apply_and_ceil_result(&self, amount: i64) -> CustomResult { + let max_amount = i64::MAX / 10000; + if amount > max_amount { + // value gets rounded off after i64::MAX/10000 + Err(PercentageError::UnableToApplyPercentage { + percentage: self.percentage, + amount, + } + .into()) + .attach_printable(format!( + "Cannot calculate percentage for amount greater than {}", + max_amount + )) + } else { + let percentage_f64 = f64::from(self.percentage); + let result = (amount as f64 * (percentage_f64 / 100.0)).ceil() as i64; + Ok(result) + } + } + fn is_valid_string_value(value: &str) -> CustomResult { let float_value = Self::is_valid_float_string(value)?; Ok(Self::is_valid_range(float_value) && Self::is_valid_precision_length(value)) } - fn is_valid_float_string(value: &str) -> CustomResult { + fn is_valid_float_string(value: &str) -> CustomResult { value .parse() .into_report() - .change_context(ApiModelsError::InvalidPercentageValue) + .change_context(PercentageError::InvalidPercentageValue) } fn is_valid_range(value: f32) -> bool { (0.0..=100.0).contains(&value) diff --git a/crates/common_utils/tests/percentage.rs b/crates/common_utils/tests/percentage.rs index 95c112523376..0858c98fd63a 100644 --- a/crates/common_utils/tests/percentage.rs +++ b/crates/common_utils/tests/percentage.rs @@ -1,5 +1,5 @@ #![allow(clippy::panic_in_result_fn)] -use common_utils::{errors::ApiModelsError, types::Percentage}; +use common_utils::{errors::PercentageError, types::Percentage}; const PRECISION_2: u8 = 2; const PRECISION_0: u8 = 0; @@ -10,7 +10,7 @@ fn invalid_range_more_than_100() -> Result<(), Box Result<(), Box Result<(), Box> { if let Err(err) = percentage { assert_eq!( *err.current_context(), - ApiModelsError::InvalidPercentageValue + PercentageError::InvalidPercentageValue ) } Ok(()) @@ -92,7 +92,7 @@ fn invalid_precision() -> Result<(), Box> { if let Err(err) = percentage { assert_eq!( *err.current_context(), - ApiModelsError::InvalidPercentageValue + PercentageError::InvalidPercentageValue ) } Ok(()) diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 029f7108507e..4e7a0923f6a9 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -47,5 +47,7 @@ pub struct PaymentIntent { // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment pub merchant_decision: Option, pub payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index cf65c3f5de24..b9021ae1ce5e 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -141,7 +141,6 @@ pub struct PaymentAttempt { // reference to the payment at connector side pub connector_response_reference_id: Option, pub amount_capturable: i64, - pub surcharge_metadata: Option, pub updated_by: String, } @@ -201,7 +200,6 @@ pub struct PaymentAttemptNew { pub connector_response_reference_id: Option, pub multiple_capture_count: Option, pub amount_capturable: i64, - pub surcharge_metadata: Option, pub updated_by: String, } @@ -250,6 +248,8 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, RejectUpdate { @@ -307,11 +307,6 @@ pub enum PaymentAttemptUpdate { multiple_capture_count: i16, updated_by: String, }, - SurchargeAmountUpdate { - surcharge_amount: Option, - tax_amount: Option, - updated_by: String, - }, AmountToCaptureUpdate { status: storage_enums::AttemptStatus, amount_capturable: i64, @@ -326,10 +321,6 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, updated_by: String, }, - SurchargeMetadataUpdate { - surcharge_metadata: Option, - updated_by: String, - }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 155e6b5ca679..5b41c74bd697 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -104,7 +104,9 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -176,6 +178,10 @@ pub enum PaymentIntentUpdate { merchant_decision: Option, updated_by: String, }, + SurchargeApplicableUpdate { + surcharge_applicable: bool, + updated_by: String, + }, } #[derive(Clone, Debug, Default)] @@ -204,7 +210,9 @@ pub struct PaymentIntentUpdateInternal { // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment pub merchant_decision: Option, pub payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } impl PaymentIntentUpdate { @@ -382,6 +390,14 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable, + updated_by, + } => Self { + surcharge_applicable: Some(surcharge_applicable), + updated_by, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 3e8efe30025d..1a0bdfe5674e 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -15,7 +15,7 @@ kv_store = [] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] [dependencies] -async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "be3d9bce50051d8c0e0c06078e8066cc27db3001" } +async-bb8-diesel = "0.1.0" aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index ce2684c937ab..a4faa45ce4bc 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -79,6 +79,7 @@ pub struct MerchantConnectorAccountUpdateInternal { pub connector_type: Option, pub connector_name: Option, pub connector_account_details: Option, + pub connector_label: Option, pub test_mode: Option, pub disabled: Option, pub merchant_connector_id: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index ca73540437e7..7d2033bcbb40 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -57,7 +57,6 @@ pub struct PaymentAttempt { // reference to the payment at connector side pub connector_response_reference_id: Option, pub amount_capturable: i64, - pub surcharge_metadata: Option, pub updated_by: String, } @@ -117,7 +116,6 @@ pub struct PaymentAttemptNew { pub connector_response_reference_id: Option, pub multiple_capture_count: Option, pub amount_capturable: i64, - pub surcharge_metadata: Option, pub updated_by: String, } @@ -166,6 +164,8 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, VoidUpdate { @@ -228,11 +228,6 @@ pub enum PaymentAttemptUpdate { amount_capturable: i64, updated_by: String, }, - SurchargeAmountUpdate { - surcharge_amount: Option, - tax_amount: Option, - updated_by: String, - }, PreprocessingUpdate { status: storage_enums::AttemptStatus, payment_method_id: Option>, @@ -242,10 +237,6 @@ pub enum PaymentAttemptUpdate { connector_response_reference_id: Option, updated_by: String, }, - SurchargeMetadataUpdate { - surcharge_metadata: Option, - updated_by: String, - }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -281,7 +272,6 @@ pub struct PaymentAttemptUpdateInternal { surcharge_amount: Option, tax_amount: Option, amount_capturable: Option, - surcharge_metadata: Option, updated_by: String, } @@ -292,24 +282,47 @@ impl PaymentAttemptUpdate { amount: pa_update.amount.unwrap_or(source.amount), currency: pa_update.currency.or(source.currency), status: pa_update.status.unwrap_or(source.status), - connector: pa_update.connector.or(source.connector), - connector_transaction_id: source + connector_transaction_id: pa_update .connector_transaction_id - .or(pa_update.connector_transaction_id), + .or(source.connector_transaction_id), + amount_to_capture: pa_update.amount_to_capture.or(source.amount_to_capture), + connector: pa_update.connector.or(source.connector), authentication_type: pa_update.authentication_type.or(source.authentication_type), payment_method: pa_update.payment_method.or(source.payment_method), error_message: pa_update.error_message.unwrap_or(source.error_message), payment_method_id: pa_update .payment_method_id .unwrap_or(source.payment_method_id), - browser_info: pa_update.browser_info.or(source.browser_info), + cancellation_reason: pa_update.cancellation_reason.or(source.cancellation_reason), modified_at: common_utils::date_time::now(), + mandate_id: pa_update.mandate_id.or(source.mandate_id), + browser_info: pa_update.browser_info.or(source.browser_info), payment_token: pa_update.payment_token.or(source.payment_token), + error_code: pa_update.error_code.unwrap_or(source.error_code), connector_metadata: pa_update.connector_metadata.or(source.connector_metadata), + payment_method_data: pa_update.payment_method_data.or(source.payment_method_data), + payment_method_type: pa_update.payment_method_type.or(source.payment_method_type), + payment_experience: pa_update.payment_experience.or(source.payment_experience), + business_sub_label: pa_update.business_sub_label.or(source.business_sub_label), + straight_through_algorithm: pa_update + .straight_through_algorithm + .or(source.straight_through_algorithm), preprocessing_step_id: pa_update .preprocessing_step_id .or(source.preprocessing_step_id), - surcharge_metadata: pa_update.surcharge_metadata.or(source.surcharge_metadata), + error_reason: pa_update.error_reason.unwrap_or(source.error_reason), + capture_method: pa_update.capture_method.or(source.capture_method), + connector_response_reference_id: pa_update + .connector_response_reference_id + .or(source.connector_response_reference_id), + multiple_capture_count: pa_update + .multiple_capture_count + .or(source.multiple_capture_count), + surcharge_amount: pa_update.surcharge_amount.or(source.surcharge_amount), + tax_amount: pa_update.tax_amount.or(source.tax_amount), + amount_capturable: pa_update + .amount_capturable + .unwrap_or(source.amount_capturable), updated_by: pa_update.updated_by, ..source } @@ -378,6 +391,8 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, } => Self { amount: Some(amount), @@ -397,6 +412,8 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, ..Default::default() }, @@ -553,24 +570,6 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, - PaymentAttemptUpdate::SurchargeMetadataUpdate { - surcharge_metadata, - updated_by, - } => Self { - surcharge_metadata, - updated_by, - ..Default::default() - }, - PaymentAttemptUpdate::SurchargeAmountUpdate { - surcharge_amount, - tax_amount, - updated_by, - } => Self { - surcharge_amount, - tax_amount, - updated_by, - ..Default::default() - }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index c6b95b5cef6d..2ffa857026ba 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -48,7 +48,9 @@ pub struct PaymentIntent { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } #[derive( @@ -101,7 +103,9 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -173,6 +177,10 @@ pub enum PaymentIntentUpdate { merchant_decision: Option, updated_by: String, }, + SurchargeApplicableUpdate { + surcharge_applicable: Option, + updated_by: String, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -202,7 +210,9 @@ pub struct PaymentIntentUpdateInternal { pub profile_id: Option, merchant_decision: Option, payment_confirm_source: Option, + pub updated_by: String, + pub surcharge_applicable: Option, } impl PaymentIntentUpdate { @@ -227,7 +237,30 @@ impl PaymentIntentUpdate { .shipping_address_id .or(source.shipping_address_id), modified_at: common_utils::date_time::now(), + active_attempt_id: internal_update + .active_attempt_id + .unwrap_or(source.active_attempt_id), + business_country: internal_update.business_country.or(source.business_country), + business_label: internal_update.business_label.or(source.business_label), + description: internal_update.description.or(source.description), + statement_descriptor_name: internal_update + .statement_descriptor_name + .or(source.statement_descriptor_name), + statement_descriptor_suffix: internal_update + .statement_descriptor_suffix + .or(source.statement_descriptor_suffix), order_details: internal_update.order_details.or(source.order_details), + attempt_count: internal_update + .attempt_count + .unwrap_or(source.attempt_count), + profile_id: internal_update.profile_id.or(source.profile_id), + merchant_decision: internal_update + .merchant_decision + .or(source.merchant_decision), + payment_confirm_source: internal_update + .payment_confirm_source + .or(source.payment_confirm_source), + updated_by: internal_update.updated_by, ..source } } @@ -379,6 +412,14 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable, + updated_by, + } => Self { + surcharge_applicable, + updated_by, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs index 33956ab4571e..667b2516bd9a 100644 --- a/crates/diesel_models/src/query/generics.rs +++ b/crates/diesel_models/src/query/generics.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use async_bb8_diesel::{AsyncRunQueryDsl, ConnectionError}; +use async_bb8_diesel::AsyncRunQueryDsl; use diesel::{ associations::HasTable, debug_query, @@ -93,10 +93,9 @@ where { Ok(value) => Ok(value), Err(err) => match err.current_context() { - ConnectionError::Query(DieselError::DatabaseError( - diesel::result::DatabaseErrorKind::UniqueViolation, - _, - )) => Err(err).change_context(errors::DatabaseError::UniqueViolation), + DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => { + Err(err).change_context(errors::DatabaseError::UniqueViolation) + } _ => Err(err).change_context(errors::DatabaseError::Others), }, } @@ -168,14 +167,12 @@ where logger::debug!(query = %debug_query::(&query).to_string()); Ok(result) } - Err(ConnectionError::Query(DieselError::QueryBuilderError(_))) => { + Err(DieselError::QueryBuilderError(_)) => { Err(report!(errors::DatabaseError::NoFieldsToUpdate)) .attach_printable_lazy(|| format!("Error while updating {debug_values}")) } - Err(ConnectionError::Query(DieselError::NotFound)) => { - Err(report!(errors::DatabaseError::NotFound)) - .attach_printable_lazy(|| format!("Error while updating {debug_values}")) - } + Err(DieselError::NotFound) => Err(report!(errors::DatabaseError::NotFound)) + .attach_printable_lazy(|| format!("Error while updating {debug_values}")), _ => Err(report!(errors::DatabaseError::Others)) .attach_printable_lazy(|| format!("Error while updating {debug_values}")), } @@ -259,14 +256,12 @@ where logger::debug!(query = %debug_query::(&query).to_string()); Ok(result) } - Err(ConnectionError::Query(DieselError::QueryBuilderError(_))) => { + Err(DieselError::QueryBuilderError(_)) => { Err(report!(errors::DatabaseError::NoFieldsToUpdate)) .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")) } - Err(ConnectionError::Query(DieselError::NotFound)) => { - Err(report!(errors::DatabaseError::NotFound)) - .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")) - } + Err(DieselError::NotFound) => Err(report!(errors::DatabaseError::NotFound)) + .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")), _ => Err(report!(errors::DatabaseError::Others)) .attach_printable_lazy(|| format!("Error while updating by ID {debug_values}")), } @@ -353,9 +348,7 @@ where { Ok(value) => Ok(value), Err(err) => match err.current_context() { - ConnectionError::Query(DieselError::NotFound) => { - Err(err).change_context(errors::DatabaseError::NotFound) - } + DieselError::NotFound => Err(err).change_context(errors::DatabaseError::NotFound), _ => Err(err).change_context(errors::DatabaseError::Others), }, } @@ -404,9 +397,7 @@ where .await .into_report() .map_err(|err| match err.current_context() { - ConnectionError::Query(DieselError::NotFound) => { - err.change_context(errors::DatabaseError::NotFound) - } + DieselError::NotFound => err.change_context(errors::DatabaseError::NotFound), _ => err.change_context(errors::DatabaseError::Others), }) .attach_printable_lazy(|| "Error finding record by predicate") diff --git a/crates/diesel_models/src/query/merchant_account.rs b/crates/diesel_models/src/query/merchant_account.rs index 598a4889a741..ef9a4165d6f4 100644 --- a/crates/diesel_models/src/query/merchant_account.rs +++ b/crates/diesel_models/src/query/merchant_account.rs @@ -1,4 +1,4 @@ -use diesel::{associations::HasTable, ExpressionMethods}; +use diesel::{associations::HasTable, ExpressionMethods, Table}; use router_env::{instrument, tracing}; use super::generics; @@ -90,4 +90,24 @@ impl MerchantAccount { ) .await } + + #[instrument(skip_all)] + pub async fn list_by_organization_id( + conn: &PgPooledConn, + organization_id: &str, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::organization_id.eq(organization_id.to_owned()), + None, + None, + None, + ) + .await + } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2cbd6027a7e1..2f3e7d345a75 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -559,7 +559,6 @@ diesel::table! { #[max_length = 128] connector_response_reference_id -> Nullable, amount_capturable -> Int8, - surcharge_metadata -> Nullable, #[max_length = 32] updated_by -> Varchar, } @@ -622,6 +621,7 @@ diesel::table! { payment_confirm_source -> Nullable, #[max_length = 32] updated_by -> Varchar, + surcharge_applicable -> Nullable, } } diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 2b0ade029304..3bf056a69b38 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -13,7 +13,7 @@ kms = ["external_services/kms"] vergen = ["router_env/vergen"] [dependencies] -async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "be3d9bce50051d8c0e0c06078e8066cc27db3001" } +async-bb8-diesel = "0.1.0" bb8 = "0.8" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } config = { version = "0.13.3", features = ["toml"] } diff --git a/crates/external_services/src/kms.rs b/crates/external_services/src/kms.rs index 31c82253fe80..1f3d1aee90d4 100644 --- a/crates/external_services/src/kms.rs +++ b/crates/external_services/src/kms.rs @@ -120,6 +120,14 @@ pub enum KmsError { /// The KMS client has not been initialized. #[error("The KMS client has not been initialized")] KmsClientNotInitialized, + + /// The KMS client has not been initialized. + #[error("Hex decode failed")] + HexDecodeFailed, + + /// The KMS client has not been initialized. + #[error("Utf8 decode failed")] + Utf8DecodeFailed, } impl KmsConfig { @@ -140,7 +148,7 @@ impl KmsConfig { /// A wrapper around a KMS value that can be decrypted. #[derive(Clone, Debug, Default, serde::Deserialize, Eq, PartialEq)] #[serde(transparent)] -pub struct KmsValue(Secret); +pub struct KmsValue(pub Secret); impl common_utils::ext_traits::ConfigExt for KmsValue { fn is_empty_after_trim(&self) -> bool { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index da207e91493f..8cbded8f5368 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -13,9 +13,8 @@ default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache", "dummy_connec s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] -basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email","accounts_cache"] +release = ["kms", "stripe", "s3", "email","accounts_cache"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] oltp = ["data_models/oltp", "storage_impl/oltp"] kv_store = ["scheduler/kv_store"] @@ -35,7 +34,7 @@ actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" actix-web = "4.3.1" -async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "be3d9bce50051d8c0e0c06078e8066cc27db3001" } +async-bb8-diesel = "0.1.0" async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 2d10e37c10ba..03b59c55036f 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -239,6 +239,8 @@ pub enum StripeErrorCode { PaymentLinkNotFound, #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Resource Busy. Please try again later")] LockTimeout, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Merchant connector account is configured with invalid {config}")] + InvalidConnectorConfiguration { config: String }, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -590,6 +592,9 @@ impl From for StripeErrorCode { Self::PaymentMethodUnactivated } errors::ApiErrorResponse::ResourceBusy => Self::PaymentMethodUnactivated, + errors::ApiErrorResponse::InvalidConnectorConfiguration { config } => { + Self::InvalidConnectorConfiguration { config } + } } } } @@ -656,7 +661,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::FileProviderNotSupported | Self::CurrencyNotSupported { .. } | Self::DuplicateCustomer - | Self::PaymentMethodUnactivated => StatusCode::BAD_REQUEST, + | Self::PaymentMethodUnactivated + | Self::InvalidConnectorConfiguration { .. } => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed | Self::PaymentLinkNotFound diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 3bb1c31d180b..c2e60bacb63e 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -49,8 +49,9 @@ impl Default for super::settings::Locker { Self { host: "localhost".into(), mock_locker: true, - basilisk_host: "localhost".into(), + locker_signing_key_id: "1".into(), + redis_temp_locker_encryption_key: "".into(), } } } diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 317ad0608b49..c3dd8ead19b7 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -1,5 +1,6 @@ use common_utils::errors::CustomResult; -use external_services::kms::{decrypt::KmsDecrypt, KmsClient, KmsError}; +use error_stack::{IntoReport, ResultExt}; +use external_services::kms::{decrypt::KmsDecrypt, KmsClient, KmsError, KmsValue}; use masking::ExposeInterface; use crate::configs::settings; @@ -41,6 +42,19 @@ impl KmsDecrypt for settings::ActiveKmsSecrets { kms_client: &KmsClient, ) -> CustomResult { self.jwekey = self.jwekey.expose().decrypt_inner(kms_client).await?.into(); + self.redis_temp_locker_encryption_key = hex::decode( + KmsValue( + String::from_utf8(self.redis_temp_locker_encryption_key.expose()) + .into_report() + .change_context(KmsError::Utf8DecodeFailed)? + .into(), + ) + .decrypt_inner(kms_client) + .await?, + ) + .into_report() + .change_context(KmsError::HexDecodeFailed)? + .into(); Ok(self) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 5dd58c5d5f26..56031c1f068e 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -52,6 +52,7 @@ pub enum Subcommand { #[derive(Clone)] pub struct ActiveKmsSecrets { pub jwekey: masking::Secret, + pub redis_temp_locker_encryption_key: masking::Secret>, } #[derive(Debug, Deserialize, Clone, Default)] @@ -410,8 +411,8 @@ pub struct Secrets { pub struct Locker { pub host: String, pub mock_locker: bool, - pub basilisk_host: String, pub locker_signing_key_id: String, + pub redis_temp_locker_encryption_key: String, } #[derive(Debug, Deserialize, Clone)] @@ -557,6 +558,7 @@ pub struct Connectors { pub paypal: ConnectorParams, pub payu: ConnectorParams, pub powertranz: ConnectorParams, + pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, pub shift4: ConnectorParams, pub square: ConnectorParams, diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 569262d0d210..f9c1c4a9b38d 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -56,10 +56,19 @@ impl super::settings::Locker { })?; when( - !self.mock_locker && self.basilisk_host.is_default_or_empty(), + self.redis_temp_locker_encryption_key.is_default_or_empty(), || { Err(ApplicationError::InvalidConfigurationValueError( - "basilisk host must not be empty when mock locker is disabled".into(), + "redis_temp_locker_encryption_key must not be empty".into(), + )) + }, + )?; + + when( + self.redis_temp_locker_encryption_key.is_default_or_empty(), + || { + Err(ApplicationError::InvalidConfigurationValueError( + "redis_temp_locker_encryption_key must not be empty".into(), )) }, ) diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 689adb2490eb..7849fd98a4d1 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -36,6 +36,7 @@ pub mod payme; pub mod paypal; pub mod payu; pub mod powertranz; +pub mod prophetpay; pub mod rapyd; pub mod shift4; pub mod square; @@ -60,7 +61,7 @@ pub use self::{ globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, - payu::Payu, powertranz::Powertranz, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, - stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, - worldpay::Worldpay, zen::Zen, + payu::Payu, powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, + square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, + wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 7d30c80c49c9..f6c1daffe4d8 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -682,12 +682,12 @@ impl } }, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.id), }), ..item.data }) diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index d34dda73eb6c..e686186c901b 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -48,6 +48,7 @@ pub struct BamboraBrowserInfo { #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct BamboraPaymentsRequest { + order_number: String, amount: i64, payment_method: PaymentMethod, customer_ip: Option, @@ -126,6 +127,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { }; let browser_info = item.request.get_browser_info()?; Ok(Self { + order_number: item.connector_request_reference_id.clone(), amount: item.request.amount, payment_method: PaymentMethod::Card, card: bambora_card, @@ -212,7 +214,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(pg_response.order_number.to_string()), }), ..item.data }), @@ -236,7 +238,9 @@ impl .change_context(errors::ConnectorError::ResponseHandlingFailed)?, ), network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some( + item.data.connector_request_reference_id.to_string(), + ), }), ..item.data }) diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index e967caaba88f..b622e041915d 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -86,8 +86,8 @@ impl TryFrom<&Option> for BraintreeMeta { 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::InvalidConfig { - field_name: "merchant connector account metadata", + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", })?; Ok(metadata) } @@ -109,8 +109,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> ) -> Result { let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConfig { - field_name: "merchant connector account metadata", + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", })?; utils::validate_currency( item.router_data.request.currency, @@ -602,8 +602,8 @@ impl TryFrom>> for Braintree ) -> Result { let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConfig { - field_name: "merchant connector account metadata", + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", })?; utils::validate_currency( @@ -712,9 +712,7 @@ impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { let metadata: BraintreeMeta = utils::to_connector_meta_from_secret( item.connector_meta_data.clone(), ) - .change_context(errors::ConnectorError::InvalidConfig { - field_name: "merchant connector account metadata", - })?; + .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" })?; utils::validate_currency( item.request.currency, Some(metadata.merchant_config_currency), @@ -1345,8 +1343,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> ) -> Result { let metadata: BraintreeMeta = utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConfig { - field_name: "merchant connector account metadata", + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", })?; utils::validate_currency( item.router_data.request.currency, diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 31deae023605..53182e65ed5b 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1,6 +1,6 @@ use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, Secret}; +use masking::{ExposeInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -282,8 +282,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme Ok(a) } api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(_) - | api_models::payments::WalletData::ApplePay(_) => { + api_models::payments::WalletData::GooglePay(_) => { Ok(PaymentSource::Wallets(WalletSource { source_type: CheckoutSourceTypes::Token, token: match item.router_data.get_payment_method_token()? { @@ -294,6 +293,48 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme }, })) } + api_models::payments::WalletData::ApplePay(_) => { + let payment_method_token = item.router_data.get_payment_method_token()?; + match payment_method_token { + types::PaymentMethodToken::Token(apple_pay_payment_token) => { + Ok(PaymentSource::Wallets(WalletSource { + source_type: CheckoutSourceTypes::Token, + token: apple_pay_payment_token, + })) + } + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + let expiry_year_4_digit = Secret::new(format!( + "20{}", + decrypt_data + .clone() + .application_expiration_date + .peek() + .get(0..2) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + )); + let exp_month = Secret::new( + decrypt_data + .clone() + .application_expiration_date + .peek() + .get(2..4) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_owned(), + ); + Ok(PaymentSource::ApplePayPredecrypt(Box::new( + ApplePayPredecrypt { + token: decrypt_data.application_primary_account_number, + decrypt_type: "network_token".to_string(), + token_type: "applepay".to_string(), + expiry_month: exp_month, + expiry_year: expiry_year_4_digit, + eci: decrypt_data.payment_data.eci_indicator, + cryptogram: decrypt_data.payment_data.online_payment_cryptogram, + }, + ))) + } + } + } api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 90be2399ba0e..b706d694a3d5 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -109,6 +109,10 @@ impl ConnectorCommon for Dlocal { "dlocal" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + fn common_get_content_type(&self) -> &'static str { "application/json" } @@ -207,7 +211,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = dlocal::DlocalPaymentsRequest::try_from(req)?; + let connector_router_data = dlocal::DlocalRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_request = dlocal::DlocalPaymentsRequest::try_from(&connector_router_data)?; let dlocal_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -508,10 +518,16 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_request = dlocal::RefundRequest::try_from(req)?; + let connector_router_data = dlocal::DlocalRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = dlocal::DlocalRefundRequest::try_from(&connector_router_data)?; let dlocal_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(dlocal_refund_request)) diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 8558836372ec..5146dd0ea031 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -51,6 +51,37 @@ pub enum PaymentMethodFlow { ReDirect, } +#[derive(Debug, Serialize)] +pub struct DlocalRouterData { + pub amount: i64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for DlocalRouterData +{ + type Error = error_stack::Report; + + fn try_from( + (_currency_unit, _currency, amount, router_data): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount, + router_data, + }) + } +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct DlocalPaymentsRequest { pub amount: i64, @@ -66,22 +97,24 @@ pub struct DlocalPaymentsRequest { pub description: Option, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for DlocalPaymentsRequest { +impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalPaymentsRequest { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let email = item.request.email.clone(); - let address = item.get_billing_address()?; + fn try_from( + item: &DlocalRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let email = item.router_data.request.email.clone(); + let address = item.router_data.get_billing_address()?; let country = address.get_country()?; let name = get_payer_name(address); - match item.request.payment_method_data { + match item.router_data.request.payment_method_data { api::PaymentMethodData::Card(ref ccard) => { let should_capture = matches!( - item.request.capture_method, + item.router_data.request.capture_method, Some(enums::CaptureMethod::Automatic) ); let payment_request = Self { - amount: item.request.amount, - currency: item.request.currency, + amount: item.amount, + currency: item.router_data.request.currency, payment_method_id: PaymentMethodId::Card, payment_method_flow: PaymentMethodFlow::Direct, country: country.to_string(), @@ -99,26 +132,45 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for DlocalPaymentsRequest { expiration_year: ccard.card_exp_year.clone(), capture: should_capture.to_string(), installments_id: item + .router_data .request .mandate_id .as_ref() .map(|ids| ids.mandate_id.clone()), // [#595[FEATURE] Pass Mandate history information in payment flows/request] - installments: item.request.mandate_id.clone().map(|_| "1".to_string()), + installments: item + .router_data + .request + .mandate_id + .clone() + .map(|_| "1".to_string()), }), - order_id: item.payment_id.clone(), - three_dsecure: match item.auth_type { + order_id: item.router_data.payment_id.clone(), + three_dsecure: match item.router_data.auth_type { diesel_models::enums::AuthenticationType::ThreeDs => { Some(ThreeDSecureReqData { force: true }) } diesel_models::enums::AuthenticationType::NoThreeDs => None, }, - callback_url: Some(item.request.get_router_return_url()?), - description: item.description.clone(), + callback_url: Some(item.router_data.request.get_router_return_url()?), + description: item.router_data.description.clone(), }; Ok(payment_request) } - _ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()), + api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::Wallet(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + crate::connector::utils::get_unimplemented_payment_method_error_message("Dlocal"), + ))?, } } } @@ -399,22 +451,24 @@ impl // REFUND : #[derive(Default, Debug, Serialize)] -pub struct RefundRequest { +pub struct DlocalRefundRequest { pub amount: String, pub payment_id: String, pub currency: enums::Currency, pub id: String, } -impl TryFrom<&types::RefundsRouterData> for RefundRequest { +impl TryFrom<&DlocalRouterData<&types::RefundsRouterData>> for DlocalRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let amount_to_refund = item.request.refund_amount.to_string(); + fn try_from( + item: &DlocalRouterData<&types::RefundsRouterData>, + ) -> Result { + let amount_to_refund = item.router_data.request.refund_amount.to_string(); Ok(Self { amount: amount_to_refund, - payment_id: item.request.connector_transaction_id.clone(), - currency: item.request.currency, - id: item.request.refund_id.clone(), + payment_id: item.router_data.request.connector_transaction_id.clone(), + currency: item.router_data.request.currency, + id: item.router_data.request.refund_id.clone(), }) } } diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index bc7c55c4f87c..7851608d11b1 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -101,9 +101,23 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { card, }) } - _ => Err(errors::ConnectorError::NotImplemented( - "Payment Method".to_string(), - ))?, + api_models::payments::PaymentMethodData::CardRedirect(_) + | api_models::payments::PaymentMethodData::Wallet(_) + | api_models::payments::PaymentMethodData::PayLater(_) + | api_models::payments::PaymentMethodData::BankRedirect(_) + | api_models::payments::PaymentMethodData::BankDebit(_) + | api_models::payments::PaymentMethodData::BankTransfer(_) + | api_models::payments::PaymentMethodData::Crypto(_) + | api_models::payments::PaymentMethodData::MandatePayment {} + | api_models::payments::PaymentMethodData::Reward {} + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::Voucher(_) + | api_models::payments::PaymentMethodData::GiftCard(_) => { + Err(errors::ConnectorError::NotSupported { + message: utils::SELECTED_PAYMENT_METHOD.to_string(), + connector: "Forte", + })? + } } } } diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 9d4ecdff197f..f98798fe5be4 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -86,7 +86,18 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for IatapayPaymentsRequest { let payment_method = item.payment_method; let country = match payment_method { PaymentMethod::Upi => "IN".to_string(), - _ => item.get_billing_country()?.to_string(), + + PaymentMethod::Card + | PaymentMethod::CardRedirect + | PaymentMethod::PayLater + | PaymentMethod::Wallet + | PaymentMethod::BankRedirect + | PaymentMethod::BankTransfer + | PaymentMethod::Crypto + | PaymentMethod::BankDebit + | PaymentMethod::Reward + | PaymentMethod::Voucher + | PaymentMethod::GiftCard => item.get_billing_country()?.to_string(), }; let return_url = item.get_return_url()?; let payer_info = match item.request.payment_method_data.clone() { diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 31e356d47651..563410ee99d0 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -159,12 +159,14 @@ impl TryFrom> ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.order_id.clone(), + ), 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.order_id.clone()), }), status: item.response.fraud_status.into(), ..item.data diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 897d8f639d35..2af3ee0a1bb8 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -9,7 +9,7 @@ use url::Url; use crate::{ connector::utils::{ - CardData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, WalletData, + self, CardData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, WalletData, }, consts, core::errors, @@ -597,12 +597,35 @@ fn get_payment_details_and_product( api_models::payments::BankRedirectData::Sofort { .. } => { Ok((None, NexinetsProduct::Sofort)) } - _ => Err(errors::ConnectorError::NotImplemented( - "Payment methods".to_string(), - ))?, + api_models::payments::BankRedirectData::BancontactCard { .. } + | api_models::payments::BankRedirectData::Blik { .. } + | api_models::payments::BankRedirectData::Bizum { .. } + | api_models::payments::BankRedirectData::Interac { .. } + | api_models::payments::BankRedirectData::OnlineBankingCzechRepublic { .. } + | api_models::payments::BankRedirectData::OnlineBankingFinland { .. } + | api_models::payments::BankRedirectData::OnlineBankingPoland { .. } + | api_models::payments::BankRedirectData::OnlineBankingSlovakia { .. } + | api_models::payments::BankRedirectData::OpenBankingUk { .. } + | api_models::payments::BankRedirectData::Przelewy24 { .. } + | api_models::payments::BankRedirectData::Trustly { .. } + | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } + | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nexinets"), + ))? + } }, - _ => Err(errors::ConnectorError::NotImplemented( - "Payment methods".to_string(), + PaymentMethodData::CardRedirect(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Reward + | PaymentMethodData::Upi(_) + | PaymentMethodData::Voucher(_) + | PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nexinets"), ))?, } } @@ -677,9 +700,34 @@ fn get_wallet_details( ))), NexinetsProduct::Applepay, )), - _ => Err(errors::ConnectorError::NotImplemented( - "Payment methods".to_string(), - ))?, + api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::AliPayRedirect(_) + | api_models::payments::WalletData::AliPayHkRedirect(_) + | api_models::payments::WalletData::MomoRedirect(_) + | api_models::payments::WalletData::KakaoPayRedirect(_) + | api_models::payments::WalletData::GoPayRedirect(_) + | api_models::payments::WalletData::GcashRedirect(_) + | api_models::payments::WalletData::ApplePayRedirect(_) + | api_models::payments::WalletData::ApplePayThirdPartySdk(_) + | api_models::payments::WalletData::DanaRedirect { .. } + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::GooglePayRedirect(_) + | api_models::payments::WalletData::GooglePayThirdPartySdk(_) + | api_models::payments::WalletData::MbWayRedirect(_) + | api_models::payments::WalletData::MobilePayRedirect(_) + | api_models::payments::WalletData::PaypalSdk(_) + | api_models::payments::WalletData::SamsungPay(_) + | api_models::payments::WalletData::TwintRedirect { .. } + | api_models::payments::WalletData::VippsRedirect { .. } + | api_models::payments::WalletData::TouchNGoRedirect(_) + | api_models::payments::WalletData::WeChatPayRedirect(_) + | api_models::payments::WalletData::WeChatPayQr(_) + | api_models::payments::WalletData::CashappQr(_) + | api_models::payments::WalletData::SwishQr(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nexinets"), + ))? + } } } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 9d727d3ce881..cde6de2e43b6 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -506,7 +506,6 @@ pub struct NoonActionTransaction { #[serde(rename_all = "camelCase")] pub struct NoonActionOrder { id: String, - cancellation_reason: Option, } #[derive(Debug, Serialize)] @@ -522,7 +521,6 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest { fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), - cancellation_reason: None, }; let transaction = NoonActionTransaction { amount: conn_utils::to_currency_base_unit( @@ -552,11 +550,6 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NoonPaymentsCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), - cancellation_reason: item - .request - .cancellation_reason - .clone() - .map(|reason| reason.chars().take(100).collect()), // Max 100 chars }; Ok(Self { api_operation: NoonApiOperations::Reverse, @@ -570,7 +563,6 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { fn try_from(item: &types::RefundsRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), - cancellation_reason: None, }; let transaction = NoonActionTransaction { amount: conn_utils::to_currency_base_unit( diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index a0a787269e37..120795bef9c5 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -8,7 +8,7 @@ use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as paypal; -use self::transformers::{PaypalAuthResponse, PaypalMeta}; +use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, @@ -30,6 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + transformers::ForeignFrom, ErrorResponse, Response, }, utils::{self, BytesExt}, @@ -174,26 +175,31 @@ impl ConnectorCommon for Paypal { .map(|error_details| { error_details .iter() - .try_fold::<_, _, CustomResult<_, errors::ConnectorError>>( - String::new(), - |mut acc, error| { - write!(acc, "description - {} ;", error.description) + .try_fold(String::new(), |mut acc, error| { + if let Some(description) = &error.description { + write!(acc, "description - {} ;", description) .into_report() .change_context( errors::ConnectorError::ResponseDeserializationFailed, ) .attach_printable("Failed to concatenate error details") .map(|_| acc) - }, - ) + } else { + Ok(acc) + } + }) }) .transpose()?; + let reason = error_reason + .unwrap_or(response.message.to_owned()) + .is_empty() + .then_some(response.message.to_owned()); Ok(ErrorResponse { status_code: res.status_code, code: response.name, message: response.message.clone(), - reason: error_reason.or(Some(response.message)), + reason, }) } } @@ -211,6 +217,10 @@ impl ConnectorValidation for Paypal { ), } } + + fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { + Ok(()) + } } impl @@ -1104,6 +1114,17 @@ impl api::IncomingWebhook for Paypal { api_models::webhooks::RefundIdType::ConnectorRefundId(resource.id), )) } + paypal::PaypalResource::PaypalDisputeWebhooks(resource) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + resource + .dispute_transactions + .first() + .map(|transaction| transaction.reference_id.clone()) + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + )) + } } } @@ -1115,7 +1136,33 @@ impl api::IncomingWebhook for Paypal { .body .parse_struct("PaypalWebooksEventType") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Ok(api::IncomingWebhookEvent::from(payload.event_type)) + let outcome = match payload.event_type { + PaypalWebhookEventType::CustomerDisputeCreated + | PaypalWebhookEventType::CustomerDisputeResolved + | PaypalWebhookEventType::CustomerDisputedUpdated + | PaypalWebhookEventType::RiskDisputeCreated => Some( + request + .body + .parse_struct::("PaypalWebooksEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)? + .outcome_code, + ), + PaypalWebhookEventType::PaymentAuthorizationCreated + | PaypalWebhookEventType::PaymentAuthorizationVoided + | PaypalWebhookEventType::PaymentCaptureDeclined + | PaypalWebhookEventType::PaymentCaptureCompleted + | PaypalWebhookEventType::PaymentCapturePending + | PaypalWebhookEventType::PaymentCaptureRefunded + | PaypalWebhookEventType::CheckoutOrderApproved + | PaypalWebhookEventType::CheckoutOrderCompleted + | PaypalWebhookEventType::CheckoutOrderProcessed + | PaypalWebhookEventType::Unknown => None, + }; + + Ok(api::IncomingWebhookEvent::foreign_from(( + payload.event_type, + outcome, + ))) } fn get_webhook_resource_object( @@ -1143,9 +1190,39 @@ impl api::IncomingWebhook for Paypal { ) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, + paypal::PaypalResource::PaypalDisputeWebhooks(_) => serde_json::to_value(details) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, }; Ok(sync_payload) } + + fn get_dispute_details( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let payload: paypal::PaypalDisputeWebhooks = request + .body + .parse_struct("PaypalDisputeWebhooks") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api::disputes::DisputePayload { + amount: connector_utils::to_currency_lower_unit( + payload.dispute_amount.value, + payload.dispute_amount.currency_code, + )?, + currency: payload.dispute_amount.currency_code.to_string(), + dispute_stage: api_models::enums::DisputeStage::from( + payload.dispute_life_cycle_stage.clone(), + ), + connector_status: payload.status.to_string(), + connector_dispute_id: payload.dispute_id, + connector_reason: payload.reason, + connector_reason_code: payload.external_reason_code, + challenge_required_by: payload.seller_response_due_date, + created_at: payload.create_time, + updated_at: payload.update_time, + }) + } } impl services::ConnectorRedirectResponse for Paypal { diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 72dccaed7332..0092363523e5 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,8 +1,9 @@ -use api_models::payments::BankRedirectData; +use api_models::{enums, payments::BankRedirectData}; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use url::Url; use crate::{ @@ -67,8 +68,8 @@ pub enum PaypalPaymentIntent { #[derive(Default, Debug, Clone, Serialize, Eq, PartialEq, Deserialize)] pub struct OrderAmount { - currency_code: storage_enums::Currency, - value: String, + pub currency_code: storage_enums::Currency, + pub value: String, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -1380,7 +1381,7 @@ pub struct PaypalOrderErrorResponse { #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ErrorDetails { pub issue: String, - pub description: String, + pub description: Option, } #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] @@ -1403,7 +1404,7 @@ pub struct PaypalWebhooksBody { pub resource: PaypalResource, } -#[derive(Deserialize, Debug, Serialize)] +#[derive(Clone, Deserialize, Debug, strum::Display, Serialize)] pub enum PaypalWebhookEventType { #[serde(rename = "PAYMENT.AUTHORIZATION.CREATED")] PaymentAuthorizationCreated, @@ -1423,6 +1424,14 @@ pub enum PaypalWebhookEventType { CheckoutOrderCompleted, #[serde(rename = "CHECKOUT.ORDER.PROCESSED")] CheckoutOrderProcessed, + #[serde(rename = "CUSTOMER.DISPUTE.CREATED")] + CustomerDisputeCreated, + #[serde(rename = "CUSTOMER.DISPUTE.RESOLVED")] + CustomerDisputeResolved, + #[serde(rename = "CUSTOMER.DISPUTE.UPDATED")] + CustomerDisputedUpdated, + #[serde(rename = "RISK.DISPUTE.CREATED")] + RiskDisputeCreated, #[serde(other)] Unknown, } @@ -1433,6 +1442,64 @@ pub enum PaypalResource { PaypalCardWebhooks(Box), PaypalRedirectsWebhooks(Box), PaypalRefundWebhooks(Box), + PaypalDisputeWebhooks(Box), +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalDisputeWebhooks { + pub dispute_id: String, + pub dispute_transactions: Vec, + pub dispute_amount: OrderAmount, + pub dispute_outcome: DisputeOutcome, + pub dispute_life_cycle_stage: DisputeLifeCycleStage, + pub status: DisputeStatus, + pub reason: Option, + pub external_reason_code: Option, + pub seller_response_due_date: Option, + pub update_time: Option, + pub create_time: Option, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct DisputeTransaction { + pub reference_id: String, +} + +#[derive(Clone, Deserialize, Debug, strum::Display, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DisputeLifeCycleStage { + Inquiry, + Chargeback, + PreArbitration, + Arbitration, +} + +#[derive(Deserialize, Debug, strum::Display, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DisputeStatus { + Open, + WaitingForBuyerResponse, + WaitingForSellerResponse, + UnderReview, + Resolved, + Other, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct DisputeOutcome { + pub outcome_code: OutcomeCode, +} + +#[derive(Deserialize, Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OutcomeCode { + ResolvedBuyerFavour, + ResolvedSellerFavour, + ResolvedWithPayout, + CanceledByBuyer, + ACCEPTED, + DENIED, + NONE, } #[derive(Deserialize, Debug, Serialize)] @@ -1482,8 +1549,8 @@ pub struct PaypalWebooksEventType { pub event_type: PaypalWebhookEventType, } -impl From for api::IncomingWebhookEvent { - fn from(event: PaypalWebhookEventType) -> Self { +impl ForeignFrom<(PaypalWebhookEventType, Option)> for api::IncomingWebhookEvent { + fn foreign_from((event, outcome): (PaypalWebhookEventType, Option)) -> Self { match event { PaypalWebhookEventType::PaymentCaptureCompleted | PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess, @@ -1491,14 +1558,49 @@ impl From for api::IncomingWebhookEvent { | PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing, PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure, PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess, + PaypalWebhookEventType::CustomerDisputeCreated => Self::DisputeOpened, + PaypalWebhookEventType::RiskDisputeCreated => Self::DisputeAccepted, + PaypalWebhookEventType::CustomerDisputeResolved => { + if let Some(outcome_code) = outcome { + Self::from(outcome_code) + } else { + Self::EventNotSupported + } + } PaypalWebhookEventType::PaymentAuthorizationCreated | PaypalWebhookEventType::PaymentAuthorizationVoided | PaypalWebhookEventType::CheckoutOrderApproved + | PaypalWebhookEventType::CustomerDisputedUpdated | PaypalWebhookEventType::Unknown => Self::EventNotSupported, } } } +impl From for api::IncomingWebhookEvent { + fn from(outcome_code: OutcomeCode) -> Self { + match outcome_code { + OutcomeCode::ResolvedBuyerFavour => Self::DisputeLost, + OutcomeCode::ResolvedSellerFavour => Self::DisputeWon, + OutcomeCode::CanceledByBuyer => Self::DisputeCancelled, + OutcomeCode::ACCEPTED => Self::DisputeAccepted, + OutcomeCode::DENIED => Self::DisputeCancelled, + OutcomeCode::NONE => Self::DisputeCancelled, + OutcomeCode::ResolvedWithPayout => Self::EventNotSupported, + } + } +} + +impl From for enums::DisputeStage { + fn from(dispute_life_cycle_stage: DisputeLifeCycleStage) -> Self { + match dispute_life_cycle_stage { + DisputeLifeCycleStage::Inquiry => Self::PreDispute, + DisputeLifeCycleStage::Chargeback => Self::Dispute, + DisputeLifeCycleStage::PreArbitration => Self::PreArbitration, + DisputeLifeCycleStage::Arbitration => Self::PreArbitration, + } + } +} + #[derive(Deserialize, Serialize, Debug)] pub struct PaypalSourceVerificationRequest { pub transmission_id: String, @@ -1617,7 +1719,11 @@ impl TryFrom for PaypalPaymentStatus { | PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending), PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created), PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Refunded), - PaypalWebhookEventType::Unknown => { + PaypalWebhookEventType::CustomerDisputeCreated + | PaypalWebhookEventType::CustomerDisputeResolved + | PaypalWebhookEventType::CustomerDisputedUpdated + | PaypalWebhookEventType::RiskDisputeCreated + | PaypalWebhookEventType::Unknown => { Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) } } @@ -1637,6 +1743,10 @@ impl TryFrom for RefundStatus { | PaypalWebhookEventType::CheckoutOrderApproved | PaypalWebhookEventType::CheckoutOrderCompleted | PaypalWebhookEventType::CheckoutOrderProcessed + | PaypalWebhookEventType::CustomerDisputeCreated + | PaypalWebhookEventType::CustomerDisputeResolved + | PaypalWebhookEventType::CustomerDisputedUpdated + | PaypalWebhookEventType::RiskDisputeCreated | PaypalWebhookEventType::Unknown => { Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) } @@ -1657,6 +1767,10 @@ impl TryFrom for PaypalOrderStatus { PaypalWebhookEventType::CheckoutOrderApproved | PaypalWebhookEventType::PaymentCaptureDeclined | PaypalWebhookEventType::PaymentCaptureRefunded + | PaypalWebhookEventType::CustomerDisputeCreated + | PaypalWebhookEventType::CustomerDisputeResolved + | PaypalWebhookEventType::CustomerDisputedUpdated + | PaypalWebhookEventType::RiskDisputeCreated | PaypalWebhookEventType::Unknown => { Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) } diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs new file mode 100644 index 000000000000..0e8d5100ea35 --- /dev/null +++ b/crates/router/src/connector/prophetpay.rs @@ -0,0 +1,534 @@ +pub mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use transformers as prophetpay; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Prophetpay; +impl api::payments::MandateSetup for Prophetpay {} +impl api::Payment for Prophetpay {} +impl api::PaymentSession for Prophetpay {} +impl api::ConnectorAccessToken for Prophetpay {} +impl api::PaymentAuthorize for Prophetpay {} +impl api::PaymentSync for Prophetpay {} +impl api::PaymentCapture for Prophetpay {} +impl api::PaymentVoid for Prophetpay {} +impl api::Refund for Prophetpay {} +impl api::RefundExecute for Prophetpay {} +impl api::RefundSync for Prophetpay {} +impl api::PaymentToken for Prophetpay {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Prophetpay +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Prophetpay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Prophetpay { + fn id(&self) -> &'static str { + "prophetpay" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.prophetpay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = prophetpay::ProphetpayAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: prophetpay::ProphetpayErrorResponse = res + .response + .parse_struct("ProphetpayErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + }) + } +} + +impl ConnectorValidation for Prophetpay { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration + for Prophetpay +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Prophetpay +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Prophetpay +{ +} + +impl ConnectorIntegration + for Prophetpay +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = prophetpay::ProphetpayPaymentsRequest::try_from(&connector_router_data)?; + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: prophetpay::ProphetpayPaymentsResponse = res + .response + .parse_struct("Prophetpay PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Prophetpay +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: prophetpay::ProphetpayPaymentsResponse = res + .response + .parse_struct("prophetpay PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Prophetpay +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: prophetpay::ProphetpayPaymentsResponse = res + .response + .parse_struct("Prophetpay PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Prophetpay +{ +} + +impl ConnectorIntegration + for Prophetpay +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = prophetpay::ProphetpayRefundRequest::try_from(&connector_router_data)?; + let prophetpay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(prophetpay_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: prophetpay::RefundResponse = res + .response + .parse_struct("prophetpay RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Prophetpay +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: prophetpay::RefundResponse = res + .response + .parse_struct("prophetpay RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Prophetpay { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs new file mode 100644 index 000000000000..1066c88df3e1 --- /dev/null +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -0,0 +1,233 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +pub struct ProphetpayRouterData { + pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for ProphetpayRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount, + router_data: item, + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ProphetpayPaymentsRequest { + amount: i64, + card: ProphetpayCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ProphetpayCard { + name: Secret, + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> + for ProphetpayPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = ProphetpayCard { + name: req_card.card_holder_name, + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.router_data.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.amount.to_owned(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +pub struct ProphetpayAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for ProphetpayAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ProphetpayPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: ProphetpayPaymentStatus) -> Self { + match item { + ProphetpayPaymentStatus::Succeeded => Self::Charged, + ProphetpayPaymentStatus::Failed => Self::Failure, + ProphetpayPaymentStatus::Processing => Self::Authorizing, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProphetpayPaymentsResponse { + status: ProphetpayPaymentStatus, + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + ProphetpayPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize)] +pub struct ProphetpayRefundRequest { + pub amount: i64, +} + +impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for ProphetpayRefundRequest { + type Error = error_stack::Report; + fn try_from( + item: &ProphetpayRouterData<&types::RefundsRouterData>, + ) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ProphetpayErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index fd2d369a7c55..912f1575e1e0 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -148,7 +148,11 @@ impl ConnectorCommon for Trustpay { } } -impl ConnectorValidation for Trustpay {} +impl ConnectorValidation for Trustpay { + fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { + Ok(()) + } +} impl api::Payment for Trustpay {} diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index d8516c8293bd..83134568c05d 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -34,6 +34,7 @@ pub struct TsysPaymentAuthSaleRequest { cardholder_authentication_method: String, #[serde(rename = "developerID")] developer_id: Secret, + order_number: String, } impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { @@ -57,6 +58,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { terminal_operating_environment: "ON_MERCHANT_PREMISES_ATTENDED".to_string(), cardholder_authentication_method: "NOT_AUTHENTICATED".to_string(), developer_id: connector_auth.developer_id, + order_number: item.connector_request_reference_id.clone(), }; if item.request.is_auto_capture()? { Ok(Self::Sale(auth_data)) diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index aabe27fc4eb1..61d04a8e9f11 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -167,10 +167,14 @@ impl debt_repayment: None, }, merchant: Merchant { - entity: item.router_data.attempt_id.clone().replace('_', "-"), + entity: item + .router_data + .connector_request_reference_id + .clone() + .replace('_', "-"), ..Default::default() }, - transaction_reference: item.router_data.attempt_id.clone(), + transaction_reference: item.router_data.connector_request_reference_id.clone(), channel: None, customer: None, }) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 1f60f11a97e1..c0d6c576dd56 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -252,6 +252,31 @@ pub async fn create_merchant_account( )) } +#[cfg(feature = "olap")] +pub async fn list_merchant_account( + state: AppState, + req: api_models::admin::MerchantAccountListRequest, +) -> RouterResponse> { + let merchant_accounts = state + .store + .list_merchant_accounts_by_organization_id(&req.organization_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant_accounts = merchant_accounts + .into_iter() + .map(|merchant_account| { + merchant_account + .try_into() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_account", + }) + }) + .collect::, _>>()?; + + Ok(services::ApplicationResponse::Json(merchant_accounts)) +} + pub async fn get_merchant_account( state: AppState, req: api::MerchantId, @@ -636,12 +661,38 @@ pub async fn create_payment_connector( &merchant_account, )?; - let connector_label = core_utils::get_connector_label( + // Business label support will be deprecated soon + let profile_id = core_utils::get_profile_id_from_business_details( req.business_country, req.business_label.as_ref(), - req.business_sub_label.as_ref(), - &req.connector_name.to_string(), - ); + &merchant_account, + req.profile_id.as_ref(), + store, + true, + ) + .await?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(&profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_owned(), + })?; + + // If connector label is not passed in the request, generate one + let connector_label = req + .connector_label + .or(core_utils::get_connector_label( + req.business_country, + req.business_label.as_ref(), + req.business_sub_label.as_ref(), + &req.connector_name.to_string(), + )) + .unwrap_or(format!( + "{}_{}", + req.connector_name, business_profile.profile_name + )); let mut vec = Vec::new(); let payment_methods_enabled = match req.payment_methods_enabled { @@ -676,11 +727,10 @@ pub async fn create_payment_connector( message: "The connector name is invalid".to_string(), }) } - errors::ConnectorError::InvalidConfig { field_name } => { - err.change_context(errors::ApiErrorResponse::InvalidRequestData { + errors::ConnectorError::InvalidConnectorConfig { config: field_name } => err + .change_context(errors::ApiErrorResponse::InvalidRequestData { message: format!("The {} is invalid", field_name), - }) - } + }), errors::ConnectorError::FailedToObtainAuthType => { err.change_context(errors::ApiErrorResponse::InvalidRequestData { message: "The auth type is invalid for the connector".to_string(), @@ -694,16 +744,6 @@ pub async fn create_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); - let profile_id = core_utils::get_profile_id_from_business_details( - req.business_country, - req.business_label.as_ref(), - &merchant_account, - req.profile_id.as_ref(), - &*state.store, - true, - ) - .await?; - let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -725,7 +765,7 @@ pub async fn create_payment_connector( disabled: req.disabled, metadata: req.metadata, frm_configs, - connector_label: connector_label.clone(), + connector_label: Some(connector_label), business_country: req.business_country, business_label: req.business_label.clone(), business_sub_label: req.business_sub_label, @@ -887,6 +927,7 @@ pub async fn update_payment_connector( connector_type: Some(req.connector_type), connector_name: None, merchant_connector_id: None, + connector_label: req.connector_label, connector_account_details: req .connector_account_details .async_lift(|inner| { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 9d75904ef660..1c062b7035af 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -179,7 +179,7 @@ pub enum ConnectorError { connector: &'static str, }, #[error("Invalid Configuration")] - InvalidConfig { field_name: &'static str }, + InvalidConnectorConfig { config: &'static str }, } #[derive(Debug, thiserror::Error)] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index e606771b372c..d34cbf88aaae 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -236,6 +236,8 @@ pub enum ApiErrorResponse { WebhookInvalidMerchantSecret, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] CurrencyNotSupported { message: String }, + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_24", message = "Merchant connector account is configured with invalid {config}")] + InvalidConnectorConfiguration { config: String }, } impl PTError for ApiErrorResponse { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 19640a931e1a..17aa6f3a207a 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -267,6 +267,9 @@ impl ErrorSwitch for ApiErrorRespon Self::PaymentLinkNotFound => { AER::NotFound(ApiError::new("HE", 2, "Payment Link does not exist in our records", None)) } + Self::InvalidConnectorConfiguration {config} => { + AER::BadRequest(ApiError::new("IR", 24, format!("Merchant connector account is configured with invalid {config}"), None)) + } } } } diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 8a86be2036be..c3cdf95b87bd 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -205,7 +205,48 @@ impl ConnectorErrorExt for error_stack::Result errors::ApiErrorResponse::InvalidDataValue { field_name } }, errors::ConnectorError::CurrencyNotSupported { message, connector} => errors::ApiErrorResponse::CurrencyNotSupported { message: format!("Credentials for the currency {message} are not configured with the connector {connector}/hyperswitch") }, - _ => errors::ApiErrorResponse::InternalServerError, + errors::ConnectorError::FailedToObtainAuthType => errors::ApiErrorResponse::InvalidConnectorConfiguration {config: "connector_account_details".to_string()}, + errors::ConnectorError::InvalidConnectorConfig { config } => errors::ApiErrorResponse::InvalidConnectorConfiguration { config: config.to_string() }, + errors::ConnectorError::FailedToObtainIntegrationUrl | + errors::ConnectorError::RequestEncodingFailed | + errors::ConnectorError::RequestEncodingFailedWithReason(_) | + errors::ConnectorError::ParsingFailed | + errors::ConnectorError::ResponseDeserializationFailed | + errors::ConnectorError::UnexpectedResponseError(_) | + errors::ConnectorError::RoutingRulesParsingError | + errors::ConnectorError::FailedToObtainPreferredConnector | + errors::ConnectorError::InvalidConnectorName | + errors::ConnectorError::InvalidWallet | + errors::ConnectorError::ResponseHandlingFailed | + errors::ConnectorError::FailedToObtainCertificate | + errors::ConnectorError::NoConnectorMetaData | + errors::ConnectorError::FailedToObtainCertificateKey | + errors::ConnectorError::CaptureMethodNotSupported | + errors::ConnectorError::MissingConnectorMandateID | + errors::ConnectorError::MissingConnectorTransactionID | + errors::ConnectorError::MissingConnectorRefundID | + errors::ConnectorError::MissingApplePayTokenData | + errors::ConnectorError::WebhooksNotImplemented | + errors::ConnectorError::WebhookBodyDecodingFailed | + errors::ConnectorError::WebhookSignatureNotFound | + errors::ConnectorError::WebhookSourceVerificationFailed | + errors::ConnectorError::WebhookVerificationSecretNotFound | + errors::ConnectorError::WebhookVerificationSecretInvalid | + errors::ConnectorError::WebhookReferenceIdNotFound | + errors::ConnectorError::WebhookEventTypeNotFound | + errors::ConnectorError::WebhookResourceObjectNotFound | + errors::ConnectorError::WebhookResponseEncodingFailed | + errors::ConnectorError::InvalidDateFormat | + errors::ConnectorError::DateFormattingFailed | + errors::ConnectorError::InvalidWalletToken | + errors::ConnectorError::MissingConnectorRelatedTransactionID { .. } | + errors::ConnectorError::FileValidationFailed { .. } | + errors::ConnectorError::MissingConnectorRedirectionPayload { .. } | + errors::ConnectorError::FailedAtConnector { .. } | + errors::ConnectorError::MissingPaymentMethodType | + errors::ConnectorError::InSufficientBalanceInPaymentMethod | + errors::ConnectorError::RequestTimeoutReceived | + errors::ConnectorError::ProcessingStepFailed(None) => errors::ApiErrorResponse::InternalServerError }; err.change_context(error) }) @@ -235,8 +276,64 @@ impl ConnectorErrorExt for error_stack::Result errors::ConnectorError::MissingRequiredField { field_name } => { errors::ApiErrorResponse::MissingRequiredField { field_name } } - _ => { - logger::error!(%error,"Verify flow failed"); + errors::ConnectorError::FailedToObtainIntegrationUrl => { + errors::ApiErrorResponse::InvalidConnectorConfiguration { + config: "connector_account_details".to_string(), + } + } + errors::ConnectorError::InvalidConnectorConfig { config: field_name } => { + errors::ApiErrorResponse::InvalidConnectorConfiguration { + config: field_name.to_string(), + } + } + errors::ConnectorError::RequestEncodingFailed + | errors::ConnectorError::RequestEncodingFailedWithReason(_) + | errors::ConnectorError::ParsingFailed + | errors::ConnectorError::ResponseDeserializationFailed + | errors::ConnectorError::UnexpectedResponseError(_) + | errors::ConnectorError::RoutingRulesParsingError + | errors::ConnectorError::FailedToObtainPreferredConnector + | errors::ConnectorError::InvalidConnectorName + | errors::ConnectorError::InvalidWallet + | errors::ConnectorError::ResponseHandlingFailed + | errors::ConnectorError::MissingRequiredFields { .. } + | errors::ConnectorError::FailedToObtainAuthType + | errors::ConnectorError::FailedToObtainCertificate + | errors::ConnectorError::NoConnectorMetaData + | errors::ConnectorError::FailedToObtainCertificateKey + | errors::ConnectorError::NotImplemented(_) + | errors::ConnectorError::NotSupported { .. } + | errors::ConnectorError::FlowNotSupported { .. } + | errors::ConnectorError::CaptureMethodNotSupported + | errors::ConnectorError::MissingConnectorMandateID + | errors::ConnectorError::MissingConnectorTransactionID + | errors::ConnectorError::MissingConnectorRefundID + | errors::ConnectorError::MissingApplePayTokenData + | errors::ConnectorError::WebhooksNotImplemented + | errors::ConnectorError::WebhookBodyDecodingFailed + | errors::ConnectorError::WebhookSignatureNotFound + | errors::ConnectorError::WebhookSourceVerificationFailed + | errors::ConnectorError::WebhookVerificationSecretNotFound + | errors::ConnectorError::WebhookVerificationSecretInvalid + | errors::ConnectorError::WebhookReferenceIdNotFound + | errors::ConnectorError::WebhookEventTypeNotFound + | errors::ConnectorError::WebhookResourceObjectNotFound + | errors::ConnectorError::WebhookResponseEncodingFailed + | errors::ConnectorError::InvalidDateFormat + | errors::ConnectorError::DateFormattingFailed + | errors::ConnectorError::InvalidDataFormat { .. } + | errors::ConnectorError::MismatchedPaymentData + | errors::ConnectorError::InvalidWalletToken + | errors::ConnectorError::MissingConnectorRelatedTransactionID { .. } + | errors::ConnectorError::FileValidationFailed { .. } + | errors::ConnectorError::MissingConnectorRedirectionPayload { .. } + | errors::ConnectorError::FailedAtConnector { .. } + | errors::ConnectorError::MissingPaymentMethodType + | errors::ConnectorError::InSufficientBalanceInPaymentMethod + | errors::ConnectorError::RequestTimeoutReceived + | errors::ConnectorError::CurrencyNotSupported { .. } + | errors::ConnectorError::ProcessingStepFailed(None) => { + logger::error!(%error,"Setup Mandate flow failed"); errors::ApiErrorResponse::PaymentAuthorizationFailed { data: None } } }; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2161ab69222e..1c094a5716d6 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2010,9 +2010,14 @@ pub async fn get_lookup_key_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Get Card Details Failed")?; let card = card_detail.clone(); - let resp = - BasiliskCardSupport::create_payment_method_data_in_locker(state, payment_token, card, pm) - .await?; + + let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( + state, + payment_token, + card, + pm, + ) + .await?; Ok(resp) } @@ -2058,106 +2063,11 @@ pub async fn get_lookup_key_for_payout_method( } } -pub struct BasiliskCardSupport; - -#[cfg(not(feature = "basilisk"))] -impl BasiliskCardSupport { - async fn create_payment_method_data_in_locker( - state: &routes::AppState, - payment_token: &str, - card: api::CardDetailFromLocker, - pm: &storage::PaymentMethod, - ) -> errors::RouterResult { - let card_number = card.card_number.clone().get_required_value("card_number")?; - let card_exp_month = card - .expiry_month - .clone() - .expose_option() - .get_required_value("expiry_month")?; - let card_exp_year = card - .expiry_year - .clone() - .expose_option() - .get_required_value("expiry_year")?; - let card_holder_name = card - .card_holder_name - .clone() - .expose_option() - .unwrap_or_default(); - let value1 = payment_methods::mk_card_value1( - card_number, - card_exp_year, - card_exp_month, - Some(card_holder_name), - None, - None, - None, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - let value2 = payment_methods::mk_card_value2( - None, - None, - None, - Some(pm.customer_id.to_string()), - Some(pm.payment_method_id.to_string()), - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let value1 = vault::VaultPaymentMethod::Card(value1); - let value2 = vault::VaultPaymentMethod::Card(value2); - - let value1 = utils::Encode::::encode_to_string_of_json(&value1) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value1 construction failed when saving card to locker")?; - - let value2 = utils::Encode::::encode_to_string_of_json(&value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Wrapped value2 construction failed when saving card to locker")?; - - let db_value = vault::MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Mock tokenize value construction failed when saving card to locker", - )?; - - let db = &*state.store; - - let already_present = db.find_config_by_key(payment_token).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: payment_token.to_string(), - config: value_string, - }; - - db.insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - - db.update_config_by_key(payment_token, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization db update failed")?; - } - - Ok(card) - } -} +pub struct TempLockerCardSupport; -#[cfg(feature = "basilisk")] -impl BasiliskCardSupport { +impl TempLockerCardSupport { #[instrument(skip_all)] - async fn create_payment_method_data_in_locker( + async fn create_payment_method_data_in_temp_locker( state: &routes::AppState, payment_token: &str, card: api::CardDetailFromLocker, diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 086133ec78a5..6e951113547f 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -580,21 +580,6 @@ pub fn get_card_detail( } //------------------------------------------------TokenizeService------------------------------------------------ -pub fn mk_crud_locker_request( - locker: &settings::Locker, - path: &str, - req: api::TokenizePayloadEncrypted, -) -> CustomResult { - let body = utils::Encode::::encode_to_value(&req) - .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.basilisk_host.to_owned(); - url.push_str(path); - let mut request = services::Request::new(services::Method::Post, &url); - request.add_default_headers(); - request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); - Ok(request) -} pub fn mk_card_value1( card_number: cards::CardNumber, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index d16269deb9b2..dfe72a6cdf6d 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1,37 +1,31 @@ -use common_utils::generate_id_with_default_len; -#[cfg(feature = "basilisk")] -use error_stack::report; -use error_stack::{IntoReport, ResultExt}; -#[cfg(feature = "basilisk")] -use josekit::jwe; +use common_utils::{ + crypto::{DecodeMessage, EncodeMessage, GcmAes256}, + ext_traits::BytesExt, + generate_id_with_default_len, +}; +use error_stack::{report, IntoReport, ResultExt}; use masking::PeekInterface; use router_env::{instrument, tracing}; -#[cfg(feature = "basilisk")] use scheduler::{types::process_data, utils as process_tracker_utils}; -#[cfg(feature = "basilisk")] -use crate::routes::metrics; #[cfg(feature = "payouts")] use crate::types::api::payouts; use crate::{ - configs::settings, core::errors::{self, CustomResult, RouterResult}, - logger, routes, + db, logger, routes, + routes::metrics, types::{ api, - storage::{self, enums}, + storage::{self, enums, ProcessTrackerExt}, }, utils::{self, StringExt}, }; -#[cfg(feature = "basilisk")] -use crate::{core::payment_methods::transformers as payment_methods, services, utils::BytesExt}; -#[cfg(feature = "basilisk")] -use crate::{db, types::storage::ProcessTrackerExt}; -#[cfg(feature = "basilisk")] const VAULT_SERVICE_NAME: &str = "CARD"; -#[cfg(feature = "basilisk")] -const VAULT_VERSION: &str = "0"; + +const LOCKER_REDIS_PREFIX: &str = "LOCKER_TOKEN"; + +const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes pub struct SupplementaryVaultData { pub customer_id: Option, @@ -622,189 +616,6 @@ pub struct MockTokenizeDBValue { pub struct Vault; -#[cfg(not(feature = "basilisk"))] -impl Vault { - #[instrument(skip_all)] - pub async fn get_payment_method_data_from_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payment_method, supp_data) = - api::PaymentMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payment Method from Values")?; - - Ok((Some(payment_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn get_payout_method_data_from_temporary_locker( - state: &routes::AppState, - lookup_key: &str, - ) -> RouterResult<(Option, SupplementaryVaultData)> { - let config = state - .store - .find_config_by_key(lookup_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not find payment method in vault")?; - - let tokenize_value: MockTokenizeDBValue = config - .config - .parse_struct("MockTokenizeDBValue") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize Mock tokenize db value")?; - - let (payout_method, supp_data) = - api::PayoutMethodData::from_values(tokenize_value.value1, tokenize_value.value2) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error parsing Payout Method from Values")?; - - Ok((Some(payout_method), supp_data)) - } - - #[cfg(feature = "payouts")] - #[instrument(skip_all)] - pub async fn store_payout_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payout_method: &api::PayoutMethodData, - customer_id: Option, - ) -> RouterResult { - let value1 = payout_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payout_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value2 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payout method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn store_payment_method_data_in_locker( - state: &routes::AppState, - token_id: Option, - payment_method: &api::PaymentMethodData, - customer_id: Option, - _pm: enums::PaymentMethod, - ) -> RouterResult { - let value1 = payment_method - .get_value1(customer_id.clone()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value1 for locker")?; - - let value2 = payment_method - .get_value2(customer_id) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Value12 for locker")?; - - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); - - let db_value = MockTokenizeDBValue { value1, value2 }; - - let value_string = - utils::Encode::::encode_to_string_of_json(&db_value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode payment method as mock tokenize db value")?; - - let already_present = state.store.find_config_by_key(&lookup_key).await; - - if already_present.is_err() { - let config = storage::ConfigNew { - key: lookup_key.clone(), - config: value_string, - }; - - state - .store - .insert_config(config) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed insert")?; - } else { - let config_update = storage::ConfigUpdate::Update { - config: Some(value_string), - }; - state - .store - .update_config_by_key(&lookup_key, config_update) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mock tokenization save to db failed update")?; - } - - Ok(lookup_key) - } - - #[instrument(skip_all)] - pub async fn delete_locker_payment_method_by_lookup_key( - state: &routes::AppState, - lookup_key: &Option, - ) { - let db = &*state.store; - if let Some(id) = lookup_key { - match db.delete_config_by_key(id).await { - Ok(_) => logger::info!("Card Deleted from locker mock up"), - Err(err) => logger::error!("Err: Card Delete from locker Failed : {}", err), - } - } - } -} - -#[cfg(feature = "basilisk")] impl Vault { #[instrument(skip_all)] pub async fn get_payment_method_data_from_locker( @@ -893,49 +704,41 @@ impl Vault { lookup_key: &Option, ) { if let Some(lookup_key) = lookup_key { - let delete_resp = delete_tokenized_data(state, lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully") - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp) - } - } - Err(err) => logger::error!("Err: Deleting Card From Locker : {:?}", err), - } + delete_tokenized_data(state, lookup_key) + .await + .map(|_| logger::info!("Card From locker deleted Successfully")) + .map_err(|err| logger::error!("Error: Deleting Card From Redis Locker : {:?}", err)) + .ok(); } } } //------------------------------------------------TokenizeService------------------------------------------------ -pub fn get_key_id(keys: &settings::Jwekey) -> &str { - let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources - if key_identifier == "1" { - &keys.locker_key_identifier1 - } else { - &keys.locker_key_identifier2 - } -} -#[cfg(feature = "basilisk")] -async fn get_locker_jwe_keys( - keys: &settings::ActiveKmsSecrets, -) -> CustomResult<(String, String), errors::EncryptionError> { - let keys = keys.jwekey.peek(); - let key_id = get_key_id(keys); - let (public_key, private_key) = if key_id == keys.locker_key_identifier1 { - (&keys.locker_encryption_key1, &keys.locker_decryption_key1) - } else if key_id == keys.locker_key_identifier2 { - (&keys.locker_encryption_key2, &keys.locker_decryption_key2) - } else { - return Err(errors::EncryptionError.into()); - }; +fn get_redis_temp_locker_encryption_key(state: &routes::AppState) -> RouterResult> { + #[cfg(feature = "kms")] + let secret = state + .kms_secrets + .redis_temp_locker_encryption_key + .peek() + .to_owned(); + + #[cfg(not(feature = "kms"))] + let secret = hex::decode( + state + .conf + .locker + .redis_temp_locker_encryption_key + .to_owned(), + ) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to decode redis temp locker data")?; - Ok((public_key.to_string(), private_key.to_string())) + Ok(secret) } -#[cfg(feature = "basilisk")] +#[instrument(skip(state, value1, value2))] pub async fn create_tokenize( state: &routes::AppState, value1: String, @@ -943,216 +746,125 @@ pub async fn create_tokenize( lookup_key: String, ) -> RouterResult { metrics::CREATED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); + + let redis_key = format!("{}_{}", LOCKER_REDIS_PREFIX, lookup_key); + let payload_to_be_encrypted = api::TokenizePayloadRequest { value1, value2: value2.unwrap_or_default(), - lookup_key, + lookup_key: lookup_key.to_owned(), service_name: VAULT_SERVICE_NAME.to_string(), }; + let payload = utils::Encode::::encode_to_string_of_json( &payload_to_be_encrypted, ) .change_context(errors::ApiErrorResponse::InternalServerError)?; - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await + let secret = get_redis_temp_locker_encryption_key(state)?; + + let encrypted_payload = GcmAes256 + .encode_message(secret.as_ref(), payload.as_bytes()) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await + .attach_printable("Failed to encode redis temp locker data")?; + + let redis_conn = state + .store + .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; + .attach_printable("Failed to get redis connection")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some(VAULT_VERSION.to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making tokenize request failed")?; - let response = services::call_connector_api(state, request) + redis_conn + .set_key_if_not_exists_with_expiry( + redis_key.as_str(), + bytes::Bytes::from(encrypted_payload), + Some(i64::from(LOCKER_REDIS_EXPIRY_SECONDS)), + ) .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::GetTokenizePayloadResponse = decrypted_payload - .parse_struct("GetTokenizePayloadResponse") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Error getting GetTokenizePayloadResponse from tokenize response", - )?; - Ok(get_response.lookup_key) - } - Err(err) => { + .map(|_| lookup_key) + .map_err(|err| { metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) - } - } + err + }) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error from redis locker") } -#[cfg(feature = "basilisk")] +#[instrument(skip(state))] pub async fn get_tokenized_data( state: &routes::AppState, lookup_key: &str, - should_get_value2: bool, + _should_get_value2: bool, ) -> RouterResult { metrics::GET_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::GetTokenizePayloadRequest { - lookup_key: lookup_key.to_string(), - get_value2: should_get_value2, - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .map_err(|_x| errors::ApiErrorResponse::InternalServerError)?; - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await + let redis_key = format!("{}_{}", LOCKER_REDIS_PREFIX, lookup_key); + + let redis_conn = state + .store + .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/get", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Get Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.get_key::(redis_key.as_str()).await; + match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") + Ok(resp) => { + let secret = get_redis_temp_locker_encryption_key(state)?; + + let decrypted_payload = GcmAes256 + .decode_message(secret.as_ref(), masking::Secret::new(resp.into())) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("GetTokenizedApi: Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::TokenizePayloadRequest = decrypted_payload + .attach_printable("Failed to decode redis temp locker data")?; + + let get_response: api::TokenizePayloadRequest = bytes::Bytes::from(decrypted_payload) .parse_struct("TokenizePayloadRequest") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting TokenizePayloadRequest from tokenize response")?; + Ok(get_response) } Err(err) => { metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - match err.status_code { - 404 => Err(errors::ApiErrorResponse::UnprocessableEntity { - message: "Token is invalid or expired".into(), - } - .into()), - _ => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got error from the basilisk locker: {err:?}")), - } + Err(err).change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".into(), + }) } } } -#[cfg(feature = "basilisk")] -pub async fn delete_tokenized_data( - state: &routes::AppState, - lookup_key: &str, -) -> RouterResult { +#[instrument(skip(state))] +pub async fn delete_tokenized_data(state: &routes::AppState, lookup_key: &str) -> RouterResult<()> { metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::DeleteTokenizeByTokenRequest { - lookup_key: lookup_key.to_string(), - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error serializing api::DeleteTokenizeByTokenRequest")?; - let (public_key, _private_key) = get_locker_jwe_keys(&state.kms_secrets.clone()) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/delete/token", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Delete Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await + let redis_key = format!("{}_{}", LOCKER_REDIS_PREFIX, lookup_key); + + let redis_conn = state + .store + .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error while making /tokenize/delete/token call to the locker")?; + .attach_printable("Failed to get redis connection")?; + + let response = redis_conn.delete_key(redis_key.as_str()).await; + match response { - Ok(r) => { - let delete_response = std::str::from_utf8(&r.response) + Ok(redis_interface::DelReply::KeyDeleted) => Ok(()), + Ok(redis_interface::DelReply::KeyNotDeleted) => { + Err(errors::ApiErrorResponse::InternalServerError) .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for basilisk delete response")?; - Ok(delete_response.to_string()) + .attach_printable("Token invalid or expired") } Err(err) => { metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); Err(errors::ApiErrorResponse::InternalServerError) .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) + .attach_printable_lazy(|| format!("Failed to delete from redis locker: {err:?}")) } } } // ********************************************** PROCESS TRACKER ********************************************** -#[cfg(feature = "basilisk")] + pub async fn add_delete_tokenized_data_task( db: &dyn db::StorageInterface, lookup_key: &str, @@ -1195,7 +907,6 @@ pub async fn add_delete_tokenized_data_task( }) } -#[cfg(feature = "basilisk")] pub async fn start_tokenize_data_workflow( state: &routes::AppState, tokenize_tracker: &storage::ProcessTracker, @@ -1213,23 +924,15 @@ pub async fn start_tokenize_data_workflow( ) })?; - let delete_resp = delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await; - match delete_resp { - Ok(resp) => { - if resp == "Ok" { - logger::info!("Card From locker deleted Successfully"); - //mark task as finished - let id = tokenize_tracker.id.clone(); - tokenize_tracker - .clone() - .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) - .await?; - } else { - logger::error!("Error: Deleting Card From Locker : {:?}", resp); - retry_delete_tokenize(db, &delete_tokenize_data.pm, tokenize_tracker.to_owned()) - .await?; - metrics::RETRIED_DELETE_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); - } + match delete_tokenized_data(state, &delete_tokenize_data.lookup_key).await { + Ok(()) => { + logger::info!("Card From locker deleted Successfully"); + //mark task as finished + let id = tokenize_tracker.id.clone(); + tokenize_tracker + .clone() + .finish_with_status(db.as_scheduler(), format!("COMPLETED_BY_PT_{id}")) + .await?; } Err(err) => { logger::error!("Err: Deleting Card From Locker : {:?}", err); @@ -1241,7 +944,6 @@ pub async fn start_tokenize_data_workflow( Ok(()) } -#[cfg(feature = "basilisk")] pub async fn get_delete_tokenize_schedule_time( db: &dyn db::StorageInterface, pm: &enums::PaymentMethod, @@ -1265,7 +967,6 @@ pub async fn get_delete_tokenize_schedule_time( process_tracker_utils::get_time_from_delta(time_delta) } -#[cfg(feature = "basilisk")] pub async fn retry_delete_tokenize( db: &dyn db::StorageInterface, pm: &enums::PaymentMethod, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 20712a64397d..20a571cd94e3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -214,6 +214,7 @@ where &operation, payment_data, &customer, + None, ) .await? } @@ -791,6 +792,7 @@ where router_data_res } +#[allow(clippy::too_many_arguments)] pub async fn call_multiple_connectors_service( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -799,6 +801,7 @@ pub async fn call_multiple_connectors_service( _operation: &Op, mut payment_data: PaymentData, customer: &Option, + session_surcharge_metadata: Option, ) -> RouterResult> where Op: Debug, @@ -818,19 +821,6 @@ where { let call_connectors_start_time = Instant::now(); let mut join_handlers = Vec::with_capacity(connectors.len()); - let surcharge_metadata = payment_data - .payment_attempt - .surcharge_metadata - .as_ref() - .map(|surcharge_metadata_value| { - surcharge_metadata_value - .clone() - .parse_value::("SurchargeMetadata") - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to Deserialize SurchargeMetadata")?; - for session_connector_data in connectors.iter() { let connector_id = session_connector_data.connector.connector.id(); @@ -843,14 +833,18 @@ where false, ) .await?; - payment_data.surcharge_details = - surcharge_metadata.as_ref().and_then(|surcharge_metadata| { - let payment_method_type = session_connector_data.payment_method_type; - surcharge_metadata - .surcharge_results - .get(&payment_method_type.to_string()) - .cloned() - }); + payment_data.surcharge_details = session_surcharge_metadata + .as_ref() + .and_then(|surcharge_metadata| { + surcharge_metadata.surcharge_results.get( + &SurchargeMetadata::get_key_for_surcharge_details_hash_map( + &session_connector_data.payment_method_type.into(), + &session_connector_data.payment_method_type, + None, + ), + ) + }) + .cloned(); let router_data = payment_data .construct_router_data( diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 3c42049bc451..c55df8e35d6e 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -167,6 +167,7 @@ default_imp_for_complete_authorize!( connector::Opennode, connector::Payeezy, connector::Payu, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -241,6 +242,7 @@ default_imp_for_webhook_source_verification!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -318,6 +320,7 @@ default_imp_for_create_customer!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -384,6 +387,7 @@ default_imp_for_connector_redirect_response!( connector::Payeezy, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -441,6 +445,7 @@ default_imp_for_connector_request_id!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -521,6 +526,7 @@ default_imp_for_accept_dispute!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -619,6 +625,7 @@ default_imp_for_file_upload!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -695,6 +702,7 @@ default_imp_for_submit_evidence!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -771,6 +779,7 @@ default_imp_for_defend_dispute!( connector::Payme, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -848,6 +857,7 @@ default_imp_for_pre_processing_steps!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Shift4, connector::Square, @@ -907,6 +917,7 @@ default_imp_for_payouts!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -984,6 +995,7 @@ default_imp_for_payouts_create!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1064,6 +1076,7 @@ default_imp_for_payouts_eligibility!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1141,6 +1154,7 @@ default_imp_for_payouts_fulfill!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1218,6 +1232,7 @@ default_imp_for_payouts_cancel!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1296,6 +1311,7 @@ default_imp_for_payouts_quote!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1374,6 +1390,7 @@ default_imp_for_payouts_recipient!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1451,6 +1468,7 @@ default_imp_for_approve!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, @@ -1529,6 +1547,7 @@ default_imp_for_reject!( connector::Paypal, connector::Payu, connector::Powertranz, + connector::Prophetpay, connector::Rapyd, connector::Square, connector::Stax, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index a72791475867..0199339e86c6 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use error_stack; +use error_stack::{self, IntoReport, ResultExt}; use super::{ConstructFlowSpecificData, Feature}; use crate::{ @@ -73,6 +73,12 @@ impl Feature for types::PaymentsAu .connector .validate_capture_method(self.request.capture_method) .to_payment_failed_response()?; + if self.request.surcharge_details.is_some() { + connector + .connector + .validate_if_surcharge_implemented() + .to_payment_failed_response()?; + } if self.should_proceed_with_authorize() { self.decide_authentication_type(); @@ -89,30 +95,67 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - let save_payment_result = tokenization::save_payment_method( - state, - connector, - resp.to_owned(), - maybe_customer, - merchant_account, - self.request.payment_method_type, - key_store, - ) - .await; - - let pm_id = match save_payment_result { - Ok(payment_method_id) => Ok(payment_method_id), - Err(error) => { - if resp.request.setup_mandate_details.clone().is_some() { - Err(error) - } else { - logger::error!(save_payment_method_error=?error); - Ok(None) - } - } - }?; - - Ok(mandate::mandate_procedure(state, resp, maybe_customer, pm_id).await?) + if resp.request.setup_mandate_details.clone().is_some() { + let payment_method_id = tokenization::save_payment_method( + state, + connector, + resp.to_owned(), + maybe_customer, + merchant_account, + self.request.payment_method_type, + key_store, + ) + .await?; + Ok( + mandate::mandate_procedure(state, resp, maybe_customer, payment_method_id) + .await?, + ) + } else { + let arbiter = actix::Arbiter::try_current() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("arbiter retrieval failure") + .map_err(|err| { + logger::error!(?err); + err + }) + .ok(); + + let connector = connector.clone(); + let response = resp.clone(); + let maybe_customer = maybe_customer.clone(); + let merchant_account = merchant_account.clone(); + let key_store = key_store.clone(); + let state = state.clone(); + + logger::info!("Initiating async call to save_payment_method in locker"); + + arbiter.map(|arb| { + arb.spawn(async move { + logger::info!("Starting async call to save_payment_method in locker"); + + let result = tokenization::save_payment_method( + &state, + &connector, + response, + &maybe_customer, + &merchant_account, + self.request.payment_method_type, + &key_store, + ) + .await; + + if let Err(err) = result { + logger::error!( + "Asynchronously saving card in locker failed : {:?}", + err + ); + } + }) + }); + + Ok(resp) + } } else { Ok(self.clone()) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index efc752355d84..d7d24b154062 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2409,6 +2409,7 @@ mod tests { profile_id: None, merchant_decision: None, payment_confirm_source: None, + surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), }; let req_cs = Some("1".to_string()); @@ -2458,6 +2459,7 @@ mod tests { profile_id: None, merchant_decision: None, payment_confirm_source: None, + surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), }; let req_cs = Some("1".to_string()); @@ -2507,6 +2509,7 @@ mod tests { profile_id: None, merchant_decision: None, payment_confirm_source: None, + surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), }; let req_cs = Some("1".to_string()); @@ -2900,7 +2903,6 @@ impl AttemptType { multiple_capture_count: None, connector_response_reference_id: None, amount_capturable: old_payment_attempt.amount, - surcharge_metadata: old_payment_attempt.surcharge_metadata, updated_by: storage_scheme.to_string(), } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index eeb94f455826..1cfcbce5532f 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -82,7 +82,10 @@ impl helpers::validate_status_with_capture_method(payment_intent.status, capture_method)?; - helpers::validate_amount_to_capture(payment_intent.amount, request.amount_to_capture)?; + helpers::validate_amount_to_capture( + payment_attempt.amount_capturable, + request.amount_to_capture, + )?; helpers::validate_capture_method(capture_method)?; diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index a045cee3548e..0e0d6c21479b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payment_methods}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::ResultExt; @@ -335,6 +335,19 @@ impl sm }); + // populate payment_data.surcharge_details from request + let surcharge_details = request.surcharge_details.map(|surcharge_details| { + payment_methods::SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount: surcharge_details.surcharge_amount, + tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_details.surcharge_amount + + surcharge_details.tax_amount.unwrap_or(0), + } + }); + Ok(( Box::new(self), PaymentData { @@ -368,7 +381,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data: None, }, @@ -543,7 +556,19 @@ impl .take(); let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - let authorized_amount = payment_data.payment_attempt.amount; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + let authorized_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.payment_attempt.amount); let payment_attempt_fut = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -564,6 +589,8 @@ impl error_code, error_message, amount_capturable: Some(authorized_amount), + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), }, storage_scheme, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2cc15fbfc3cc..87195510fc68 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -720,6 +720,7 @@ impl PaymentCreate { merchant_decision: None, payment_link_id, payment_confirm_source: None, + surcharge_applicable: None, updated_by: merchant_account.storage_scheme.to_string(), }) } diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 6af25221b228..6d97f7b66cd1 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -401,6 +401,7 @@ impl PaymentMethodValidate { profile_id: Default::default(), merchant_decision: Default::default(), payment_confirm_source: Default::default(), + surcharge_applicable: Default::default(), payment_link_id: Default::default(), updated_by: storage_scheme.to_string(), } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index af84152120a4..1467da7f816d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -559,7 +559,7 @@ async fn payment_response_update_tracker( payment_attempt_update = Some(storage::PaymentAttemptUpdate::AmountToCaptureUpdate { status: multiple_capture_data.get_attempt_status(authorized_amount), - amount_capturable: payment_data.payment_attempt.amount + amount_capturable: authorized_amount - multiple_capture_data.get_total_blocked_amount(), updated_by: storage_scheme.to_string(), }); diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f7831465e1ce..794180e2112e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1,6 +1,7 @@ use common_utils::{ext_traits::ValueExt, pii}; use error_stack::{report, ResultExt}; use masking::ExposeInterface; +use router_env::{instrument, tracing}; use super::helpers; use crate::{ @@ -20,6 +21,7 @@ use crate::{ utils::OptionExt, }; +#[instrument(skip_all)] pub async fn save_payment_method( state: &AppState, connector: &api::ConnectorData, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 66453315d446..d2ced5af3466 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; -use api_models::payments::FrmMessage; +use api_models::payments::{FrmMessage, RequestSurchargeDetails}; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; use error_stack::{IntoReport, ResultExt}; @@ -424,7 +424,13 @@ where .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_method_data", })?; - + let surcharge_details = + payment_attempt + .surcharge_amount + .map(|surcharge_amount| RequestSurchargeDetails { + surcharge_amount, + tax_amount: payment_attempt.tax_amount, + }); let merchant_decision = payment_intent.merchant_decision.to_owned(); let frm_message = payment_data.frm_message.map(FrmMessage::foreign_from); @@ -557,6 +563,7 @@ where .set_amount(payment_attempt.amount) .set_amount_capturable(Some(payment_attempt.amount_capturable)) .set_amount_received(payment_intent.amount_captured) + .set_surcharge_details(surcharge_details) .set_connector(routed_through) .set_client_secret(payment_intent.client_secret.map(masking::Secret::new)) .set_created(Some(payment_intent.created_at)) @@ -743,6 +750,7 @@ where reference_id: payment_attempt.connector_response_reference_id, attempt_count: payment_intent.attempt_count, payment_link: payment_link_data, + surcharge_details, ..Default::default() }, headers, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 4085b5b3bdb9..44e2b84dbd75 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -541,106 +541,85 @@ pub async fn validate_and_create_refund( .attach_printable("invalid merchant_id in request")) })?; - let refund = match validator::validate_uniqueness_of_refund_id_against_merchant_id( - db, - &payment_intent.payment_id, - &merchant_account.merchant_id, - &refund_id, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!( - "Unique violation while checking refund_id: {} against merchant_id: {}", - refund_id, merchant_account.merchant_id + let connecter_transaction_id = payment_attempt.clone().connector_transaction_id.ok_or_else(|| { + report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Transaction in invalid. Missing field \"connector_transaction_id\" in payment_attempt.") + })?; + + all_refunds = db + .find_refund_by_merchant_id_connector_transaction_id( + &merchant_account.merchant_id, + &connecter_transaction_id, + merchant_account.storage_scheme, ) - })? { - Some(refund) => refund, - None => { - let connecter_transaction_id = payment_attempt.clone().connector_transaction_id.ok_or_else(|| { - report!(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Transaction in invalid. Missing field \"connector_transaction_id\" in payment_attempt.") - })?; - all_refunds = db - .find_refund_by_merchant_id_connector_transaction_id( - &merchant_account.merchant_id, - &connecter_transaction_id, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; + .await + .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; - currency = payment_attempt.currency.get_required_value("currency")?; + currency = payment_attempt.currency.get_required_value("currency")?; - //[#249]: Add Connector Based Validation here. - validator::validate_payment_order_age( - &payment_intent.created_at, + //[#249]: Add Connector Based Validation here. + validator::validate_payment_order_age(&payment_intent.created_at, state.conf.refund.max_age) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "created_at".to_string(), + expected_format: format!( + "created_at not older than {} days", state.conf.refund.max_age, - ) - .change_context(errors::ApiErrorResponse::InvalidDataFormat { - field_name: "created_at".to_string(), - expected_format: format!( - "created_at not older than {} days", - state.conf.refund.max_age, - ), - })?; + ), + })?; - validator::validate_refund_amount(payment_attempt.amount, &all_refunds, refund_amount) - .change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?; + validator::validate_refund_amount(payment_attempt.amount, &all_refunds, refund_amount) + .change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?; - validator::validate_maximum_refund_against_payment_attempt( - &all_refunds, - state.conf.refund.max_attempts, - ) - .change_context(errors::ApiErrorResponse::MaximumRefundCount)?; + validator::validate_maximum_refund_against_payment_attempt( + &all_refunds, + state.conf.refund.max_attempts, + ) + .change_context(errors::ApiErrorResponse::MaximumRefundCount)?; - let connector = payment_attempt - .connector - .clone() - .ok_or(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("No connector populated in payment attempt")?; - - refund_create_req = storage::RefundNew::default() - .set_refund_id(refund_id.to_string()) - .set_internal_reference_id(utils::generate_id(consts::ID_LENGTH, "refid")) - .set_external_reference_id(Some(refund_id)) - .set_payment_id(req.payment_id) - .set_merchant_id(merchant_account.merchant_id.clone()) - .set_connector_transaction_id(connecter_transaction_id.to_string()) - .set_connector(connector) - .set_refund_type(req.refund_type.unwrap_or_default().foreign_into()) - .set_total_amount(payment_attempt.amount) - .set_refund_amount(refund_amount) - .set_currency(currency) - .set_created_at(Some(common_utils::date_time::now())) - .set_modified_at(Some(common_utils::date_time::now())) - .set_refund_status(enums::RefundStatus::Pending) - .set_metadata(req.metadata) - .set_description(req.reason.clone()) - .set_attempt_id(payment_attempt.attempt_id.clone()) - .set_refund_reason(req.reason) - .to_owned(); - - refund = db - .insert_refund(refund_create_req, merchant_account.storage_scheme) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicateRefundRequest)?; + let connector = payment_attempt + .connector + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("No connector populated in payment attempt")?; + + refund_create_req = storage::RefundNew::default() + .set_refund_id(refund_id.to_string()) + .set_internal_reference_id(utils::generate_id(consts::ID_LENGTH, "refid")) + .set_external_reference_id(Some(refund_id)) + .set_payment_id(req.payment_id) + .set_merchant_id(merchant_account.merchant_id.clone()) + .set_connector_transaction_id(connecter_transaction_id.to_string()) + .set_connector(connector) + .set_refund_type(req.refund_type.unwrap_or_default().foreign_into()) + .set_total_amount(payment_attempt.amount) + .set_refund_amount(refund_amount) + .set_currency(currency) + .set_created_at(Some(common_utils::date_time::now())) + .set_modified_at(Some(common_utils::date_time::now())) + .set_refund_status(enums::RefundStatus::Pending) + .set_metadata(req.metadata) + .set_description(req.reason.clone()) + .set_attempt_id(payment_attempt.attempt_id.clone()) + .set_refund_reason(req.reason) + .to_owned(); - schedule_refund_execution( - state, - refund, - refund_type, - merchant_account, - key_store, - payment_attempt, - payment_intent, - creds_identifier, - ) - .await? - } - }; + refund = db + .insert_refund(refund_create_req, merchant_account.storage_scheme) + .await + .to_duplicate_response(errors::ApiErrorResponse::DuplicateRefundRequest)?; + + schedule_refund_execution( + state, + refund.clone(), + refund_type, + merchant_account, + key_store, + payment_attempt, + payment_intent, + creds_identifier, + ) + .await?; Ok(refund.foreign_into()) } diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 88e8e59ed32d..6198a6f79a68 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -4,8 +4,6 @@ use time::PrimitiveDateTime; use crate::{ core::errors::{self, CustomResult, RouterResult}, - db::StorageInterface, - logger, types::storage::{self, enums}, utils::{self, OptionExt}, }; @@ -92,41 +90,6 @@ pub fn validate_maximum_refund_against_payment_attempt( }) } -#[instrument(skip(db))] -pub async fn validate_uniqueness_of_refund_id_against_merchant_id( - db: &dyn StorageInterface, - payment_id: &str, - merchant_id: &str, - refund_id: &str, - storage_scheme: enums::MerchantStorageScheme, -) -> RouterResult> { - let refund = db - .find_refund_by_merchant_id_refund_id(merchant_id, refund_id, storage_scheme) - .await; - logger::debug!(?refund); - match refund { - Err(err) => { - if err.current_context().is_db_not_found() { - // Empty vec should be returned by query in case of no results, this check exists just - // to be on the safer side. Fixed this, now vector is not returned but should check the flow in detail later. - Ok(None) - } else { - Err(err - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while finding refund, database error")) - } - } - - Ok(refund) => { - if refund.payment_id == payment_id { - Ok(Some(refund)) - } else { - Ok(None) - } - } - } -} - pub fn validate_refund_list(limit: Option) -> CustomResult { match limit { Some(limit_val) => { diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 056cc8c2e54f..433430507fb1 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -59,6 +59,7 @@ pub async fn check_existence_and_add_domain_to_db( connector_webhook_details: None, applepay_verified_domains: Some(already_verified_domains.clone()), pm_auth_config: None, + connector_label: None, }; state .store diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 726ae55f91eb..e0bff7d9069c 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -55,6 +55,12 @@ where publishable_key: &str, ) -> CustomResult; + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + organization_id: &str, + ) -> CustomResult, errors::StorageError>; + async fn delete_merchant_account_by_merchant_id( &self, merchant_id: &str, @@ -212,6 +218,47 @@ impl MerchantAccountInterface for Store { }) } + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + organization_id: &str, + ) -> CustomResult, errors::StorageError> { + use futures::future::try_join_all; + let conn = connection::pg_connection_read(self).await?; + + let encrypted_merchant_accounts = + storage::MerchantAccount::list_by_organization_id(&conn, organization_id) + .await + .map_err(Into::into) + .into_report()?; + + let db_master_key = self.get_master_key().to_vec().into(); + + let merchant_key_stores = + try_join_all(encrypted_merchant_accounts.iter().map(|merchant_account| { + self.get_merchant_key_store_by_merchant_id( + &merchant_account.merchant_id, + &db_master_key, + ) + })) + .await?; + + let merchant_accounts = try_join_all( + encrypted_merchant_accounts + .into_iter() + .zip(merchant_key_stores.iter()) + .map(|(merchant_account, key_store)| async { + merchant_account + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }), + ) + .await?; + + Ok(merchant_accounts) + } + async fn delete_merchant_account_by_merchant_id( &self, merchant_id: &str, @@ -337,6 +384,14 @@ impl MerchantAccountInterface for MockDb { // [#172]: Implement function for `MockDb` Err(errors::StorageError::MockDbError)? } + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + _organization_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } } #[cfg(feature = "accounts_cache")] diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index f573a4970b25..88d7ff668490 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,9 +1,18 @@ use serde::Serialize; +pub mod api_logs; pub mod event_logger; pub trait EventHandler: Sync + Send + dyn_clone::DynClone { - fn log_event(&self, event: T, previous: Option); + fn log_event(&self, event: RawEvent); +} + +dyn_clone::clone_trait_object!(EventHandler); + +pub struct RawEvent { + event_type: EventType, + key: String, + payload: serde_json::Value, } #[derive(Debug, Serialize)] @@ -14,12 +23,3 @@ pub enum EventType { Refund, ApiLogs, } - -pub trait Event -where - Self: Serialize, -{ - fn event_type() -> EventType; - - fn key(&self) -> String; -} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs new file mode 100644 index 000000000000..784d80b278da --- /dev/null +++ b/crates/router/src/events/api_logs.rs @@ -0,0 +1,43 @@ +use router_env::{tracing_actix_web::RequestId, types::FlowMetric}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use super::{EventType, RawEvent}; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiEvent { + api_flow: String, + created_at_timestamp: i128, + request_id: String, + latency: u128, + status_code: i64, +} + +impl ApiEvent { + pub fn new( + api_flow: &impl FlowMetric, + request_id: &RequestId, + latency: u128, + status_code: i64, + ) -> Self { + Self { + api_flow: api_flow.to_string(), + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos(), + request_id: request_id.as_hyphenated().to_string(), + latency, + status_code, + } + } +} + +impl TryFrom for RawEvent { + type Error = serde_json::Error; + + fn try_from(value: ApiEvent) -> Result { + Ok(Self { + event_type: EventType::ApiLogs, + key: value.request_id.clone(), + payload: serde_json::to_value(value)?, + }) + } +} diff --git a/crates/router/src/events/event_logger.rs b/crates/router/src/events/event_logger.rs index d8254b2cc4e9..f589a3c040dd 100644 --- a/crates/router/src/events/event_logger.rs +++ b/crates/router/src/events/event_logger.rs @@ -1,15 +1,11 @@ -use super::{Event, EventHandler}; +use super::{EventHandler, RawEvent}; use crate::services::logger; #[derive(Clone, Debug, Default)] pub struct EventLogger {} impl EventHandler for EventLogger { - fn log_event(&self, event: T, previous: Option) { - if let Some(prev) = previous { - logger::info!(previous = ?serde_json::to_string(&prev).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), current = ?serde_json::to_string(&event).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), event_type =? T::event_type(), event_id =? event.key(), log_type = "event"); - } else { - logger::info!(current = ?serde_json::to_string(&event).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), event_type =? T::event_type(), event_id =? event.key(), log_type = "event"); - } + fn log_event(&self, event: RawEvent) { + logger::info!(event = ?serde_json::to_string(&event.payload).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), event_type =? event.event_type, event_id =? event.key, log_type = "event"); } } diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index f0a0dc40317d..a93556202aab 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -77,6 +77,28 @@ pub async fn retrieve_merchant_account( ) .await } + +#[cfg(feature = "olap")] +#[instrument(skip_all, fields(flow = ?Flow::MerchantAccountList))] +pub async fn merchant_account_list( + state: web::Data, + req: HttpRequest, + query_params: web::Query, +) -> HttpResponse { + let flow = Flow::MerchantAccountList; + + Box::pin(api::server_wrap( + flow, + state, + &req, + query_params.into_inner(), + |state, _, request| list_merchant_account(state, request), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + /// Merchant Account - Update /// /// To update an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc @@ -113,6 +135,7 @@ pub async fn update_merchant_account( ) .await } + /// Merchant Account - Delete /// /// To delete a merchant account diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5ebafe9a66e0..863d0e8230e5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -5,6 +5,7 @@ use actix_web::{web, Scope}; use external_services::email::{AwsSes, EmailClient}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; +use router_env::tracing_actix_web::RequestId; use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; @@ -31,11 +32,11 @@ use crate::{ }; #[derive(Clone)] -pub struct AppStateBase { +pub struct AppState { pub flow_name: String, pub store: Box, pub conf: Arc, - pub event_handler: E, + pub event_handler: Box, #[cfg(feature = "email")] pub email_client: Arc, #[cfg(feature = "kms")] @@ -43,8 +44,6 @@ pub struct AppStateBase { pub api_client: Box, } -pub type AppState = AppStateBase; - impl scheduler::SchedulerAppState for AppState { fn get_db(&self) -> Box { self.store.get_scheduler_db() @@ -52,20 +51,18 @@ impl scheduler::SchedulerAppState for AppState { } pub trait AppStateInfo { - type Event: EventHandler; fn conf(&self) -> settings::Settings; fn store(&self) -> Box; - fn event_handler(&self) -> &Self::Event; + fn event_handler(&self) -> Box; #[cfg(feature = "email")] fn email_client(&self) -> Arc; - fn add_request_id(&mut self, request_id: Option); + fn add_request_id(&mut self, request_id: RequestId); fn add_merchant_id(&mut self, merchant_id: Option); fn add_flow_name(&mut self, flow_name: String); fn get_request_id(&self) -> Option; } impl AppStateInfo for AppState { - type Event = EventLogger; fn conf(&self) -> settings::Settings { self.conf.as_ref().to_owned() } @@ -76,10 +73,10 @@ impl AppStateInfo for AppState { fn email_client(&self) -> Arc { self.email_client.to_owned() } - fn event_handler(&self) -> &Self::Event { - &self.event_handler + fn event_handler(&self) -> Box { + self.event_handler.to_owned() } - fn add_request_id(&mut self, request_id: Option) { + fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); } fn add_merchant_id(&mut self, merchant_id: Option) { @@ -131,6 +128,12 @@ impl AppState { #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { jwekey: conf.jwekey.clone().into(), + redis_temp_locker_encryption_key: conf + .locker + .redis_temp_locker_encryption_key + .clone() + .into_bytes() + .into(), } .decrypt_inner(kms_client) .await @@ -138,6 +141,7 @@ impl AppState { #[cfg(feature = "email")] let email_client = Arc::new(AwsSes::new(&conf.email).await); + Self { flow_name: String::from("default"), store, @@ -147,7 +151,7 @@ impl AppState { #[cfg(feature = "kms")] kms_secrets: Arc::new(kms_secrets), api_client, - event_handler: EventLogger::default(), + event_handler: Box::::default(), } } @@ -394,6 +398,7 @@ impl MerchantAccount { web::scope("/accounts") .app_data(web::Data::new(state)) .service(web::resource("").route(web::post().to(merchant_account_create))) + .service(web::resource("/list").route(web::get().to(merchant_account_list))) .service( web::resource("/{id}/kv") .route(web::post().to(merchant_account_toggle_kv)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6a69db6257cc..5be361098bcc 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -30,7 +30,8 @@ impl From for ApiIdentifier { Flow::MerchantsAccountCreate | Flow::MerchantsAccountRetrieve | Flow::MerchantsAccountUpdate - | Flow::MerchantsAccountDelete => Self::MerchantAccount, + | Flow::MerchantsAccountDelete + | Flow::MerchantAccountList => Self::MerchantAccount, Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 327a901d7c26..b2cb29bbd2c3 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -13,7 +13,10 @@ use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, Re use api_models::enums::CaptureMethod; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; -use common_utils::{consts::X_HS_LATENCY, errors::ReportSwitchExt}; +use common_utils::{ + consts::X_HS_LATENCY, + errors::{ErrorSwitch, ReportSwitchExt}, +}; use error_stack::{report, IntoReport, Report, ResultExt}; use masking::{ExposeOptionInterface, PeekInterface}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; @@ -22,6 +25,7 @@ use serde_json::json; use tera::{Context, Tera}; use self::request::{HeaderExt, RequestBuilderExt}; +use super::authentication::{AuthInfo, AuthenticateAndFetch}; use crate::{ configs::settings::{Connectors, Settings}, consts, @@ -30,13 +34,13 @@ use crate::{ errors::{self, CustomResult}, payments, }, + events::api_logs::ApiEvent, logger, routes::{ app::AppStateInfo, metrics::{self, request as metrics_request}, AppState, }, - services::authentication as auth, types::{ self, api::{self, ConnectorCommon}, @@ -92,6 +96,14 @@ pub trait ConnectorValidation: ConnectorCommon { fn is_webhook_source_verification_mandatory(&self) -> bool { false } + + fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented(format!( + "Surcharge not implemented for {}", + self.id() + )) + .into()) + } } #[async_trait::async_trait] @@ -739,7 +751,7 @@ pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>( request: &'a HttpRequest, payload: T, func: F, - api_auth: &dyn auth::AuthenticateAndFetch, + api_auth: &dyn AuthenticateAndFetch, lock_action: api_locking::LockAction, ) -> CustomResult, OErr> where @@ -749,20 +761,21 @@ where Q: Serialize + Debug + 'a, T: Debug, A: AppStateInfo + Clone, - U: auth::AuthInfo, - CustomResult, E>: ReportSwitchExt, OErr>, - CustomResult: ReportSwitchExt, - CustomResult<(), errors::ApiErrorResponse>: ReportSwitchExt<(), OErr>, - OErr: ResponseError + Sync + Send + 'static, + U: AuthInfo, + E: ErrorSwitch + error_stack::Context, + OErr: ResponseError + error_stack::Context, + errors::ApiErrorResponse: ErrorSwitch, { let request_id = RequestId::extract(request) .await - .ok() - .map(|id| id.as_hyphenated().to_string()); + .into_report() + .attach_printable("Unable to extract request id from request") + .change_context(errors::ApiErrorResponse::InternalServerError.switch())?; let mut request_state = state.get_ref().clone(); request_state.add_request_id(request_id); + let start_instant = Instant::now(); let auth_out = api_auth .authenticate_and_fetch(request.headers(), &request_state) @@ -795,11 +808,23 @@ where .switch()?; res }; + let request_duration = Instant::now() + .saturating_duration_since(start_instant) + .as_millis(); let status_code = match output.as_ref() { Ok(res) => metrics::request::track_response_status_code(res), Err(err) => err.current_context().status_code().as_u16().into(), }; + let api_event = ApiEvent::new(flow, &request_id, request_duration, status_code); + match api_event.clone().try_into() { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, event=?api_event, "Error Logging API Event"); + } + } metrics::request::status_code_metrics(status_code, flow.to_string(), merchant_id.to_string()); @@ -816,7 +841,7 @@ pub async fn server_wrap<'a, A, T, U, Q, F, Fut, E>( request: &'a HttpRequest, payload: T, func: F, - api_auth: &dyn auth::AuthenticateAndFetch, + api_auth: &dyn AuthenticateAndFetch, lock_action: api_locking::LockAction, ) -> HttpResponse where @@ -824,11 +849,10 @@ where Fut: Future, E>>, Q: Serialize + Debug + 'a, T: Debug, - U: auth::AuthInfo, + U: AuthInfo, A: AppStateInfo + Clone, ApplicationResponse: Debug, - CustomResult, E>: - ReportSwitchExt, api_models::errors::types::ApiErrorResponse>, + E: ErrorSwitch + error_stack::Context, { let request_method = request.method().as_str(); let url_path = request.path(); diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index f9bbff00846c..c47a28da0844 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -5,6 +5,7 @@ use http::{HeaderValue, Method}; use masking::PeekInterface; use once_cell::sync::OnceCell; use reqwest::multipart::Form; +use router_env::tracing_actix_web::RequestId; use super::{request::Maskable, Request}; use crate::{ @@ -109,7 +110,6 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); - let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), @@ -117,10 +117,6 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec { format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), - format!("{basilisk_host}/tokenize"), - format!("{basilisk_host}/tokenize/get"), - format!("{basilisk_host}/tokenize/delete"), - format!("{basilisk_host}/tokenize/delete/token"), ] } @@ -167,10 +163,10 @@ where forward_to_kafka: bool, ) -> CustomResult; - fn add_request_id(&mut self, _request_id: Option); + fn add_request_id(&mut self, request_id: RequestId); fn get_request_id(&self) -> Option; fn add_merchant_id(&mut self, _merchant_id: Option); - fn add_flow_name(&mut self, _flow_name: String); + fn add_flow_name(&mut self, flow_name: String); } dyn_clone::clone_trait_object!(ApiClient); @@ -350,8 +346,9 @@ impl ApiClient for ProxyClient { crate::services::send_request(state, request, option_timeout_secs).await } - fn add_request_id(&mut self, _request_id: Option) { - self.request_id = _request_id + fn add_request_id(&mut self, request_id: RequestId) { + self.request_id + .replace(request_id.as_hyphenated().to_string()); } fn get_request_id(&self) -> Option { @@ -402,7 +399,7 @@ impl ApiClient for MockApiClient { Err(ApiClientError::UnexpectedState.into()) } - fn add_request_id(&mut self, _request_id: Option) { + fn add_request_id(&mut self, _request_id: RequestId) { // [#2066]: Add Mock implementation for ApiClient } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index e56285d05dd4..491f9a0851d1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -342,6 +342,7 @@ impl ConnectorData { enums::Connector::Payme => Ok(Box::new(&connector::Payme)), enums::Connector::Payu => Ok(Box::new(&connector::Payu)), enums::Connector::Powertranz => Ok(Box::new(&connector::Powertranz)), + // enums::Connector::Prophetpay => Ok(Box::new(&connector::Prophetpay)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)), enums::Connector::Square => Ok(Box::new(&connector::Square)), diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index e5bf1d8dd1bf..39ef57dfa9fd 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,12 +1,11 @@ use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByDateRequest, DeleteTokenizeByTokenRequest, - GetTokenizePayloadRequest, GetTokenizePayloadResponse, PaymentMethodCreate, - PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, PaymentMethodListRequest, - PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, - TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, - TokenizedWalletValue1, TokenizedWalletValue2, + CustomerPaymentMethodsListResponse, 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/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index a8dce1e6f078..58c2e018316c 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -53,6 +53,7 @@ pub enum MerchantConnectorAccountUpdate { connector_webhook_details: Option, applepay_verified_domains: Option>, pm_auth_config: Option, + connector_label: Option, }, } @@ -175,6 +176,7 @@ impl From for MerchantConnectorAccountUpdateInte connector_webhook_details, applepay_verified_domains, pm_auth_config, + connector_label, } => Self { merchant_id, connector_type, @@ -191,6 +193,7 @@ impl From for MerchantConnectorAccountUpdateInte connector_webhook_details, applepay_verified_domains, pm_auth_config, + connector_label, }, } } diff --git a/crates/router/src/workflows/tokenized_data.rs b/crates/router/src/workflows/tokenized_data.rs index 2f5d33173276..0674982f92fe 100644 --- a/crates/router/src/workflows/tokenized_data.rs +++ b/crates/router/src/workflows/tokenized_data.rs @@ -1,14 +1,13 @@ use scheduler::consumer::workflows::ProcessTrackerWorkflow; -#[cfg(feature = "basilisk")] -use crate::core::payment_methods::vault; -use crate::{errors, logger::error, routes::AppState, types::storage}; +use crate::{ + core::payment_methods::vault, errors, logger::error, routes::AppState, types::storage, +}; pub struct DeleteTokenizeDataWorkflow; #[async_trait::async_trait] impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { - #[cfg(feature = "basilisk")] async fn execute_workflow<'a>( &'a self, state: &'a AppState, @@ -17,15 +16,6 @@ impl ProcessTrackerWorkflow for DeleteTokenizeDataWorkflow { Ok(vault::start_tokenize_data_workflow(state, &process).await?) } - #[cfg(not(feature = "basilisk"))] - async fn execute_workflow<'a>( - &'a self, - _state: &'a AppState, - _process: storage::ProcessTracker, - ) -> Result<(), errors::ProcessTrackerError> { - Ok(()) - } - async fn error_handler<'a>( &'a self, _state: &'a AppState, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 9e117649e9a2..ed06312b77ac 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -42,6 +42,7 @@ mod payme; mod paypal; mod payu; mod powertranz; +mod prophetpay; mod rapyd; mod shift4; mod square; diff --git a/crates/router/tests/connectors/prophetpay.rs b/crates/router/tests/connectors/prophetpay.rs new file mode 100644 index 000000000000..ac288533cfee --- /dev/null +++ b/crates/router/tests/connectors/prophetpay.rs @@ -0,0 +1,419 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct ProphetpayTest; +impl ConnectorActions for ProphetpayTest {} +impl utils::Connector for ProphetpayTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Prophetpay; + types::api::ConnectorData { + connector: Box::new(&Prophetpay), + connector_name: types::Connector::DummyConnector1, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .prophetpay + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "prophetpay".to_string() + } +} + +static CONNECTOR: ProphetpayTest = ProphetpayTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index b55aae498c70..0966db95a42f 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -176,10 +176,11 @@ key1 = "transaction key" [helcim] api_key="API Key" - [gocardless] api_key="API Key" - [volt] api_key="API Key" + +[prophetpay] +api_key="API Key" \ No newline at end of file diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 1c56c4c7c2f0..d63ddce58f30 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -66,6 +66,8 @@ pub enum Flow { MerchantConnectorsCreate, /// Merchant Connectors retrieve flow. MerchantConnectorsRetrieve, + /// Merchant account list + MerchantAccountList, /// Merchant Connectors update flow. MerchantConnectorsUpdate, /// Merchant Connectors delete flow. diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index e4c41c41b6f4..8fb59d213364 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -28,7 +28,7 @@ router_derive = { version = "0.1.0", path = "../router_derive" } # Third party crates actix-web = "4.3.1" -async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "be3d9bce50051d8c0e0c06078e8066cc27db3001" } +async-bb8-diesel = "0.1.0" async-trait = "0.1.72" bb8 = "0.8.1" bytes = "1.4.0" diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index 8674fd711ac0..34bf88171774 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -140,7 +140,6 @@ impl PaymentAttemptInterface for MockDb { multiple_capture_count: payment_attempt.multiple_capture_count, connector_response_reference_id: None, amount_capturable: payment_attempt.amount_capturable, - surcharge_metadata: payment_attempt.surcharge_metadata, updated_by: storage_scheme.to_string(), }; payment_attempts.push(payment_attempt.clone()); diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index edc17a0cf54a..d1979cba01de 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -104,6 +104,7 @@ impl PaymentIntentInterface for MockDb { payment_link_id: new.payment_link_id, payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), + surcharge_applicable: new.surcharge_applicable, }; payment_intents.push(payment_intent.clone()); Ok(payment_intent) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index e1311bda67b7..386c673b36a9 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -360,7 +360,7 @@ impl PaymentAttemptInterface for KVRouterStore { multiple_capture_count: payment_attempt.multiple_capture_count, connector_response_reference_id: None, amount_capturable: payment_attempt.amount_capturable, - surcharge_metadata: payment_attempt.surcharge_metadata.clone(), + updated_by: storage_scheme.to_string(), }; @@ -956,7 +956,7 @@ impl DataModelExt for PaymentAttempt { multiple_capture_count: self.multiple_capture_count, connector_response_reference_id: self.connector_response_reference_id, amount_capturable: self.amount_capturable, - surcharge_metadata: self.surcharge_metadata, + updated_by: self.updated_by, } } @@ -1006,7 +1006,7 @@ impl DataModelExt for PaymentAttempt { multiple_capture_count: storage_model.multiple_capture_count, connector_response_reference_id: storage_model.connector_response_reference_id, amount_capturable: storage_model.amount_capturable, - surcharge_metadata: storage_model.surcharge_metadata, + updated_by: storage_model.updated_by, } } @@ -1056,7 +1056,7 @@ impl DataModelExt for PaymentAttemptNew { connector_response_reference_id: self.connector_response_reference_id, multiple_capture_count: self.multiple_capture_count, amount_capturable: self.amount_capturable, - surcharge_metadata: self.surcharge_metadata, + updated_by: self.updated_by, } } @@ -1104,7 +1104,7 @@ impl DataModelExt for PaymentAttemptNew { connector_response_reference_id: storage_model.connector_response_reference_id, multiple_capture_count: storage_model.multiple_capture_count, amount_capturable: storage_model.amount_capturable, - surcharge_metadata: storage_model.surcharge_metadata, + updated_by: storage_model.updated_by, } } @@ -1181,6 +1181,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, } => DieselPaymentAttemptUpdate::ConfirmUpdate { amount, @@ -1199,6 +1201,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, }, Self::VoidUpdate { @@ -1326,22 +1330,6 @@ impl DataModelExt for PaymentAttemptUpdate { amount_capturable, updated_by, }, - Self::SurchargeMetadataUpdate { - surcharge_metadata, - updated_by, - } => DieselPaymentAttemptUpdate::SurchargeMetadataUpdate { - surcharge_metadata, - updated_by, - }, - Self::SurchargeAmountUpdate { - surcharge_amount, - tax_amount, - updated_by, - } => DieselPaymentAttemptUpdate::SurchargeAmountUpdate { - surcharge_amount, - tax_amount, - updated_by, - }, } } @@ -1413,6 +1401,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, } => Self::ConfirmUpdate { amount, @@ -1431,6 +1421,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, }, DieselPaymentAttemptUpdate::VoidUpdate { @@ -1558,22 +1550,6 @@ impl DataModelExt for PaymentAttemptUpdate { amount_capturable, updated_by, }, - DieselPaymentAttemptUpdate::SurchargeMetadataUpdate { - surcharge_metadata, - updated_by, - } => Self::SurchargeMetadataUpdate { - surcharge_metadata, - updated_by, - }, - DieselPaymentAttemptUpdate::SurchargeAmountUpdate { - surcharge_amount, - tax_amount, - updated_by, - } => Self::SurchargeAmountUpdate { - surcharge_amount, - tax_amount, - updated_by, - }, } } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index d0f56a40d02b..7c0939414aeb 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -1,5 +1,5 @@ #[cfg(feature = "olap")] -use async_bb8_diesel::AsyncRunQueryDsl; +use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; use common_utils::{date_time, ext_traits::Encode}; #[cfg(feature = "olap")] use data_models::payments::payment_intent::PaymentIntentFetchConstraints; @@ -34,6 +34,8 @@ use redis_interface::HsetnxReply; use router_env::logger; use router_env::{instrument, tracing}; +#[cfg(feature = "olap")] +use crate::connection; use crate::{ diesel_error_to_data_error, redis::kv_store::{kv_wrapper, KvOperation}, @@ -94,6 +96,7 @@ impl PaymentIntentInterface for KVRouterStore { payment_link_id: new.payment_link_id.clone(), payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), + surcharge_applicable: new.surcharge_applicable, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -387,7 +390,10 @@ impl PaymentIntentInterface for crate::RouterStore { filters: &PaymentIntentFetchConstraints, storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, StorageError> { - let conn = self.get_replica_pool(); + use common_utils::errors::ReportSwitchExt; + + let conn = connection::pg_connection_read(self).await.switch()?; + let conn = async_bb8_diesel::Connection::as_async_conn(&conn); //[#350]: Replace this with Boxable Expression and pass it into generic filter // when https://github.com/rust-lang/rust/issues/52662 becomes stable @@ -509,8 +515,10 @@ impl PaymentIntentInterface for crate::RouterStore { constraints: &PaymentIntentFetchConstraints, storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, StorageError> { - let conn = self.get_replica_pool(); + use common_utils::errors::ReportSwitchExt; + let conn = connection::pg_connection_read(self).await.switch()?; + let conn = async_bb8_diesel::Connection::as_async_conn(&conn); let mut query = DieselPaymentIntent::table() .inner_join( diesel_models::schema::payment_attempt::table @@ -646,8 +654,10 @@ impl PaymentIntentInterface for crate::RouterStore { constraints: &PaymentIntentFetchConstraints, _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, StorageError> { - let conn = self.get_replica_pool(); + use common_utils::errors::ReportSwitchExt; + let conn = connection::pg_connection_read(self).await.switch()?; + let conn = async_bb8_diesel::Connection::as_async_conn(&conn); let mut query = DieselPaymentIntent::table() .select(pi_dsl::active_attempt_id) .filter(pi_dsl::merchant_id.eq(merchant_id.to_owned())) @@ -743,6 +753,7 @@ impl DataModelExt for PaymentIntentNew { payment_link_id: self.payment_link_id, payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, + surcharge_applicable: self.surcharge_applicable, } } @@ -782,6 +793,7 @@ impl DataModelExt for PaymentIntentNew { payment_link_id: storage_model.payment_link_id, payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, + surcharge_applicable: storage_model.surcharge_applicable, } } } @@ -826,6 +838,7 @@ impl DataModelExt for PaymentIntent { payment_link_id: self.payment_link_id, payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, + surcharge_applicable: self.surcharge_applicable, } } @@ -866,6 +879,7 @@ impl DataModelExt for PaymentIntent { payment_link_id: storage_model.payment_link_id, payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, + surcharge_applicable: storage_model.surcharge_applicable, } } } @@ -995,6 +1009,13 @@ impl DataModelExt for PaymentIntentUpdate { merchant_decision, updated_by, }, + Self::SurchargeApplicableUpdate { + surcharge_applicable, + updated_by, + } => DieselPaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: Some(surcharge_applicable), + updated_by, + }, } } diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 8edec7ee39c8..d774e2530e9d 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -48,6 +48,7 @@ pub struct ConnectorAuthentication { pub paypal: Option, pub payu: Option, pub powertranz: Option, + pub prophetpay: Option, pub rapyd: Option, pub shift4: Option, pub square: Option, @@ -78,7 +79,7 @@ impl ConnectorAuthentication { /// Will panic if `CONNECTOR_AUTH_FILE_PATH` env is not set #[allow(clippy::expect_used)] pub fn new() -> Self { - // Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_sample_auth.toml"` + // Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml"` // before running tests in shell let path = env::var("CONNECTOR_AUTH_FILE_PATH") .expect("Connector authentication file path not set"); @@ -110,7 +111,7 @@ impl ConnectorAuthenticationMap { /// Will panic if `CONNECTOR_AUTH_FILE_PATH` env is not set #[allow(clippy::expect_used)] pub fn new() -> Self { - // Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_sample_auth.toml"` + // Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml"` // before running tests in shell let path = env::var("CONNECTOR_AUTH_FILE_PATH") .expect("connector authentication file path not set"); diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index bee79de458d6..47235601cb0e 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -32,6 +32,7 @@ jwt_secret = "secret" host = "" mock_locker = true basilisk_host = "" +redis_temp_locker_encryption_key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" [eph_key] validity = 1 @@ -100,6 +101,7 @@ payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" @@ -159,6 +161,7 @@ cards = [ "paypal", "payu", "powertranz", + "prophetpay", "shift4", "square", "stax", diff --git a/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/down.sql b/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/down.sql new file mode 100644 index 000000000000..8d4394202e1b --- /dev/null +++ b/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE payment_attempt +ADD COLUMN IF NOT EXISTS surcharge_metadata JSONB DEFAULT NULL; + +ALTER TABLE payment_intent +DROP COLUMN surcharge_applicable; diff --git a/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/up.sql b/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/up.sql new file mode 100644 index 000000000000..8d5730ba098e --- /dev/null +++ b/migrations/2023-10-19-075810_add_surcharge_applicable_payment_intent/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE payment_attempt +DROP COLUMN surcharge_metadata; + + +ALTER TABLE payment_intent +ADD surcharge_applicable boolean; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index bacefa4b2785..a224fac57d4a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -6508,6 +6508,12 @@ "connector_name": { "$ref": "#/components/schemas/Connector" }, + "connector_label": { + "type": "string", + "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", + "example": "stripe_US_travel", + "nullable": true + }, "merchant_connector_id": { "type": "string", "description": "Unique ID of the connector", @@ -6720,6 +6726,12 @@ "description": "Name of the Connector", "example": "stripe" }, + "connector_label": { + "type": "string", + "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", + "example": "stripe_US_travel", + "nullable": true + }, "merchant_connector_id": { "type": "string", "description": "Unique ID of the connector", @@ -6859,6 +6871,11 @@ "connector_type": { "$ref": "#/components/schemas/ConnectorType" }, + "connector_label": { + "type": "string", + "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", + "nullable": true + }, "connector_account_details": { "type": "object", "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", @@ -9738,6 +9755,14 @@ "description": "The business profile that is associated with this payment", "nullable": true }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestSurchargeDetails" + } + ], + "nullable": true + }, "attempt_count": { "type": "integer", "format": "int32", diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/.meta.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/.meta.json new file mode 100644 index 000000000000..e6b348a60be2 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/.event.meta.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/event.test.js new file mode 100644 index 000000000000..432edf05d37b --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/event.test.js @@ -0,0 +1,116 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - 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/capture - 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 "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Validate the connector +pm.test("[POST]::/payments - connector", function () { + pm.expect(jsonData.connector).to.eql("paypal"); +}); + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount_capturable) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 0", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + +// Response body should have a valid surcharge_details field" +pm.test("Check if valid 'surcharge_details' is present in the response", function () { + pm.response.to.have.jsonBody('surcharge_details'); + pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5); + pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5); +}); diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/request.json @@ -0,0 +1,39 @@ +{ + "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_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "capture"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/response.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/.event.meta.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..ba5d852e9bda --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/event.test.js @@ -0,0 +1,116 @@ +// 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.", + ); +} + +// Validate the connector +pm.test("[POST]::/payments - connector", function () { + pm.expect(jsonData.connector).to.eql("paypal"); +}); + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "null" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'", + function () { + pm.expect(jsonData.amount_received).to.eql(null); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount_capturable) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(6550); + }, + ); +} + +// Response body should have a valid surcharge_details field" +pm.test("Check if valid 'surcharge_details' is present in the response", function () { + pm.response.to.have.jsonBody('surcharge_details'); + pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5); + pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5); +}); \ No newline at end of file diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json new file mode 100644 index 000000000000..c60989439784 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json @@ -0,0 +1,60 @@ +{ + "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": { + "client_secret": "{{client_secret}}", + "surcharge_details": { + "surcharge_amount": 5, + "tax_amount": 5 + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], + "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/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/response.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/.event.meta.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js new file mode 100644 index 000000000000..fe83ca7852a5 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/event.test.js @@ -0,0 +1,101 @@ +// 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_confirmation" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_confirmation"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "null" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'", + function () { + pm.expect(jsonData.amount_received).to.eql(null); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount_capturable) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(6540); + }, + ); +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json new file mode 100644 index 000000000000..b080ff1a6b95 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -0,0 +1,87 @@ +{ + "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": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+1", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4005519200000004", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "paypal" + } + } + }, + "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/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/response.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/.event.meta.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..5097d1cf94a7 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/event.test.js @@ -0,0 +1,113 @@ +// 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 "processing" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'processing'", + function () { + pm.expect(jsonData.status).to.eql("processing"); + }, + ); +} + +// Validate the connector +pm.test("[POST]::/payments - connector", function () { + pm.expect(jsonData.connector).to.eql("paypal"); +}); + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} + +// Response body should have value "6540" for "amount_capturable" +if (jsonData?.amount_capturable) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(540); + }, + ); +} + +// Response body should have a valid surcharge_details field" +pm.test("Check if valid 'surcharge_details' is present in the response", function () { + pm.response.to.have.jsonBody('surcharge_details'); + pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5); + pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5); +}); diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/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/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/response.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Merchant Account - Create/request.json index 7df5315150cd..5aa155f64c37 100644 --- a/postman/collection-dir/paypal/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -79,6 +79,10 @@ "metadata": { "city": "NY", "unit": "245" + }, + "routing_algorithm": { + "type": "single", + "data": "paypal" } } }, diff --git a/postman/collection-dir/stripe/Customers/.meta.json b/postman/collection-dir/stripe/Customers/.meta.json index db26627bb97e..3b7497b1ea1b 100644 --- a/postman/collection-dir/stripe/Customers/.meta.json +++ b/postman/collection-dir/stripe/Customers/.meta.json @@ -1,6 +1,7 @@ { "childrenOrder": [ "Create Customer", + "List Customer", "Retrieve Customer", "Update Customer", "Ephemeral Key", diff --git a/postman/collection-dir/stripe/Customers/List Customer/.event.meta.json b/postman/collection-dir/stripe/Customers/List Customer/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Customers/List Customer/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Customers/List Customer/event.test.js b/postman/collection-dir/stripe/Customers/List Customer/event.test.js new file mode 100644 index 000000000000..2d1ce4bd2f2a --- /dev/null +++ b/postman/collection-dir/stripe/Customers/List Customer/event.test.js @@ -0,0 +1,64 @@ +// Validate status 2xx +pm.test("[POST]::/customers - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/customers - 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]::/customers - 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 a minimum length of "1" for "customer_id" +if (jsonData?.customer_id) { + pm.test( + "[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '2'", + function () { + pm.expect(jsonData.customer_id.length).is.at.least(2); + }, + ); +} + + +// Define the regular expression pattern to match customer_id +var customerIdPattern = /^[a-zA-Z0-9_]+$/; + +// Define an array to store the validation results +var validationResults = []; + +// Iterate through the JSON array +jsonData.forEach(function(item, index) { + if (item.hasOwnProperty("customer_id")) { + if (customerIdPattern.test(item.customer_id)) { + validationResults.push("customer_id " + item.customer_id + " is valid."); + } else { + validationResults.push("customer_id " + item.customer_id + " is not valid."); + } + } else { + validationResults.push("customer_id is missing for item at index " + index); + } +}); + +// Check if any customer_id is not valid and fail the test if necessary +if (validationResults.some(result => !result.includes("is valid"))) { + pm.test("Customer IDs validation failed: " + validationResults.join(", "), function() { + pm.expect(false).to.be.true; + }); +} else { + pm.test("All customer IDs are valid: " + validationResults.join(", "), function() { + pm.expect(true).to.be.true; + }); +} diff --git a/postman/collection-dir/stripe/Customers/List Customer/request.json b/postman/collection-dir/stripe/Customers/List Customer/request.json new file mode 100644 index 000000000000..40d39448bbe8 --- /dev/null +++ b/postman/collection-dir/stripe/Customers/List Customer/request.json @@ -0,0 +1,19 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + "list" + ] + } +} diff --git a/postman/collection-dir/stripe/Customers/List Customer/response.json b/postman/collection-dir/stripe/Customers/List Customer/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Customers/List Customer/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js index 7de0d5beb316..9c5a8900dc03 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js @@ -14,7 +14,7 @@ pm.test("[POST]::/accounts - Content-Type is application/json", function () { let jsonData = {}; try { jsonData = pm.response.json(); -} catch (e) {} +} catch (e) { } // pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id if (jsonData?.merchant_id) { @@ -54,3 +54,16 @@ if (jsonData?.publishable_key) { "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", ); } + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("organization_id", jsonData.organization_id); + console.log( + "- use {{organization_id}} as collection variable for value", + jsonData.organization_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.", + ); +} diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/.event.meta.json b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/event.test.js b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/event.test.js new file mode 100644 index 000000000000..0ba15a15ee6a --- /dev/null +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/event.test.js @@ -0,0 +1,43 @@ +// Validate status 2xx +pm.test("[GET]::/accounts/list - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/accounts/list - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json new file mode 100644 index 000000000000..841485a0a048 --- /dev/null +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json @@ -0,0 +1,49 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list", + "host": ["{{baseUrl}}"], + "path": ["accounts", "list"], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "disabled": false + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" +} diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/response.json b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/event.test.js b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/event.test.js index aa1bc4845b74..bd543edd2c31 100644 --- a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/event.test.js +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/event.test.js @@ -20,7 +20,7 @@ pm.test( let jsonData = {}; try { jsonData = pm.response.json(); -} catch (e) {} +} catch (e) { } // pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id if (jsonData?.merchant_connector_id) { @@ -37,3 +37,11 @@ if (jsonData?.merchant_connector_id) { "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", ); } + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated", + function () { + pm.expect(jsonData.connector_label).to.eql("first_stripe_connector") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/request.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/request.json index 6fe1be149f8d..6f89fa181d13 100644 --- a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create/request.json @@ -42,6 +42,7 @@ "connector_name": "stripe", "business_country": "US", "business_label": "default", + "connector_label": "first_stripe_connector", "connector_account_details": { "auth_type": "HeaderKey", "api_key": "{{connector_api_key}}" diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/event.test.js b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/event.test.js index d7259b6a840b..98f405d8bb85 100644 --- a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/event.test.js +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/event.test.js @@ -20,7 +20,7 @@ pm.test( let jsonData = {}; try { jsonData = pm.response.json(); -} catch (e) {} +} catch (e) { } // pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id if (jsonData?.merchant_connector_id) { @@ -37,3 +37,11 @@ if (jsonData?.merchant_connector_id) { "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", ); } + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated", + function () { + pm.expect(jsonData.connector_label).to.eql("updated_stripe_connector") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/request.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/request.json index 1d77181f262d..322a86724c35 100644 --- a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/request.json +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Update/request.json @@ -43,6 +43,7 @@ "auth_type": "HeaderKey", "api_key": "{{connector_api_key}}" }, + "connector_label": "updated_stripe_connector", "test_mode": false, "disabled": false, "payment_methods_enabled": [ diff --git a/postman/collection-json/authorizedotnet.postman_collection.json b/postman/collection-json/authorizedotnet.postman_collection.json index 3f497d56f3cd..8130f1fd9a10 100644 --- a/postman/collection-json/authorizedotnet.postman_collection.json +++ b/postman/collection-json/authorizedotnet.postman_collection.json @@ -845,6 +845,278 @@ { "name": "Happy Cases", "item": [ + { + "name": "Scenario4-Create failed payment with confirm true copy", + "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 \"processing\" or \"failed\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_code' matches '3' or '11' \",", + " function () {", + " pm.expect(jsonData.error_code).to.be.oneOf([\"3\", \"11\"]);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" or \"failed\" for \"status\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'processing' or 'This transaction has been declined.' \",", + " function () {", + " pm.expect(jsonData.error_message).to.be.oneOf([\"A duplicate transaction has been submitted.\", \"This transaction has been declined.\"]);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" or \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing' or 'failed' \",", + " function () {", + " pm.expect(jsonData.status).to.be.oneOf([\"failed\"]);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.environment.set(\"random_number\", _.random(1000, 100000));", + "" + ], + "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\":7003,\"currency\":\"USD\",\"confirm\":true,\"routing\":{\"data\":\"authorizedotnet\",\"type\":\"single\"},\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"370000000000002\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"900\"}},\"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 - Retrieve", + "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 \"processing\" or \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing' or 'failed' \",", + " function () {", + " pm.expect(jsonData.status).to.be.oneOf([\"processing\", \"failed\"]);", + " },", + " );", + "}", + "" + ], + "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": [ @@ -1904,6 +2176,290 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario12-Failed case for wrong api keys", + "item": [ + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"authorizedot\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}", + "description": "(Required) The unique identifier for the merchant connector account" + } + ] + }, + "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." + }, + "response": [] + }, + { + "name": "Payments - Create", + "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 \"processing\" or \"failed\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_code' matches 'E00007' or '11' \",", + " function () {", + " pm.expect(jsonData.error_code).to.be.oneOf([\"E00007\", \"11\"]);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" or \"failed\" for \"status\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'processing' or 'This transaction has been declined.' \",", + " function () {", + " pm.expect(jsonData.error_message).to.be.oneOf([\"A duplicate transaction has been submitted.\", \"User authentication failed due to invalid authentication values.\"]);", + " },", + " );", + "}", + "", + "// Response body should have value \"processing\" or \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing' or 'failed' \",", + " function () {", + " pm.expect(jsonData.status).to.be.oneOf([\"failed\"]);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.environment.set(\"random_number\", _.random(1000, 100000));", + "" + ], + "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\":7003,\"currency\":\"USD\",\"confirm\":true,\"routing\":{\"data\":\"authorizedotnet\",\"type\":\"single\"},\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"370000000000002\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"900\"}},\"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": "Scenario1-Create payment with Invalid card details", "item": [ diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index de2fa2a3a537..345695007a8b 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -191,7 +191,7 @@ "language": "json" } }, - "raw": "{\"merchant_id\":\"merchant_{{$timestamp}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"merchant_id\":\"merchant_{{$timestamp}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"routing_algorithm\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/accounts", @@ -646,6 +646,682 @@ { "name": "Happy Cases", "item": [ + { + "name": "Scenario8-Create payment with Manual capture with confirm false and surcharge_data", + "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_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "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\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + }, + "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.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"paypal\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6550);", + " },", + " );", + "}", + "", + "// Response body should have a valid surcharge_details field\"", + "pm.test(\"Check if valid 'surcharge_details' is present in the response\", function () {", + " pm.response.to.have.jsonBody('surcharge_details');", + " pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5);", + " pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5);", + "});" + ], + "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": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "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 - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - 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/capture - 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 \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"paypal\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 0\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "// Response body should have a valid surcharge_details field\"", + "pm.test(\"Check if valid 'surcharge_details' is present in the response\", function () {", + " pm.response.to.have.jsonBody('surcharge_details');", + " pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5);", + " pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5);", + "});", + "" + ], + "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_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "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 \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"paypal\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "", + "// Response body should have a valid surcharge_details field\"", + "pm.test(\"Check if valid 'surcharge_details' is present in the response\", function () {", + " pm.response.to.have.jsonBody('surcharge_details');", + " pm.expect(jsonData.surcharge_details.surcharge_amount).to.eql(5);", + " pm.expect(jsonData.surcharge_details.tax_amount).to.eql(5);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index a4631fd8d613..0f29c9ff1ba3 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -80,6 +80,118 @@ { "name": "MerchantAccounts", "item": [ + { + "name": "Merchant Account - List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "disabled": false + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" + }, + "response": [] + }, { "name": "Merchant Account - Create", "event": [ @@ -103,7 +215,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", "if (jsonData?.merchant_id) {", @@ -143,6 +255,19 @@ " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", "" ], "type": "text/javascript" @@ -1003,7 +1128,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", "if (jsonData?.merchant_connector_id) {", @@ -1020,7 +1145,14 @@ " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", " );", "}", - "" + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"first_stripe_connector\")", + " },", + ");" ], "type": "text/javascript" } @@ -1065,7 +1197,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_stripe_connector\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -1226,7 +1358,7 @@ "let jsonData = {};", "try {", " jsonData = pm.response.json();", - "} catch (e) {}", + "} catch (e) { }", "", "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", "if (jsonData?.merchant_connector_id) {", @@ -1243,7 +1375,14 @@ " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", " );", "}", - "" + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", + " },", + ");" ], "type": "text/javascript" } @@ -1288,7 +1427,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"connector_label\":\"updated_stripe_connector\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", @@ -2725,6 +2864,104 @@ }, "response": [] }, + { + "name": "List Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/customers - 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]::/customers - 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 a minimum length of \"1\" for \"customer_id\"", + "if (jsonData?.customer_id) {", + " pm.test(", + " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '2'\",", + " function () {", + " pm.expect(jsonData.customer_id.length).is.at.least(2);", + " },", + " );", + "}", + "", + "", + "// Define the regular expression pattern to match customer_id", + "var customerIdPattern = /^[a-zA-Z0-9_]+$/;", + "", + "// Define an array to store the validation results", + "var validationResults = [];", + "", + "// Iterate through the JSON array", + "jsonData.forEach(function(item, index) {", + " if (item.hasOwnProperty(\"customer_id\")) {", + " if (customerIdPattern.test(item.customer_id)) {", + " validationResults.push(\"customer_id \" + item.customer_id + \" is valid.\");", + " } else {", + " validationResults.push(\"customer_id \" + item.customer_id + \" is not valid.\");", + " }", + " } else {", + " validationResults.push(\"customer_id is missing for item at index \" + index);", + " }", + "});", + "", + "// Check if any customer_id is not valid and fail the test if necessary", + "if (validationResults.some(result => !result.includes(\"is valid\"))) {", + " pm.test(\"Customer IDs validation failed: \" + validationResults.join(\", \"), function() {", + " pm.expect(false).to.be.true;", + " });", + "} else {", + " pm.test(\"All customer IDs are valid: \" + validationResults.join(\", \"), function() {", + " pm.expect(true).to.be.true;", + " });", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + "list" + ] + } + }, + "response": [] + }, { "name": "Retrieve Customer", "event": [ diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 4327619dbb97..bcd02f6cbd68 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp @@ -78,7 +78,7 @@ git checkout ${tests}/main.rs ${test_utils}/connector_auth.rs ${tests}/sample_au # Add enum for this connector in test folder sed -i'' -e "s/mod utils;/mod ${payment_gateway};\nmod utils;/" ${tests}/main.rs -sed -i'' -e "s/ pub $previous_connector: \(.*\)/\tpub $previous_connector: \1\n\tpub ${payment_gateway}: Option,/; s/auth.toml/sample_auth.toml/" ${test_utils}/connector_auth.rs +sed -i'' -e "s/ pub $previous_connector: \(.*\)/\tpub $previous_connector: \1\n\tpub ${payment_gateway}: Option,/" ${test_utils}/connector_auth.rs echo "\n\n[${payment_gateway}]\napi_key=\"API Key\"" >> ${tests}/sample_auth.toml # Remove temporary files created in above step